-
-
Notifications
You must be signed in to change notification settings - Fork 1
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
Start impact prediction mode #83
Conversation
|
||
use crate::{IntersectionID, MapModel, RoadID}; | ||
|
||
// TODO Rename? |
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.
There's a few too many things called "impact" in the frontend. Maybe "area-wide traffic prediction"?
} | ||
} | ||
|
||
/// Deterministically produce a bunch of OD pairs, just as a fallback when there's no real data |
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.
Not sure yet what approach to take to have real OD data only for some places, will tackle next.
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.
Seems like a reasonable approach for expediency.
It might avoid trouble to regenerate trips where i1 == i2
, but maybe it won't be an issue in practice.
@@ -294,6 +295,31 @@ impl LTN { | |||
.map_err(err_to_js)?) | |||
} | |||
|
|||
/// Returns GJ with a LineString per road, with before/after counts |
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.
General reflection: It's unsatisfying to have 3 layers where types are expressed either as code or docs:
- In the wasm.ts layer as TS
- Here in the Rust WASM "interface" layer, which usually just returns JSON strings
- In a more internal method on
MapModel
, where usually we could have more info on types. (If we used https://docs.rs/geojson/latest/geojson/ser/index.html structs, maybe)
It works fine so far, just one of the costs of a cross-language layer.
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.
Yeah, I can see the frustration.
While writing rust, my instinct is that json (including geojson), should only be used as a serialization format to talk to wasm - in other words avoid ever doing application logic with geojson, and do everything with fully expressive rust types.
By that rubrik, this kind of thing is "bad": https://github.com/a-b-street/ltn/pull/83/files#diff-f71d03fb4aef16f1e3c21b14e2c5e077e0d40c8c1af7f897bba4a71f337040d6R87
In typescript I'm less sure - in theory we should be able to write typescript signatures compatible with geojson that also encapsulate our more specific needs (e.g. a LineString Feature that has a "foo: Number" foreign member). (Update: looks like you're already doing this part, great! Feature<LineString, { kind: "before" }>
)
Given that I'm brand new to this code base, maybe such a principled stance isn't worth it in practice, so I wouldn't make any radical changes based just on my instincts yet, but consider it.
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.
avoid ever doing application logic with geojson, and do everything with fully expressive rust types.
There's a bunch of other places in the code currently violating this, creating features and doing lots of set_property
. The WASM layer in lib.rs
is at least the one place to stringify JSON or parse inputs. It would indeed be nice to write structs using geojson::ser
for most app-level code to output, especially if we could wire up something like https://docs.rs/ts-rs/latest/ts_rs/ to have more rigor about keeping wasm.ts
in sync.
For the moment, I think the codebase is small/simple enough to keep playing it kind of loose, so I won't prioritize it yet.
pub fn predict_impact(&mut self) -> Result<String, JsValue> { | ||
self.map.rebuild_router(1.0); | ||
let mut impact = self.map.impact.take().unwrap(); | ||
let out = impact.recalculate(&self.map); |
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.
impact
is mutable, but also needs to access other parts of the map model, hence the Option
and this "take, then put back" approach. Could be restructured to lift impact out of MapModel, but not too important.
@@ -33,11 +34,11 @@ pub struct MapModel { | |||
|
|||
// TODO Wasteful, can share some | |||
// This is guaranteed to exist, only Option during MapModel::new internals | |||
pub router_original: Option<Router>, | |||
pub router_before: Option<Router>, |
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.
Renamed for consistency. "before" means before any user edits are applied. "after" means the current state of user edits.
@@ -14,6 +14,11 @@ pub struct Router { | |||
pub main_road_penalty: f64, | |||
} | |||
|
|||
pub struct Route { |
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.
I did some detangling here to separately express a route as a sequence of steps and then render as a linestring. I keep reinventing variations of this code for the "OSM-based map model" layer in many projects, and in many others, I've recently consolidated on https://github.com/a-b-street/15m/tree/main/graph/src. It could be later worth trying to migrate this codebase to use that too (and take advantage of all its nice routing and route-rendering bits), but there are complications with doing so (how modal filters are modelled, partly) and it's not really a priority. The general tension is between duplicate code between projects and coming up with something general enough to handle a bunch of projects.
web/src/App.svelte
Outdated
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.
This is an example how to wire up a new UI mode
Pick a different road | ||
</Link> | ||
|
||
<div style="display: flex; justify-content: space-between;"> |
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.
Some of this and above could be refactored with ViewShortcuts into some kind of list-navigation, maybe
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.
I'm not really happy with any of the styling choices here yet, but it's a start and roughly recreates what's in v1. There's two things we probably want to communicate:
- Did a particular road get more or less traffic than before edits? What roads changed the most? A diverging red/green color is used for this.
- What's the level of traffic after, in absolute terms?
A road going from 10 trips to 50 trips seems like a dramatic change by color, but 50 trips total is nothing, so it should be thin.
}) | ||
.as_object() | ||
.unwrap() | ||
.clone(), |
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.
minor nit: is this clone necessary - since it's the last thing in the method, I'd expect a move should be sufficient.
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.
Sadly yes, as_object returns a ref, and there's no into_object
helper
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.
Embarrassing question, but how do I get to this view?
@@ -294,6 +295,31 @@ impl LTN { | |||
.map_err(err_to_js)?) | |||
} | |||
|
|||
/// Returns GJ with a LineString per road, with before/after counts |
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.
Yeah, I can see the frustration.
While writing rust, my instinct is that json (including geojson), should only be used as a serialization format to talk to wasm - in other words avoid ever doing application logic with geojson, and do everything with fully expressive rust types.
By that rubrik, this kind of thing is "bad": https://github.com/a-b-street/ltn/pull/83/files#diff-f71d03fb4aef16f1e3c21b14e2c5e077e0d40c8c1af7f897bba4a71f337040d6R87
In typescript I'm less sure - in theory we should be able to write typescript signatures compatible with geojson that also encapsulate our more specific needs (e.g. a LineString Feature that has a "foo: Number" foreign member). (Update: looks like you're already doing this part, great! Feature<LineString, { kind: "before" }>
)
Given that I'm brand new to this code base, maybe such a principled stance isn't worth it in practice, so I wouldn't make any radical changes based just on my instincts yet, but consider it.
LineString::new(pts) | ||
} | ||
|
||
pub fn crosses_road(&self, road: RoadID) -> bool { |
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.
To make sure I'm understanding our terminology: a RoadID corresponds to a segment of the road network which has no intersections in its interior. Is that right?
So there's no way to "cross" a road without traveling along it.
In common language, traveling "through" an intersection would mean traveling from one RoadID and then switching to another RoadID at the point of the intersection (even though in common language we'd call it the same "road" all along).
The above is all totally fine - just check if I'm misunderstanding.
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.
All correct. It will get a bit more complicated for #30, because the transition from one Road to another Road within an Intersection will have to be modeled explicitly. (Called "turn" or "movement" in abstreet.) Any ideas for more clear terms, happy to use
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.
Embarrassing question, but how do I get to this view?
Thanks for the instructions! For transparency, I did look in the breadcrumbs, but I was drilled down into the neighborhood screen, not up a level at the "study area" screen.
I think it makes sense where it is, since we measure impact spanning all the neighborhoods.
I think mostly this speaks to me not having a good intuition yet for the data hierarchy.
Let me see if I have it right:
A "project" vs. "study area" are synonymous (or one-to-one).
vs. a neighborhood, which is a subset of a study area. A study area can have many neighborhoods.
When you are placing filters, you are drilled down to "editing a neighborhood", not editing the entire study area.
But because edits in one neighborhood might affect traffic in another, impact measurements must be done at the scale of the entire project/study-area.
Is all that correct?
Anyway, the changes look good, and seem like a good basis for this view. Thanks for walking me through it.
web/src/PredictImpactMode.svelte
Outdated
Pick neighbourhood | ||
</Link> | ||
</li> | ||
<li>Predict impact mode</li> |
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.
<li>Predict impact mode</li> | |
<li>Predict impact</li> |
"mode" feels redundant in the UI
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.
Done
Data hierarchy understanding is almost right! It is useful to see the confusing points here, because they're definitely confusing parts of the UX.
A study area is a place like Edinburgh or a section of Seattle. A project is meant to be like a file somebody makes (and later shares) to use the tool. The project is tied to one study area, but a study area can have multiple files. Right now, the project has just one set of edits (changes to one-ways, modal filters, turn restrictions, speeds and a definition of neighbourhood boundaries). But in the old tool, there were multiple "proposals" per file/project, and a way to switch quickly between them. I'm not sure we need all that complexity here yet. Something closer to what I'm picturing is https://nptscot.github.io/npw/ (only a few places here have working files online -- Edinburgh or Glasgow, if you're going to try this). First the user restores a project file they were working on previously, or makes a new one by first picking the study area.
Correct, but this is probably confusing: #77
Exactly. The flow a user might take:
-- Thanks for the review! I'll wait for feedback from upstream about "predict impact" vs "predict areawide traffic impacts" or something else before renaming. |
A start to #36. After some changes to one neighbourhood in Edinburgh:
The impact is mostly localized around there:
Justifying why Queen Street got busier with examples: