-
Notifications
You must be signed in to change notification settings - Fork 192
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Developed initial HilbertSpaceFillingCurve 2D to 1D mapper
- Loading branch information
Showing
2 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
132 changes: 132 additions & 0 deletions
132
src/main/java/org/neo4j/gis/spatial/index/hilbert/HilbertSpaceFillingCurve.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package org.neo4j.gis.spatial.index.hilbert; | ||
|
||
import org.neo4j.gis.spatial.rtree.Envelope; | ||
|
||
public class HilbertSpaceFillingCurve { | ||
|
||
static class CurveRule { | ||
private int[] npointValues; | ||
private CurveRule[] children = null; | ||
|
||
private CurveRule(int... npointValues) { | ||
this.npointValues = npointValues; | ||
} | ||
|
||
private int indexForNPoint(int npoint) { | ||
for (int index = 0; index < npointValues.length; index++) { | ||
if (npointValues[index] == npoint) { | ||
return index; | ||
} | ||
} | ||
return -1; | ||
} | ||
|
||
public void setChildren(CurveRule... children) { | ||
this.children = children; | ||
} | ||
|
||
public CurveRule childAt(int npoint) { | ||
return children[npoint]; | ||
} | ||
} | ||
|
||
public static final CurveRule curveUp = new CurveRule(0, 1, 3, 2); | ||
public static final CurveRule curveRight = new CurveRule(0, 2, 3, 1); | ||
public static final CurveRule curveLeft = new CurveRule(3, 1, 0, 2); | ||
public static final CurveRule curveDown = new CurveRule(3, 2, 0, 1); | ||
|
||
static { | ||
curveUp.setChildren(curveRight, curveUp, curveUp, curveLeft); | ||
curveRight.setChildren(curveUp, curveRight, curveRight, curveDown); | ||
curveDown.setChildren(curveLeft, curveDown, curveDown, curveRight); | ||
curveLeft.setChildren(curveDown, curveLeft, curveLeft, curveUp); | ||
} | ||
|
||
public static final int MAX_LEVEL = 27; | ||
|
||
private final Envelope range; | ||
private final int maxLevel; | ||
private final long width; | ||
|
||
private double[] scalingFactor; | ||
|
||
public HilbertSpaceFillingCurve(Envelope range) { | ||
this(range, MAX_LEVEL); | ||
} | ||
|
||
public HilbertSpaceFillingCurve(Envelope range, int maxLevel) { | ||
this.range = range; | ||
this.maxLevel = maxLevel; | ||
if (maxLevel < 1) { | ||
throw new IllegalArgumentException("Hilbert index needs at least one level"); | ||
} | ||
if (range.getDimension() != 2) { | ||
throw new IllegalArgumentException("Hilbert index does not yet support more than 2 dimensions"); | ||
} | ||
this.width = (long) Math.pow(2, maxLevel); | ||
this.scalingFactor = new double[range.getDimension()]; | ||
for (int dim = 0; dim < range.getDimension(); dim++) { | ||
scalingFactor[dim] = this.width / range.getWidth(dim); | ||
} | ||
} | ||
|
||
public int getMaxLevel() { | ||
return maxLevel; | ||
} | ||
|
||
public long getWidth() { | ||
return this.width; | ||
} | ||
|
||
public long getMaxValue() { | ||
return (long) Math.pow(2, maxLevel * range.getDimension()) - 1; | ||
} | ||
|
||
public double getTileWidth(int dimension) { | ||
return 1.0/scalingFactor[dimension]; | ||
} | ||
|
||
public Long longValueFor(double x, double y) { | ||
return longValueFor(x, y, maxLevel); | ||
} | ||
|
||
private long getLongCoord(double value, int dimension) { | ||
if (value >= range.getMax(dimension)) { | ||
return width - 1; | ||
} else if (value < range.getMin(dimension)) { | ||
return 0; | ||
} else { | ||
return (long) ((value - range.getMin(dimension)) * scalingFactor[dimension]); | ||
} | ||
} | ||
|
||
public Long longValueFor(double x, double y, int level) { | ||
if (level > maxLevel) { | ||
throw new IllegalArgumentException("Level " + level + " greater than max-level " + maxLevel); | ||
} | ||
long longX = getLongCoord(x, 0); | ||
long longY = getLongCoord(y, 1); | ||
long newValue = 0; | ||
long mask = 1 << (maxLevel - 1); | ||
int dimensions = range.getDimension(); | ||
|
||
// First level is a single curveUp | ||
CurveRule currentCurve = curveUp; | ||
|
||
for (int i = 1; i <= maxLevel; i++) { | ||
if (i <= level) { | ||
int bitIndex = maxLevel - i; | ||
int bitX = (int) ((longX & mask) >> bitIndex); | ||
int bitY = (int) ((longY & mask) >> bitIndex); | ||
int npoint = bitX << 1 | bitY; | ||
int derivedIndex = currentCurve.indexForNPoint(npoint); | ||
newValue = (newValue << 2) | derivedIndex; | ||
mask = mask >> 1; | ||
currentCurve = currentCurve.childAt(derivedIndex); | ||
} else { | ||
newValue = newValue << dimensions; | ||
} | ||
} | ||
return newValue; | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
src/test/java/org/neo4j/gis/spatial/index/hilbert/HilbertSpaceFillingCurveTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package org.neo4j.gis.spatial.index.hilbert; | ||
|
||
import org.junit.Test; | ||
import org.neo4j.gis.spatial.rtree.Envelope; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.equalTo; | ||
|
||
public class HilbertSpaceFillingCurveTest { | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfOneLevel() { | ||
assertAtLevel(new Envelope(-8, 8, -8, 8), 1); | ||
} | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfTwoLevels() { | ||
assertAtLevel(new Envelope(-8, 8, -8, 8), 2); | ||
} | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfThreeLevels() { | ||
assertAtLevel(new Envelope(-8, 8, -8, 8), 3); | ||
} | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfFourLevels() { | ||
assertAtLevel(new Envelope(-8, 8, -8, 8), 4); | ||
} | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfFiveLevels() { | ||
assertAtLevel(new Envelope(-8, 8, -8, 8), 5); | ||
} | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfTwentyFourLevels() { | ||
assertAtLevel(new Envelope(-8, 8, -8, 8), 24); | ||
} | ||
|
||
@Test | ||
public void shouldCreateSimpleHilberCurveOfDefaultLevels() { | ||
Envelope envelope = new Envelope(-8, 8, -8, 8); | ||
assertAtLevel(new HilbertSpaceFillingCurve(envelope), envelope); | ||
} | ||
|
||
@Test | ||
public void shouldCreateHilbertCurveWithRectangularEnvelope() { | ||
assertAtLevel(new Envelope(-8, 8, -20,20 ), 3); | ||
} | ||
|
||
@Test | ||
public void shouldCreateHilbertCurveWithNonCenteredEnvelope() { | ||
assertAtLevel(new Envelope(2, 7, 2,7 ), 3); | ||
} | ||
|
||
@Test | ||
public void shouldCreateHilbertCurveOfThreeLevelsFromExampleInThePaper() { | ||
HilbertSpaceFillingCurve curve = new HilbertSpaceFillingCurve(new Envelope(0, 8, 0, 8), 3); | ||
assertThat("Example should evaluate to 101110", curve.longValueFor(6, 4), equalTo(46L)); | ||
} | ||
|
||
@Test | ||
public void shouldWorkWithNormalGPSCoordinates() { | ||
Envelope envelope = new Envelope(-180, 180, -90, 90); | ||
HilbertSpaceFillingCurve curve = new HilbertSpaceFillingCurve(envelope); | ||
assertAtLevel(curve, envelope); | ||
} | ||
|
||
private Envelope getTileEnvelope(Envelope envelope, int xindex, int yindex, int divisor) { | ||
double width = envelope.getWidth(0) / divisor; | ||
double height = envelope.getWidth(1) / divisor; | ||
return new Envelope( | ||
envelope.getMinX() + xindex * width, | ||
envelope.getMinX() + (xindex + 1) * width, | ||
envelope.getMinY() + yindex * height, | ||
envelope.getMinY() + (yindex + 1) * height | ||
); | ||
} | ||
|
||
private void assertRange(String message, HilbertSpaceFillingCurve curve, Envelope range, long value) { | ||
for (double x = range.getMinX(); x < range.getMaxX(); x += 1.0) { | ||
for (double y = range.getMinY(); y < range.getMaxY(); y += 1.0) { | ||
assertThat(message + ": (" + x + "," + y + ")", curve.longValueFor(x, y), equalTo(value)); | ||
} | ||
} | ||
} | ||
|
||
private void assertAtLevel(Envelope envelope, int level) { | ||
assertAtLevel(new HilbertSpaceFillingCurve(envelope, level), envelope); | ||
} | ||
|
||
private void assertAtLevel(HilbertSpaceFillingCurve curve, Envelope envelope) { | ||
int level = curve.getMaxLevel(); | ||
long width = (long) Math.pow(2, level); | ||
long maxValue = width * width - 1; | ||
double justInsideMaxX = envelope.getMaxX() - curve.getTileWidth(0)/2.0; | ||
double justInsideMaxY = envelope.getMaxY() - curve.getTileWidth(1)/2.0; | ||
double midX = (envelope.getMinX() + envelope.getMaxX()) / 2.0; | ||
double midY = (envelope.getMinY() + envelope.getMaxY()) / 2.0; | ||
|
||
long topRight = 0L; | ||
long topRightFactor = 2L; | ||
StringBuilder topRightDescription = new StringBuilder(); | ||
for (int l = 0; l < level; l++) { | ||
topRight += topRightFactor; | ||
if (topRightDescription.length() == 0) { | ||
topRightDescription.append(topRightFactor); | ||
} else { | ||
topRightDescription.append(" + ").append(topRightFactor); | ||
} | ||
topRightFactor *= 4; | ||
} | ||
|
||
assertThat("Level " + level + " should have width of " + width, curve.getWidth(), equalTo(width)); | ||
assertThat("Level " + level + " should have max value of " + maxValue, curve.getMaxValue(), equalTo(maxValue)); | ||
|
||
assertThat("Bottom-left should evaluate to zero", curve.longValueFor(envelope.getMinX(), envelope.getMinY()), equalTo(0L)); | ||
assertThat("Just inside right edge on the bottom should evaluate to max-value", curve.longValueFor(justInsideMaxX, envelope.getMinY()), equalTo(curve.getMaxValue())); | ||
assertThat("Just inside top-right corner should evaluate to " + topRightDescription, curve.longValueFor(justInsideMaxX, justInsideMaxY), equalTo(topRight)); | ||
assertThat("Right on top-right corner should evaluate to " + topRightDescription, curve.longValueFor(envelope.getMaxX(), envelope.getMaxY()), equalTo(topRight)); | ||
assertThat("Bottom-right should evaluate to max-value", curve.longValueFor(envelope.getMaxX(), envelope.getMinY()), equalTo(curve.getMaxValue())); | ||
assertThat("Middle value should evaluate to (max-value+1) / 2", curve.longValueFor(midX, midY), equalTo((curve.getMaxValue() + 1) / 2)); | ||
} | ||
|
||
} |