a-b-street / abstreet

Transportation planning and traffic simulation software for creating cities friendlier to walking, biking, and public transit
https://a-b-street.github.io/docs/
Apache License 2.0
7.59k stars 339 forks source link

Refactor map-space objects #763

Open dabreegster opened 2 years ago

dabreegster commented 2 years ago

widgetry now has pretty good abstractions for handling most "screen-space" interactions, with nice APIs like the button builders. But the world of "map-space" objects is the wild west -- just ad-hoc code for figuring out what exists, hovering, selecting, dragging, tooltips, z-ordering. Can we do better?

Use cases

All of these have some kind of hovering behavior.

And I'm sure I've missed some.

An extreme take: Almost no app code should call ctx.redo_mouseover(), but there are... >50 callers right now!

What makes this stuff hard?

Caching and maintaining state -- when you hover or select something different, we usually want to draw something, maybe add a tooltip. Ad-hoc right now.

When hovering or actively dragging a draggable object, we still want to allow zooming, but not panning.

Dragging is complicated and buggy. In the route tool, if you drag a waypoint close to an alternate path, stuff breaks. Z-ordering there is implicit in the code, has to match in event and draw. Conflicts between waypoints and alt paths.

Ideas

Extend map_editor's World abstraction. There'll be something for a State that knows about all of the map-space objects. Objects are:

What's in a world?

In most of the use cases, the map-space stuff is "extra" objects on top of the basemap. The basemap stuff is mostly managed by map_gui, which has its own system for managing these kinds of concerns. Except there are also some places that "subset" the basemap objects:

Hot take: app.primary.current_selection is a bad idea? I count around 30 places that reset it to None when entering/leaving the state. I think the world of selectable objects shouldn't be shared App-wide. Most states should control this more directly.

How to drag "correctly"

1) Bail out if the cursor isn't in map-space 2) Go through all the objects, maybe start a drag 3) Only then allow normal canvas movement (not while hovering on something that's draggable)

Where's the state live?

The World approach is to be generic over some ID type. Shoving in custom data for the objects is an option, or letting the State manage it and map things. (Hey, we're inventing parts of ECS...)

Similar to Panels, could we make do with strings, or do we need IDs? Could the world container thing assign its own opaque IDs and make the caller do the mapping?

Should we subsume chunks of map_gui?

Stepping back, DrawMap solves lots of these problems too. Should we try to subsume it?

pub trait Renderable {
    fn get_id(&self) -> ID;
    fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike, opts: &DrawOptions);
    fn get_zorder(&self) -> isize;
    fn get_outline(&self, map: &Map) -> Polygon;
    fn contains_pt(&self, pt: Pt2D, map: &Map) -> bool;
}

Things that're maybe different (but maybe not):

Delegation

We have a few places in the code where different logical components all shove their widgets in a single Panel, so we do scary dances to try to delegate outcomes. Will we have the same problems? Think about the route tool -- InputWaypoints is a common thing, living alongside the one-off alt route clickables.

Prior art

Any good ideas from Leaflet, mapbox, other game engine APIs?

dabreegster commented 2 years ago

Subsume ColorNetwork and ColorDiscrete?!

I thought about the bike tool's new mode shift thing. It's got tooltips for certain roads, and I've thought about clicking to show sample routes there, so it seems like a candidate for this.

But then I realized that this, and loads of the simulation layers, use ColorDiscrete and ColorNetwork as attempts to refactor the problem of just drawing a bunch of basemap objects in a special way. If they were switched over to this new API, they could get much simpler, and also take advantage of new features easily! For example, the parking layer shows a heatmap of capacity -- what if was as easy as .tooltip(format!("{} / {} spots", free, total)) to add an unzoomed tooltip, or clicking unzoomed to open up the info panel set to the parking-over-time plot?

Styling: Similar to the benefits of the massive button refactor, this could be a chance to consolidate the million ad-hoc ways we have of styling different roads, buildings, etc!