Skip to content

Commit

Permalink
Prioritize neighborhood by SIMD (UI only) (#152)
Browse files Browse the repository at this point in the history
* Expose "neighbourhood" boundary to rust.

Previously we dealt with these as "opaque" GeoJSON Features, but for the
prioritization work, we're going to be working directly with these
entities in rust, so it makes sense to have a 1st class rust
representation, rather than going through GeoJSON.

The GeoJSON representation is now only used for serialization, and
relegated to the js/rust boundary.

* Show area on hover.

* Mock SIMD output

* WIP: new binary to gen simd data

* WIP: show prioritization in left column

Also zhuzh the project list a bit

TODO

-[ ] Only cnt
-[/] Color map accordingly
 -[x] simd is colored
 -[ ] area is colored strangely
-[x] info hierarchy of neighborhood list

* Highlight list item when hovering map.

Unfortunately, the reverse seems tricky: highlighting the map when
hovering on the list:

I had a go at using `<JoinedData>` to associate a highlighted
feature-state. Doing so requires joining the source layer by layer id
and thus specifying a specific layer id for the GeoJSON.

But then the JoinedData calls some maplibre code that complained:

> GeoJSON sources cannot have a sourceLayer parameter.

* wip: url params

* extract selection widget

* Prioritize on autogen

* better name?

* unpatch now that PR is merged

* minimize diff, clarify code placement

* revert failed ui experiment

* Only prioritize in CNT

I'm still showing "area" when hovering over neighbourhoods in the
PickeNeighbourhoodMode and AutoBoundariesMode, even in non-CNT, cuz why
not?

* more exciting font icons
  • Loading branch information
michaelkirk authored Feb 21, 2025
1 parent e6133f8 commit 2846e97
Show file tree
Hide file tree
Showing 24 changed files with 602 additions and 162 deletions.
2 changes: 1 addition & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 4 additions & 8 deletions backend/benches/build_neighbourhood.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fn benchmark_build_neighbourhood(c: &mut Criterion) {
NeighbourhoodFixture::BRISTOL_WEST,
NeighbourhoodFixture::STRASBOURG,
] {
let (map, boundary_geo) = neighbourhood.neighbourhood_params().unwrap();
let (neighbourhood_stats, map) = neighbourhood.neighbourhood_params().unwrap();
let edit_perimeter_roads = false;
c.bench_function(
&format!(
Expand All @@ -17,13 +17,9 @@ fn benchmark_build_neighbourhood(c: &mut Criterion) {
),
|b| {
b.iter(|| {
let neighbourhood = Neighbourhood::new(
&map,
neighbourhood.neighbourhood_name.to_string(),
boundary_geo.clone(),
edit_perimeter_roads,
)
.unwrap();
let neighbourhood =
Neighbourhood::new(&map, neighbourhood_stats.clone(), edit_perimeter_roads)
.unwrap();
black_box(neighbourhood);
});
},
Expand Down
59 changes: 47 additions & 12 deletions backend/src/auto_boundaries.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use geo::{Area, Coord, Intersects, LineString, Polygon};
use geojson::FeatureCollection;
use crate::boundary_stats::BoundaryStats;
use crate::MapModel;
use geo::{Coord, Intersects, LineString, Polygon};
use geojson::{Feature, FeatureCollection};
use i_overlay::core::fill_rule::FillRule;
use i_overlay::float::slice::FloatSlice;

use crate::MapModel;
use serde::{Deserialize, Serialize};

impl MapModel {
pub fn render_auto_boundaries(&self) -> FeatureCollection {
Expand Down Expand Up @@ -63,14 +64,16 @@ impl MapModel {
let touches_railway = boundary_touches_any(&polygon, &self.railways);
let touches_waterway = boundary_touches_any(&polygon, &self.waterways);

let mut f = self.mercator.to_wgs84_gj(&polygon);
f.set_property("kind", "area");
f.set_property("touches_big_road", touches_big_road);
f.set_property("touches_railway", touches_railway);
f.set_property("touches_waterway", touches_waterway);
// Convert from m^2 to km^2. Use unsigned area to ignore polygon orientation.
f.set_property("area_km2", polygon.unsigned_area() / 1_000_000.0);
features.push(f);
let boundary_stats = BoundaryStats::new(&polygon);
let generated_boundary = GeneratedBoundary {
geometry: polygon,
touches_big_road,
touches_railway,
touches_waterway,
boundary_stats,
};

features.push(generated_boundary.to_feature(self));
}

FeatureCollection {
Expand All @@ -81,6 +84,38 @@ impl MapModel {
}
}

/// The static data that defines where exactly a neighbourhood is.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct GeneratedBoundary {
/// `geometry` is always Mercator.
/// We convert it to/from wgs84 only when serializizing/deserializing GeoJSON.
#[serde(
serialize_with = "geojson::ser::serialize_geometry",
deserialize_with = "geojson::de::deserialize_geometry"
)]
pub geometry: Polygon,
pub touches_big_road: bool,
pub touches_railway: bool,
pub touches_waterway: bool,
#[serde(flatten)]
pub boundary_stats: BoundaryStats,
}

impl GeneratedBoundary {
pub fn to_feature(&self, map: &MapModel) -> Feature {
let mut projected = self.clone();
map.mercator.to_wgs84_in_place(&mut projected.geometry);
let mut feature =
geojson::ser::to_feature(projected).expect("should have no unserializable fields");
let props = feature
.properties
.as_mut()
.expect("GeneratedBoundary always has properties");
props.insert("kind".to_string(), "area".into());
feature
}
}

// TODO Revisit some of this; conversions are now in geo
fn split_polygon(polygon: Polygon, linestrings: &Vec<LineString>) -> Vec<Polygon> {
let mut shape = to_i_overlay_contour(polygon.exterior());
Expand Down
20 changes: 20 additions & 0 deletions backend/src/boundary_stats.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use geo::{Area, Polygon};
use nanorand::{RandomGen, WyRand};
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct BoundaryStats {
pub area_km2: f64,
pub simd: f64,
}

impl BoundaryStats {
pub fn new(polygon: &Polygon) -> Self {
// Convert from m^2 to km^2. Use unsigned area to ignore polygon orientation.
let area_km2 = polygon.unsigned_area() / 1_000_000.0;
// TODO: SIMD
let mut rng = WyRand::new_seed((area_km2 * 1000000.0) as u64);
let simd = f64::random(&mut rng) * 100.0;
Self { area_km2, simd }
}
}
11 changes: 11 additions & 0 deletions backend/src/geo_helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,17 @@ pub fn thicken_line(line: Line, thickness: f64) -> Polygon {
)
}

pub fn invert_feature_geometry_in_place(wgs84_feature: &mut geojson::Feature) {
let Some(geometry) = &wgs84_feature.geometry else {
unreachable!("unexpected missing geometry");
};
let wgs84_polygon = geo::Polygon::try_from(geometry).unwrap();
let inverted = invert_polygon(wgs84_polygon);

let geojson_geometry = geojson::Geometry::from(&inverted);
wgs84_feature.geometry = Some(geojson_geometry)
}

/// Create a polygon covering the world, minus a hole for the input polygon. Assumes the input is
/// in WGS84 and has no holes itself.
pub fn invert_polygon(wgs84_polygon: Polygon) -> Polygon {
Expand Down
44 changes: 28 additions & 16 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ extern crate log;

use std::sync::Once;

use geo::{Coord, LineString, Polygon};
use geo::{Coord, LineString};
use geojson::{Feature, FeatureCollection, GeoJson, Geometry};
use serde::Deserialize;
use wasm_bindgen::prelude::*;
Expand All @@ -14,12 +14,13 @@ use self::cells::Cell;
pub use self::map_model::{
FilterKind, Intersection, IntersectionID, MapModel, ModalFilter, Road, RoadID, TravelFlow,
};
pub use self::neighbourhood::Neighbourhood;
pub use self::neighbourhood::{Neighbourhood, NeighbourhoodBoundary, NeighbourhoodDefinition};
use self::render_cells::RenderCells;
pub use self::route::Router;
pub use self::shortcuts::Shortcuts;

mod auto_boundaries;
mod boundary_stats;
mod cells;
mod create;
mod geo_helpers;
Expand Down Expand Up @@ -148,17 +149,19 @@ impl LTN {
Ok(serde_json::to_string(&self.map.render_auto_boundaries()).map_err(err_to_js)?)
}

/// Takes a name and boundary GJ polygon
/// `input`: GeoJson Feature w/ Polygon Geometry
#[wasm_bindgen(js_name = setNeighbourhoodBoundary)]
pub fn set_neighbourhood_boundary(
&mut self,
name: String,
input: JsValue,
neighborhood_feature: JsValue,
) -> Result<(), JsValue> {
let mut boundary_gj: Feature = serde_wasm_bindgen::from_value(input)?;
boundary_gj.set_property("kind", "boundary");
boundary_gj.set_property("name", name.clone());
self.map.boundaries.insert(name, boundary_gj);
let mut feature: Feature = serde_wasm_bindgen::from_value(neighborhood_feature)?;
feature.set_property("name", name.clone());
let neighbourhood_definition =
NeighbourhoodDefinition::from_feature(feature, &self.map).map_err(err_to_js)?;
let boundary = NeighbourhoodBoundary::new(neighbourhood_definition);
self.map.boundaries.insert(name, boundary);
Ok(())
}

Expand All @@ -169,9 +172,9 @@ impl LTN {

#[wasm_bindgen(js_name = renameNeighbourhoodBoundary)]
pub fn rename_neighbourhood_boundary(&mut self, old_name: String, new_name: String) {
let mut boundary_gj = self.map.boundaries.remove(&old_name).unwrap();
boundary_gj.set_property("name", new_name.clone());
self.map.boundaries.insert(new_name, boundary_gj);
let mut boundary = self.map.boundaries.remove(&old_name).unwrap();
boundary.definition.name = new_name.clone();
self.map.boundaries.insert(new_name, boundary);
}

#[wasm_bindgen(js_name = setCurrentNeighbourhood)]
Expand All @@ -180,18 +183,16 @@ impl LTN {
name: String,
edit_perimeter_roads: bool,
) -> Result<(), JsValue> {
let boundary_gj = self.map.boundaries.get(&name).cloned().unwrap();
let mut boundary_geo: Polygon = boundary_gj.try_into().map_err(err_to_js)?;
self.map.mercator.to_mercator_in_place(&mut boundary_geo);
let boundary = self.map.boundaries.get(&name).unwrap();

// Are we still editing the same neighbourhood, just switching edit_perimeter_roads?
let editing_same = self
.neighbourhood
.as_ref()
.map(|n| n.name == name)
.map(|n| n.name() == name)
.unwrap_or(false);
self.neighbourhood = Some(
Neighbourhood::new(&self.map, name, boundary_geo, edit_perimeter_roads)
Neighbourhood::new(&self.map, boundary.clone(), edit_perimeter_roads)
.map_err(err_to_js)?,
);

Expand Down Expand Up @@ -380,6 +381,17 @@ impl LTN {
.map_err(err_to_js)?)
}

#[wasm_bindgen(js_name = getAllNeighbourhoods)]
pub fn get_all_neighbourhoods(&self) -> Result<String, JsValue> {
let features = self
.map
.boundaries
.values()
.map(|neighbourhood| neighbourhood.to_feature(&self.map));
let fc = FeatureCollection::from_iter(features);
Ok(serde_json::to_string(&fc).map_err(err_to_js)?)
}

#[wasm_bindgen(js_name = getAllIntersections)]
pub fn get_all_intersections(&self) -> Result<String, JsValue> {
Ok(serde_json::to_string(&GeoJson::from(
Expand Down
21 changes: 14 additions & 7 deletions backend/src/map_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::geo_helpers::{
invert_multi_polygon, limit_angle, linestring_intersection,
};
use crate::impact::Impact;
use crate::neighbourhood::{NeighbourhoodBoundary, NeighbourhoodDefinition};
use crate::route::RouterInput;
use crate::{od::DemandModel, Router};
use anyhow::Result;
Expand Down Expand Up @@ -53,9 +54,7 @@ pub struct MapModel {
// TODO Keep edits / state here or not?
pub undo_stack: Vec<Command>,
pub redo_queue: Vec<Command>,
// Stores boundary polygons in WGS84, with ALL of their GeoJSON props.
// TODO Reconsider
pub boundaries: BTreeMap<String, Feature>,
pub boundaries: BTreeMap<String, NeighbourhoodBoundary>,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize)]
Expand Down Expand Up @@ -514,13 +513,19 @@ impl MapModel {
}
}

gj.features.extend(self.boundaries.values().cloned());
for neighbourhood_boundary in self.boundaries.values() {
// we don't save the derived "stats" just the boundary definition
gj.features
.push(neighbourhood_boundary.definition.to_feature(self))
}

let mut f = Feature::from(Geometry::from(&self.boundary_wgs84));
f.set_property("kind", "study_area_boundary");
gj.features.push(f);

gj.foreign_members = Some(
// The features are elements within the study area, we store properties of the
// StudyArea itself as foreign members.
serde_json::json!({
"study_area_name": self.study_area_name,
})
Expand Down Expand Up @@ -601,11 +606,13 @@ impl MapModel {
cmds.push(Command::SetTravelFlow(r, dir));
}
"boundary" => {
let name = get_str_prop(&f, "name")?;
if self.boundaries.contains_key(name) {
let name = get_str_prop(&f, "name")?.to_string();
if self.boundaries.contains_key(&name) {
bail!("Multiple boundaries named {name} in savefile");
}
self.boundaries.insert(name.to_string(), f);
let neighbourhood_definition = NeighbourhoodDefinition::from_feature(f, self)?;
let neighbourhood_stats = NeighbourhoodBoundary::new(neighbourhood_definition);
self.boundaries.insert(name, neighbourhood_stats);
}
"study_area_boundary" => {
// TODO Detect if it's close enough to boundary_polygon? Overwrite?
Expand Down
Loading

0 comments on commit 2846e97

Please sign in to comment.