1use crate::error::{AlgorithmError, Result};
12use crate::vector::network::{EdgeId, Graph, NodeId, ShortestPathOptions};
13use oxigdal_core::vector::{Coordinate, LineString, Polygon};
14use std::collections::{BinaryHeap, HashMap, HashSet};
15
16#[derive(Debug, Clone)]
18pub struct ServiceAreaOptions {
19 pub max_cost: f64,
21 pub intervals: Vec<f64>,
23 pub include_unreachable: bool,
25 pub cost_type: ServiceAreaCostType,
27 pub weight_criteria: Option<HashMap<String, f64>>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ServiceAreaCostType {
34 Distance,
36 Time,
38 MultiCriteria,
40}
41
42impl Default for ServiceAreaOptions {
43 fn default() -> Self {
44 Self {
45 max_cost: 1000.0,
46 intervals: vec![250.0, 500.0, 750.0, 1000.0],
47 include_unreachable: false,
48 cost_type: ServiceAreaCostType::Distance,
49 weight_criteria: None,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct ServiceArea {
57 pub origin: NodeId,
59 pub reachable_nodes: HashMap<NodeId, f64>,
61 pub intervals: Vec<ServiceAreaInterval>,
63 pub reachable_edges: Vec<(EdgeId, f64, f64)>, }
66
67#[derive(Debug, Clone)]
69pub struct ServiceAreaInterval {
70 pub max_cost: f64,
72 pub nodes: Vec<NodeId>,
74 pub count: usize,
76 pub boundary: Option<Polygon>,
78 pub area: Option<f64>,
80}
81
82#[derive(Debug, Clone)]
84pub struct IsochroneOptions {
85 pub time_intervals: Vec<f64>,
87 pub smooth: bool,
89 pub smoothing_factor: f64,
91 pub polygon_method: IsochronePolygonMethod,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum IsochronePolygonMethod {
98 ConvexHull,
100 ConcaveHull,
102}
103
104impl Default for IsochroneOptions {
105 fn default() -> Self {
106 Self {
107 time_intervals: vec![300.0, 600.0, 900.0, 1200.0], smooth: true,
109 smoothing_factor: 0.5,
110 polygon_method: IsochronePolygonMethod::ConvexHull,
111 }
112 }
113}
114
115#[derive(Debug, Clone)]
117pub struct Isochrone {
118 pub value: f64,
120 pub polygon: Option<Polygon>,
122 pub nodes: Vec<NodeId>,
124 pub area: Option<f64>,
126 pub perimeter: Option<f64>,
128}
129
130#[derive(Debug, Clone)]
132pub struct MultiFacilityResult {
133 pub facility_areas: Vec<ServiceArea>,
135 pub nearest_facility: HashMap<NodeId, (NodeId, f64)>, pub overlap_zones: Vec<OverlapZone>,
139 pub unreachable_nodes: Vec<NodeId>,
141}
142
143#[derive(Debug, Clone)]
145pub struct OverlapZone {
146 pub facilities: Vec<NodeId>,
148 pub nodes: Vec<NodeId>,
150 pub boundary: Option<Polygon>,
152}
153
154pub fn calculate_service_area(
166 graph: &Graph,
167 origin: NodeId,
168 options: &ServiceAreaOptions,
169) -> Result<ServiceArea> {
170 if graph.get_node(origin).is_none() {
171 return Err(AlgorithmError::InvalidGeometry(format!(
172 "Origin node {:?} not found",
173 origin
174 )));
175 }
176
177 let mut distances: HashMap<NodeId, f64> = HashMap::new();
179 let mut heap = BinaryHeap::new();
180 let mut reachable_edges: Vec<(EdgeId, f64, f64)> = Vec::new();
181
182 distances.insert(origin, 0.0);
183 heap.push(ServiceAreaState {
184 cost: 0.0,
185 node: origin,
186 });
187
188 while let Some(ServiceAreaState { cost, node }) = heap.pop() {
189 if cost > options.max_cost {
191 continue;
192 }
193
194 if let Some(&best_cost) = distances.get(&node) {
196 if cost > best_cost {
197 continue;
198 }
199 }
200
201 for &edge_id in graph.outgoing_edges(node) {
203 if let Some(edge) = graph.get_edge(edge_id) {
204 let next_node = edge.target;
205 let edge_cost = compute_edge_cost(edge, options);
206 let next_cost = cost + edge_cost;
207
208 if next_cost <= options.max_cost {
209 let is_better = distances
210 .get(&next_node)
211 .is_none_or(|¤t| next_cost < current);
212
213 if is_better {
214 distances.insert(next_node, next_cost);
215 reachable_edges.push((edge_id, cost, next_cost));
216 heap.push(ServiceAreaState {
217 cost: next_cost,
218 node: next_node,
219 });
220 }
221 }
222 }
223 }
224 }
225
226 let mut intervals = Vec::new();
228
229 for &interval_cost in &options.intervals {
230 let nodes: Vec<NodeId> = distances
231 .iter()
232 .filter(|(_, cost)| **cost <= interval_cost)
233 .map(|(node, _)| *node)
234 .collect();
235
236 let boundary = build_isochrone_polygon(graph, &nodes, 0.5).ok().flatten();
238 let area = boundary.as_ref().map(|p| compute_polygon_area(p));
239
240 intervals.push(ServiceAreaInterval {
241 max_cost: interval_cost,
242 count: nodes.len(),
243 nodes,
244 boundary,
245 area,
246 });
247 }
248
249 Ok(ServiceArea {
250 origin,
251 reachable_nodes: distances,
252 intervals,
253 reachable_edges,
254 })
255}
256
257fn compute_edge_cost(edge: &crate::vector::network::Edge, options: &ServiceAreaOptions) -> f64 {
259 match options.cost_type {
260 ServiceAreaCostType::Distance => edge.weight,
261 ServiceAreaCostType::Time => {
262 edge.travel_time()
263 .map(|t| t * 3600.0) .unwrap_or(edge.weight / 13.89) }
266 ServiceAreaCostType::MultiCriteria => {
267 if let Some(ref criteria) = options.weight_criteria {
268 edge.multi_weight.weighted_cost(criteria)
269 } else {
270 edge.weight
271 }
272 }
273 }
274}
275
276pub fn calculate_multi_facility_service_area(
281 graph: &Graph,
282 facilities: &[NodeId],
283 options: &ServiceAreaOptions,
284) -> Result<MultiFacilityResult> {
285 if facilities.is_empty() {
286 return Err(AlgorithmError::InvalidInput(
287 "At least one facility is required".to_string(),
288 ));
289 }
290
291 for &facility in facilities {
293 if graph.get_node(facility).is_none() {
294 return Err(AlgorithmError::InvalidGeometry(format!(
295 "Facility node {:?} not found",
296 facility
297 )));
298 }
299 }
300
301 let mut distances: HashMap<NodeId, f64> = HashMap::new();
303 let mut nearest_facility: HashMap<NodeId, (NodeId, f64)> = HashMap::new();
304 let mut heap = BinaryHeap::new();
305
306 for &facility in facilities {
308 distances.insert(facility, 0.0);
309 nearest_facility.insert(facility, (facility, 0.0));
310 heap.push(MultiFacilityState {
311 cost: 0.0,
312 node: facility,
313 facility,
314 });
315 }
316
317 let mut all_facility_costs: HashMap<NodeId, Vec<(NodeId, f64)>> = HashMap::new();
319
320 while let Some(MultiFacilityState {
321 cost,
322 node,
323 facility,
324 }) = heap.pop()
325 {
326 if cost > options.max_cost {
327 continue;
328 }
329
330 let entry = all_facility_costs.entry(node).or_default();
332 let already_visited_by_facility = entry.iter().any(|(f, c)| *f == facility && *c <= cost);
333 if already_visited_by_facility {
334 continue;
335 }
336 entry.push((facility, cost));
337
338 let update_nearest = nearest_facility
340 .get(&node)
341 .is_none_or(|&(_, current_cost)| cost < current_cost);
342
343 if update_nearest {
344 distances.insert(node, cost);
345 nearest_facility.insert(node, (facility, cost));
346 }
347
348 for &edge_id in graph.outgoing_edges(node) {
350 if let Some(edge) = graph.get_edge(edge_id) {
351 let next_node = edge.target;
352 let edge_cost = compute_edge_cost(edge, options);
353 let next_cost = cost + edge_cost;
354
355 if next_cost <= options.max_cost {
356 heap.push(MultiFacilityState {
357 cost: next_cost,
358 node: next_node,
359 facility,
360 });
361 }
362 }
363 }
364 }
365
366 let mut facility_areas = Vec::new();
368 for &facility in facilities {
369 let reachable: HashMap<NodeId, f64> = all_facility_costs
370 .iter()
371 .filter_map(|(&node, costs)| {
372 costs
373 .iter()
374 .find(|(f, _)| *f == facility)
375 .map(|(_, c)| (node, *c))
376 })
377 .collect();
378
379 let mut intervals = Vec::new();
380 for &interval_cost in &options.intervals {
381 let nodes: Vec<NodeId> = reachable
382 .iter()
383 .filter(|(_, cost)| **cost <= interval_cost)
384 .map(|(node, _)| *node)
385 .collect();
386
387 intervals.push(ServiceAreaInterval {
388 max_cost: interval_cost,
389 count: nodes.len(),
390 nodes,
391 boundary: None,
392 area: None,
393 });
394 }
395
396 facility_areas.push(ServiceArea {
397 origin: facility,
398 reachable_nodes: reachable,
399 intervals,
400 reachable_edges: Vec::new(),
401 });
402 }
403
404 let overlap_zones = compute_overlap_zones(&all_facility_costs, facilities, options);
406
407 let all_nodes: HashSet<NodeId> = graph.node_ids().into_iter().collect();
409 let reachable_nodes: HashSet<NodeId> = distances.keys().copied().collect();
410 let unreachable_nodes: Vec<NodeId> = all_nodes.difference(&reachable_nodes).copied().collect();
411
412 Ok(MultiFacilityResult {
413 facility_areas,
414 nearest_facility,
415 overlap_zones,
416 unreachable_nodes,
417 })
418}
419
420fn compute_overlap_zones(
422 all_facility_costs: &HashMap<NodeId, Vec<(NodeId, f64)>>,
423 facilities: &[NodeId],
424 _options: &ServiceAreaOptions,
425) -> Vec<OverlapZone> {
426 let mut facility_sets: HashMap<Vec<NodeId>, Vec<NodeId>> = HashMap::new();
428
429 for (&node, costs) in all_facility_costs {
430 let mut reaching_facilities: Vec<NodeId> = costs.iter().map(|(f, _)| *f).collect();
431 reaching_facilities.sort();
432 reaching_facilities.dedup();
433
434 if reaching_facilities.len() >= 2 {
435 facility_sets
436 .entry(reaching_facilities)
437 .or_default()
438 .push(node);
439 }
440 }
441
442 let mut zones = Vec::new();
444 for (fac_set, nodes) in facility_sets {
445 if nodes.is_empty() {
446 continue;
447 }
448 zones.push(OverlapZone {
449 facilities: fac_set,
450 nodes,
451 boundary: None,
452 });
453 }
454
455 zones
456}
457
458#[derive(Debug, Clone)]
460struct ServiceAreaState {
461 cost: f64,
462 node: NodeId,
463}
464
465impl PartialEq for ServiceAreaState {
466 fn eq(&self, other: &Self) -> bool {
467 self.cost == other.cost
468 }
469}
470
471impl Eq for ServiceAreaState {}
472
473impl PartialOrd for ServiceAreaState {
474 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
475 Some(self.cmp(other))
476 }
477}
478
479impl Ord for ServiceAreaState {
480 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
481 other
483 .cost
484 .partial_cmp(&self.cost)
485 .unwrap_or(std::cmp::Ordering::Equal)
486 }
487}
488
489#[derive(Debug, Clone)]
491struct MultiFacilityState {
492 cost: f64,
493 node: NodeId,
494 facility: NodeId,
495}
496
497impl PartialEq for MultiFacilityState {
498 fn eq(&self, other: &Self) -> bool {
499 self.cost == other.cost
500 }
501}
502
503impl Eq for MultiFacilityState {}
504
505impl PartialOrd for MultiFacilityState {
506 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
507 Some(self.cmp(other))
508 }
509}
510
511impl Ord for MultiFacilityState {
512 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
513 other
514 .cost
515 .partial_cmp(&self.cost)
516 .unwrap_or(std::cmp::Ordering::Equal)
517 }
518}
519
520pub fn calculate_isochrones(
532 graph: &Graph,
533 origin: NodeId,
534 options: &IsochroneOptions,
535) -> Result<Vec<Isochrone>> {
536 let max_interval = options
538 .time_intervals
539 .iter()
540 .copied()
541 .fold(f64::NEG_INFINITY, f64::max);
542
543 let max_cost = if max_interval.is_finite() {
544 max_interval
545 } else {
546 1000.0
547 };
548
549 let service_area_options = ServiceAreaOptions {
551 max_cost,
552 intervals: options.time_intervals.clone(),
553 include_unreachable: false,
554 cost_type: ServiceAreaCostType::Distance,
555 weight_criteria: None,
556 };
557
558 let service_area = calculate_service_area(graph, origin, &service_area_options)?;
559
560 let mut isochrones = Vec::new();
562
563 for interval in service_area.intervals {
564 let polygon = if options.smooth {
565 match options.polygon_method {
566 IsochronePolygonMethod::ConvexHull => {
567 build_isochrone_polygon(graph, &interval.nodes, options.smoothing_factor)?
568 }
569 IsochronePolygonMethod::ConcaveHull => {
570 build_concave_hull_polygon(graph, &interval.nodes, options.smoothing_factor)?
571 }
572 }
573 } else {
574 build_isochrone_polygon(graph, &interval.nodes, 0.0)?
575 };
576
577 let area = polygon.as_ref().map(|p| compute_polygon_area(p));
578 let perimeter = polygon.as_ref().map(|p| compute_polygon_perimeter(p));
579
580 isochrones.push(Isochrone {
581 value: interval.max_cost,
582 polygon,
583 nodes: interval.nodes,
584 area,
585 perimeter,
586 });
587 }
588
589 Ok(isochrones)
590}
591
592pub fn calculate_multi_isochrones(
594 graph: &Graph,
595 origins: &[NodeId],
596 options: &IsochroneOptions,
597) -> Result<Vec<Vec<Isochrone>>> {
598 let mut all_isochrones = Vec::new();
599
600 for &origin in origins {
601 let isochrones = calculate_isochrones(graph, origin, options)?;
602 all_isochrones.push(isochrones);
603 }
604
605 Ok(all_isochrones)
606}
607
608pub fn calculate_drive_time_polygons(
610 graph: &Graph,
611 origin: NodeId,
612 time_breaks: &[f64], speed_kmh: f64,
614) -> Result<Vec<Isochrone>> {
615 if graph.get_node(origin).is_none() {
616 return Err(AlgorithmError::InvalidGeometry(format!(
617 "Origin node {:?} not found",
618 origin
619 )));
620 }
621
622 let max_time = time_breaks.iter().copied().fold(0.0f64, f64::max);
623
624 let speed_ms = speed_kmh / 3.6; let mut arrival_times: HashMap<NodeId, f64> = HashMap::new();
628 let mut heap = BinaryHeap::new();
629
630 arrival_times.insert(origin, 0.0);
631 heap.push(ServiceAreaState {
632 cost: 0.0,
633 node: origin,
634 });
635
636 while let Some(ServiceAreaState { cost, node }) = heap.pop() {
637 if cost > max_time {
638 continue;
639 }
640
641 if let Some(&best) = arrival_times.get(&node) {
642 if cost > best {
643 continue;
644 }
645 }
646
647 for &edge_id in graph.outgoing_edges(node) {
648 if let Some(edge) = graph.get_edge(edge_id) {
649 let next_node = edge.target;
650
651 let edge_speed = edge.speed_limit.map(|s| s / 3.6).unwrap_or(speed_ms);
653
654 let edge_distance = edge.weight; let travel_time = if edge_speed > 0.0 {
657 edge_distance / edge_speed
658 } else {
659 edge_distance / speed_ms
660 };
661
662 let next_time = cost + travel_time;
663
664 if next_time <= max_time {
665 let is_better = arrival_times
666 .get(&next_node)
667 .is_none_or(|¤t| next_time < current);
668
669 if is_better {
670 arrival_times.insert(next_node, next_time);
671 heap.push(ServiceAreaState {
672 cost: next_time,
673 node: next_node,
674 });
675 }
676 }
677 }
678 }
679 }
680
681 let mut isochrones = Vec::new();
683
684 for &time_break in time_breaks {
685 let nodes: Vec<NodeId> = arrival_times
686 .iter()
687 .filter(|(_, t)| **t <= time_break)
688 .map(|(n, _)| *n)
689 .collect();
690
691 let polygon = build_isochrone_polygon(graph, &nodes, 0.5)?;
692 let area = polygon.as_ref().map(|p| compute_polygon_area(p));
693 let perimeter = polygon.as_ref().map(|p| compute_polygon_perimeter(p));
694
695 isochrones.push(Isochrone {
696 value: time_break,
697 polygon,
698 nodes,
699 area,
700 perimeter,
701 });
702 }
703
704 Ok(isochrones)
705}
706
707fn build_isochrone_polygon(
709 graph: &Graph,
710 nodes: &[NodeId],
711 _smoothing_factor: f64,
712) -> Result<Option<Polygon>> {
713 if nodes.len() < 3 {
714 return Ok(None);
715 }
716
717 let mut coords: Vec<Coordinate> = nodes
719 .iter()
720 .filter_map(|&node_id| graph.get_node(node_id).map(|n| n.coordinate))
721 .collect();
722
723 if coords.len() < 3 {
724 return Ok(None);
725 }
726
727 coords = convex_hull(&coords);
729
730 if coords.len() < 3 {
731 return Ok(None);
732 }
733
734 if let Some(first) = coords.first().copied() {
736 coords.push(first);
737 }
738
739 let exterior = LineString::new(coords)
741 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid linestring: {}", e)))?;
742
743 let polygon = Polygon::new(exterior, vec![])
744 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid polygon: {}", e)))?;
745
746 Ok(Some(polygon))
747}
748
749fn build_concave_hull_polygon(
751 graph: &Graph,
752 nodes: &[NodeId],
753 alpha: f64,
754) -> Result<Option<Polygon>> {
755 if nodes.len() < 3 {
756 return Ok(None);
757 }
758
759 let coords: Vec<Coordinate> = nodes
760 .iter()
761 .filter_map(|&node_id| graph.get_node(node_id).map(|n| n.coordinate))
762 .collect();
763
764 if coords.len() < 3 {
765 return Ok(None);
766 }
767
768 if coords.len() < 10 {
770 return build_isochrone_polygon(graph, nodes, alpha);
771 }
772
773 let hull = convex_hull(&coords);
777
778 if hull.len() < 3 {
779 return Ok(None);
780 }
781
782 let mut refined = hull.clone();
786 let avg_edge_len = compute_average_edge_length(&refined);
787 let threshold = avg_edge_len * (1.0 + alpha * 2.0);
788
789 let coord_set: HashSet<u64> = coords.iter().map(|c| hash_coordinate(c)).collect();
790
791 for _ in 0..3 {
793 let mut new_refined = Vec::new();
794 let mut changed = false;
795
796 for i in 0..refined.len() {
797 new_refined.push(refined[i]);
798 let j = (i + 1) % refined.len();
799
800 let edge_len = euclidean_dist(&refined[i], &refined[j]);
801 if edge_len > threshold {
802 let mid_x = (refined[i].x + refined[j].x) / 2.0;
804 let mid_y = (refined[i].y + refined[j].y) / 2.0;
805 let mid = Coordinate::new_2d(mid_x, mid_y);
806
807 let nearest = coords
808 .iter()
809 .filter(|c| {
810 let h = hash_coordinate(c);
811 coord_set.contains(&h)
812 && !is_same_coord(c, &refined[i])
813 && !is_same_coord(c, &refined[j])
814 })
815 .min_by(|a, b| {
816 let da = euclidean_dist(a, &mid);
817 let db = euclidean_dist(b, &mid);
818 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
819 });
820
821 if let Some(nearest_pt) = nearest {
822 let dist_to_mid = euclidean_dist(nearest_pt, &mid);
823 if dist_to_mid < edge_len / 2.0 {
824 new_refined.push(*nearest_pt);
825 changed = true;
826 }
827 }
828 }
829 }
830
831 refined = new_refined;
832 if !changed {
833 break;
834 }
835 }
836
837 if refined.len() < 3 {
838 return Ok(None);
839 }
840
841 if let Some(first) = refined.first().copied() {
843 refined.push(first);
844 }
845
846 let exterior = LineString::new(refined)
847 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid linestring: {}", e)))?;
848
849 let polygon = Polygon::new(exterior, vec![])
850 .map_err(|e| AlgorithmError::InvalidGeometry(format!("Invalid polygon: {}", e)))?;
851
852 Ok(Some(polygon))
853}
854
855fn hash_coordinate(c: &Coordinate) -> u64 {
857 let x_bits = c.x.to_bits();
858 let y_bits = c.y.to_bits();
859 x_bits.wrapping_mul(31).wrapping_add(y_bits)
860}
861
862fn is_same_coord(a: &Coordinate, b: &Coordinate) -> bool {
864 (a.x - b.x).abs() < 1e-12 && (a.y - b.y).abs() < 1e-12
865}
866
867fn euclidean_dist(a: &Coordinate, b: &Coordinate) -> f64 {
869 let dx = a.x - b.x;
870 let dy = a.y - b.y;
871 (dx * dx + dy * dy).sqrt()
872}
873
874fn compute_average_edge_length(coords: &[Coordinate]) -> f64 {
876 if coords.len() < 2 {
877 return 0.0;
878 }
879
880 let mut total = 0.0;
881 let n = coords.len();
882 for i in 0..n {
883 let j = (i + 1) % n;
884 total += euclidean_dist(&coords[i], &coords[j]);
885 }
886
887 total / n as f64
888}
889
890fn convex_hull(points: &[Coordinate]) -> Vec<Coordinate> {
892 if points.len() < 3 {
893 return points.to_vec();
894 }
895
896 let mut points = points.to_vec();
897
898 let start_idx = points
900 .iter()
901 .enumerate()
902 .min_by(|(_, a), (_, b)| {
903 a.y.partial_cmp(&b.y)
904 .unwrap_or(std::cmp::Ordering::Equal)
905 .then(a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal))
906 })
907 .map(|(idx, _)| idx)
908 .unwrap_or(0);
909
910 points.swap(0, start_idx);
911 let start = points[0];
912
913 points[1..].sort_by(|a, b| {
915 let angle_a = (a.y - start.y).atan2(a.x - start.x);
916 let angle_b = (b.y - start.y).atan2(b.x - start.x);
917 angle_a
918 .partial_cmp(&angle_b)
919 .unwrap_or(std::cmp::Ordering::Equal)
920 });
921
922 let mut unique_points = vec![points[0]];
924 for i in 1..points.len() {
925 if unique_points.len() >= 2 {
926 let last = unique_points[unique_points.len() - 1];
927 let angle_last = (last.y - start.y).atan2(last.x - start.x);
928 let angle_curr = (points[i].y - start.y).atan2(points[i].x - start.x);
929
930 if (angle_last - angle_curr).abs() < 1e-12 {
931 let dist_last = euclidean_dist(&start, &last);
933 let dist_curr = euclidean_dist(&start, &points[i]);
934 if dist_curr > dist_last {
935 unique_points.pop();
936 } else {
937 continue;
938 }
939 }
940 }
941 unique_points.push(points[i]);
942 }
943
944 let mut hull = Vec::new();
946 for point in unique_points {
947 while hull.len() >= 2 && !is_left_turn(&hull[hull.len() - 2], &hull[hull.len() - 1], &point)
948 {
949 hull.pop();
950 }
951 hull.push(point);
952 }
953
954 hull
955}
956
957fn is_left_turn(p1: &Coordinate, p2: &Coordinate, p3: &Coordinate) -> bool {
959 let cross = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
960 cross > 0.0
961}
962
963fn compute_polygon_area(polygon: &Polygon) -> f64 {
965 let coords = &polygon.exterior.coords;
966 if coords.len() < 3 {
967 return 0.0;
968 }
969
970 let mut area = 0.0;
971 let n = coords.len();
972
973 for i in 0..n {
974 let j = (i + 1) % n;
975 area += coords[i].x * coords[j].y;
976 area -= coords[j].x * coords[i].y;
977 }
978
979 (area / 2.0).abs()
980}
981
982fn compute_polygon_perimeter(polygon: &Polygon) -> f64 {
984 let coords = &polygon.exterior.coords;
985 let mut perimeter = 0.0;
986
987 for i in 0..coords.len().saturating_sub(1) {
988 perimeter += euclidean_dist(&coords[i], &coords[i + 1]);
989 }
990
991 perimeter
992}
993
994pub fn accessibility_score(
997 graph: &Graph,
998 node: NodeId,
999 destinations: &[NodeId],
1000 max_cost: f64,
1001) -> Result<AccessibilityResult> {
1002 if graph.get_node(node).is_none() {
1003 return Err(AlgorithmError::InvalidGeometry(format!(
1004 "Node {:?} not found",
1005 node
1006 )));
1007 }
1008
1009 let options = ShortestPathOptions {
1010 max_length: Some(max_cost),
1011 ..Default::default()
1012 };
1013
1014 let sssp = crate::vector::network::dijkstra_single_source(graph, node, &options)?;
1015
1016 let mut reachable_count = 0;
1017 let mut total_cost = 0.0;
1018 let mut destination_costs = Vec::new();
1019
1020 for &dest in destinations {
1021 if let Some(&(cost, _)) = sssp.get(&dest) {
1022 if cost <= max_cost {
1023 reachable_count += 1;
1024 total_cost += cost;
1025 destination_costs.push((dest, cost));
1026 }
1027 }
1028 }
1029
1030 let score = if destinations.is_empty() {
1031 0.0
1032 } else {
1033 reachable_count as f64 / destinations.len() as f64
1034 };
1035
1036 let avg_cost = if reachable_count > 0 {
1037 total_cost / reachable_count as f64
1038 } else {
1039 f64::INFINITY
1040 };
1041
1042 Ok(AccessibilityResult {
1043 node,
1044 score,
1045 reachable_count,
1046 total_destinations: destinations.len(),
1047 average_cost: avg_cost,
1048 destination_costs,
1049 })
1050}
1051
1052#[derive(Debug, Clone)]
1054pub struct AccessibilityResult {
1055 pub node: NodeId,
1057 pub score: f64,
1059 pub reachable_count: usize,
1061 pub total_destinations: usize,
1063 pub average_cost: f64,
1065 pub destination_costs: Vec<(NodeId, f64)>,
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071 use super::*;
1072
1073 fn create_test_graph() -> Graph {
1074 let mut graph = Graph::new();
1075
1076 let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1078 let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1079 let n2 = graph.add_node(Coordinate::new_2d(0.0, 1.0));
1080 let n3 = graph.add_node(Coordinate::new_2d(-1.0, 0.0));
1081 let n4 = graph.add_node(Coordinate::new_2d(0.0, -1.0));
1082
1083 let _ = graph.add_edge(n0, n1, 100.0);
1084 let _ = graph.add_edge(n0, n2, 200.0);
1085 let _ = graph.add_edge(n0, n3, 300.0);
1086 let _ = graph.add_edge(n0, n4, 400.0);
1087 let _ = graph.add_edge(n1, n2, 150.0);
1089
1090 graph
1091 }
1092
1093 #[test]
1094 fn test_service_area_options() {
1095 let options = ServiceAreaOptions::default();
1096 assert_eq!(options.max_cost, 1000.0);
1097 assert!(!options.intervals.is_empty());
1098 }
1099
1100 #[test]
1101 fn test_isochrone_options() {
1102 let options = IsochroneOptions::default();
1103 assert!(options.smooth);
1104 assert_eq!(options.time_intervals.len(), 4);
1105 }
1106
1107 #[test]
1108 fn test_service_area_basic() {
1109 let graph = create_test_graph();
1110 let nodes = graph.sorted_node_ids();
1111 let origin = nodes[0]; let options = ServiceAreaOptions {
1114 max_cost: 500.0,
1115 intervals: vec![100.0, 200.0, 300.0, 500.0],
1116 include_unreachable: false,
1117 cost_type: ServiceAreaCostType::Distance,
1118 weight_criteria: None,
1119 };
1120
1121 let sa = calculate_service_area(&graph, origin, &options);
1122 assert!(sa.is_ok());
1123
1124 let service_area = sa.expect("service area");
1125 assert!(!service_area.reachable_nodes.is_empty());
1126
1127 for i in 1..service_area.intervals.len() {
1129 assert!(service_area.intervals[i].count >= service_area.intervals[i - 1].count);
1130 }
1131 }
1132
1133 #[test]
1134 fn test_service_area_break_values() {
1135 let graph = create_test_graph();
1136 let nodes = graph.sorted_node_ids();
1137 let origin = nodes[0];
1138
1139 let options = ServiceAreaOptions {
1140 max_cost: 500.0,
1141 intervals: vec![100.0, 250.0, 500.0],
1142 include_unreachable: false,
1143 cost_type: ServiceAreaCostType::Distance,
1144 weight_criteria: None,
1145 };
1146
1147 let sa = calculate_service_area(&graph, origin, &options).expect("service area");
1148
1149 assert_eq!(sa.intervals.len(), 3);
1150 assert_eq!(sa.intervals[0].max_cost, 100.0);
1152 assert!(sa.intervals[1].count >= sa.intervals[0].count);
1154 }
1155
1156 #[test]
1157 fn test_multi_facility() {
1158 let mut graph = Graph::new();
1159 let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1160 let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1161 let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1162 let n3 = graph.add_node(Coordinate::new_2d(3.0, 0.0));
1163
1164 let _ = graph.add_edge(n0, n1, 1.0);
1165 let _ = graph.add_edge(n1, n2, 1.0);
1166 let _ = graph.add_edge(n2, n3, 1.0);
1167 let _ = graph.add_edge(n3, n2, 1.0);
1168
1169 let options = ServiceAreaOptions {
1170 max_cost: 5.0,
1171 intervals: vec![1.0, 2.0, 5.0],
1172 ..Default::default()
1173 };
1174
1175 let result = calculate_multi_facility_service_area(&graph, &[n0, n3], &options);
1176 assert!(result.is_ok());
1177
1178 let mf = result.expect("multi-facility");
1179 assert_eq!(mf.facility_areas.len(), 2);
1180
1181 if let Some(&(facility, _cost)) = mf.nearest_facility.get(&n1) {
1183 assert_eq!(facility, n0);
1184 }
1185 }
1186
1187 #[test]
1188 fn test_convex_hull() {
1189 let points = vec![
1190 Coordinate::new_2d(0.0, 0.0),
1191 Coordinate::new_2d(1.0, 0.0),
1192 Coordinate::new_2d(0.5, 0.5),
1193 Coordinate::new_2d(0.0, 1.0),
1194 ];
1195
1196 let hull = convex_hull(&points);
1197 assert!(hull.len() >= 3);
1198 }
1199
1200 #[test]
1201 fn test_is_left_turn() {
1202 let p1 = Coordinate::new_2d(0.0, 0.0);
1203 let p2 = Coordinate::new_2d(1.0, 0.0);
1204 let p3 = Coordinate::new_2d(0.5, 1.0);
1205
1206 assert!(is_left_turn(&p1, &p2, &p3));
1207 }
1208
1209 #[test]
1210 fn test_polygon_area() {
1211 let coords = vec![
1213 Coordinate::new_2d(0.0, 0.0),
1214 Coordinate::new_2d(1.0, 0.0),
1215 Coordinate::new_2d(1.0, 1.0),
1216 Coordinate::new_2d(0.0, 1.0),
1217 Coordinate::new_2d(0.0, 0.0),
1218 ];
1219 let exterior = LineString::new(coords).expect("linestring");
1220 let polygon = Polygon::new(exterior, vec![]).expect("polygon");
1221 let area = compute_polygon_area(&polygon);
1222 assert!((area - 1.0).abs() < 1e-10);
1223 }
1224
1225 #[test]
1226 fn test_isochrone_generation() {
1227 let graph = create_test_graph();
1228 let nodes = graph.sorted_node_ids();
1229 let origin = nodes[0];
1230
1231 let options = IsochroneOptions {
1232 time_intervals: vec![150.0, 300.0, 500.0],
1233 smooth: false,
1234 smoothing_factor: 0.5,
1235 polygon_method: IsochronePolygonMethod::ConvexHull,
1236 };
1237
1238 let result = calculate_isochrones(&graph, origin, &options);
1239 assert!(result.is_ok());
1240
1241 let isochrones = result.expect("isochrones");
1242 assert_eq!(isochrones.len(), 3);
1243
1244 for i in 1..isochrones.len() {
1246 assert!(isochrones[i].nodes.len() >= isochrones[i - 1].nodes.len());
1247 }
1248 }
1249
1250 #[test]
1251 fn test_accessibility_score() {
1252 let mut graph = Graph::new();
1253 let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1254 let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1255 let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1256 let n3 = graph.add_node(Coordinate::new_2d(10.0, 0.0)); let _ = graph.add_edge(n0, n1, 1.0);
1259 let _ = graph.add_edge(n1, n2, 1.0);
1260
1261 let result = accessibility_score(&graph, n0, &[n1, n2, n3], 5.0);
1262 assert!(result.is_ok());
1263
1264 let acc = result.expect("accessibility");
1265 assert_eq!(acc.reachable_count, 2); assert_eq!(acc.total_destinations, 3);
1267 assert!((acc.score - 2.0 / 3.0).abs() < 1e-10);
1268 }
1269
1270 #[test]
1271 fn test_drive_time_polygons() {
1272 let graph = create_test_graph();
1273 let nodes = graph.sorted_node_ids();
1274 let origin = nodes[0];
1275
1276 let result = calculate_drive_time_polygons(
1277 &graph,
1278 origin,
1279 &[60.0, 120.0, 300.0], 50.0, );
1282 assert!(result.is_ok());
1283
1284 let polygons = result.expect("drive time polygons");
1285 assert_eq!(polygons.len(), 3);
1286 }
1287
1288 #[test]
1289 fn test_multi_isochrones() {
1290 let graph = create_test_graph();
1291 let nodes = graph.sorted_node_ids();
1292
1293 let options = IsochroneOptions {
1294 time_intervals: vec![200.0, 500.0],
1295 smooth: false,
1296 ..Default::default()
1297 };
1298
1299 let result = calculate_multi_isochrones(&graph, &[nodes[0], nodes[3]], &options);
1300 assert!(result.is_ok());
1301
1302 let all = result.expect("multi isochrones");
1303 assert_eq!(all.len(), 2); }
1305
1306 #[test]
1307 fn test_overlap_zone_detection() {
1308 let mut graph = Graph::new();
1309 let n0 = graph.add_node(Coordinate::new_2d(0.0, 0.0));
1310 let n1 = graph.add_node(Coordinate::new_2d(1.0, 0.0));
1311 let n2 = graph.add_node(Coordinate::new_2d(2.0, 0.0));
1312
1313 let _ = graph.add_edge(n0, n1, 1.0);
1315 let _ = graph.add_edge(n2, n1, 1.0);
1316
1317 let options = ServiceAreaOptions {
1318 max_cost: 5.0,
1319 intervals: vec![2.0],
1320 ..Default::default()
1321 };
1322
1323 let result = calculate_multi_facility_service_area(&graph, &[n0, n2], &options);
1324 assert!(result.is_ok());
1325
1326 let mf = result.expect("multi-facility");
1327 assert!(!mf.overlap_zones.is_empty());
1329 }
1330}