14

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.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
mlkwrz
  • 189
  • 1
  • 13
  • 4
    The obvious catch is that it's incredibly hard to define "fairly straightforward user experience". I think you need to add more detail to that to get good responses. (For example one of the criteria could be to make sure that all the rectangles are reachable from any other.) – biziclop Nov 16 '12 at 16:45

4 Answers4

6

[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.

j_random_hacker
  • 50,331
  • 10
  • 105
  • 169
  • 1
    I don't think your weight scaling scheme is sufficient. Can't you still end up with cycles smaller than the full set of rectangles? If your method worked, it could be used to find a Hamiltonian cycle in polynomial time, and this problem is NP complete. – Gene May 20 '13 at 16:16
  • @Gene: You're absolutely right! The weighting scheme will only guarantee that every vertex is connected to some other vertex, which is a much weaker property. I'll edit to update. – j_random_hacker May 21 '13 at 08:29
  • "You are free to choose the form for this scoring function..." Unfortunately, the scoring function is the one I want. Otherwise, this is a comprehensive answer and well thought through. The only things it's missing is the magic sauce. – Oliver Moran May 21 '13 at 21:37
  • @OliverMoran: I've added an example scoring function. There's an inevitable tradeoff between the importance of the distance between 2 rectangles and the importance of the angle between them -- this can be controlled by varying `Z`. – j_random_hacker May 24 '13 at 11:39
3

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

Gene
  • 46,253
  • 4
  • 58
  • 96
  • "As a user I'd really like this scheme. But YMMV." — The approach here is not suitable for my context of use. However, the question was about user experience and the answer above is the one attempts to address it that most. So, I've given the 100 reputation points to Gene for this answer. – Oliver Moran May 21 '13 at 21:34
2

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.

Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • That wouldn't really be an algorithm, since the navigation path would be predetermined. The "rectangles of random sizes and positions" would be rectangles of known sizes and positions instead. – Oliver Moran May 14 '13 at 16:51
  • @OliverMoran: I thought you meant they would be generated once & then not change. How often do they change? if it's not too often then re-run the above whenever something moves. – Claudiu May 14 '13 at 17:33
  • @Claudia, imagine icons on a desktop. TBH, even at that, the answer is fairly naive (sorry!). For example, what is "the nearest rectangle, in the given direction, whose center point is the middle of the current rectangle's side". Target rectangles will be at different distances and different angles from the current rectangle. Which should we prioritise: angle or distance? What if neither is entirely satisfactory: how do we balance angle against distance? These are questions that are easy for a human to answer but difficult to explain exactly. That exact explanation is the algorithm. – Oliver Moran May 14 '13 at 18:43
  • @OliverMoran: this is something you'll have to implement different things to try and simply see what works best. I thought about it a little bit when positing this algorithm and it seemed that this would work well (combined with the 'eliminate loops' step). in this algorithm you would use the distance from *the center point of the potential target rectangle* to *the point that is the middle of the current rectangle's side*. the angle is implicit in that (i.e. it finds the nearest rect according to concentric semicircles emanating from the source rectangle's side). – Claudiu May 14 '13 at 19:54
  • @OliverMoran: so a rectangle straight above the source, 100 pixels away, is weighted the same as a rectangle 70 pixels to the right and 70 pixels up (45 degrees to the side). thus it is a very specific, exact, and machine-understandable algorithm. yes, naive, but those often work well! maybe that translates to a nice human experience. why not try it and see how it works? – Claudiu May 14 '13 at 19:55
  • @Claudui, I have been working on an algorithm like this for a few days: trying different approaches and seeing what works well. What I have is OK but it's a non-trivial problem. It is surprisingly complex, I promise you. I placed a bounty on the question because it is a problem that has been solved before. I would like a proven answer: a tried-and-tested algorithm that ends in a high-quality user experience. I can try stuff out and see "what works best" — but that's not the question. What I am asking for here is already-existing algorithm. One that is known to work well. – Oliver Moran May 14 '13 at 21:26
  • @OliverMoran: gotcha, I understand. This answer is certainly not that. I also just realized you're not the person who asked the question. I'm curious to see what the best answer will be. – Claudiu May 14 '13 at 22:18
0

This problem can be modeled as a graph problem and algorithm of navigation can be used as a shortest path routing.

Here is the modelling.

Each rectangle is a vertex in the graph. From each vertex (aka rectangle) you have four options - up, down, left, right. So, you can reach four different rectangles, i.e this vertex will have four neighbors and you add these edges to graph.

I am not sure if this is part of the problem -- "multiple rectangles can be reached from a rectangle using a particular action (e.g. up)". If not, the above modelling is good enough. If yes, then add all such vertices as the neighbor for this vertex. Therefore, you may not end up with a 4-regular graph. Otherwise, you will model your problem into a 4-regular graph.

Now, the question is how do you define your "navigation" algorithm. If you do not want to distinguish between your actions, i.e. if up, down, left, and right are all equal, then you can add weight of 1 to all edges.

If you decide to give a particular action more precedence than other, say up is better than the rest, then you can give weight for edges resulted from up movement as 1, and the remaining edges as 2. The idea is by assigning different weights you can distinguish between the edges you will travel.

If you decide that all up edges are not equal, i.e. the up distance between A and B, is shorter than the up distance between C and D, then you can accordingly assign weights to the edges during the graph construction process.

Here is the routing

Now how to find the route -- You can use dijkstra's algorithm to find a shortest path between a given pair of vertices. If you are interested in multiple shortest paths, you can use k-shortest path algorithm to find k shortest paths between a pair of node, and then pick your best path.

Note that the graph you end up with does not have to be a directed graph. If you prefer a directed graph, you can assign directions to the edges when you are constructing them. Otherwise you should be good using an undirected graph, as all you care is to use an edge to reach a vertex from another. Moreover, if rectangle A can be reached using up from rectangle B, then rectangle B can be reached using down from rectangle A. Thus, directions really do not matter, if you do not need them for other reasons. If you do not like the assumption I just made, then you need to construct a directed graph.

Hope this helps.

Bill
  • 5,263
  • 6
  • 35
  • 50
  • I don't see how finding the shortest path between a pair of vertices helps. What s/he wants to do is build a function f(v, d) that returns the rectangle that the user will be moved to if they are at rectangle v and press direction d (up, down, left or right). This function should be "nice", e.g. pressing "left" should probably take you to a rectangle to the left of the current one, and all rectangles should be reachable. – j_random_hacker May 15 '13 at 23:13
  • @j_random_hacker I may not have properly understood your comment, I apologize in advance. If the objective is to find set of moves to reach a destination rectangle from a source rectangle, then the solution is the shortest path between source and destination rectangles. Each edge taken in the route can be mapped to a direction, i.e. up/down/left/right from the current rectangle. – Bill May 15 '13 at 23:17
  • 1
    One of us has misunderstood the OP's question... I think it's you, but I could be wrong :-P I think what they want to do is figure out what the program *ought* to do when the user presses one of the 4 direction keys at a particular rectangle. – j_random_hacker May 15 '13 at 23:29