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