Skip to content

Make the route sketcher work nicely at all zoom levels #784

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

Merged
merged 4 commits into from
Oct 19, 2021
Merged

Conversation

dabreegster
Copy link
Collaborator

Sorry for all the churn with drafts. This mostly supercedes #782. I think the code is reasonably future-proof and it fixes the usability problems with the route sketcher at least, so I think it's worth merging.

The problem is sketching a route to modify is hard to do when zoomed very far out: https://user-images.githubusercontent.com/1664407/137545910-a1ec508a-5341-4628-8151-8051d0dfe5b4.gif

The solution scales the lines and circles with the zoom. Ideally they'd always be exactly the same size on the screen, but for the moment, we discretize:
screencast

How the code works

The new DrawUnzoomedShapes has a builder API for drawing colored polylines and circles with a base width or radius. The implementation right now just holds onto the base shape and lazily scales it for different zoom levels, invisible to the caller. In the future, we can make this more efficient by writing a dedicated shader to draw thick lines or circles in the GPU.

That should be purely an internal widgetry change when it happens. I've at least sketched out the steps to do it:

  • upload a second pair of vertex/fragment shaders and plumb around two glow programs -- not hard
  • Make a new internal-only Drawable struct and variation of actually_upload to plumb the proper data to the GPU
  • actually write the shaders. This is the hard part, but I know it can be done and have found examples to follow

Making the shapes interactive

The tough next step is to make this work for the "your trip" page. Currently that uses a World to handle dragging waypoints and hovering on/clicking alternate routes. I think World will need to understand these unzoomed shapes that change size.

At least calculating mouseover collisions seems straightforward. We can use FindClosest for the thick lines. This finds the closest line segment, projects the cursor onto that line, and can tell us the distance to the line. We can look at the current zoom level and calculate if the line is thick enough to count as a hit. Similar for circles, figuring out if a circle with some changing radius contains a point is simple math.

Some of the things I've yet to think through are:

  • scaling the "A", "B", "C" text on the waypoints
  • drawing the outline for alternate routes (to punt on this, we could just pick a different color to indicate alternate routes)
  • making World dynamically generate hovered polygons (which we should do anyway even for pure map-space stuff to save on memory and upfront calculation)

To start, just make the bike network use this.
… to see the route on large maps.

Note the draggable waypoint circles are still a fixed size; you can't
easily manipulate the route when unzoomed far.
…ute sketcher nicer to use at all zoom levels!
}
}

fn mouseover_i(&self, ctx: &EventCtx) -> Option<IntersectionID> {
let pt = ctx.canvas.get_cursor_in_map_space()?;
// When zoomed really far out, it's harder to click small intersections, so snap more
// aggressively.
// aggressively. Note this should always be a larger hitbox than how the waypoint circles
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still want to convert this code to use World instead of handling dragging directly, but not urgent.


// Arbitrarily pick a color when two different types of roads meet
intersections.insert(r.src_i, color);
intersections.insert(r.dst_i, color);
}

let mut batch = GeomBatch::new();
for (i, color) in intersections {
// No clear way to thicken the intersection at different zoom levels
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot from 2021-10-18 15-49-07
The "choppiness" is annoying. The nice thing would be to merge contiguous chunk of a bike path along multiple road segments into one PolyLine and thicken the whole thing.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 agree!

/// specify the behavior when barely unzoomed or zoomed in -- the shape starts being drawn in
/// map-space "normally" without a constant screen-space size.
pub struct DrawUnzoomedShapes {
lines: Vec<UnzoomedLine>,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, this could be just a lightweight Drawable variation that just points to stuff uploaded to the GPU

});
}

// TODO We might take EventCtx here to upload something to the GPU.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where we would upload vertex data with the points, widths, and colors to the GPU, in some TBD format.

if value.is_none() {
// Thicker shapes as we zoom out. Scale up to 5x. Never shrink past the original size.
let mut thickness = (0.5 / zoom).max(1.0);
// And on gigantic maps, zoom may approach 0, so avoid NaNs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused trying to reconcile these comments with the code.

Do you mean that the min should be 1.0 and the max should be 5.0?

(see also clamp)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also confused. :P Here's the table of values:

0.13822713522160424: rounded 0.1, idx 1.   thickness 5
0.15030187423656013: rounded 0.2, idx 2.   thickness 2.5
0.25051174719224095: rounded 0.3, idx 3.   thickness 1.6666666666666667
0.35019705644415605: rounded 0.4, idx 4.   thickness 1.25
0.4540073629671436: rounded 0.5, idx 5.   thickness 1
0.5504481570314438: rounded 0.6, idx 6.   thickness 1
0.6526359457120697: rounded 0.7, idx 7.   thickness 1
0.7956998190130887: rounded 0.8, idx 8.   thickness 1
0.9865105800157065: rounded 1, idx 10.   thickness 1

The first is a raw canvas zoom. We round to 1 decimal place. But I think 10 buckets and a max thickness of 5 causes half the buckets to look the same. I'll play around with this...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why the first formula was so complicated. I rewrote it to just take linear step sizes as you zoom in/out. Now the thickness is actually different for each zoom level, and the effect looks good.

0.04989743996161477: rounded 0, idx 0.   thickness 5
0.10543709075265414: rounded 0.1, idx 1.   thickness 4.6
0.15114338288839774: rounded 0.2, idx 2.   thickness 4.2
0.2526185331120044: rounded 0.3, idx 3.   thickness 3.8
0.356112087542117: rounded 0.4, idx 4.   thickness 3.4
0.45654925750906206: rounded 0.5, idx 5.   thickness 3
0.600205398261662: rounded 0.6, idx 6.   thickness 2.6
0.6843547482453398: rounded 0.7, idx 7.   thickness 2.2
0.846099434807287: rounded 0.8, idx 8.   thickness 1.7999999999999998
0.8508365622424429: rounded 0.9, idx 9.   thickness 1.4
1.005977672452568: rounded 1, idx 10.   thickness 1

Why 10 buckets and a max scale of 5? Not actually sure, I think I previously just tuned this to look decent. It still looks OK for all the use cases so far. Can adjust and maybe make configurable per use, though I don't know if that's necessary.

/// ... But not yet. As an approximation of that, just discretize zoom into 10 buckets. Also,
/// specify the behavior when barely unzoomed or zoomed in -- the shape starts being drawn in
/// map-space "normally" without a constant screen-space size.
pub struct DrawUnzoomedShapes {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By "unzoomed" shapes, I am assuming the coordinates of the geometries stored in DrawUnzoomedShapes are with respect to zoom == 1.0, and then we scale them up/down based on how far the current zoom is from 1.0.

Is that right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, in reading the implementation, I guess it's just the thickness that gets scaled - not the coords, which are WRT the map space axes.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote to hopefully be more clear. We're not just storing the full geometry and scaling it; we could do that already with the current shader. We're just scaling circle radius or line thickness.

@dabreegster
Copy link
Collaborator Author

Thanks for the reviews!

@dabreegster dabreegster merged commit 48ed368 into master Oct 19, 2021
@dabreegster dabreegster deleted the lines_api branch October 19, 2021 00:55
// zoom ranges between [0.0, 1.0], and we want thicker shapes as zoom approaches 0.
let max = 5.0;
// So thickness ranges between [1.0, 5.0]
let thickness = 1.0 + (max - 1.0) * (1.0 - zoom);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much simpler!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants