Skip to content

Commit

Permalink
Lift zone code out of Graph. #17
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Aug 24, 2024
1 parent e3dd9ea commit 4415675
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 126 deletions.
18 changes: 13 additions & 5 deletions backend/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ use geojson::{Feature, FeatureCollection, Geometry};
use rstar::RTreeObject;

use crate::graph::{Graph, Mode, PathStep, Route};
use crate::Zones;

pub fn buffer_route(
graph: &Graph,
zones: &Zones,
mode: Mode,
routes: Vec<Route>,
start_time: NaiveTime,
Expand Down Expand Up @@ -65,7 +67,7 @@ pub fn buffer_route(
f.set_property("kind", "hull");
features.push(f);

let total_population = intersect_zones(graph, &mut features, hull);
let total_population = intersect_zones(graph, zones, &mut features, hull);

Ok(serde_json::to_string(&FeatureCollection {
features,
Expand All @@ -81,11 +83,17 @@ pub fn buffer_route(
})?)
}

fn intersect_zones(graph: &Graph, features: &mut Vec<Feature>, hull: Polygon) -> u32 {
// TODO Move to Zones?
fn intersect_zones(
graph: &Graph,
zones: &Zones,
features: &mut Vec<Feature>,
hull: Polygon,
) -> u32 {
// We might intersect a Zone multiple times if it was a MultiPolygon itself originally
let mut ids = HashSet::new();
for obj in graph
.zone_rtree
for obj in zones
.rtree
.locate_in_envelope_intersecting(&hull.envelope())
{
ids.insert(obj.data);
Expand All @@ -94,7 +102,7 @@ fn intersect_zones(graph: &Graph, features: &mut Vec<Feature>, hull: Polygon) ->

let mut total = 0;
for id in ids {
let zone = &graph.zones[id.0];
let zone = &zones.zones[id.0];
// TODO This crashes sometimes and can't be reasonably caught in WASM
let hit = hull_mp.intersection(&zone.geom);
let hit_area_km2 = 1e-6 * hit.unsigned_area();
Expand Down
45 changes: 2 additions & 43 deletions backend/src/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ mod transit_route;

use anyhow::Result;
use enum_map::{Enum, EnumMap};
use geo::{Coord, LineLocatePoint, LineString, MultiPolygon, Point, Polygon};
use geojson::{Feature, FeatureCollection, GeoJson, Geometry};
use geo::{Coord, LineLocatePoint, LineString, Point, Polygon};
use geojson::{Feature, GeoJson, Geometry};
use rstar::{primitives::GeomWithData, RTree};
use serde::{Deserialize, Serialize};
use utils::Mercator;
Expand All @@ -34,9 +34,6 @@ pub struct Graph {
pub amenities: Vec<Amenity>,

pub gtfs: GtfsModel,

pub zones: Vec<Zone>,
pub zone_rtree: RTree<GeomWithData<Polygon, ZoneID>>,
}

pub type EdgeLocation = GeomWithData<LineString, RoadID>;
Expand All @@ -47,8 +44,6 @@ pub struct RoadID(pub usize);
pub struct IntersectionID(pub usize);
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AmenityID(pub usize);
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ZoneID(pub usize);

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub enum Direction {
Expand Down Expand Up @@ -191,33 +186,6 @@ impl Graph {
intersection,
}
}

/// Returns a GeoJSON string
pub fn render_zones(&self) -> Result<String> {
let mut features = Vec::new();
let mut max_density: f64 = 0.0;
for zone in &self.zones {
let mut f = Feature::from(Geometry::from(&self.mercator.to_wgs84(&zone.geom)));
f.set_property("population", zone.population);
let density = (zone.population as f64) / zone.area_km2;
f.set_property("density", density);
features.push(f);

max_density = max_density.max(density);
}
Ok(serde_json::to_string(&FeatureCollection {
features,
bbox: None,
foreign_members: Some(
serde_json::json!({
"max_density": max_density,
})
.as_object()
.unwrap()
.clone(),
),
})?)
}
}

impl Road {
Expand Down Expand Up @@ -272,12 +240,3 @@ pub enum PathStep {
stop2: StopID,
},
}

#[derive(Serialize, Deserialize)]
pub struct Zone {
// TODO Maybe split these upfront, including area and population, and just store in the RTree?
pub geom: MultiPolygon,
// TODO Later on, this could be generic or user-supplied
pub population: u32,
pub area_km2: f64,
}
71 changes: 7 additions & 64 deletions backend/src/graph/scrape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ use std::collections::HashMap;

use anyhow::Result;
use enum_map::EnumMap;
use flatgeobuf::{FeatureProperties, FgbFeature, GeozeroGeometry, HttpFgbReader};
use geo::{Area, Coord, EuclideanLength, MultiPolygon};
use geo::{Coord, EuclideanLength};
use muv_osm::{AccessLevel, TMode};
use osm_reader::OsmID;
use rstar::{primitives::GeomWithData, RTree};
use utils::{Mercator, Tags};
use rstar::RTree;
use utils::Tags;

use super::amenity::Amenity;
use super::route::Router;
use crate::graph::{
AmenityID, Direction, EdgeLocation, Graph, GtfsSource, Intersection, IntersectionID, Mode,
Road, RoadID, Zone, ZoneID,
Road, RoadID,
};
use crate::gtfs::{GtfsModel, StopID};
use crate::timer::Timer;
Expand Down Expand Up @@ -56,8 +55,7 @@ impl Graph {
pub async fn new(
input_bytes: &[u8],
gtfs_source: GtfsSource,
population_url: Option<String>,
mut timer: Timer,
timer: &mut Timer,
) -> Result<Graph> {
timer.step("parse OSM and split graph");

Expand Down Expand Up @@ -126,7 +124,7 @@ impl Graph {
for a in &mut amenities.amenities {
a.point = graph.mercator.pt_to_mercator(a.point.into()).into();
}
snap_amenities(&mut roads, &amenities.amenities, &closest_road, &mut timer);
snap_amenities(&mut roads, &amenities.amenities, &closest_road, timer);

timer.push("building router");
let router = EnumMap::from_fn(|mode| {
Expand All @@ -142,27 +140,9 @@ impl Graph {
GtfsSource::Geomedea(url) => GtfsModel::from_geomedea(&url, &graph.mercator).await?,
GtfsSource::None => GtfsModel::empty(),
};
snap_stops(&mut roads, &mut gtfs, &closest_road[Mode::Foot], &mut timer);
snap_stops(&mut roads, &mut gtfs, &closest_road[Mode::Foot], timer);
timer.pop();

let zones = if let Some(url) = population_url {
timer.step("load population zones");
load_zones(url, &graph.mercator).await?
} else {
Vec::new()
};
let mut zone_objects = Vec::new();
for (idx, zone) in zones.iter().enumerate() {
let id = ZoneID(idx);
// MultiPolygon isn't supported, so just insert multiple
for polygon in &zone.geom {
zone_objects.push(GeomWithData::new(polygon.clone(), id));
}
}
let zone_rtree = RTree::bulk_load(zone_objects);

timer.done();

Ok(Graph {
roads,
intersections,
Expand All @@ -173,8 +153,6 @@ impl Graph {

amenities: amenities.amenities,
gtfs,
zones,
zone_rtree,
})
}
}
Expand Down Expand Up @@ -300,38 +278,3 @@ fn snap_stops(
}
}
}

async fn load_zones(url: String, mercator: &Mercator) -> Result<Vec<Zone>> {
let bbox = mercator.wgs84_bounds;
let mut fgb = HttpFgbReader::open(&url)
.await?
.select_bbox(bbox.min().x, bbox.min().y, bbox.max().x, bbox.max().y)
.await?;

let mut zones = Vec::new();
while let Some(feature) = fgb.next().await? {
// TODO Could intersect with boundary_polygon, but some extras nearby won't hurt anything
let mut geom = get_multipolygon(feature)?;
mercator.to_mercator_in_place(&mut geom);
let area_km2 = 1e-6 * geom.unsigned_area();
// TODO Re-encode as UInt
let population = feature.property::<i64>("population")?.try_into()?;

zones.push(Zone {
geom,
population,
area_km2,
});
}
Ok(zones)
}

fn get_multipolygon(f: &FgbFeature) -> Result<MultiPolygon> {
let mut p = geozero::geo_types::GeoWriter::new();
f.process_geom(&mut p)?;
match p.take_geometry().unwrap() {
geo::Geometry::Polygon(p) => Ok(MultiPolygon(vec![p])),
geo::Geometry::MultiPolygon(mp) => Ok(mp),
_ => bail!("Wrong type in fgb"),
}
}
47 changes: 33 additions & 14 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use geojson::{de::deserialize_geometry, Feature, GeoJson, Geometry};
use serde::Deserialize;
use wasm_bindgen::prelude::*;

use crate::zone::Zones;
pub use graph::{Graph, GtfsSource, Mode};
pub use gtfs::GtfsModel;
pub use timer::Timer;
Expand All @@ -23,13 +24,15 @@ mod gtfs;
mod isochrone;
mod score;
mod timer;
mod zone;

static START: Once = Once::new();

// TODO Rename
#[wasm_bindgen]
pub struct MapModel {
graph: Graph,
zones: Zones,
}

#[wasm_bindgen]
Expand All @@ -54,19 +57,22 @@ impl MapModel {
Some(url) => graph::GtfsSource::Geomedea(url),
None => graph::GtfsSource::None,
};
let mut timer = Timer::new("build graph", progress_cb);
let graph = if is_osm {
Graph::new(
input_bytes,
gtfs,
population_url,
Timer::new("build graph", progress_cb),
)
.await
.map_err(err_to_js)?
Graph::new(input_bytes, gtfs, &mut timer)
.await
.map_err(err_to_js)?
} else {
bincode::deserialize_from(input_bytes).map_err(err_to_js)?
};
Ok(MapModel { graph })
// TODO Serialize this too
let zones = Zones::load(population_url, &graph.mercator, &mut timer)
.await
.map_err(err_to_js)?;

timer.done();

Ok(MapModel { graph, zones })
}

/// Returns a GeoJSON string. Just shows the full network
Expand Down Expand Up @@ -96,7 +102,9 @@ impl MapModel {

#[wasm_bindgen(js_name = renderZones)]
pub fn render_zones(&self) -> Result<String, JsValue> {
self.graph.render_zones().map_err(err_to_js)
self.zones
.render_zones(&self.graph.mercator)
.map_err(err_to_js)
}

#[wasm_bindgen(js_name = isochrone)]
Expand Down Expand Up @@ -158,8 +166,15 @@ impl MapModel {
let start_time = NaiveTime::parse_from_str(&req.start_time, "%H:%M").map_err(err_to_js)?;
let limit = Duration::from_secs(req.max_seconds);

crate::buffer::buffer_route(&self.graph, mode, vec![route], start_time, limit)
.map_err(err_to_js)
crate::buffer::buffer_route(
&self.graph,
&self.zones,
mode,
vec![route],
start_time,
limit,
)
.map_err(err_to_js)
}

#[wasm_bindgen(js_name = score)]
Expand Down Expand Up @@ -203,7 +218,8 @@ impl MapModel {
let start_time = NaiveTime::parse_from_str(&req.start_time, "%H:%M").map_err(err_to_js)?;
let limit = Duration::from_secs(req.max_seconds);

crate::buffer::buffer_route(&self.graph, mode, routes, start_time, limit).map_err(err_to_js)
crate::buffer::buffer_route(&self.graph, &self.zones, mode, routes, start_time, limit)
.map_err(err_to_js)
}
}

Expand All @@ -212,7 +228,10 @@ impl MapModel {
impl MapModel {
pub fn from_graph_bytes(input_bytes: &[u8]) -> Result<MapModel, JsValue> {
let graph = bincode::deserialize_from(input_bytes).map_err(err_to_js)?;
Ok(MapModel { graph })
Ok(MapModel {
graph,
zones: Zones::empty(),
})
}

pub fn route_from_req(&self, req: &RouteRequest) -> Result<String, JsValue> {
Expand Down
Loading

0 comments on commit 4415675

Please sign in to comment.