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