From 2b6901164990aa86a87df9b1596a42faeee6476e Mon Sep 17 00:00:00 2001 From: mdb Date: Sat, 15 Dec 2001 03:19:03 +0000 Subject: [PATCH] Implemented a class (and a unit test) that can quickly locate the nearest coordinate-having object to a specified coordinate from a set of such objects. It does so using an efficient algorithm so that we can do things like highlight the object nearest the mouse cursor. git-svn-id: https://samskivert.googlecode.com/svn/trunk@516 6335cc39-0255-0410-8fd6-9bcaacd3b74c --- .../swing/util/ProximityTracker.java | 293 ++++++++++++++++++ .../swing/util/ProximityTrackerTest.java | 104 +++++++ 2 files changed, 397 insertions(+) create mode 100644 projects/samskivert/src/java/com/samskivert/swing/util/ProximityTracker.java create mode 100644 projects/samskivert/tests/src/java/com/samskivert/swing/util/ProximityTrackerTest.java diff --git a/projects/samskivert/src/java/com/samskivert/swing/util/ProximityTracker.java b/projects/samskivert/src/java/com/samskivert/swing/util/ProximityTracker.java new file mode 100644 index 00000000..d4dea4b3 --- /dev/null +++ b/projects/samskivert/src/java/com/samskivert/swing/util/ProximityTracker.java @@ -0,0 +1,293 @@ +// +// $Id: ProximityTracker.java,v 1.1 2001/12/15 03:19:03 mdb Exp $ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.swing.util; + +import com.samskivert.util.StringUtil; + +/** + * A mechanism for registering a set of objects with x and y screen + * coordinates and then efficiently determining which of those objects is + * closest to a given screen coordinate. Useful for highlighting the + * object closest to the mouse and that sort of thing. The data structure + * and algorithm for tracking and looking up the closest object are highly + * optimized so that one can track large numbers of objects and obtain the + * most proximous one with frequencies on par with the frequencies of + * mouse moved events. + */ +public class ProximityTracker +{ + /** + * Creates a proximity tracker with the default initial capacity. + */ + public ProximityTracker () + { + this(DEFAULT_INIT_CAPACITY); + } + + /** + * Creates a proximity tracker with the specified initial capacity. + */ + public ProximityTracker (int initialCapacity) + { + _records = new Record[initialCapacity]; + } + + /** + * Adds an object to the tracker. + */ + public void addObject (int x, int y, Object object) + { + Record record = new Record(x, y, object); + + // if this is the very first element, we have to insert it + // straight away because our binary search algorithm doesn't work + // on empty arrays + if (_size == 0) { + _records[_size++] = record; + return; + } + + // figure out where to insert it + int ipoint = binarySearch(x); + + // expand the records array if necessary + if (_size >= _records.length) { + int nsize = _size*2; + Record[] records = new Record[nsize]; + System.arraycopy(_records, 0, records, 0, _size); + _records = records; + } + + // shift everything down + if (ipoint < _size) { + System.arraycopy(_records, ipoint, _records, ipoint+1, + _size-ipoint); + } + + // insert the record + _records[ipoint] = record; + _size++; + } + + /** + * Removes from the tracker the object that is referentially equal to + * (o1 == object) the specified object. + * + * @return true if an object was located and removed, false if not. + */ + public boolean removeObject (Object object) + { + for (int i = 0; i < _size; i++) { + if (_records[i].object == object) { + // shift everything down + System.arraycopy(_records, i+1, _records, i, _size-(i+1)); + // clear out the trailing reference + _records[--_size] = null; + return true; + } + } + + return false; + } + + /** + * Removes from the tracker the object that is equal to + * (o1.equals(object)) the specified object. + * + * @return true if an object was located and removed, false if not. + */ + public boolean removeObjectEquals (Object object) + { + for (int i = 0; i < _size; i++) { + if (_records[i].object.equals(object)) { + // shift everything down + System.arraycopy(_records, i+1, _records, i, _size-(i+1)); + // clear out the trailing reference + _records[--_size] = null; + return true; + } + } + + return false; + } + + /** + * Returns the object nearest to the supplied coordinates. If + * distance is non-null and at least one element in + * length, the actual between the supplied coordinates and the object + * coordinates will be filled into the first element of the array. + * + * @return the object nearest the supplied coordinates or null if the + * tracker contains no objects. + */ + public Object findClosestObject (int x, int y, int[] distance) + { + // make sure we're tracking at least one object + if (_size == 0) { + return null; + } + + // locate the object nearest the x coordinate + int sr = binarySearch(x), sl = sr-1; + int mindist = Integer.MAX_VALUE, minidx = -1; + + // we search outward from the nearest x coordinate, looking for + // the nearest object and refining the bounds of our search each + // time we find a nearer object + for (boolean expanded = true; expanded;) { + expanded = false; + + // look to the right + if (sr < _size) { + Record rec = _records[sr]; + // we can stop searching in this direction when the + // distance in the x direction becomes larger than the + // known shortest distance + if (rec.x-x < mindist) { + int dist = distance(rec.x, rec.y, x, y); + if (dist < mindist) { + minidx = sr; + mindist = dist; + } + // move to the next element + sr += 1; + expanded = true; + } + } + + // look to the left + if (sl >= 0) { + Record rec = _records[sl]; + // we can stop searching in this direction when the + // distance in the x direction becomes larger than the + // known shortest distance + if (x-rec.x < mindist) { + int dist = distance(rec.x, rec.y, x, y); + if (dist < mindist) { + minidx = sl; + mindist = dist; + } + // move to the next element + sl -= 1; + expanded = true; + } + } + } + + // as we ensured above that there was at least one element in our + // array, we are required to have found some element as the + // closest element to the given point. if we didn't, we're hosed! + if (minidx == -1) { + throw new RuntimeException("Proximity algorithm failed!"); + } + + // communicate the minimum distance back to the caller + if (distance != null && distance.length > 0) { + distance[0] = mindist; + } + + return _records[minidx].object; + } + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + return "[size=" + _size + + ", elems=" + StringUtil.toString(_records) + "]"; + } + + /** + * Computes the geometric distance between the supplied two points. + */ + public static int distance (int x1, int y1, int x2, int y2) + { + int dx = x1-x2, dy = y1-y2; + return (int)Math.sqrt(dx*dx+dy*dy); + } + + /** + * Returns the index of a record with the specified x coordinate, or a + * value representing the index where such a record would be inserted + * while preserving the sort order of the array. Doesn't work if the + * records array contains zero elements. + */ + protected int binarySearch (int x) + { + // copied from java.util.Arrays which I wouldn't have to have done + // had the provided a means by which to binarySearch in a subset + // of an array. alas. + int low = 0; + int high = _size-1; + + while (low <= high) { + int mid = (low + high) >> 1; + int cmp = (_records[mid].x - x); + + if (cmp < 0) { + low = mid + 1; + } else if (cmp > 0) { + high = mid - 1; + } else { + return mid; // key found + } + } + + return low; // key not found + } + + /** + * This is used to track our object records. + */ + protected static class Record + { + /** The x coordinate of the object. */ + public int x; + + /** The y coordinate of the object. */ + public int y; + + /** The object itself. */ + public Object object; + + public Record (int x, int y, Object object) + { + this.x = x; + this.y = y; + this.object = object; + } + + public String toString () + { + return "[x=" + x + ", y=" + y + "]"; // + ", object=" + object + "]"; + } + } + + /** The object records. */ + protected Record[] _records; + + /** The number of records being tracked. */ + protected int _size; + + /** Assume a non-trivial number of objects will be tracked. */ + protected static final int DEFAULT_INIT_CAPACITY = 16; +} diff --git a/projects/samskivert/tests/src/java/com/samskivert/swing/util/ProximityTrackerTest.java b/projects/samskivert/tests/src/java/com/samskivert/swing/util/ProximityTrackerTest.java new file mode 100644 index 00000000..95f9a559 --- /dev/null +++ b/projects/samskivert/tests/src/java/com/samskivert/swing/util/ProximityTrackerTest.java @@ -0,0 +1,104 @@ +// +// $Id: ProximityTrackerTest.java,v 1.1 2001/12/15 03:19:03 mdb Exp $ +// +// samskivert library - useful routines for java programs +// Copyright (C) 2001 Michael Bayne +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.samskivert.swing.util; + +import java.awt.Point; +import java.util.ArrayList; +import java.util.Random; + +import junit.framework.Test; +import junit.framework.TestCase; + +import com.samskivert.Log; +import com.samskivert.util.StringUtil; + +public class ProximityTrackerTest extends TestCase +{ + public ProximityTrackerTest () + { + super(ProximityTrackerTest.class.getName()); + } + + public void runTest () + { + Random rand = new Random(); + ProximityTracker tracker = new ProximityTracker(); + ArrayList points = new ArrayList(); + + // create 100 random points and add them to the tracker and our + // comparison list + for (int i = 0; i < 100; i++) { + int x = rand.nextInt(MAX_X); + int y = rand.nextInt(MAX_Y); + Point p = new Point(x, y); + tracker.addObject(x, y, p); + points.add(p); + } + + // now choose 100 new random points and confirm that the tracker + // reports the same closest point that our brute force check + // reports + for (int i = 0; i < 100; i++) { + int x = rand.nextInt(MAX_X); + int y = rand.nextInt(MAX_Y); + + // get the closest point via the tracker + Point tp = (Point)tracker.findClosestObject(x, y, null); + + // get the closest point via brute force + Point cp = null; + int mindist = Integer.MAX_VALUE; + for (int p = 0; p < points.size(); p++) { + Point hp = (Point)points.get(p); + int dist = ProximityTracker.distance(hp.x, hp.y, x, y); + if (dist < mindist) { + mindist = dist; + cp = hp; + } + } + + // the points might actually be different, but in that case, + // the distances should be equal + int tdist = ProximityTracker.distance(x, y, tp.x, tp.y); + int cdist = ProximityTracker.distance(x, y, cp.x, cp.y); + + String tps = tp.x + "," + tp.y; + String cps = cp.x + "," + cp.y; + String ps = x + "," + y; + assert(ps + " => " + cps + " (" + cdist + ") ! " + + tps + " (" + tdist + ")", tp.equals(cp) || (tdist == cdist)); + } + } + + public static Test suite () + { + return new ProximityTrackerTest(); + } + + public static void main (String[] args) + { + ProximityTrackerTest test = new ProximityTrackerTest(); + test.runTest(); + } + + protected static final int MAX_X = 1000; + protected static final int MAX_Y = 1000; +}