Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Four-way navigation algorithm

Consider a rectangular shaped canvas, containing rectangles of random sizes and positions. To navigate between these rectangles, a user can use four arrows: up, down, left, right.

Are you familiar with any algorithm of navigation that would produce a fairly straightforward user experience?

I came across a few solutions but none of them seemed suitable. I am aware that no solution will be "ideal". However, the kind of algorithm I am looking for is the sort used to navigate between icons on a desktop using only the arrow keys.

like image 439
mlkwrz Avatar asked Nov 16 '12 16:11

mlkwrz


3 Answers

[EDIT 21/5/2013: As pointed out by Gene in a comment, my weighting scheme actually does not guarantee that every rectangle will be reachable from every other rectangle -- only that every rectangle will be connected to some other rectangle in each direction.]

A nice way to do this is using maximum weighted bipartite matching.

What we want to do is build a table defining a function f(r, d) that returns the rectangle that the user will be moved to if they are currently at rectangle r and hit direction d (up, down, left or right). We would like this function to have some nice properties, such as:

  1. It must be possible to reach every rectangle from every other rectangle
  2. Pressing left then right or vice versa, or up then down or vice versa, should leave the user in the same place
  3. Pressing e.g. left should take the user to a rectangle to the left (this is a bit more difficult to state precisely, but we can use a scoring system to measure the quality)

For each rectangle, create 4 vertices in a graph: one for each possible key that could be pressed while at that rectangle. For a particular rectangle r, call them rU, rD, rL and rR. For every pair of rectangles r and s, create 4 edges:

  • (rU, sD)
  • (rD, sU)
  • (rL, sR)
  • (rR, sL)

This graph has 2 connected components: one contains all U and D vertices, and the other contains all L and R vertices. Each component is bipartite, because e.g. no U vertex is ever connected to another U vertex. We could in fact run maximum weighted bipartite matching on each component separately, although it's easier just to talk about running it once on the entire graph after grouping, say, U vertices with L vertices and D vertices with R vertices.

Assign each of these edges a nonnegative weight according to how much sense it makes for that pair of rectangles to be connected by that pair of keys. You are free to choose the form for this scoring function, but it should probably be:

  • inversely proportional to the distances between the rectangles (you could use the distance between their centres), and
  • inversely proportional to how far the angle between the centres of the rectangles differs from the desired horizontal or vertical line, and
  • zero whenever the rectangles are oriented the wrong way (e.g. if for the edge (rU, sD) if the centre of r is actually above the centre of s). Alternatively, you can just delete these zero-weight edges.

This function attempts to satisfy requirement 3 at the top.

[EDIT #2 24/5/2013: Added an example function below.]

Here is C-ish pseudocode for an example function satisfying these properties. It takes the centre points of 2 rectangles and the direction from rectangle 1 (the direction from rectangle 2 is always the opposite of this direction):

const double MAXDISTSQUARED = /* The maximum possible squared distance */;
const double Z = /* A +ve number. Z > 1 => distance more important than angle */

// Return a weight in the range [0, 1], with higher indicating a better fit.
double getWeight(enum direction d, int x1, int y1, int x2, int y2) {
    if (d == LEFT  && x1 < x2 ||
        d == RIGHT && x1 > x2 ||
        d == UP    && y1 < y2 ||
        d == DOWN  && y1 > y2) return 0;

    // Don't need to take sqrt(); in fact it's probably better not to
    double distSquared = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
    double angle = abs(atan2(x1 - x2, y1 - y2));   // 0 => horiz; PI/2 => vert
    if (d == UP || d == DOWN) angle = PI / 2 - angle;
    return 1 - pow(distSquared / MAXDISTSQUARED, Z) * (2 * angle / PI);
}

Now run maximum weighted bipartite matching. This will attempt to find the set of edges having highest total weight such that every vertex (or at least as many as possible) are adjacent to a selected edge, but no vertex is adjacent to more than one edge. (If we allowed a vertex to be adjacent to more than one edge, it would mean that pressing that key while at that rectangle would take you to more than one destination rectangle, which doesn't make sense.) Each edge in this matching corresponds to a bidirectional pair of keypresses, so that pressing e.g. up and then down will take to back to where you were, automatically satisfying requirement 2 at the top.

The only requirement not automatically satisfied by this approach so far is the important one, number 1: it does not necessarily guarantee that every rectangle will be reachable. If we just use the "raw" quality scores as edge weights, then this can actually occur for certain configurations, e.g. when there is one rectangle in each of the 4 corners of the screen, plus one at the centre, the centre one might be unreachable.

[EDIT 21/5/2013: As Gene says, my claim below that property 1 is satisfied by the new weighting scheme I propose is wrong. In many cases every rectangle will be reachable, but in general, you need to solve the NP-hard Hamiltonian Cycle problem to guarantee this. I'll leave the explanation in as it gets us some of the way there. In any case it can be hacked around by adjusting weights between connected components upward whenever subcycles are detected.]

In order to guarantee that the matching algorithm always returns a matching in which every rectangle is reachable, we need to adjust the edge weights so that it is never possible for a matching to score higher than a matching with more edges. This can be achieved by scaling the scoring function to between 0 and 1, and adding the number of rectangles, n, to each edge's weight. This works because a full matching then has score at least 4n^2 (i.e. even if the quality score is 0, the edge itself has a weight of n and there are 4n of them), while any matching with fewer edges has score at most 4(n-1)(n+1) = 4n^2 - 4, which is strictly less.

like image 95
j_random_hacker Avatar answered Nov 07 '22 08:11

j_random_hacker


It's a truism that to a person with a hammer everything looks like a nail. Shortest path algorithms are an obvious tool here because shortest distance seems intuitive.

However we are designing a UI where logical distance is much more important than physical distance.

So let's try thinking differently.

One constraint is that repeatedly hitting the up (right, down or left) arrow ought to eventually cycle through all the rectangles. Otherwise some unreachable "orphans" are likely. Achieving this with an algorithm based on physical (2d) distance is difficult because the closest item in 2d might be in the wrong direction in the 1d projection corresponding to the arrow pair being used. I.e. hitting the up arrow could easily select a box below the current. Ouch.

So let's adopt an extremely simple solution. Just sort all the rectangles on the x-coordinate of their centroids. Hitting the right and left arrow cycles through rectangles in this order: right to the next highest x and left to the next lowest x. Wrap at the screen edges.

Also do the same with y-coordinates. Using up and down cycles in this order.

The key (pun intended) to success is adding dynamic information to the screen while cycling to show the user the logic of what is occurring. Here is a proposal. Others are possible.

At first vertical (up or down) key, a pale translucent overlay appears over the rectangles. They are shaded pale red or blue in a pattern that alternates by y coordinate of centroid. There are also horizontal hash marks of matching color across the entire window. The only reason for two colors is to provide a visual indicator of correspondence between lines and rectangles. The currently selected rectangle is non-translucent and the hash mark is brighter than all the others. When you continue to hit the up or down key, the highlighted box changes in the centroid y-order as described above. The overlay disappears when no arrow key has been struck for a half second or so.

A very similar overlay appears if a horizontal key is hit, only it's vertical hash marks and x-order.

As a user I'd really like this scheme. But YMMV.

The algorithm and data structures needed to implement this are obvious, trivial, and scale very well. The effort will go into making the overlays look good.

NB Now that I have done all the drawings I realize it would be a good idea to place a correctly colored dot at the centroid of each box to show which of the lines is intersecting it. Some illustrative diagrams follow.

Bare Boxes

Bare boxes

Selection with up or down arrow in progress

Selection with up or down arrow

Selection with left or right arrow in progress

Selection with left or right arrow

like image 29
Gene Avatar answered Nov 07 '22 08:11

Gene


What about building a movement graph as follows:

  • for any direction, try to go to the nearest rectangle, in the given direction, whose center point is the middle of the current rectangle's side.
  • try to eliminate loops, e.g. moving 'right' from A should try to yield a different rectangle than moving 'up-right' from A. For example in this drawing, the 'right' from green should be orange, even though pink would be the nearest mid-point
  • (Thanks to biziclop): if any rectangles aren't reachable in the graph, then re-map one of the adjoining rectangles to get to it, likely the one with the least error. Repeat until all rectangles are reachable (I think that algorithm would terminate...)

Then store the graph and only use that to navigate. You don't want to change the directions in the middle of the session.

like image 2
Claudiu Avatar answered Nov 07 '22 08:11

Claudiu