user30189
user30189

Reputation: 169

Fastest way to check which rectangle is clicked in a list of rectangles

I have a rectangle Object with x, y, width and height. I have a list of these rectangles which are displayed on a screen. It is guaranteed that none of them overlap. Given a user's click position (x and y coordinates), I want to see which of these rectangles were clicked (since they do not overlap, there is a maximum of one rect that can be clicked).

I can obviously look through all of them and check for each one if the user clicked it but this is very slow because there are many on the screen. I can use some kind of comparison to keep the rectangles sorted when I insert a new one into the list. Is there some way to use something similar to binary search in order to decrease the time it takes to find which rect was clicked?

Note: the rectangles can be any size. Thanks:)

Edit: To get an idea of what I am making visit koalastothemax.com

Upvotes: 1

Views: 600

Answers (3)

sprinter
sprinter

Reputation: 27986

I have tackled a very similar problem in the past when developing a simulation. In my case the coordinates were doubles (so no integer indexing was possible) and there could be hundreds of millions of them that needed to be searched.

My solution was to create an Axis class to represent each axis as a sequence of ranges. The ranges were guaranteed to go from a minimum to a maximum and the class was smart enough to split itself into pieces when new ranges were added. Each range has a single generic object stored. The class used a binary search to find a range quickly.

So roughly the class looks like:

class Axis<T> {
    public Axis(double min, double max, Supplier<T> creator);
    public Stream<T> add(double from, double to);
    public T get(double coord);
}

The add method needs to return a stream because the added range may cover several ranges.

To store rectanges:

Axis<Axis<Rectangle>> rectanges = new Axis<>(0.0, 100.0, 
    () -> new Axis<>(0.0, 100.0, Rectangle::new));

rectangles.add(x, x + w).forEach(r -> r.add(y, y + h).forEach(Rectangle::setPresent));

And to find a rectangle:

rectangles.get(x).get(y);

Note that there's always an object stored so you need a representation such as Rectangle.NULL for 'not present'. Or you could make it Optional<Rectangle> (though that indirection eats a lot of memory and processing for large numbers of rectangles).

I've just given the high level design here rather than any implementation details so let me know if you want more info on how to make it work. Getting the logic right on the range splits is not trivial. But I can guarantee that it's very fast even with very large numbers of rectangles.

Upvotes: 1

CodeMonkey
CodeMonkey

Reputation: 1835

It highly depends upon your application and details we're not quite aware of yet for what the best solution would be. BUT, with as little as I know, I'd say you can make a 2D array that points to your rectangles. That 2D array would map directly to the pixels on the screen. So if you make the array 10x20, then the coordinate x divided by screen width times 10 (casted to int) will be the first index and y divided screen height times 20 would be your y index. With your x and y index, you can map directly to the rectangle that it points to. Some indexes might be empty and some might point to more than one rectangle if they're not perfectly laid out, but that seems the easiest way to me without knowing much about the application.

Upvotes: 1

vandench
vandench

Reputation: 2257

The fastest way I can come up with is definitely not the most memory efficient. This works by exploiting the fact that an amortized hash table has constant lookup time. It will map every point that a rectangle has to that rectangle. This is only really effective if your are using integers. You might be able to get it to work with floats if you use a bit of rounding.

Make sure that the Point class has a hash code and equals function.

public class PointCheck
{
    public Map<Point, Rect> pointMap;

    public PointCheck()
    {
        pointMap = new HashMap<>();
    }

    /**
     *  Map all points that contain the rectangle
     * to the rectangle.
     */
    public void addRect(Rect rect)
    {
        for(int i = rect.x; i < rect.x + rect.width; ++i)
        {
            for(int j = rect.y; j < rect.y + rect.height; ++i)
            {
                pointMap.put(new Point(i, j), rect);
            }
        }
    }

    /**
     *  Returns the rectangle clicked, null
     * if there is no rectangle.
     */
    public Rect checkClick(Point click)
    {
        return pointMap.get(click);
    }
}

Edit: Just thought I should mention this: All of the rectangles held in the value of the hash map are references to the original rectangle, they are not clones.

Upvotes: 0

Related Questions