1use std::collections::HashMap;
4
5pub mod arrange;
6
7use bevy::ecs::entity::{EntityHash, EntityHashMap, EntityHashSet};
8use bevy::prelude::*;
9use bevy::tasks::futures_lite::future::poll_once;
10use bevy::tasks::{AsyncComputeTaskPool, Task, block_on};
11use moonshine_core::kind::{Instance, SpawnInstance};
12use moonshine_core::prelude::{MapEntities, ReflectMapEntities};
13use petgraph::algo::astar;
14use petgraph::prelude::DiGraphMap;
15use petgraph::visit::EdgeRef;
16use rstar::{AABB, PointDistance, RTree, RTreeObject};
17use serde::{Deserialize, Serialize};
18use smallvec::SmallVec;
19
20use crate::entry::EntryStop;
21use crate::interval::{Interval, IntervalQuery};
22use crate::route::Route;
23use crate::station::{Platforms, Station};
24use crate::units::distance::Distance;
25
26pub struct GraphPlugin;
28impl Plugin for GraphPlugin {
29 fn build(&self, app: &mut App) {
30 app.init_resource::<Graph>()
31 .init_resource::<GraphSpatialIndex>()
32 .init_resource::<GraphSpatialIndexState>()
33 .init_resource::<GraphIntervalSpatialIndex>()
34 .init_resource::<GraphIntervalSpatialIndexState>()
35 .add_systems(Update, arrange::apply_graph_layout_task)
36 .add_systems(
37 Update,
38 (
39 mark_graph_spatial_index_dirty,
40 start_graph_spatial_index_rebuild,
41 apply_graph_spatial_index_task,
42 )
43 .chain(),
44 )
45 .add_systems(
46 Update,
47 (
48 mark_graph_interval_spatial_index_dirty,
49 start_graph_interval_spatial_index_rebuild,
50 apply_graph_interval_spatial_index_task,
51 )
52 .chain(),
53 )
54 .add_observer(update_graph_on_station_removal)
55 .add_observer(update_graph_on_interval_removal)
56 .add_observer(add_interval_pair);
57 #[cfg(debug_assertions)]
58 {
59 use bevy::time::common_conditions::on_real_timer;
60 app.add_systems(
61 PostUpdate,
62 check_stations_in_graph.run_if(on_real_timer(std::time::Duration::from_secs(10))),
63 );
64 }
65 }
66}
67
68#[derive(Reflect, Clone, Resource, Serialize, Deserialize, Default, Deref, DerefMut)]
71#[reflect(Resource, opaque, Serialize, Deserialize, MapEntities)]
72pub struct Graph {
73 pub map: DiGraphMap<Entity, Entity, EntityHash>,
75}
76
77impl MapEntities for Graph {
79 fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
80 let (nodes, edges) = self.capacity();
82 let mut new_graph = DiGraphMap::with_capacity(nodes, edges);
83 for mut node in self.nodes() {
84 node.map_entities(entity_mapper);
85 new_graph.add_node(node);
86 }
87 for (mut source, mut target, weight) in self.all_edges() {
88 let mut weight = *weight;
89 source.map_entities(entity_mapper);
90 target.map_entities(entity_mapper);
91 weight.map_entities(entity_mapper);
92 new_graph.add_edge(source, target, weight);
93 }
94 self.map = new_graph;
95 }
96}
97
98impl Graph {
99 pub fn route_between(
103 &self,
104 source: Entity,
105 target: Entity,
106 interval_q: &Query<IntervalQuery>,
107 ) -> Option<(i32, Vec<Entity>)> {
108 astar(
109 &self.map,
110 source,
111 |f| f == target,
112 |e| {
113 let Ok(i) = interval_q.get(*e.weight()) else {
114 return i32::MAX;
115 };
116 i.distance().0
117 },
118 |_| 0,
119 )
120 }
121 pub fn route_between_source_waypoint_target(
125 &self,
126 mut points: impl Iterator<Item = Entity>,
127 interval_q: &Query<IntervalQuery>,
128 ) -> Option<(i32, Vec<Entity>)> {
129 let mut prev = points.next()?;
130 let mut total_length = 0;
131 let mut passes = vec![prev];
132 for curr in points {
133 let (leg_length, leg_points) = astar(
134 &self.map,
135 prev,
136 |f| f == curr,
137 |e| {
138 let Ok(i) = interval_q.get(*e.weight()) else {
139 return i32::MAX;
140 };
141 i.distance().0
142 },
143 |_| 0,
144 )?;
145 total_length += leg_length;
146 passes.extend_from_slice(&leg_points[1..]);
147 prev = curr;
148 }
149 Some((total_length, passes))
150 }
151
152 pub fn into_graph(self) -> petgraph::Graph<Entity, Entity> {
155 self.map.into_graph()
156 }
157}
158
159#[derive(Clone, Copy, Debug)]
160struct SpatialIndexedEntity {
161 entity: Entity,
162 point: [f64; 2],
163}
164
165#[derive(Clone, Copy, Debug)]
166struct IntervalSpatialIndexedEntity {
167 interval: Entity,
168 p0: [f64; 2],
169 p1: [f64; 2],
170}
171
172impl RTreeObject for SpatialIndexedEntity {
173 type Envelope = AABB<[f64; 2]>;
174
175 fn envelope(&self) -> Self::Envelope {
176 AABB::from_point(self.point)
177 }
178}
179
180impl PointDistance for SpatialIndexedEntity {
181 fn distance_2(&self, point: &[f64; 2]) -> f64 {
182 let dx = self.point[0] - point[0];
183 let dy = self.point[1] - point[1];
184 dx * dx + dy * dy
185 }
186}
187
188impl RTreeObject for IntervalSpatialIndexedEntity {
189 type Envelope = AABB<[f64; 2]>;
190
191 fn envelope(&self) -> Self::Envelope {
192 AABB::from_corners(
193 [self.p0[0].min(self.p1[0]), self.p0[1].min(self.p1[1])],
194 [self.p0[0].max(self.p1[0]), self.p0[1].max(self.p1[1])],
195 )
196 }
197}
198
199#[derive(Resource, Default)]
200pub struct GraphSpatialIndex {
201 tree: RTree<SpatialIndexedEntity>,
202}
203
204#[derive(Clone, Copy, Debug)]
205pub struct GraphIntervalSpatialSample {
206 pub interval: Entity,
207 pub p0: [f64; 2],
208 pub p1: [f64; 2],
209}
210
211#[derive(Resource, Default)]
212pub struct GraphIntervalSpatialIndex {
213 tree: RTree<IntervalSpatialIndexedEntity>,
214}
215
216impl GraphSpatialIndex {
217 pub fn is_empty(&self) -> bool {
218 self.tree.size() == 0
219 }
220
221 pub fn clear(&mut self) {
222 self.tree = RTree::new();
223 }
224
225 pub fn insert_xy(&mut self, entity: Entity, x: f64, y: f64) {
226 self.tree.insert(SpatialIndexedEntity {
227 entity,
228 point: [x, y],
229 });
230 }
231
232 pub fn insert_lon_lat(&mut self, entity: Entity, lon: f64, lat: f64) {
233 let (x, y) = lon_lat_to_xy(lon, lat);
234 self.insert_xy(entity, x, y);
235 }
236
237 pub fn entities_in_xy_aabb(
238 &self,
239 min_x: f64,
240 min_y: f64,
241 max_x: f64,
242 max_y: f64,
243 ) -> Vec<Entity> {
244 let envelope = AABB::from_corners(
245 [min_x.min(max_x), min_y.min(max_y)],
246 [min_x.max(max_x), min_y.max(max_y)],
247 );
248 self.tree
249 .locate_in_envelope_intersecting(&envelope)
250 .map(|entry| entry.entity)
251 .collect()
252 }
253
254 pub fn entities_in_lon_lat_aabb(
255 &self,
256 min_lon: f64,
257 min_lat: f64,
258 max_lon: f64,
259 max_lat: f64,
260 ) -> Vec<Entity> {
261 let (x0, y0) = lon_lat_to_xy(min_lon, min_lat);
262 let (x1, y1) = lon_lat_to_xy(max_lon, max_lat);
263 self.entities_in_xy_aabb(x0, y0, x1, y1)
264 }
265
266 pub fn nearest_in_xy(&self, x: f64, y: f64) -> Option<Entity> {
267 self.tree
268 .nearest_neighbor(&[x, y])
269 .map(|entry| entry.entity)
270 }
271
272 pub fn nearest_in_lon_lat(&self, lon: f64, lat: f64) -> Option<Entity> {
273 let (x, y) = lon_lat_to_xy(lon, lat);
274 self.nearest_in_xy(x, y)
275 }
276
277 fn replace_tree(&mut self, tree: RTree<SpatialIndexedEntity>) {
278 self.tree = tree;
279 }
280}
281
282impl GraphIntervalSpatialIndex {
283 pub fn is_empty(&self) -> bool {
284 self.tree.size() == 0
285 }
286
287 pub fn query_xy_aabb(
288 &self,
289 min_x: f64,
290 min_y: f64,
291 max_x: f64,
292 max_y: f64,
293 ) -> Vec<GraphIntervalSpatialSample> {
294 if self.is_empty() {
295 return Vec::new();
296 }
297
298 let envelope = AABB::from_corners(
299 [min_x.min(max_x), min_y.min(max_y)],
300 [min_x.max(max_x), min_y.max(max_y)],
301 );
302
303 self.tree
304 .locate_in_envelope_intersecting(&envelope)
305 .map(|item| GraphIntervalSpatialSample {
306 interval: item.interval,
307 p0: item.p0,
308 p1: item.p1,
309 })
310 .collect()
311 }
312
313 fn replace_tree(&mut self, tree: RTree<IntervalSpatialIndexedEntity>) {
314 self.tree = tree;
315 }
316}
317
318#[derive(Resource)]
319struct GraphSpatialIndexState {
320 dirty: bool,
321 task: Option<Task<RTree<SpatialIndexedEntity>>>,
322}
323
324#[derive(Resource)]
325struct GraphIntervalSpatialIndexState {
326 dirty: bool,
327 task: Option<Task<RTree<IntervalSpatialIndexedEntity>>>,
328}
329
330impl Default for GraphSpatialIndexState {
331 fn default() -> Self {
332 Self {
333 dirty: true,
334 task: None,
335 }
336 }
337}
338
339impl Default for GraphIntervalSpatialIndexState {
340 fn default() -> Self {
341 Self {
342 dirty: true,
343 task: None,
344 }
345 }
346}
347
348const EARTH_RADIUS_METERS: f64 = 6_378_137.0;
350const WEB_MERCATOR_MAX_LAT: f64 = 85.051_128_78;
351
352pub fn lon_lat_to_xy(lon: f64, lat: f64) -> (f64, f64) {
353 let x = EARTH_RADIUS_METERS * lon.to_radians();
354 let lat = lat.clamp(-WEB_MERCATOR_MAX_LAT, WEB_MERCATOR_MAX_LAT);
355 let lat_rad = lat.to_radians();
356 let y = -EARTH_RADIUS_METERS * (std::f64::consts::FRAC_PI_4 + lat_rad / 2.0).tan().ln();
357 (x, y)
358}
359
360pub fn xy_to_lon_lat(x: f64, y: f64) -> (f64, f64) {
361 let lon = (x / EARTH_RADIUS_METERS).to_degrees();
362 let lat =
363 (2.0 * (-y / EARTH_RADIUS_METERS).exp().atan() - std::f64::consts::FRAC_PI_2).to_degrees();
364 (lon, lat)
365}
366
367fn mark_graph_spatial_index_dirty(
369 mut state: ResMut<GraphSpatialIndexState>,
370 changed_nodes: Query<(), Or<(Added<Node>, Changed<Node>)>>,
371 mut removed_nodes: RemovedComponents<Node>,
372) {
373 if !changed_nodes.is_empty() || removed_nodes.read().next().is_some() {
374 state.dirty = true;
375 }
376}
377
378fn mark_graph_interval_spatial_index_dirty(
379 mut state: ResMut<GraphIntervalSpatialIndexState>,
380 graph: Res<Graph>,
381 changed_nodes: Query<(), Or<(Added<Node>, Changed<Node>)>>,
382 changed_intervals: Query<(), Or<(Added<Interval>, Changed<Interval>)>>,
383 mut removed_nodes: RemovedComponents<Node>,
384 mut removed_intervals: RemovedComponents<Interval>,
385) {
386 if graph.is_added()
387 || graph.is_changed()
388 || !changed_nodes.is_empty()
389 || !changed_intervals.is_empty()
390 || removed_nodes.read().next().is_some()
391 || removed_intervals.read().next().is_some()
392 {
393 state.dirty = true;
394 }
395}
396
397fn start_graph_spatial_index_rebuild(
398 mut state: ResMut<GraphSpatialIndexState>,
399 nodes: Query<(Entity, &Node)>,
400) {
401 if !state.dirty || state.task.is_some() {
402 return;
403 }
404 state.dirty = false;
405
406 let snapshot: Vec<(Entity, [f64; 2])> = nodes
407 .iter()
408 .map(|(entity, node)| (entity, node.coor.to_xy_arr()))
409 .collect();
410 state.task = Some(AsyncComputeTaskPool::get().spawn(async move {
411 let entries: Vec<SpatialIndexedEntity> = snapshot
412 .into_iter()
413 .map(|(entity, point)| SpatialIndexedEntity { entity, point })
414 .collect();
415 RTree::bulk_load(entries)
416 }));
417}
418
419fn start_graph_interval_spatial_index_rebuild(
420 mut state: ResMut<GraphIntervalSpatialIndexState>,
421 graph: Res<Graph>,
422 nodes: Query<&Node>,
423) {
424 if !state.dirty || state.task.is_some() {
425 return;
426 }
427 state.dirty = false;
428
429 let mut snapshot = Vec::<IntervalSpatialIndexedEntity>::new();
430 for (source, target, interval) in graph.all_edges() {
431 let Ok(source_node) = nodes.get(source) else {
432 continue;
433 };
434 let Ok(target_node) = nodes.get(target) else {
435 continue;
436 };
437 snapshot.push(IntervalSpatialIndexedEntity {
438 interval: *interval,
439 p0: source_node.coor.to_xy_arr(),
440 p1: target_node.coor.to_xy_arr(),
441 });
442 }
443
444 state.task = Some(AsyncComputeTaskPool::get().spawn(async move { RTree::bulk_load(snapshot) }));
445}
446
447fn apply_graph_spatial_index_task(
448 mut state: ResMut<GraphSpatialIndexState>,
449 mut index: ResMut<GraphSpatialIndex>,
450) {
451 let Some(task) = state.task.as_mut() else {
452 return;
453 };
454 let Some(tree) = block_on(poll_once(task)) else {
455 return;
456 };
457 index.replace_tree(tree);
458 state.task = None;
459}
460
461fn apply_graph_interval_spatial_index_task(
462 mut state: ResMut<GraphIntervalSpatialIndexState>,
463 mut index: ResMut<GraphIntervalSpatialIndex>,
464) {
465 let Some(task) = state.task.as_mut() else {
466 return;
467 };
468 let Some(tree) = block_on(poll_once(task)) else {
469 return;
470 };
471 index.replace_tree(tree);
472 state.task = None;
473}
474
475#[derive(Reflect, Clone, Copy, Debug, PartialEq)]
479pub struct NodeCoor {
480 pub lon: f64,
481 pub lat: f64,
482}
483
484impl std::fmt::Display for NodeCoor {
485 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
486 let lat_dir = if self.lat < 0.0 { 'S' } else { 'N' };
487 let lon_dir = if self.lon < 0.0 { 'W' } else { 'E' };
488 write!(
489 f,
490 "{:.4}°{}, {:.4}°{}",
491 self.lat.abs(),
492 lat_dir,
493 self.lon.abs(),
494 lon_dir
495 )
496 }
497}
498
499impl Default for NodeCoor {
500 fn default() -> Self {
501 Self::new(0.0, 0.0)
502 }
503}
504
505impl NodeCoor {
506 pub fn new(lon: f64, lat: f64) -> Self {
507 Self { lon, lat }
508 }
509 pub fn from_xy(x: f64, y: f64) -> Self {
510 let (lon, lat) = xy_to_lon_lat(x, y);
511 Self::new(lon, lat)
512 }
513 pub fn to_xy(&self) -> (f64, f64) {
514 lon_lat_to_xy(self.lon, self.lat)
515 }
516 pub fn to_xy_arr(&self) -> [f64; 2] {
517 let (x, y) = self.to_xy();
518 [x, y]
519 }
520 pub fn shift(&mut self, dx: f64, dy: f64) {
522 self.lon += dx;
523 self.lat += dy;
524 }
525 pub fn lerp(&self, other: &Self, t: f64) -> Self {
528 let end_lon = other.lon;
529 let end_lat = other.lat;
530
531 Self {
532 lon: self.lon + (end_lon - self.lon) * t,
533 lat: self.lat + (end_lat - self.lat) * t,
534 }
535 }
536}
537
538#[derive(Default, Reflect, Component, Debug)]
539#[reflect(Component)]
540pub struct Node {
541 pub coor: NodeCoor,
542}
543
544fn update_graph_on_station_removal(
545 removed_station: On<Remove, Station>,
546 mut commands: Commands,
547 mut graph: ResMut<Graph>,
548) {
549 let s = removed_station.entity;
550 for e in graph
551 .neighbors_directed(s, petgraph::Direction::Incoming)
552 .chain(graph.neighbors_directed(s, petgraph::Direction::Outgoing))
553 {
554 commands.entity(e).despawn();
555 }
556 graph.remove_node(s);
557}
558
559fn update_graph_on_interval_removal(
560 removed_interval: On<Remove, Interval>,
561 mut graph: ResMut<Graph>,
562) {
563 let i = removed_interval.entity;
564 let mut source = None;
565 let mut target = None;
566 for (s, t, weight) in graph.all_edges() {
567 if i != *weight {
568 continue;
569 }
570 source = Some(s);
571 target = Some(t);
572 break;
573 }
574 let (Some(s), Some(t)) = (source, target) else {
575 return;
576 };
577 graph.remove_edge(s, t);
578}
579
580#[cfg(debug_assertions)]
581fn check_stations_in_graph(
582 graph: Res<Graph>,
583 stations: Populated<Entity, With<Station>>,
584 intervals: Populated<Entity, With<Interval>>,
585 names: Query<&Name>,
586) {
587 let queried_station_set: EntityHashSet = stations.iter().collect();
588 let queried_interval_set: EntityHashSet = intervals.iter().collect();
589 let mut graphed_station_set = EntityHashSet::new();
590 let mut graphed_interval_set = EntityHashSet::new();
591 for (_, _, w) in graph.all_edges() {
592 graphed_interval_set.insert(*w);
593 }
594 for node in graph.nodes() {
595 graphed_station_set.insert(node);
596 }
597 if queried_station_set != graphed_station_set {
598 debug_graph_set_diff(
599 "station",
600 &queried_station_set,
601 &graphed_station_set,
602 &names,
603 );
604 }
605 if queried_interval_set != graphed_interval_set {
606 debug_graph_set_diff(
607 "interval",
608 &queried_interval_set,
609 &graphed_interval_set,
610 &names,
611 );
612 }
613 debug_assert_eq!(queried_station_set, graphed_station_set);
614 debug_assert_eq!(queried_interval_set, graphed_interval_set);
615}
616
617#[cfg(debug_assertions)]
618fn debug_graph_set_diff(
619 label: &str,
620 queried: &EntityHashSet,
621 graphed: &EntityHashSet,
622 names: &Query<&Name>,
623) {
624 let intersection: EntityHashSet = queried.intersection(graphed).copied().collect();
625 let only_queried: EntityHashSet = queried.difference(graphed).copied().collect();
626 let only_graphed: EntityHashSet = graphed.difference(queried).copied().collect();
627
628 let list_with_names = |set: &EntityHashSet| -> Vec<String> {
629 let mut out: Vec<String> = set
630 .iter()
631 .map(|e| match names.get(*e) {
632 Ok(name) => format!("{} ({})", name.as_str(), e.index()),
633 Err(_) => format!("<??> ({})", e.index()),
634 })
635 .collect();
636 out.sort_unstable();
637 out
638 };
639
640 warn!(
641 "Graph {label} set mismatch: intersection={:#?} | only_queried={:#?} | only_graphed={:#?}",
642 list_with_names(&intersection),
643 list_with_names(&only_queried),
644 list_with_names(&only_graphed)
645 );
646}
647
648pub fn merge_station_by_name(
650 mut commands: Commands,
651 mut graph: ResMut<Graph>,
652 stations: Query<(Entity, &Name, &Platforms), With<Station>>,
653 entry_stops: Query<(Entity, &EntryStop)>,
654 mut routes: Query<&mut Route>,
655) {
656 let mut name_map: HashMap<&str, SmallVec<[Entity; 1]>> = HashMap::new();
657 for (entity, name, _) in &stations {
658 let v = name_map.entry(name.as_str()).or_default();
659 v.push(entity);
660 }
661
662 let mut remap: EntityHashMap<Entity> = EntityHashMap::default();
663
664 for (_name, mut entities) in name_map.into_iter().filter(|(_, v)| v.len() > 1) {
665 entities.sort_unstable_by_key(|entity| entity.index());
666 let keep = entities[0];
667
668 for duplicate in entities.into_iter().skip(1) {
669 if let Ok((_, _, platforms)) = stations.get(duplicate) {
670 let to_move: SmallVec<[Entity; 8]> = platforms.iter().collect();
671 if !to_move.is_empty() {
672 commands.entity(keep).add_children(&to_move);
673 }
674 }
675 remap.insert(duplicate, keep);
676 }
677 }
678
679 if remap.is_empty() {
680 return;
681 }
682
683 let (nodes, edges) = graph.capacity();
684 let mut new_graph = DiGraphMap::with_capacity(nodes, edges);
685 let mut removed_intervals = EntityHashSet::default();
686 for (source, target, weight) in graph.all_edges() {
687 let source = remap.get(&source).copied().unwrap_or(source);
688 let target = remap.get(&target).copied().unwrap_or(target);
689 if let Some(existing_weight) = new_graph.edge_weight(source, target) {
690 if *existing_weight != *weight {
691 removed_intervals.insert(*weight);
692 }
693 continue;
694 }
695 new_graph.add_edge(source, target, *weight);
696 }
697 for node in graph.nodes() {
698 let node = remap.get(&node).copied().unwrap_or(node);
699 new_graph.add_node(node);
700 }
701 graph.map = new_graph;
702
703 for (entry, stop) in &entry_stops {
704 if let Some(new_stop) = remap.get(&stop.0) {
705 commands.entity(entry).insert(EntryStop(*new_stop));
706 }
707 }
708
709 for mut route in &mut routes {
710 for stop in &mut route.stops {
711 if let Some(new_stop) = remap.get(stop) {
712 *stop = *new_stop;
713 }
714 }
715 }
716
717 for interval in removed_intervals {
718 commands.entity(interval).despawn();
719 }
720
721 for duplicate in remap.into_keys() {
722 commands.entity(duplicate).despawn();
723 }
724}
725
726#[derive(Event, Clone, Copy)]
727pub struct AddIntervalPair {
728 pub source: Entity,
729 pub target: Entity,
730 pub length: Distance,
731}
732
733fn add_interval_pair(msg: On<AddIntervalPair>, mut graph: ResMut<Graph>, mut commands: Commands) {
734 if !graph.contains_edge(msg.source, msg.target) {
735 let e1: Instance<Interval> = commands
736 .spawn_instance(Interval { length: msg.length })
737 .into();
738 graph.add_edge(msg.source, msg.target, e1.entity());
739 }
740 if !graph.contains_edge(msg.target, msg.source) {
741 let e2: Instance<Interval> = commands
742 .spawn_instance(Interval { length: msg.length })
743 .into();
744 graph.add_edge(msg.target, msg.source, e2.entity());
745 }
746}