headless/
main.rs

1//! This runs a simulation without any graphics and serves a very basic API to control things. See
2//! https://a-b-street.github.io/docs/tech/dev/api.html for documentation and
3//! https://github.com/mhabedank/abstreet-docker/ for an example of using with Docker. To run this:
4//!
5//! > cd headless; cargo run -- --port=1234
6//! > curl http://localhost:1234/sim/get-time
7//! 00:00:00.0
8//! > curl http://localhost:1234/sim/goto-time?t=01:01:00
9//! it's now 01:01:00.0
10//! > curl http://localhost:1234/data/get-road-thruput
11//! ... huge JSON blob
12
13#[macro_use]
14extern crate anyhow;
15#[macro_use]
16extern crate log;
17
18use std::collections::{BTreeMap, BTreeSet, HashMap};
19use std::net::IpAddr;
20use std::sync::RwLock;
21
22use anyhow::Result;
23use hyper::{Body, Request, Response, Server, StatusCode};
24use rand::SeedableRng;
25use rand_xorshift::XorShiftRng;
26use serde::{Deserialize, Serialize};
27use structopt::StructOpt;
28
29use abstio::MapName;
30use abstutil::{serialize_btreemap, Timer};
31use geom::{Distance, Duration, FindClosest, LonLat, Time};
32use map_model::{
33    CompressedMovementID, ControlTrafficSignal, EditIntersectionControl, IntersectionID, Map,
34    MovementID, PermanentMapEdits, RoadID, TurnID,
35};
36use sim::{
37    AgentID, AgentType, DelayCause, PersonID, Sim, SimFlags, SimOptions, TripID, VehicleType,
38};
39use synthpop::{ExternalPerson, Scenario, ScenarioModifier, TripMode};
40
41lazy_static::lazy_static! {
42    static ref MAP: RwLock<Map> = RwLock::new(Map::blank());
43    static ref SIM: RwLock<Sim> = RwLock::new(Sim::new(&Map::blank(), SimOptions::new("tmp")));
44    static ref LOAD: RwLock<LoadSim> = RwLock::new({
45        LoadSim {
46            scenario: abstio::path_scenario(&MapName::seattle("montlake"), "weekday"),
47            modifiers: Vec::new(),
48            edits: None,
49            rng_seed: SimFlags::RNG_SEED,
50            opts: SimOptions::default(),
51        }
52    });
53}
54
55#[derive(StructOpt)]
56#[structopt(
57    name = "headless",
58    about = "Simulate traffic with a JSON API, not a GUI"
59)]
60struct Args {
61    /// What IP to run the JSON API on.
62    #[structopt(long, default_value = "127.0.0.1")]
63    ip: IpAddr,
64    /// What port to run the JSON API on.
65    #[structopt(long)]
66    port: u16,
67    /// If specified, start with this scenario loaded instead of the Montlake weekday default. Use
68    /// `/sim/load` to change this after startup or control more options.
69    #[structopt(long)]
70    scenario: Option<String>,
71    /// An arbitrary number to seed the random number generator. This is input to the deterministic
72    /// simulation, so different values affect results.
73    // TODO default_value can only handle strings, so copying SimFlags::RNG_SEED
74    #[structopt(long, default_value = "42")]
75    rng_seed: u64,
76    #[structopt(flatten)]
77    opts: SimOptions,
78}
79
80#[tokio::main]
81async fn main() {
82    abstutil::logger::setup();
83    let args = Args::from_args();
84
85    {
86        let mut load = LOAD.write().unwrap();
87        load.rng_seed = args.rng_seed;
88        load.opts = args.opts;
89        if let Some(path) = args.scenario {
90            load.scenario = path;
91        }
92
93        let (map, sim) = load.setup(&mut Timer::new("setup headless"));
94        *MAP.write().unwrap() = map;
95        *SIM.write().unwrap() = sim;
96    }
97
98    let addr = std::net::SocketAddr::from((args.ip, args.port));
99    info!("Listening on http://{}", addr);
100    let serve_future = Server::bind(&addr).serve(hyper::service::make_service_fn(|_| async {
101        Ok::<_, hyper::Error>(hyper::service::service_fn(serve_req))
102    }));
103    if let Err(err) = serve_future.await {
104        panic!("Server error: {}", err);
105    }
106}
107
108async fn serve_req(req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
109    let path = req.uri().path().to_string();
110    // Url::parse needs an absolute URL
111    let params: HashMap<String, String> =
112        url::Url::parse(&format!("http://localhost{}", req.uri()))
113            .unwrap()
114            .query_pairs()
115            .map(|(k, v)| (k.to_string(), v.to_string()))
116            .collect();
117    let body = hyper::body::to_bytes(req).await?.to_vec();
118    info!("Handling {}", path);
119    Ok(
120        match handle_command(
121            &path,
122            &params,
123            &body,
124            &mut SIM.write().unwrap(),
125            &mut MAP.write().unwrap(),
126            &mut LOAD.write().unwrap(),
127        ) {
128            Ok(resp) => Response::new(Body::from(resp)),
129            Err(err) => {
130                error!("{}: {}", path, err);
131                Response::builder()
132                    .status(StatusCode::BAD_REQUEST)
133                    .body(Body::from(format!("Bad command {}: {}", path, err)))
134                    .unwrap()
135            }
136        },
137    )
138}
139
140fn handle_command(
141    path: &str,
142    params: &HashMap<String, String>,
143    body: &[u8],
144    sim: &mut Sim,
145    map: &mut Map,
146    load: &mut LoadSim,
147) -> Result<String> {
148    let get = |key: &str| {
149        params
150            .get(key)
151            .ok_or_else(|| anyhow!("missing GET parameter {}", key))
152    };
153
154    match path {
155        // Controlling the simulation
156        "/sim/reset" => {
157            let (new_map, new_sim) = load.setup(&mut Timer::new("reset sim"));
158            *map = new_map;
159            *sim = new_sim;
160            Ok("sim reloaded".to_string())
161        }
162        "/sim/load" => {
163            let args: LoadSim = abstutil::from_json(body)?;
164
165            load.scenario = args.scenario;
166            load.modifiers = args.modifiers;
167            load.edits = args.edits;
168
169            // Also reset
170            let (new_map, new_sim) = load.setup(&mut Timer::new("reset sim"));
171            *map = new_map;
172            *sim = new_sim;
173
174            Ok("flags changed and sim reloaded".to_string())
175        }
176        "/sim/load-blank" => {
177            *map =
178                Map::load_synchronously(get("map")?.to_string(), &mut Timer::new("load new map"));
179            *sim = Sim::new(&map, SimOptions::default());
180            Ok("map changed, blank simulation".to_string())
181        }
182        "/sim/get-time" => Ok(sim.time().to_string()),
183        "/sim/goto-time" => {
184            let t = Time::parse(get("t")?)?;
185            if t <= sim.time() {
186                bail!("{} is in the past. call /sim/reset first?", t)
187            } else {
188                let dt = t - sim.time();
189                sim.timed_step(map, dt, &mut None, &mut Timer::new("goto-time"));
190                Ok(format!("it's now {}", t))
191            }
192        }
193        "/sim/new-person" => {
194            let input: ExternalPerson = abstutil::from_json(body)?;
195            for trip in &input.trips {
196                if trip.departure < sim.time() {
197                    bail!(
198                        "It's {} now, so you can't start a trip at {}",
199                        sim.time(),
200                        trip.departure
201                    )
202                }
203            }
204
205            let mut scenario = Scenario::empty(map, "one-shot");
206            scenario.people = ExternalPerson::import(map, vec![input], false)?;
207            let mut rng = XorShiftRng::seed_from_u64(load.rng_seed);
208            sim.instantiate(&scenario, map, &mut rng, &mut Timer::throwaway());
209            Ok(format!(
210                "{} created",
211                sim.get_all_people().last().unwrap().id
212            ))
213        }
214        // Traffic signals
215        "/traffic-signals/get" => {
216            let i = IntersectionID(get("id")?.parse::<usize>()?);
217            if let Some(ts) = map.maybe_get_traffic_signal(i) {
218                Ok(abstutil::to_json(ts))
219            } else {
220                bail!("{} isn't a traffic signal", i)
221            }
222        }
223        "/traffic-signals/set" => {
224            let ts: ControlTrafficSignal = abstutil::from_json(body)?;
225            let id = ts.id;
226
227            // incremental_edit_traffic_signal is the cheap option, but since we may need to call
228            // get-edits later, go through the proper flow.
229            let mut edits = map.get_edits().clone();
230            edits.commands.push(map.edit_intersection_cmd(id, |new| {
231                new.control = EditIntersectionControl::TrafficSignal(ts.export(map));
232            }));
233            map.must_apply_edits(edits, &mut Timer::throwaway());
234            map.recalculate_pathfinding_after_edits(&mut Timer::throwaway());
235
236            Ok(format!("{} has been updated", id))
237        }
238        "/traffic-signals/get-delays" => {
239            let i = map.get_i(IntersectionID(get("id")?.parse::<usize>()?));
240            let t1 = Time::parse(get("t1")?)?;
241            let t2 = Time::parse(get("t2")?)?;
242            if !i.is_traffic_signal() {
243                bail!("{} isn't a traffic signal", i.id);
244            }
245            let movements: Vec<&MovementID> = i.movements.keys().collect();
246
247            let mut delays = Delays {
248                per_direction: BTreeMap::new(),
249            };
250            for m in i.movements.keys() {
251                delays.per_direction.insert(*m, Vec::new());
252            }
253            if let Some(list) = sim.get_analytics().intersection_delays.get(&i.id) {
254                for (idx, t, dt, _) in list {
255                    if *t >= t1 && *t <= t2 {
256                        delays
257                            .per_direction
258                            .get_mut(movements[*idx as usize])
259                            .unwrap()
260                            .push(*dt);
261                    }
262                }
263            }
264            Ok(abstutil::to_json(&delays))
265        }
266        "/traffic-signals/get-cumulative-thruput" => {
267            let i = map.get_i(IntersectionID(get("id")?.parse::<usize>()?));
268            if !i.is_traffic_signal() {
269                bail!("{} isn't a traffic signal", i.id);
270            }
271
272            let mut thruput = Throughput {
273                per_direction: BTreeMap::new(),
274            };
275            for (idx, m) in i.movements.keys().enumerate() {
276                thruput.per_direction.insert(
277                    *m,
278                    sim.get_analytics()
279                        .traffic_signal_thruput
280                        .total_for(CompressedMovementID {
281                            i: i.id,
282                            idx: u8::try_from(idx).unwrap(),
283                        }),
284                );
285            }
286            Ok(abstutil::to_json(&thruput))
287        }
288        "/traffic-signals/get-all-current-state" => {
289            let mut all_state = BTreeMap::new();
290            for i in map.all_intersections() {
291                if !i.is_traffic_signal() {
292                    continue;
293                }
294                let (current_stage_idx, remaining_time) =
295                    sim.current_stage_and_remaining_time(i.id);
296                all_state.insert(
297                    i.id,
298                    TrafficSignalState {
299                        current_stage_idx,
300                        remaining_time,
301                        accepted: sim
302                            .get_accepted_agents(i.id)
303                            .into_iter()
304                            .map(|(a, _)| a)
305                            .collect(),
306                        waiting: sim.get_waiting_agents(i.id),
307                    },
308                );
309            }
310            Ok(abstutil::to_json(&all_state))
311        }
312        // Querying data
313        "/data/get-finished-trips" => {
314            let mut trips = Vec::new();
315            for (_, id, mode, maybe_duration) in &sim.get_analytics().finished_trips {
316                let distance_crossed = if maybe_duration.is_some() {
317                    sim.finished_trip_details(*id).unwrap().2
318                } else {
319                    Distance::ZERO
320                };
321                trips.push(FinishedTrip {
322                    id: *id,
323                    person: sim.trip_to_person(*id).unwrap(),
324                    duration: *maybe_duration,
325                    distance_crossed,
326                    mode: *mode,
327                });
328            }
329            Ok(abstutil::to_json(&trips))
330        }
331        "/data/get-agent-positions" => Ok(abstutil::to_json(&AgentPositions {
332            agents: sim
333                .get_unzoomed_agents(map)
334                .into_iter()
335                .chain(sim.get_unzoomed_transit_riders(map))
336                .map(|a| AgentPosition {
337                    id: a.id,
338                    trip: sim.agent_to_trip(a.id),
339                    person: a.person,
340                    vehicle_type: a.id.to_vehicle_type(),
341                    pos: a.pos.to_gps(map.get_gps_bounds()),
342                    distance_crossed: sim.agent_properties(map, a.id).dist_crossed,
343                })
344                .collect(),
345        })),
346        "/data/get-road-thruput" => Ok(abstutil::to_json(&RoadThroughput {
347            counts: sim
348                .get_analytics()
349                .road_thruput
350                .counts
351                .iter()
352                .map(|((r, a, hr), cnt)| (*r, *a, *hr, *cnt))
353                .collect(),
354        })),
355        "/data/get-blocked-by-graph" => Ok(abstutil::to_json(&BlockedByGraph {
356            blocked_by: sim
357                .get_blocked_by_graph(map)
358                .into_iter()
359                .map(|(id, (delay, cause))| {
360                    (
361                        id,
362                        (delay, cause, sim.agent_to_trip(id), sim.agent_to_person(id)),
363                    )
364                })
365                .collect(),
366        })),
367        "/data/trip-time-lower-bound" => {
368            let id = TripID(get("id")?.parse::<usize>()?);
369            let duration = sim.get_trip_time_lower_bound(map, id)?;
370            Ok(duration.inner_seconds().to_string())
371        }
372        "/data/all-trip-time-lower-bounds" => {
373            let results: BTreeMap<TripID, Duration> = Timer::throwaway()
374                .parallelize(
375                    "calculate all trip time lower bounds",
376                    sim.all_trip_info(),
377                    |(id, _)| {
378                        sim.get_trip_time_lower_bound(map, id)
379                            .ok()
380                            .map(|dt| (id, dt))
381                    },
382                )
383                .into_iter()
384                .flatten()
385                .collect();
386            Ok(abstutil::to_json(&results))
387        }
388        // Controlling the map
389        "/map/get-edits" => {
390            let mut edits = map.get_edits().clone();
391            edits.commands.clear();
392            edits.compress(map);
393            Ok(abstutil::to_json(&edits.to_permanent(map)))
394        }
395        "/map/get-edit-road-command" => {
396            let r = RoadID(get("id")?.parse::<usize>()?);
397            Ok(abstutil::to_json(
398                &map.edit_road_cmd(r, |_| {}).to_perma(map),
399            ))
400        }
401        "/map/get-intersection-geometry" => {
402            let i = IntersectionID(get("id")?.parse::<usize>()?);
403            Ok(abstutil::to_json(&export_geometry(map, i)))
404        }
405        "/map/get-all-geometry" => Ok(abstutil::to_json(&map.export_geometry())),
406        "/map/get-nearest-road" => {
407            let pt = LonLat::new(get("lon")?.parse::<f64>()?, get("lat")?.parse::<f64>()?);
408            let mut closest = FindClosest::new();
409            for r in map.all_roads() {
410                closest.add(r.id, r.center_pts.points());
411            }
412            let threshold = Distance::meters(get("threshold_meters")?.parse::<f64>()?);
413            match closest.closest_pt(pt.to_pt(map.get_gps_bounds()), threshold) {
414                Some((r, _)) => Ok(r.0.to_string()),
415                None => bail!("No road within {} of {}", threshold, pt),
416            }
417        }
418        _ => Err(anyhow!("Unknown command")),
419    }
420}
421
422// TODO I think specifying the API with protobufs or similar will be a better idea.
423
424#[derive(Serialize)]
425struct FinishedTrip {
426    id: TripID,
427    person: PersonID,
428    duration: Option<Duration>,
429    distance_crossed: Distance,
430    mode: TripMode,
431}
432
433#[derive(Serialize)]
434struct Delays {
435    #[serde(serialize_with = "serialize_btreemap")]
436    per_direction: BTreeMap<MovementID, Vec<Duration>>,
437}
438
439#[derive(Serialize)]
440struct Throughput {
441    #[serde(serialize_with = "serialize_btreemap")]
442    per_direction: BTreeMap<MovementID, usize>,
443}
444
445#[derive(Serialize)]
446struct AgentPositions {
447    agents: Vec<AgentPosition>,
448}
449
450#[derive(Serialize)]
451struct AgentPosition {
452    /// The agent's ID
453    id: AgentID,
454    /// None for buses
455    trip: Option<TripID>,
456    /// None for buses
457    person: Option<PersonID>,
458    /// None for pedestrians
459    vehicle_type: Option<VehicleType>,
460    /// The agent's current position. For pedestrians, this is their center. For vehicles, this
461    /// represents the front of the vehicle.
462    pos: LonLat,
463    /// The distance crossed so far by the agent, in meters. There are some caveats to this value:
464    /// - The distance along driveways between buildings/parking lots and the road doesn't count
465    ///   here.
466    /// - The distance only represents the current leg of the trip. If somebody walks to a car, the
467    ///   distance will reset when they begin driving, and also vehicle_type will change.
468    /// - No meaning for bus passengers currently.
469    /// - For buses and trains, the value will reset every time the vehicle reaches the next
470    ///   transit stop.
471    /// - At the very end of a driving trip, the agent may wind up crossing slightly more or less
472    ///   than the total path length, due to where they park along that last road.
473    distance_crossed: Distance,
474}
475
476#[derive(Serialize)]
477struct RoadThroughput {
478    // (road, agent type, hour since midnight, throughput for that one hour period)
479    counts: Vec<(RoadID, AgentType, usize, usize)>,
480}
481
482#[derive(Serialize)]
483struct TrafficSignalState {
484    current_stage_idx: usize,
485    remaining_time: Duration,
486    accepted: BTreeSet<AgentID>,
487    // Some agent has been waiting to start a turn since some time
488    waiting: Vec<(AgentID, TurnID, Time)>,
489}
490
491#[derive(Serialize)]
492struct BlockedByGraph {
493    /// Each entry indicates that some agent has been stuck in one place for some amount of time,
494    /// due to being blocked by another agent or because they're waiting at an intersection. Unless
495    /// the agent is a bus, then the TripID and PersonID will also be filled out.
496    #[serde(serialize_with = "serialize_btreemap")]
497    blocked_by: BTreeMap<AgentID, (Duration, DelayCause, Option<TripID>, Option<PersonID>)>,
498}
499
500#[derive(Deserialize)]
501struct LoadSim {
502    scenario: String,
503    modifiers: Vec<ScenarioModifier>,
504    edits: Option<PermanentMapEdits>,
505    // These are fixed from the initial command line flags
506    #[serde(skip_deserializing)]
507    rng_seed: u64,
508    #[serde(skip_deserializing)]
509    opts: SimOptions,
510}
511
512impl LoadSim {
513    fn setup(&self, timer: &mut Timer) -> (Map, Sim) {
514        let mut scenario: Scenario = abstio::must_read_object(self.scenario.clone(), timer);
515
516        let mut map = Map::load_synchronously(scenario.map_name.path(), timer);
517        if let Some(perma) = self.edits.clone() {
518            let edits = perma.into_edits(&map).unwrap();
519            map.must_apply_edits(edits, timer);
520            map.recalculate_pathfinding_after_edits(timer);
521        }
522
523        let mut rng = XorShiftRng::seed_from_u64(self.rng_seed);
524        for m in &self.modifiers {
525            scenario = m.apply(&map, scenario, &mut rng);
526        }
527
528        let mut sim = Sim::new(&map, self.opts.clone());
529        sim.instantiate(&scenario, &map, &mut rng, timer);
530
531        (map, sim)
532    }
533}
534
535fn export_geometry(map: &Map, i: IntersectionID) -> geojson::GeoJson {
536    let mut pairs = Vec::new();
537
538    let i = map.get_i(i);
539    // Translate all geometry to center around the intersection, with distances in meters.
540    let center = i.polygon.center();
541
542    // The intersection itself
543    let mut props = serde_json::Map::new();
544    props.insert("type".to_string(), "intersection".into());
545    props.insert("id".to_string(), i.orig_id.to_string().into());
546    pairs.push((
547        i.polygon
548            .translate(-center.x(), -center.y())
549            .get_outer_ring()
550            .to_geojson(None),
551        props,
552    ));
553
554    // Each connected road
555    for r in &i.roads {
556        let r = map.get_r(*r);
557        let mut props = serde_json::Map::new();
558        props.insert("type".to_string(), "road".into());
559        props.insert("id".to_string(), r.orig_id.osm_way_id.to_string().into());
560        pairs.push((
561            r.center_pts
562                .to_thick_ring(r.get_width())
563                .translate(-center.x(), -center.y())
564                .to_geojson(None),
565            props,
566        ));
567    }
568
569    geom::geometries_with_properties_to_geojson(pairs)
570}