rearranging some sim code into make/

This commit is contained in:
Dustin Carlino 2018-11-13 10:16:24 -08:00
parent ecae90eb8a
commit df5a151f8b
8 changed files with 355 additions and 338 deletions

View File

@ -1,161 +1,13 @@
use abstutil;
use control::ControlMap;
use driving::DrivingGoal;
use map_model::{BuildingID, BusRoute, BusStopID, LaneID, Map, RoadID};
use std::collections::{BTreeSet, VecDeque};
use walking::SidewalkSpot;
use {
BorderSpawnOverTime, CarID, Event, MapEdits, OriginDestination, PedestrianID, RouteID,
Scenario, SeedParkedCars, Sim, SpawnOverTime, Tick, WeightedUsizeChoice,
BorderSpawnOverTime, CarID, Event, OriginDestination, PedestrianID, RouteID, Scenario,
SeedParkedCars, Sim, SpawnOverTime, Tick, WeightedUsizeChoice,
};
#[derive(StructOpt, Debug, Clone)]
#[structopt(name = "sim_flags")]
pub struct SimFlags {
/// Map, scenario, or savestate to load
#[structopt(name = "load")]
pub load: String,
/// Optional RNG seed
#[structopt(long = "rng_seed")]
pub rng_seed: Option<u8>,
/// Run name for savestating
#[structopt(long = "run_name", default_value = "unnamed")]
pub run_name: String,
/// Name of map edits. Shouldn't be a full path or have the ".json"
#[structopt(long = "edits_name", default_value = "no_edits")]
pub edits_name: String,
}
impl SimFlags {
pub fn for_test(run_name: &str) -> SimFlags {
SimFlags {
load: "../data/raw_maps/montlake.abst".to_string(),
rng_seed: Some(42),
run_name: run_name.to_string(),
edits_name: "no_edits".to_string(),
}
}
}
// Convenience method to setup everything.
pub fn load(
flags: SimFlags,
savestate_every: Option<Tick>,
timer: &mut abstutil::Timer,
) -> (Map, ControlMap, Sim) {
if flags.load.contains("data/save/") {
assert_eq!(flags.edits_name, "no_edits");
info!("Resuming from {}", flags.load);
timer.start("read sim savestate");
let sim: Sim = abstutil::read_json(&flags.load).expect("loading sim state failed");
timer.stop("read sim savestate");
let edits: MapEdits = if sim.edits_name == "no_edits" {
MapEdits::new()
} else {
abstutil::read_json(&format!(
"../data/edits/{}/{}.json",
sim.map_name, sim.edits_name
)).unwrap()
};
// Try loading the pre-baked map first
let map: Map = abstutil::read_binary(
&format!("../data/maps/{}_{}.abst", sim.map_name, sim.edits_name),
timer,
).unwrap_or_else(|_| {
let map_path = format!("../data/raw_maps/{}.abst", sim.map_name);
Map::new(&map_path, edits.road_edits.clone(), timer)
.expect(&format!("Couldn't load map from {}", map_path))
});
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
(map, control_map, sim)
} else if flags.load.contains("data/scenarios/") {
info!("Seeding the simulation from scenario {}", flags.load);
let scenario: Scenario = abstutil::read_json(&flags.load).expect("loading scenario failed");
let edits = load_edits(&scenario.map_name, &flags);
// Try loading the pre-baked map first
let map: Map = abstutil::read_binary(
&format!(
"../data/maps/{}_{}.abst",
scenario.map_name, edits.edits_name
),
timer,
).unwrap_or_else(|_| {
let map_path = format!("../data/raw_maps/{}.abst", scenario.map_name);
Map::new(&map_path, edits.road_edits.clone(), timer)
.expect(&format!("Couldn't load map from {}", map_path))
});
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
let mut sim = Sim::new(
&map,
// TODO or the scenario name if no run name
flags.run_name,
flags.rng_seed,
savestate_every,
);
scenario.instantiate(&mut sim, &map);
(map, control_map, sim)
} else if flags.load.contains("data/raw_maps/") {
// TODO relative dir is brittle; match more cautiously
let map_name = flags
.load
.trim_left_matches("../data/raw_maps/")
.trim_right_matches(".abst")
.to_string();
info!("Loading map {}", flags.load);
let edits = load_edits(&map_name, &flags);
let map =
Map::new(&flags.load, edits.road_edits.clone(), timer).expect("Couldn't load map");
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
timer.start("create sim");
let sim = Sim::new(&map, flags.run_name, flags.rng_seed, savestate_every);
timer.stop("create sim");
(map, control_map, sim)
} else if flags.load.contains("data/maps/") {
assert_eq!(flags.edits_name, "no_edits");
info!("Loading map {}", flags.load);
let map: Map = abstutil::read_binary(&flags.load, timer).expect("Couldn't load map");
// TODO Bit sad to load edits to reconstitute ControlMap, but this is necessary right now
let edits: MapEdits = abstutil::read_json(&format!(
"../data/edits/{}/{}.json",
map.get_name(),
map.get_road_edits().edits_name
)).unwrap();
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
timer.start("create sim");
let sim = Sim::new(&map, flags.run_name, flags.rng_seed, savestate_every);
timer.stop("create sim");
(map, control_map, sim)
} else {
panic!("Don't know how to load {}", flags.load);
}
}
fn load_edits(map_name: &str, flags: &SimFlags) -> MapEdits {
if flags.edits_name == "no_edits" {
return MapEdits::new();
}
if flags.edits_name.contains("data/") || flags.edits_name.contains(".json") {
panic!(
"{} should just be a plain name, not a full path",
flags.edits_name
);
}
let edits: MapEdits = abstutil::read_json(&format!(
"../data/edits/{}/{}.json",
map_name, flags.edits_name
)).unwrap();
edits
}
// Helpers to run the sim
impl Sim {
// TODO share the helpers for spawning specific parking spots and stuff?

View File

@ -30,18 +30,16 @@ extern crate structopt;
#[macro_use]
mod macros;
mod a_b_test;
mod driving;
mod edits;
mod events;
mod helpers;
mod instrument;
mod intersections;
// TODO pub only for tests...
pub mod kinematics;
mod make;
mod parking;
mod router;
mod scenario;
mod sim;
mod spawn;
mod stats;
@ -50,20 +48,17 @@ mod trips;
mod view;
mod walking;
pub use a_b_test::{ABTest, ABTestResults};
use abstutil::Cloneable;
use dimensioned::si;
pub use edits::MapEdits;
pub use events::Event;
use geom::{Angle, Pt2D};
pub use helpers::{load, SimFlags};
pub use instrument::save_backtraces;
pub use make::{
load, ABTest, ABTestResults, BorderSpawnOverTime, MapEdits, Neighborhood, NeighborhoodBuilder,
OriginDestination, Scenario, SeedParkedCars, SimFlags, SpawnOverTime,
};
use map_model::{BuildingID, LaneID, Trace, TurnID};
use rand::{RngCore, SeedableRng, XorShiftRng};
pub use scenario::{
BorderSpawnOverTime, Neighborhood, NeighborhoodBuilder, OriginDestination, Scenario,
SeedParkedCars, SpawnOverTime,
};
pub use sim::{Benchmark, Sim, Summary};
pub use stats::SimStats;
use std::fmt;

151
sim/src/make/load.rs Normal file
View File

@ -0,0 +1,151 @@
use abstutil;
use control::ControlMap;
use map_model::Map;
use {MapEdits, Scenario, Sim, Tick};
#[derive(StructOpt, Debug, Clone)]
#[structopt(name = "sim_flags")]
pub struct SimFlags {
/// Map, scenario, or savestate to load
#[structopt(name = "load")]
pub load: String,
/// Optional RNG seed
#[structopt(long = "rng_seed")]
pub rng_seed: Option<u8>,
/// Run name for savestating
#[structopt(long = "run_name", default_value = "unnamed")]
pub run_name: String,
/// Name of map edits. Shouldn't be a full path or have the ".json"
#[structopt(long = "edits_name", default_value = "no_edits")]
pub edits_name: String,
}
impl SimFlags {
pub fn for_test(run_name: &str) -> SimFlags {
SimFlags {
load: "../data/raw_maps/montlake.abst".to_string(),
rng_seed: Some(42),
run_name: run_name.to_string(),
edits_name: "no_edits".to_string(),
}
}
}
// Convenience method to setup everything.
pub fn load(
flags: SimFlags,
savestate_every: Option<Tick>,
timer: &mut abstutil::Timer,
) -> (Map, ControlMap, Sim) {
if flags.load.contains("data/save/") {
assert_eq!(flags.edits_name, "no_edits");
info!("Resuming from {}", flags.load);
timer.start("read sim savestate");
let sim: Sim = abstutil::read_json(&flags.load).expect("loading sim state failed");
timer.stop("read sim savestate");
let edits: MapEdits = if sim.edits_name == "no_edits" {
MapEdits::new()
} else {
abstutil::read_json(&format!(
"../data/edits/{}/{}.json",
sim.map_name, sim.edits_name
)).unwrap()
};
// Try loading the pre-baked map first
let map: Map = abstutil::read_binary(
&format!("../data/maps/{}_{}.abst", sim.map_name, sim.edits_name),
timer,
).unwrap_or_else(|_| {
let map_path = format!("../data/raw_maps/{}.abst", sim.map_name);
Map::new(&map_path, edits.road_edits.clone(), timer)
.expect(&format!("Couldn't load map from {}", map_path))
});
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
(map, control_map, sim)
} else if flags.load.contains("data/scenarios/") {
info!("Seeding the simulation from scenario {}", flags.load);
let scenario: Scenario = abstutil::read_json(&flags.load).expect("loading scenario failed");
let edits = load_edits(&scenario.map_name, &flags);
// Try loading the pre-baked map first
let map: Map = abstutil::read_binary(
&format!(
"../data/maps/{}_{}.abst",
scenario.map_name, edits.edits_name
),
timer,
).unwrap_or_else(|_| {
let map_path = format!("../data/raw_maps/{}.abst", scenario.map_name);
Map::new(&map_path, edits.road_edits.clone(), timer)
.expect(&format!("Couldn't load map from {}", map_path))
});
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
let mut sim = Sim::new(
&map,
// TODO or the scenario name if no run name
flags.run_name,
flags.rng_seed,
savestate_every,
);
scenario.instantiate(&mut sim, &map);
(map, control_map, sim)
} else if flags.load.contains("data/raw_maps/") {
// TODO relative dir is brittle; match more cautiously
let map_name = flags
.load
.trim_left_matches("../data/raw_maps/")
.trim_right_matches(".abst")
.to_string();
info!("Loading map {}", flags.load);
let edits = load_edits(&map_name, &flags);
let map =
Map::new(&flags.load, edits.road_edits.clone(), timer).expect("Couldn't load map");
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
timer.start("create sim");
let sim = Sim::new(&map, flags.run_name, flags.rng_seed, savestate_every);
timer.stop("create sim");
(map, control_map, sim)
} else if flags.load.contains("data/maps/") {
assert_eq!(flags.edits_name, "no_edits");
info!("Loading map {}", flags.load);
let map: Map = abstutil::read_binary(&flags.load, timer).expect("Couldn't load map");
// TODO Bit sad to load edits to reconstitute ControlMap, but this is necessary right now
let edits: MapEdits = abstutil::read_json(&format!(
"../data/edits/{}/{}.json",
map.get_name(),
map.get_road_edits().edits_name
)).unwrap();
let control_map = ControlMap::new(&map, edits.stop_signs, edits.traffic_signals);
timer.start("create sim");
let sim = Sim::new(&map, flags.run_name, flags.rng_seed, savestate_every);
timer.stop("create sim");
(map, control_map, sim)
} else {
panic!("Don't know how to load {}", flags.load);
}
}
fn load_edits(map_name: &str, flags: &SimFlags) -> MapEdits {
if flags.edits_name == "no_edits" {
return MapEdits::new();
}
if flags.edits_name.contains("data/") || flags.edits_name.contains(".json") {
panic!(
"{} should just be a plain name, not a full path",
flags.edits_name
);
}
let edits: MapEdits = abstutil::read_json(&format!(
"../data/edits/{}/{}.json",
map_name, flags.edits_name
)).unwrap();
edits
}

16
sim/src/make/mod.rs Normal file
View File

@ -0,0 +1,16 @@
// This roughly contains code to specify and instantiate a simulation, not the mechanics of running
// it.
mod a_b_test;
mod edits;
mod load;
mod neighborhood;
mod scenario;
pub use self::a_b_test::{ABTest, ABTestResults};
pub use self::edits::MapEdits;
pub use self::load::{load, SimFlags};
pub use self::neighborhood::{Neighborhood, NeighborhoodBuilder};
pub use self::scenario::{
BorderSpawnOverTime, OriginDestination, Scenario, SeedParkedCars, SpawnOverTime,
};

View File

@ -0,0 +1,117 @@
use abstutil;
use geom::{GPSBounds, LonLat, Polygon, Pt2D};
use map_model::{BuildingID, Map, RoadID};
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{Error, Write};
// This form is used by the editor plugin to edit and for serialization. Storing points in GPS is
// more compatible with slight changes to the bounding box of a map over time.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct NeighborhoodBuilder {
pub map_name: String,
pub name: String,
pub points: Vec<LonLat>,
}
impl NeighborhoodBuilder {
pub fn finalize(&self, gps_bounds: &GPSBounds) -> Neighborhood {
assert!(self.points.len() >= 3);
Neighborhood {
map_name: self.map_name.clone(),
name: self.name.clone(),
polygon: Polygon::new(
&self
.points
.iter()
.map(|pt| {
Pt2D::from_gps(*pt, gps_bounds)
.expect(&format!("Polygon {} has bad pt {}", self.name, pt))
}).collect(),
),
}
}
pub fn save(&self) {
abstutil::save_object("neighborhoods", &self.map_name, &self.name, self);
}
// https://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format
pub fn save_as_osmosis(&self) -> Result<(), Error> {
let path = format!("../data/polygons/{}.poly", self.name);
let mut f = File::create(&path)?;
write!(f, "{}\n", self.name);
write!(f, "1\n");
for gps in &self.points {
write!(f, " {} {}\n", gps.longitude, gps.latitude);
}
// Have to repeat the first point
{
write!(
f,
" {} {}\n",
self.points[0].longitude, self.points[0].latitude
);
}
write!(f, "END\n");
write!(f, "END\n");
println!("Exported {}", path);
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct Neighborhood {
pub map_name: String,
pub name: String,
pub polygon: Polygon,
}
impl Neighborhood {
pub fn load_all(map_name: &str, gps_bounds: &GPSBounds) -> Vec<(String, Neighborhood)> {
abstutil::load_all_objects::<NeighborhoodBuilder>("neighborhoods", map_name)
.into_iter()
.map(|(name, builder)| (name, builder.finalize(gps_bounds)))
.collect()
}
// TODO This should use quadtrees and/or not just match the center of each building.
pub fn find_matching_buildings(&self, map: &Map) -> Vec<BuildingID> {
let mut results: Vec<BuildingID> = Vec::new();
for b in map.all_buildings() {
if self.polygon.contains_pt(Pt2D::center(&b.points)) {
results.push(b.id);
}
}
results
}
// TODO This should use quadtrees and/or not just match one point of each road.
pub fn find_matching_roads(&self, map: &Map) -> BTreeSet<RoadID> {
let mut results: BTreeSet<RoadID> = BTreeSet::new();
for r in map.all_roads() {
if self.polygon.contains_pt(r.center_pts.first_pt()) {
results.insert(r.id);
}
}
results
}
pub fn make_everywhere(map: &Map) -> Neighborhood {
let bounds = map.get_bounds();
Neighborhood {
map_name: map.get_name().to_string(),
name: "_everywhere_".to_string(),
polygon: Polygon::new(&vec![
Pt2D::new(0.0, 0.0),
Pt2D::new(bounds.max_x, 0.0),
Pt2D::new(bounds.max_x, bounds.max_y),
Pt2D::new(0.0, bounds.max_y),
Pt2D::new(0.0, 0.0),
]),
}
}
}

View File

@ -1,14 +1,11 @@
use abstutil;
use driving::DrivingGoal;
use geom::{GPSBounds, LonLat, Polygon, Pt2D};
use map_model::{BuildingID, IntersectionID, LaneType, Map, RoadID};
use rand::Rng;
use rand::XorShiftRng;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::fs::File;
use std::io::{Error, Write};
use walking::SidewalkSpot;
use {CarID, Sim, Tick, WeightedUsizeChoice};
use {CarID, Neighborhood, Sim, Tick, WeightedUsizeChoice};
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Scenario {
@ -20,69 +17,6 @@ pub struct Scenario {
pub border_spawn_over_time: Vec<BorderSpawnOverTime>,
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum OriginDestination {
Neighborhood(String),
// TODO A serialized Scenario won't last well as the map changes...
Border(IntersectionID),
}
impl OriginDestination {
fn pick_driving_goal(
&self,
map: &Map,
bldgs_per_neighborhood: &HashMap<String, Vec<BuildingID>>,
rng: &mut XorShiftRng,
) -> Option<DrivingGoal> {
match self {
OriginDestination::Neighborhood(ref n) => {
if let Some(bldgs) = bldgs_per_neighborhood.get(n) {
Some(DrivingGoal::ParkNear(*rng.choose(bldgs).unwrap()))
} else {
panic!("Neighborhood {} isn't defined", n);
}
}
OriginDestination::Border(i) => {
let lanes = map.get_i(*i).get_incoming_lanes(map, LaneType::Driving);
if lanes.is_empty() {
warn!(
"Can't spawn a car ending at border {}; no driving lane there",
i
);
None
} else {
// TODO ideally could use any
Some(DrivingGoal::Border(*i, lanes[0]))
}
}
}
}
fn pick_walking_goal(
&self,
map: &Map,
bldgs_per_neighborhood: &HashMap<String, Vec<BuildingID>>,
rng: &mut XorShiftRng,
) -> Option<SidewalkSpot> {
match self {
OriginDestination::Neighborhood(ref n) => {
if let Some(bldgs) = bldgs_per_neighborhood.get(n) {
Some(SidewalkSpot::building(*rng.choose(bldgs).unwrap(), map))
} else {
panic!("Neighborhood {} isn't defined", n);
}
}
OriginDestination::Border(i) => {
let goal = SidewalkSpot::end_at_border(*i, map);
if goal.is_none() {
warn!("Can't end_at_border for {} without a sidewalk", i);
}
goal
}
}
}
}
// SpawnOverTime and BorderSpawnOverTime should be kept separate. Agents in SpawnOverTime pick
// their mode (use a car, walk, bus) based on the situation. When spawning directly a border,
// agents have to start as a car or pedestrian already.
@ -114,117 +48,6 @@ pub struct SeedParkedCars {
pub cars_per_building: WeightedUsizeChoice,
}
// This form is used by the editor plugin to edit and for serialization. Storing points in GPS is
// more compatible with slight changes to the bounding box of a map over time.
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct NeighborhoodBuilder {
pub map_name: String,
pub name: String,
pub points: Vec<LonLat>,
}
impl NeighborhoodBuilder {
pub fn finalize(&self, gps_bounds: &GPSBounds) -> Neighborhood {
assert!(self.points.len() >= 3);
Neighborhood {
map_name: self.map_name.clone(),
name: self.name.clone(),
polygon: Polygon::new(
&self
.points
.iter()
.map(|pt| {
Pt2D::from_gps(*pt, gps_bounds)
.expect(&format!("Polygon {} has bad pt {}", self.name, pt))
}).collect(),
),
}
}
pub fn save(&self) {
abstutil::save_object("neighborhoods", &self.map_name, &self.name, self);
}
// https://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format
pub fn save_as_osmosis(&self) -> Result<(), Error> {
let path = format!("../data/polygons/{}.poly", self.name);
let mut f = File::create(&path)?;
write!(f, "{}\n", self.name);
write!(f, "1\n");
for gps in &self.points {
write!(f, " {} {}\n", gps.longitude, gps.latitude);
}
// Have to repeat the first point
{
write!(
f,
" {} {}\n",
self.points[0].longitude, self.points[0].latitude
);
}
write!(f, "END\n");
write!(f, "END\n");
println!("Exported {}", path);
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct Neighborhood {
pub map_name: String,
pub name: String,
pub polygon: Polygon,
}
impl Neighborhood {
pub fn load_all(map_name: &str, gps_bounds: &GPSBounds) -> Vec<(String, Neighborhood)> {
abstutil::load_all_objects::<NeighborhoodBuilder>("neighborhoods", map_name)
.into_iter()
.map(|(name, builder)| (name, builder.finalize(gps_bounds)))
.collect()
}
// TODO This should use quadtrees and/or not just match the center of each building.
fn find_matching_buildings(&self, map: &Map) -> Vec<BuildingID> {
let mut results: Vec<BuildingID> = Vec::new();
for b in map.all_buildings() {
if self.polygon.contains_pt(Pt2D::center(&b.points)) {
results.push(b.id);
}
}
results
}
// TODO This should use quadtrees and/or not just match one point of each road.
fn find_matching_roads(&self, map: &Map) -> BTreeSet<RoadID> {
let mut results: BTreeSet<RoadID> = BTreeSet::new();
for r in map.all_roads() {
if self.polygon.contains_pt(r.center_pts.first_pt()) {
results.insert(r.id);
}
}
results
}
fn make_everywhere(map: &Map) -> Neighborhood {
let bounds = map.get_bounds();
Neighborhood {
map_name: map.get_name().to_string(),
name: "_everywhere_".to_string(),
polygon: Polygon::new(&vec![
Pt2D::new(0.0, 0.0),
Pt2D::new(bounds.max_x, 0.0),
Pt2D::new(bounds.max_x, bounds.max_y),
Pt2D::new(0.0, bounds.max_y),
Pt2D::new(0.0, 0.0),
]),
}
}
}
impl Scenario {
pub fn describe(&self) -> Vec<String> {
abstutil::to_json(self)
@ -379,3 +202,66 @@ impl Scenario {
abstutil::save_object("scenarios", &self.map_name, &self.scenario_name, self);
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
pub enum OriginDestination {
Neighborhood(String),
// TODO A serialized Scenario won't last well as the map changes...
Border(IntersectionID),
}
impl OriginDestination {
fn pick_driving_goal(
&self,
map: &Map,
bldgs_per_neighborhood: &HashMap<String, Vec<BuildingID>>,
rng: &mut XorShiftRng,
) -> Option<DrivingGoal> {
match self {
OriginDestination::Neighborhood(ref n) => {
if let Some(bldgs) = bldgs_per_neighborhood.get(n) {
Some(DrivingGoal::ParkNear(*rng.choose(bldgs).unwrap()))
} else {
panic!("Neighborhood {} isn't defined", n);
}
}
OriginDestination::Border(i) => {
let lanes = map.get_i(*i).get_incoming_lanes(map, LaneType::Driving);
if lanes.is_empty() {
warn!(
"Can't spawn a car ending at border {}; no driving lane there",
i
);
None
} else {
// TODO ideally could use any
Some(DrivingGoal::Border(*i, lanes[0]))
}
}
}
}
fn pick_walking_goal(
&self,
map: &Map,
bldgs_per_neighborhood: &HashMap<String, Vec<BuildingID>>,
rng: &mut XorShiftRng,
) -> Option<SidewalkSpot> {
match self {
OriginDestination::Neighborhood(ref n) => {
if let Some(bldgs) = bldgs_per_neighborhood.get(n) {
Some(SidewalkSpot::building(*rng.choose(bldgs).unwrap(), map))
} else {
panic!("Neighborhood {} isn't defined", n);
}
}
OriginDestination::Border(i) => {
let goal = SidewalkSpot::end_at_border(*i, map);
if goal.is_none() {
warn!("Can't end_at_border for {} without a sidewalk", i);
}
goal
}
}
}
}