Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

census areas import #425

Merged
merged 18 commits into from
Dec 15, 2020
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
544 changes: 297 additions & 247 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ members = [
# update dependencies often).
[profile.dev.package."*"]
opt-level = 3

[patch.crates-io]
geojson = { git = "https://github.com/georust/geojson", branch = "mkirk/try_from" }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general I think it's a little nicer to patch things here at the top level rather than updating across all the individual crates. It's maybe a little more convenient, but more importantly, it works for transitive deps too, which you can't conveniently edit.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. I always forget about this trick

4 changes: 2 additions & 2 deletions fifteen_min/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ default = ["map_gui/native", "widgetry/native-backend"]

[dependencies]
abstutil = { path = "../abstutil" }
contour = { git = "https://github.com/dabreegster/contour-rs" }
geojson = "0.20.1"
contour = "0.3.0"
geojson = "0.21.0"
geom = { path = "../geom" }
log = "0.4"
map_gui = { path = "../map_gui" }
Expand Down
4 changes: 2 additions & 2 deletions game/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ built = { version = "0.4.3", optional = true, features=["chrono"] }
chrono = "0.4.15"
collisions = { path = "../collisions" }
colorous = "1.0.3"
contour = { git = "https://github.com/dabreegster/contour-rs" }
contour = "0.3.0"
downcast-rs = "1.2.0"
enumset = "1.0.1"
geojson = "0.20.1"
geojson = "0.21.0"
geom = { path = "../geom" }
instant = "0.1.7"
kml = { path = "../kml" }
Expand Down
3 changes: 2 additions & 1 deletion game/src/sandbox/gameplay/census.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ impl State<App> for CensusGenerator {
config,
&app.primary.map,
&mut app.primary.current_flags.sim_flags.make_rng(),
);
)
.expect("failed to generate scenario");
// TODO Do something with it -- save it, launch it in sandboxmode, display some
// stats about it?
return Transition::Pop;
Expand Down
2 changes: 1 addition & 1 deletion geom/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ abstutil = { path = "../abstutil" }
earcutr = { git = "https://github.com/donbright/earcutr" }
geo = "0.15.0"
geo-booleanop = "= 0.3.2"
geojson = "0.20.1"
geojson = "0.21.0"
histogram = "0.6.9"
instant = "0.1.7"
ordered-float = { version = "2.0.0", features=["serde"] }
Expand Down
39 changes: 29 additions & 10 deletions geom/src/polygon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ impl Polygon {

pub fn convex_hull(list: Vec<Polygon>) -> Polygon {
let mp: geo::MultiPolygon<f64> = list.into_iter().map(|p| to_geo(p.points())).collect();
from_geo(mp.convex_hull())
mp.convex_hull().into()
}

pub fn polylabel(&self) -> Pt2D {
Expand Down Expand Up @@ -476,19 +476,38 @@ fn to_geo(pts: &Vec<Pt2D>) -> geo::Polygon<f64> {
)
}

fn from_geo(p: geo::Polygon<f64>) -> Polygon {
Polygon::buggy_new(
p.into_inner()
.0
.into_points()
impl From<geo::Polygon<f64>> for Polygon {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you ok with adding the idiomatic conversation traits? It's a little easier to discover this way since I know where to look rather than custom named methods.

I could add more for other geo types, but only added the minimum of what we needed for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, these're better! I probably wrote the old thing before I understood From / Into

fn from(poly: geo::Polygon<f64>) -> Self {
Polygon::buggy_new(
poly.into_inner()
.0
.into_points()
.into_iter()
.map(|pt| Pt2D::new(pt.x(), pt.y()))
.collect(),
)
}
}

impl From<Polygon> for geo::Polygon<f64> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woo, thanks for fixing this! When I originally wrote this, I didn't know about earcutr for triangulating holes, and I think I confused multipolygons / polygons with inner portions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated these From implementations now that I fully understand what ABStreet geom::Polygon's expect.

fn from(poly: Polygon) -> Self {
let exterior_coords = poly
.points
.into_iter()
.map(|pt| Pt2D::new(pt.x(), pt.y()))
.collect(),
)
.map(geo::Coordinate::from)
.collect::<Vec<_>>();
let exterior = geo::LineString(exterior_coords);

let interiors: Vec<geo::LineString<f64>> = poly
.rings
.map(|rings| rings.into_iter().map(geo::LineString::from).collect())
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of assumed that rings were interior only and that geom::Polygon.points was the exterior - but after looking at the docs more, I'm not clear.

Maybe if rings is set then rings.0 is the exterior? Is that right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polygon only has half support for holes... preserving the original rings (both exterior and interior) is nice for generating outlines later. Places calling buggy_new or precomputed (notably PolyLine::make_polygons) don't fill out the rings.

If rings is set, then [0] is the exterior, correct. points contain every point in both the exterior and interior, with indices defining the triangles.

I wonder if it would be simpler and maybe more performant to just make Polygon store an exterior Ring and some interior Rings, and delay triangulation entirely until it's needed for rendering. contains_pt also uses the triangles now, but there might be other algorithms for figuring that out without triangulating. Maybe geom::Polygon could actually just be a zero-cost wrapper around geo::Polygon! Disclaimer, I haven't thought through these ideas.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your thoughts.

I think it'd be nice to be uniform about where the exterior lives. either always polygon.rings[0] or as a new polygon.exterior.

Maybe geom::Polygon could actually just be a zero-cost wrapper around geo::Polygon
I am in theory on board with this!
I wonder if it would be simpler and maybe more performant to just make Polygon store an exterior Ring and some interior Rings, and delay triangulation entirely until it's needed for rendering

I haven't spent time surveying the existing usage of geom::polygon - but maybe it makes sense to have separate entities for "polygon" vs. "polygon that I'm going to draw"

I'm not intending to change anything drastically in this PR, but I'll try to keep it in mind as I work with more of the existing geometry code in ABStreet.

.unwrap_or(Vec::new());
Self::new(exterior, interiors)
}
}

fn from_multi(multi: geo::MultiPolygon<f64>) -> Vec<Polygon> {
multi.into_iter().map(from_geo).collect()
multi.into_iter().map(Polygon::from).collect()
}

fn downsize(input: Vec<usize>) -> Vec<u16> {
Expand Down
9 changes: 9 additions & 0 deletions geom/src/pt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,12 @@ impl HashablePt2D {
Pt2D::new(self.x_nan.into_inner(), self.y_nan.into_inner())
}
}

impl From<Pt2D> for geo::Coordinate<f64> {
fn from(pt: Pt2D) -> Self {
geo::Coordinate {
x: pt.inner_x,
y: pt.inner_y,
}
}
}
11 changes: 11 additions & 0 deletions geom/src/ring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,14 @@ impl fmt::Display for Ring {
write!(f, "])")
}
}

impl From<Ring> for geo::LineString<f64> {
fn from(ring: Ring) -> Self {
let coords = ring
.pts
.into_iter()
.map(geo::Coordinate::from)
.collect::<Vec<_>>();
Self(coords)
}
}
2 changes: 1 addition & 1 deletion headless/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ edition = "2018"

[dependencies]
abstutil = { path = "../abstutil" }
geojson = "0.20.1"
geojson = "0.21.0"
geom = { path = "../geom" }
hyper = "0.13.9"
lazy_static = "1.4.0"
Expand Down
2 changes: 1 addition & 1 deletion importer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ abstutil = { path = "../abstutil" }
collisions = { path = "../collisions" }
convert_osm = { path = "../convert_osm" }
csv = "1.1.4"
geojson = "0.20.1"
geojson = "0.21.0"
geom = { path = "../geom" }
gdal = { version = "0.6.0", optional = true }
kml = { path = "../kml" }
Expand Down
4 changes: 2 additions & 2 deletions map_gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ release_s3 = []
aabb-quadtree = "0.1.0"
abstutil = { path = "../abstutil" }
colorous = "1.0.3"
contour = { git = "https://github.com/dabreegster/contour-rs" }
contour = "0.3.0"
flate2 = "1.0.19"
futures = { version = "0.3.8", optional = true }
futures-channel = { version = "0.3.8", optional = true }
geojson = "0.20.1"
geojson = "0.21.0"
geom = { path = "../geom" }
instant = "0.1.7"
js-sys = { version = "0.3.45", optional = true }
Expand Down
4 changes: 4 additions & 0 deletions popdat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ edition = "2018"

[dependencies]
abstutil = { path = "../abstutil" }
geo = "*"
geojson = { version = "0.21.0", features = ["geo-types"] }
geom = { path = "../geom" }
log = "0.4.11"
map_model = { path = "../map_model" }
rand = "0.7.0"
rand_xorshift = "0.2.0"
sim = { path = "../sim" }
serde_json = "*"

96 changes: 90 additions & 6 deletions popdat/src/import_census.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,95 @@
use map_model::Map;

use crate::CensusArea;
use abstutil::Timer;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: For consistency, we should still split the imports based on category... std, external crates, other crates in this workspace, the current crate. I guess we don't have any contributors using an IDE that forces this order, so not a huge deal either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

happy to abide

use geo::algorithm::intersects::Intersects;
use geojson::GeoJson;
use map_model::Map;
use std::convert::TryFrom;

impl CensusArea {
pub fn find_data_for_map(_map: &Map) -> Result<Vec<CensusArea>, String> {
// TODO importer/src/utils.rs has a download() helper that we could copy here. (And later
// dedupe, after deciding how this crate will integrate with the importer)
todo!()
pub fn find_data_for_map(map: &Map, timer: &mut Timer) -> Result<Vec<CensusArea>, String> {
// TODO eventually it'd be nice to lazily download the info needed. For now we expect a
// prepared geojson file to exist in data/system/<city>/population_areas.geojson
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

system makes sense to me, because we'll need this at runtime. (Just thinking aloud)

//
// When we implement downloading, importer/src/utils.rs has a download() helper that we
// could copy here. (And later dedupe, after deciding how this crate will integrate with
// the importer)
let path = abstutil::path(format!(
"system/{}/population_areas.geojson",
map.get_name().city
));
let bytes = abstutil::slurp_file(&path)?;
debug!("parsing geojson at path: {}", &path);

// TODO - can we change to Result<_,Box<dyn std::error::Error>> and avoid all these map_err?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I've bounced back and forth how to deal with error types. I don't think there's anywhere that we actually need to look for different error cases, but many places where we need to stringify an error, so I've tried making most APIs use String. But this is internal to popdat, so popdat::generate_scenario can be the one to map_err instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My instincts would be to change it all to Box<dyn Error>, but maybe there's a reason you prefer String errors. If so, do you have any advice on making this less verbose?

I'd love to be able to write:

let str = String::from_utf8(bytes)?;
timer.start("parsing geojson");
let geojson = str.parse::<GeoJson>()?;

Have you worked with anyhow at all? It might be a nice middle ground in all these places where we return String errors, which aren't really meant to be "handled" other than just logged.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should say, I don't have much experience with anyhow... just some reading and conversations with other people who are happy with it for "non-graceful" error handling (and it's sister crate this_error for graceful library error handling)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, missed this thread. https://crates.io/crates/anyhow looks nice, pretty much what we need! I've avoided the Rust error ecosystem because there's so much churn, but this one seems simple and popular. I'd be fine moving everything over to this.

let str = String::from_utf8(bytes).map_err(|e| e.to_string())?;
timer.start("parsing geojson");
let geojson = str.parse::<GeoJson>().map_err(|e| e.to_string())?;
timer.stop("parsing geojson");
let mut results = vec![];
let collection =
geojson::FeatureCollection::try_from(geojson).map_err(|e| e.to_string())?;

let map_area = map.get_boundary_polygon();
let bounds = map.get_gps_bounds();

use geo::algorithm::map_coords::MapCoordsInplace;
let mut geo_map_area: geo::Polygon<_> = map_area.clone().into();
geo_map_area.map_coords_inplace(|c| {
let projected = geom::Pt2D::new(c.0, c.1).to_gps(bounds);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to get the map poly in wgs84?

Though maybe it's irrelevant, because the other question I need to answer is: is there a blessed way to convert a poly from wgs84 to map coords?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GPSBounds::convert? Or if you want to enforce all points are in bounds, try_convert?

(projected.x(), projected.y())
});

debug!("collection.features: {}", &collection.features.len());
timer.start("converting to `CensusArea`s");
for feature in collection.features.into_iter() {
let population = feature.property("population");
let total_population = match population {
Some(serde_json::Value::Number(n)) => n
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

geojson prop parsing is depressingly verbose. I hope this gets better soon! georust/geojson#157

.as_u64()
.expect(&format!("unexpected total population number: {:?}", n))
as usize,
_ => {
return Err(format!(
"unexpected format for 'population': {:?}",
population
));
}
};

let geometry = feature.geometry.expect("geojson feature missing geometry");
let mut multi_poly =
geo::MultiPolygon::<f64>::try_from(geometry.value).map_err(|e| e.to_string())?;
let geo_polygon = multi_poly
.0
.pop()
.expect("multipolygon was unexpectedly empty");
if !multi_poly.0.is_empty() {
// Annoyingly upstream GIS has packaged all these individual polygons into
// "multipolygon" of length 1 Make sure nothing surprising is
// happening since we only use the first poly
error!(
"unexpectedly had {} extra area polygons",
multi_poly.0.len()
);
}

if !geo_polygon.intersects(&geo_map_area) {
debug!(
"skipping polygon outside of map area. polygon: {:?}, map_area: {:?}",
geo_polygon, geo_map_area
);
continue;
}

let polygon = geom::Polygon::from(geo_polygon);
results.push(CensusArea {
polygon,
total_population,
});
}
debug!("built {} CensusAreas within map bounds", results.len());
timer.stop("converting to `CensusArea`s");

Ok(results)
}
}
11 changes: 10 additions & 1 deletion popdat/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extern crate log;

use rand_xorshift::XorShiftRng;

use abstutil::Timer;
use geom::Polygon;
use geom::{Distance, Time};
use map_model::{BuildingID, Map};
Expand Down Expand Up @@ -105,16 +106,24 @@ pub fn generate_scenario(
) -> Result<Scenario, String> {
// find_data_for_map may return an error. If so, just plumb it back to the caller using the ?
// operator
let areas = CensusArea::find_data_for_map(map)?;
let mut timer = Timer::new("generate census scenario");
timer.start("building population areas for map");
let areas = CensusArea::find_data_for_map(map, &mut timer)?;
timer.stop("building population areas for map");

timer.start("assigning people to houses");
let people = distribute_people::assign_people_to_houses(areas, map, rng, &config);
timer.stop("assigning people to houses");

let mut scenario = Scenario::empty(map, scenario_name);
timer.start("building people");
for person in people {
// TODO If we need to parallelize because make_person is slow, the sim crate has a fork_rng
// method that could be useful
scenario
.people
.push(make_person::make_person(person, map, rng, &config));
}
timer.stop("building people");
Ok(scenario)
}