/* This program 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 3 of
the License, or (at your option) any later version.
This program 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package org.opentripplanner.common.geometry;
import java.util.ArrayList;
import java.util.List;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.LineSegment;
import com.vividsolutions.jts.geom.LineString;
/**
* A spatial index using a 2D hashtable that wraps around at the edges.
* Mapping geographic space to a region near the origin using the modulo operator
* preserves proximity allowing 100 percent recall (no false dismissals) in linear time,
* at the cost of false positives (hash collisions) which are filtered out of query results.
*
* 2d objects are handled by 'conservatively' rasterizing them into grid cells.
* It would also be possible to use a hierarchical grid.
*
* For more background see:
* Schornbaum, Florian. Hierarchical Hash Grids for Coarse Collision Detection (2009)
*
* @author andrewbyrd
*
* @param <T> The type of objects to be stored in the HashGrid. Must implement the Pointlike
* interface.
*/
public class HashGrid {
// private static final Logger LOG = LoggerFactory.getLogger(HashGrid.class);
private static final double PROJECTION_MERIDIAN = -120;
private final double binSizeMeters;
private final int nBinsX, nBinsY;
private final List<Object>[][] bins;
private int nBins = 0;
private int nEntries = 0;
private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance();
@SuppressWarnings("unchecked")
public HashGrid(double binSizeMeters, int xBins, int yBins) {
if (binSizeMeters < 0)
throw new IllegalStateException("bin size must be positive.");
this.binSizeMeters = binSizeMeters;
this.nBinsX = xBins;
this.nBinsY = yBins;
bins = (List<Object>[][]) new List[xBins][yBins];
}
public void put(Coordinate c, Object t) {
bin(c).add(t);
nEntries +=1;
}
private List<Object> bin(Coordinate c) {
int xBin = xBin(c);
int yBin = yBin(c);
return bin(xBin, yBin);
}
private List<Object> bin(int xBin, int yBin) {
List<Object> bin = bins[xBin][yBin];
if (bin == null) {
bin = new ArrayList<Object>();
bins[xBin][yBin] = bin;
nBins += 1;
}
return bin;
}
// TODO pull class out to application
public static class RasterizedSegment extends LineSegment {
public final Object payload;
public final double distAlongLinestring;
RasterizedSegment(Coordinate c0, Coordinate c1, double d, Object t) {
super(c0, c1);
distAlongLinestring = d;
payload = t;
}
}
// TODO pull method out
public void rasterize(LineString ls, Object t) {
Coordinate [] coords = ls.getCoordinates();
double dist = 0;
for (int i = 0; i < coords.length - 1; i++) {
Coordinate c0 = coords[i];
Coordinate c1 = coords[i+1];
RasterizedSegment rs = new RasterizedSegment(c0, c1, dist, t);
rasterize(c0, c1, rs);
dist += distanceLibrary.fastDistance(coords[i], coords[i+1]);
}
}
public int rasterize(Coordinate a, Coordinate b, Object obj) {
int n = 0;
Coordinate c0, c1;
// we will work in direction of increasing x
if (a.x <= b.x) {
c0 = a;
c1 = b;
} else {
c0 = b;
c1 = a;
}
double x0, y0, x1, y1;
x0 = projLon(c0.x, c0.y);
y0 = projLat(c0.y);
x1 = projLon(c1.x, c1.y);
y1 = projLat(c1.y);
// get slope of line segment to rasterize
double slope = (y1 - y0) / (x1 - x0);
boolean negSlope = slope < 0;
// reference point is left edge of the initial bin
double xStartBinFloor = Math.floor(x0 / binSizeMeters) * binSizeMeters;
double yStartBinFloor = Math.floor(y0 / binSizeMeters) * binSizeMeters;
if (negSlope)
yStartBinFloor += binSizeMeters;
double yBinFloor = yStartBinFloor;
double x = x0;
double y = y0;
// step forward bin by bin
int xSteps = 0, ySteps = 0;
int xBin = xBin(c0);
int yBin = yBin(c0);
boolean done = false;
while ( ! done) {
xSteps += 1;
// place x marker at right edge of the current bin column
x = xStartBinFloor + xSteps * binSizeMeters;
// check for final iteration
if (x >= x1) {
x = x1;
y = y1; // set y (undefined slope in vertical case)
done = true;
} else {
// find y value at current x (usually the right edge of the bin)
y = (x-x0) * slope + y0;
}
// if new y is in a different bin row, advance to that bin row
if (negSlope) {
while (yBinFloor - binSizeMeters > y) {
bin(xBin, yBin).add(obj);
ySteps += 1;
yBinFloor = yStartBinFloor - ySteps * binSizeMeters;
// advance down to next row of bins
yBin = yWrap(yBin - 1);
}
} else {
while (yBinFloor + binSizeMeters < y) {
bin(xBin, yBin).add(obj);
ySteps += 1;
yBinFloor = yStartBinFloor + ySteps * binSizeMeters;
// advance up to next row of bins
yBin = yWrap(yBin + 1);
}
}
// insert object even if we did not advance to another row
bin(xBin, yBin).add(obj);
// advance to next column of bins
xBin = xWrap(xBin + 1);
}
return n;
}
private int xBin(Coordinate c) {
double x = projLon(c.x, c.y);
return xWrap( (int) Math.floor(x / binSizeMeters) );
}
private int yBin(Coordinate c) {
double y = projLat(c.y);
return yWrap( (int) Math.floor(y / binSizeMeters) );
}
private int xWrap(int xBin) {
int x = xBin % nBinsX;
if (x < 0)
x += nBinsX;
return x;
}
private int yWrap(int yBin) {
int y = yBin % nBinsY;
if (y < 0)
y += nBinsY;
return y;
}
public Object closest(double x, double y, double radiusMeters) {
return closest(new Coordinate(x, y), radiusMeters);
}
public Object closest(Coordinate c, double radiusMeters) {
Object closestT = null;
double closestDistance = Double.POSITIVE_INFINITY;
int radiusBins = (int) Math.ceil(radiusMeters / binSizeMeters);
int xBin = xBin(c);
int yBin = yBin(c);
int minBinX = xBin - radiusBins;
int maxBinX = xBin + radiusBins;
int minBinY = yBin - radiusBins;
int maxBinY = yBin + radiusBins;
for (int x = minBinX; x <= maxBinX; x += 1) {
int wrappedX = xWrap(x);
for (int y = minBinY; y <=maxBinY; y += 1) {
int wrappedY = yWrap(y);
List<Object> bin = bins[wrappedX][wrappedY];
if (bin != null) {
for (Object t : bin) {
//if (t == p)
// continue;
// FIXME
double distance = distanceLibrary .fastDistance(c, c);
// bins may contain distant colliding objects
if (distance > radiusMeters)
continue;
if (distance < closestDistance) {
closestT = t;
closestDistance = distance;
}
}
}
}
}
return closestT;
}
public List<Object> query (double lon, double lat, double radiusMeters) {
Coordinate c = new Coordinate(lon, lat);
List<Object> ret = new ArrayList<Object>();
int radiusBins = (int) Math.ceil(radiusMeters / binSizeMeters);
int xBin = xBin(c);
int yBin = yBin(c);
int minBinX = xBin - radiusBins;
int maxBinX = xBin + radiusBins;
int minBinY = yBin - radiusBins;
int maxBinY = yBin + radiusBins;
for (int x = minBinX; x <= maxBinX; x += 1) {
int wrappedX = xWrap(x);
for (int y = minBinY; y <=maxBinY; y += 1) {
int wrappedY = yWrap(y);
List<Object> bin = bins[wrappedX][wrappedY];
if (bin != null) {
ret.addAll(bin);
}
}
}
return ret;
}
static final double M_PER_DEGREE_LAT = 111111.111111;
static double mPerDegreeLon(double lat) {
return M_PER_DEGREE_LAT * Math.cos(lat * Math.PI / 180);
}
private static double projLat(double lat) {
return M_PER_DEGREE_LAT * lat;
}
private static double projLon(double lon, double lat) {
return mPerDegreeLon(lat) * (lon - PROJECTION_MERIDIAN);
}
public String toString() {
return String.format("HashGrid %dx%d at %d meter resolution, %d/%d bins allocated",
nBinsX, nBinsY, (int)binSizeMeters, nBins, nBinsX*nBinsY);
}
public String toStringVerbose() {
return String.format("HashGrid %dx%d at %d meter resolution, %d/%d bins allocated (%4.1f%%), load factor %4f, average allocated bin size %2f",
nBinsX, nBinsY, (int)binSizeMeters, nBins, nBinsX*nBinsY, (double)nBins/(nBinsX*nBinsY) * 100.0,
(double)nEntries/(nBinsX*nBinsY), (double)nEntries/nBins);
}
public String densityMap() {
final int SATURATE_AT = 10;
String[] block = new String[] {"░░", "▒▒", "▓▓", "██"};
StringBuilder sb = new StringBuilder();
sb.append("HashGrid:\n");
// flip map for northern hemisphere
for (int y=nBinsY-1; y>=0; y--) {
for (int x=0; x<nBinsX; x++) {
if (bins[x][y] == null) {
sb.append(" ");
} else {
int z = bins[x][y].size();
int i = z * 3 / SATURATE_AT;
if (i > 3)
i = 3;
sb.append(block[i]);
}
}
sb.append('\n');
}
return sb.toString();
}
}