synthpop/
external.rs

1//! Some users of the API (https://a-b-street.github.io/docs/tech/dev/api.html) have their own
2//! simulation input data; import it here.
3
4use anyhow::Result;
5use serde::Deserialize;
6
7use geom::{Distance, FindClosest, LonLat, Time};
8use map_model::Map;
9
10use crate::{IndividTrip, MapBorders, PersonSpec, TripEndpoint, TripMode, TripPurpose};
11
12#[derive(Deserialize)]
13pub struct ExternalPerson {
14    pub trips: Vec<ExternalTrip>,
15}
16
17#[derive(Deserialize)]
18pub struct ExternalTrip {
19    pub departure: Time,
20    pub origin: ExternalTripEndpoint,
21    pub destination: ExternalTripEndpoint,
22    pub mode: TripMode,
23    pub purpose: TripPurpose,
24}
25
26#[derive(Deserialize)]
27pub enum ExternalTripEndpoint {
28    TripEndpoint(TripEndpoint),
29    Position(LonLat),
30}
31
32impl ExternalPerson {
33    /// Import external scenario data. The main difference between `ExternalPerson` and
34    /// `PersonSpec` is a way to specify endpoints by a `LonLat`. This is snapped to the nearest
35    /// building. If the point is outside of the map boundary, it's snapped to the nearest border
36    /// (by Euclidean distance -- the network outside the given map isn't known). Failure happens
37    /// if a point is within the map, but not close enough to any buildings. If `skip_problems` is
38    /// true, then those failures are logged; otherwise this panics at the first problem.
39    pub fn import(
40        map: &Map,
41        input: Vec<ExternalPerson>,
42        skip_problems: bool,
43    ) -> Result<Vec<PersonSpec>> {
44        let mut closest: FindClosest<TripEndpoint> = FindClosest::new();
45        for b in map.all_buildings() {
46            closest.add_polygon(TripEndpoint::Building(b.id), &b.polygon);
47        }
48        let borders = MapBorders::new(map);
49
50        let lookup_pt = |endpt, is_origin, mode| match endpt {
51            ExternalTripEndpoint::TripEndpoint(endpt) => Ok(endpt),
52            ExternalTripEndpoint::Position(gps) => {
53                let pt = gps.to_pt(map.get_gps_bounds());
54                if map.get_boundary_polygon().contains_pt(pt) {
55                    match closest.closest_pt(pt, Distance::meters(100.0)) {
56                        Some((x, _)) => Ok(x),
57                        None => Err(anyhow!("No building within 100m of {}", gps)),
58                    }
59                } else {
60                    let (incoming, outgoing) = borders.for_mode(mode);
61                    let candidates = if is_origin { incoming } else { outgoing };
62                    Ok(TripEndpoint::Border(
63                        candidates
64                            .iter()
65                            .min_by_key(|border| border.gps_pos.fast_dist(gps))
66                            .ok_or_else(|| anyhow!("No border for {}", mode.ongoing_verb()))?
67                            .i,
68                    ))
69                }
70            }
71        };
72
73        let mut results = Vec::new();
74        for person in input {
75            let mut spec = PersonSpec {
76                orig_id: None,
77                trips: Vec::new(),
78            };
79            for trip in person.trips {
80                if trip.departure < Time::START_OF_DAY {
81                    if skip_problems {
82                        warn!(
83                            "Skipping trip with negative departure time {:?}",
84                            trip.departure
85                        );
86                        continue;
87                    } else {
88                        bail!("Some trip has negative departure time {:?}", trip.departure);
89                    }
90                }
91
92                spec.trips.push(IndividTrip::new(
93                    trip.departure,
94                    trip.purpose,
95                    match lookup_pt(trip.origin, true, trip.mode) {
96                        Ok(endpt) => endpt,
97                        Err(err) => {
98                            if skip_problems {
99                                warn!("Skipping person: {}", err);
100                                continue;
101                            } else {
102                                return Err(err);
103                            }
104                        }
105                    },
106                    match lookup_pt(trip.destination, false, trip.mode) {
107                        Ok(endpt) => endpt,
108                        Err(err) => {
109                            if skip_problems {
110                                warn!("Skipping person: {}", err);
111                                continue;
112                            } else {
113                                return Err(err);
114                            }
115                        }
116                    },
117                    trip.mode,
118                ));
119            }
120            results.push(spec);
121        }
122        Ok(results)
123    }
124}