Skip to main content

paiagram_core/
graph.rs

1//! Definitions for the graph.
2
3use 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
26/// The graph plugin. Remember to register this plugin.
27pub 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/// The graph. Graph stores the node using an entity and the edge also using an entity entity in a
69/// [`DiGraphMap`] using [`EntityHash`].
70#[derive(Reflect, Clone, Resource, Serialize, Deserialize, Default, Deref, DerefMut)]
71#[reflect(Resource, opaque, Serialize, Deserialize, MapEntities)]
72pub struct Graph {
73    /// The actual map
74    pub map: DiGraphMap<Entity, Entity, EntityHash>,
75}
76
77/// Reconstructs the map when mapping entities
78impl MapEntities for Graph {
79    fn map_entities<E: EntityMapper>(&mut self, entity_mapper: &mut E) {
80        // construct a new graph instead
81        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    /// Find a route between the source stop and the target stop.
100    /// Returns [`None`] if no valid route exists.
101    /// Returns the total length in i32 and the stations on the route if a valid route is found.
102    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    /// Find a route given a set of stations that must be on the route.
122    /// Returns [`None`] if no valid route exists.
123    /// Returns the total length in i32 and the stations on the route if a valid route is found.
124    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    /// Transforms the graph into a normal [`petgraph::Graph`], which could be useful for mapping to
153    /// e.g. graphviz graphs.
154    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
348// EPSG:3857
349const 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
367// TODO: partial update
368fn 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/// The position of the node.
476///
477/// This stores longitude and latitude values only.
478#[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    /// Shift the node on the canvas by x and y
521    pub fn shift(&mut self, dx: f64, dy: f64) {
522        self.lon += dx;
523        self.lat += dy;
524    }
525    /// Linearly interpolates between `self` and `other` by fraction `t`.
526    /// `t` is typically between 0.0 and 1.0.
527    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
648// TODO: instead of merging them, make stations platforms instead
649pub 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}