-
-
Notifications
You must be signed in to change notification settings - Fork 353
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
census areas import #425
Changes from 11 commits
7d76d93
2ee584d
e37da2e
567733c
51f656a
2843e18
ad5d395
249a504
3d05a19
a02ec99
bca09d4
1b6b591
c5f9008
7600bd8
f4661ad
3c65fd8
07b846c
45f5506
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've updated these |
||
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I sort of assumed that Maybe if rings is set then rings.0 is the exterior? Is that right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If rings is set, then [0] is the exterior, correct. I wonder if it would be simpler and maybe more performant to just make There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
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> { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,95 @@ | ||
use map_model::Map; | ||
|
||
use crate::CensusArea; | ||
use abstutil::Timer; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
// | ||
// 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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My instincts would be to change it all to I'd love to be able to write:
Have you worked with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I should say, I don't have much experience with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
(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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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