importer/soundcast/
popdat.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use serde::{Deserialize, Serialize};
4
5use abstio::{CityName, FileWithProgress};
6use abstutil::{prettyprint_usize, Counter, Timer};
7use geom::{Distance, Duration, LonLat, Time};
8use kml::{ExtraShape, ExtraShapes};
9use map_model::{osm, Map};
10use synthpop::{OrigPersonID, TripMode, TripPurpose};
11
12#[derive(Serialize, Deserialize)]
13pub struct PopDat {
14    pub trips: Vec<OrigTrip>,
15}
16
17// Extract trip demand data from PSRC's Soundcast outputs.
18pub fn import_data(huge_map: &Map, timer: &mut Timer) -> PopDat {
19    let trips = import_trips(huge_map, timer);
20    let popdat = PopDat { trips };
21    abstio::write_binary(abstio::path_popdat(), &popdat);
22    popdat
23}
24
25fn import_trips(huge_map: &Map, timer: &mut Timer) -> Vec<OrigTrip> {
26    let (parcels, mut keyed_shapes) = import_parcels(huge_map, timer);
27
28    let mut trips = Vec::new();
29    let (reader, done) =
30        FileWithProgress::new(&CityName::seattle().input_path("trips_2014.csv")).unwrap();
31    let mut total_records = 0;
32    let mut people: HashSet<OrigPersonID> = HashSet::new();
33    let mut trips_from_parcel: Counter<usize> = Counter::new();
34    let mut trips_to_parcel: Counter<usize> = Counter::new();
35
36    for rec in csv::Reader::from_reader(reader).deserialize() {
37        total_records += 1;
38        let rec: RawTrip = rec.unwrap();
39
40        let from = parcels[&(rec.opcl as usize)].clone();
41        let to = parcels[&(rec.dpcl as usize)].clone();
42        trips_from_parcel.inc(from.parcel_id);
43        trips_to_parcel.inc(to.parcel_id);
44
45        // If both are None, then skip -- the trip doesn't start or end within huge_seattle.
46        // If both are the same building, also skip -- that's a redundant trip.
47        if from.osm_building == to.osm_building {
48            if from.osm_building.is_some() {
49                /*warn!(
50                    "Skipping trip from parcel {} to {}; both match OSM building {:?}",
51                    rec.opcl, rec.dpcl, from.osm_building
52                );*/
53            }
54            continue;
55        }
56
57        let depart_at = Time::START_OF_DAY + Duration::minutes(rec.deptm as usize);
58
59        let mode = get_mode(&rec.mode);
60        let purpose = get_purpose(&rec.dpurp);
61
62        let trip_time = Duration::f64_minutes(rec.travtime);
63        let trip_dist = Distance::miles(rec.travdist);
64
65        let person = OrigPersonID(rec.hhno as usize, rec.pno as usize);
66        people.insert(person);
67        #[allow(clippy::float_cmp)]
68        let seq = (rec.tour as usize, rec.half == 2.0, rec.tseg as usize);
69
70        trips.push(OrigTrip {
71            from,
72            to,
73            depart_at,
74            mode,
75            person,
76            seq,
77            purpose,
78            trip_time,
79            trip_dist,
80        });
81    }
82    done(timer);
83
84    info!(
85        "{} trips total, over {} people. {} records filtered out",
86        prettyprint_usize(trips.len()),
87        prettyprint_usize(people.len()),
88        prettyprint_usize(total_records - trips.len())
89    );
90
91    trips.sort_by_key(|t| t.depart_at);
92
93    // Dump debug info about parcels. ALL trips are counted here, but parcels are filtered.
94    for (id, cnt) in trips_from_parcel.consume() {
95        if let Some(ref mut es) = keyed_shapes.get_mut(&id) {
96            es.attributes
97                .insert("trips_from".to_string(), cnt.to_string());
98        }
99    }
100    for (id, cnt) in trips_to_parcel.consume() {
101        if let Some(ref mut es) = keyed_shapes.get_mut(&id) {
102            es.attributes
103                .insert("trips_to".to_string(), cnt.to_string());
104        }
105    }
106    let shapes: Vec<ExtraShape> = keyed_shapes.into_iter().map(|(_, v)| v).collect();
107    abstio::write_binary(
108        CityName::seattle().input_path("parcels.bin"),
109        &ExtraShapes { shapes },
110    );
111
112    trips
113}
114
115// TODO Do we also need the zone ID, or is parcel ID globally unique?
116// Keyed by parcel ID
117#[cfg(feature = "scenarios")]
118fn import_parcels(
119    huge_map: &Map,
120    timer: &mut Timer,
121) -> (HashMap<usize, Endpoint>, BTreeMap<usize, ExtraShape>) {
122    use geom::FindClosest;
123
124    // TODO I really just want to do polygon containment with a quadtree. FindClosest only does
125    // line-string stuff right now, which'll be weird for the last->first pt line and stuff.
126    let mut closest_bldg: FindClosest<osm::OsmID> = FindClosest::new();
127    for b in huge_map.all_buildings() {
128        closest_bldg.add_polygon(b.orig_id, &b.polygon);
129    }
130
131    let mut x_coords: Vec<f64> = Vec::new();
132    let mut y_coords: Vec<f64> = Vec::new();
133    // Dummy values
134    let mut z_coords: Vec<f64> = Vec::new();
135    // (parcel ID, number of households, number of parking spots)
136    let mut parcel_metadata = Vec::new();
137
138    let (reader, done) =
139        FileWithProgress::new(&CityName::seattle().input_path("parcels_urbansim.txt")).unwrap();
140    for rec in csv::ReaderBuilder::new()
141        .delimiter(b' ')
142        .from_reader(reader)
143        .deserialize()
144    {
145        let rec: RawParcel = rec.unwrap();
146        // Note parkdy_p and parkhr_p might overlap, so this could be double-counting. >_<
147        parcel_metadata.push((rec.parcelid, rec.hh_p, rec.parkdy_p + rec.parkhr_p));
148        x_coords.push(rec.xcoord_p);
149        y_coords.push(rec.ycoord_p);
150        z_coords.push(0.0);
151    }
152    done(timer);
153
154    timer.start(format!("transform {} points", parcel_metadata.len()));
155
156    // From https://epsg.io/102748 to https://epsg.io/4326
157    let transform = gdal::spatial_ref::CoordTransform::new(
158        &gdal::spatial_ref::SpatialRef::from_proj4(
159            "+proj=lcc +lat_1=47.5 +lat_2=48.73333333333333 +lat_0=47 +lon_0=-120.8333333333333 \
160             +x_0=500000.0000000002 +y_0=0 +datum=NAD83 +units=us-ft +no_defs",
161        )
162        .expect("washington state plane"),
163        &gdal::spatial_ref::SpatialRef::from_epsg(4326).unwrap(),
164    )
165    .expect("regular GPS");
166    transform
167        .transform_coords(&mut x_coords, &mut y_coords, &mut z_coords)
168        .expect("transform coords");
169
170    timer.stop(format!("transform {} points", parcel_metadata.len()));
171
172    let bounds = huge_map.get_gps_bounds();
173    let boundary = huge_map.get_boundary_polygon();
174    let mut result = HashMap::new();
175    let mut shapes = BTreeMap::new();
176    timer.start_iter("finalize parcel output", parcel_metadata.len());
177    for ((x, y), (id, num_households, offstreet_parking_spaces)) in x_coords
178        .into_iter()
179        .zip(y_coords.into_iter())
180        .zip(parcel_metadata.into_iter())
181    {
182        timer.next();
183        // This maybe got inverted with some new version of GDAL?!
184        let gps = LonLat::new(y, x);
185        let pt = gps.to_pt(bounds);
186        let osm_building = if bounds.contains(gps) {
187            closest_bldg
188                .closest_pt(pt, Distance::meters(30.0))
189                .map(|(b, _)| b)
190        } else {
191            None
192        };
193        result.insert(
194            id,
195            Endpoint {
196                pos: gps,
197                osm_building,
198                parcel_id: id,
199            },
200        );
201
202        if boundary.contains_pt(pt) {
203            let mut attributes = BTreeMap::new();
204            attributes.insert("id".to_string(), id.to_string());
205            if num_households > 0 {
206                attributes.insert("households".to_string(), num_households.to_string());
207            }
208            if offstreet_parking_spaces > 0 {
209                attributes.insert("parking".to_string(), offstreet_parking_spaces.to_string());
210            }
211            if let Some(b) = osm_building {
212                attributes.insert("osm_bldg".to_string(), b.inner_id().to_string());
213            }
214            shapes.insert(
215                id,
216                ExtraShape {
217                    points: vec![gps],
218                    attributes,
219                },
220            );
221        }
222    }
223    info!("{} parcels", prettyprint_usize(result.len()));
224
225    (result, shapes)
226}
227
228#[cfg(not(feature = "scenarios"))]
229fn import_parcels(
230    _: &Map,
231    _: &mut Timer,
232) -> (HashMap<usize, Endpoint>, BTreeMap<usize, ExtraShape>) {
233    panic!("Can't import_parcels for popdat.bin without the scenarios feature (GDAL dependency)");
234}
235
236// From https://github.com/psrc/soundcast/wiki/Outputs#trip-file-_triptsv, dpurp
237fn get_purpose(code: &str) -> TripPurpose {
238    match code {
239        "0.0" => TripPurpose::Home,
240        "1.0" => TripPurpose::Work,
241        "2.0" => TripPurpose::School,
242        "3.0" => TripPurpose::Escort,
243        "4.0" => TripPurpose::PersonalBusiness,
244        "5.0" => TripPurpose::Shopping,
245        "6.0" => TripPurpose::Meal,
246        "7.0" => TripPurpose::Social,
247        "8.0" => TripPurpose::Recreation,
248        "9.0" => TripPurpose::Medical,
249        "10.0" => TripPurpose::ParkAndRideTransfer,
250        _ => panic!("Unknown dpurp {}", code),
251    }
252}
253
254// From https://github.com/psrc/soundcast/wiki/Outputs#trip-file-_triptsv, mode
255fn get_mode(code: &str) -> TripMode {
256    match code {
257        "1.0" => TripMode::Walk,
258        "2.0" => TripMode::Bike,
259        "3.0" | "4.0" | "5.0" => TripMode::Drive,
260        // TODO Park-and-ride and school bus as walk-to-transit is a little weird.
261        "6.0" | "7.0" | "8.0" => TripMode::Transit,
262        // TODO Invalid code, what's this one mean? I only see a few examples, so just default to
263        // walking.
264        "0.0" => TripMode::Walk,
265        _ => panic!("Unknown mode {}", code),
266    }
267}
268
269// See https://github.com/psrc/soundcast/wiki/Outputs#trip-file-_triptsv
270//
271// A/B Street flattens a person's trips into a simple list, but the Soundcast model is more
272// detailed:
273//
274// A person takes 1+ tours a day. Each tour starts and ends at the same place (usually home) and
275// has some primary destination. A tour has two legs (to the destination, then returning from it),
276// each split into individual trips.
277//
278// An example: someone takes the bus to work, but stops for a coffee and walks the final bit to
279// work. Then later they bus home. This would be encoded like so:
280//
281// - Tour 1 (purpose work), leg = to destination, trip 1 (purpose eat, using transit)
282// - Tour 1 (purpose work), leg = to destination, trip 2 (purpose work, walking)
283// - Tour 1 (purpose work), leg = return from destination, trip 1 (purpose home, using transit)
284#[derive(Debug, Deserialize)]
285struct RawTrip {
286    opcl: f64,
287    dpcl: f64,
288    deptm: f64,
289    mode: String,
290    dpurp: String,
291    travtime: f64,
292    travdist: f64,
293    hhno: f64,
294    pno: f64,
295    tour: f64,
296    half: f64,
297    tseg: f64,
298}
299
300// See https://github.com/psrc/soundcast/wiki/Outputs#buffered-parcel-file-buffered_parcelsdat
301#[derive(Debug, Deserialize)]
302// When the 'scenarios' feature is disabled, these fields look unused
303#[allow(unused)]
304struct RawParcel {
305    parcelid: usize,
306    hh_p: usize,
307    parkdy_p: usize,
308    parkhr_p: usize,
309    xcoord_p: f64,
310    ycoord_p: f64,
311}
312
313#[derive(Clone, Debug, Serialize, Deserialize)]
314pub struct OrigTrip {
315    pub from: Endpoint,
316    pub to: Endpoint,
317    pub depart_at: Time,
318    pub mode: TripMode,
319
320    // (household, person within household)
321    pub person: OrigPersonID,
322    // (tour, false is to destination and true is back from dst, trip within half-tour)
323    pub seq: (usize, bool, usize),
324    // Purpose at the destination of this trip, not the entire tour.
325    pub purpose: TripPurpose,
326    pub trip_time: Duration,
327    pub trip_dist: Distance,
328}
329
330#[derive(Clone, Debug, Serialize, Deserialize)]
331pub struct Endpoint {
332    pub pos: LonLat,
333    pub osm_building: Option<osm::OsmID>,
334    pub parcel_id: usize,
335}