package mil.nga.giat.geowave.core.index.sfc.hilbert;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.google.uzaygezen.core.CompactHilbertCurve;
import com.google.uzaygezen.core.MultiDimensionalSpec;
import mil.nga.giat.geowave.core.index.ByteArrayUtils;
import mil.nga.giat.geowave.core.index.PersistenceUtils;
import mil.nga.giat.geowave.core.index.sfc.RangeDecomposition;
import mil.nga.giat.geowave.core.index.sfc.SFCDimensionDefinition;
import mil.nga.giat.geowave.core.index.sfc.SpaceFillingCurve;
import mil.nga.giat.geowave.core.index.sfc.data.MultiDimensionalNumericData;
/***
* Implementation of a Compact Hilbert space filling curve
*
*/
public class HilbertSFC implements
SpaceFillingCurve
{
private static class QueryCacheKey
{
private final double[] minsPerDimension;
private final double[] maxesPerDimension;
private final boolean overInclusiveOnEdge;
private final int maxFilteredIndexedRanges;
public QueryCacheKey(
final double[] minsPerDimension,
final double[] maxesPerDimension,
final boolean overInclusiveOnEdge,
final int maxFilteredIndexedRanges ) {
this.minsPerDimension = minsPerDimension;
this.maxesPerDimension = maxesPerDimension;
this.overInclusiveOnEdge = overInclusiveOnEdge;
this.maxFilteredIndexedRanges = maxFilteredIndexedRanges;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = (prime * result) + maxFilteredIndexedRanges;
result = (prime * result) + Arrays.hashCode(maxesPerDimension);
result = (prime * result) + Arrays.hashCode(minsPerDimension);
result = (prime * result) + (overInclusiveOnEdge ? 1231 : 1237);
return result;
}
@Override
public boolean equals(
final Object obj ) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final QueryCacheKey other = (QueryCacheKey) obj;
if (maxFilteredIndexedRanges != other.maxFilteredIndexedRanges) {
return false;
}
if (!Arrays.equals(
maxesPerDimension,
other.maxesPerDimension)) {
return false;
}
if (!Arrays.equals(
minsPerDimension,
other.minsPerDimension)) {
return false;
}
if (overInclusiveOnEdge != other.overInclusiveOnEdge) {
return false;
}
return true;
}
}
private static final int MAX_CACHED_QUERIES = 500;
private final Map<QueryCacheKey, RangeDecomposition> queryDecompositionCache = new LinkedHashMap<QueryCacheKey, RangeDecomposition>(
MAX_CACHED_QUERIES + 1,
.75F,
true) {
private static final long serialVersionUID = 1L;
@Override
public boolean removeEldestEntry(
final Map.Entry<QueryCacheKey, RangeDecomposition> eldest ) {
return size() > MAX_CACHED_QUERIES;
}
};
protected CompactHilbertCurve compactHilbertCurve;
protected SFCDimensionDefinition[] dimensionDefinitions;
protected int totalPrecision;
/** Tunables **/
private final static boolean REMOVE_VACUUM = true;
protected HilbertSFCOperations getIdOperations;
protected HilbertSFCOperations decomposeQueryOperations;
protected HilbertSFC() {}
/***
* Use the SFCFactory.createSpaceFillingCurve method - don't call this
* constructor directly
*
*/
public HilbertSFC(
final SFCDimensionDefinition[] dimensionDefs ) {
init(dimensionDefs);
}
protected void init(
final SFCDimensionDefinition[] dimensionDefs ) {
final List<Integer> bitsPerDimension = new ArrayList<Integer>();
totalPrecision = 0;
for (final SFCDimensionDefinition dimension : dimensionDefs) {
bitsPerDimension.add(dimension.getBitsOfPrecision());
totalPrecision += dimension.getBitsOfPrecision();
}
compactHilbertCurve = new CompactHilbertCurve(
new MultiDimensionalSpec(
bitsPerDimension));
dimensionDefinitions = dimensionDefs;
setOptimalOperations(
totalPrecision,
bitsPerDimension,
dimensionDefs);
}
protected void setOptimalOperations(
final int totalPrecision,
final List<Integer> bitsPerDimension,
final SFCDimensionDefinition[] dimensionDefs ) {
boolean primitiveForGetId = true;
final boolean primitiveForQueryDecomposition = totalPrecision <= 62L;
for (final Integer bits : bitsPerDimension) {
if (bits > 48) {
// if in any one dimension, more than 48 bits are used, we need
// to use bigdecimals
primitiveForGetId = false;
break;
}
}
if (primitiveForGetId) {
final PrimitiveHilbertSFCOperations primitiveOps = new PrimitiveHilbertSFCOperations();
primitiveOps.init(dimensionDefs);
getIdOperations = primitiveOps;
if (primitiveForQueryDecomposition) {
decomposeQueryOperations = primitiveOps;
}
else {
final UnboundedHilbertSFCOperations unboundedOps = new UnboundedHilbertSFCOperations();
unboundedOps.init(dimensionDefs);
decomposeQueryOperations = unboundedOps;
}
}
else {
final UnboundedHilbertSFCOperations unboundedOps = new UnboundedHilbertSFCOperations();
unboundedOps.init(dimensionDefs);
getIdOperations = unboundedOps;
if (primitiveForQueryDecomposition) {
final PrimitiveHilbertSFCOperations primitiveOps = new PrimitiveHilbertSFCOperations();
primitiveOps.init(dimensionDefs);
decomposeQueryOperations = primitiveOps;
}
else {
decomposeQueryOperations = unboundedOps;
}
}
}
/***
* {@inheritDoc}
*/
@Override
public byte[] getId(
final double[] values ) {
return getIdOperations.convertToHilbert(
values,
compactHilbertCurve,
dimensionDefinitions);
}
/***
* {@inheritDoc}
*/
@Override
public RangeDecomposition decomposeRangeFully(
final MultiDimensionalNumericData query ) {
return decomposeRange(
query,
true,
-1);
}
// TODO: improve this method - min/max not being calculated optimally
/***
* {@inheritDoc}
*/
@Override
public RangeDecomposition decomposeRange(
final MultiDimensionalNumericData query,
final boolean overInclusiveOnEdge,
int maxFilteredIndexedRanges ) {
if (maxFilteredIndexedRanges == -1) {
maxFilteredIndexedRanges = Integer.MAX_VALUE;
}
final QueryCacheKey key = new QueryCacheKey(
query.getMinValuesPerDimension(),
query.getMaxValuesPerDimension(),
overInclusiveOnEdge,
maxFilteredIndexedRanges);
RangeDecomposition rangeDecomp = queryDecompositionCache.get(key);
if (rangeDecomp == null) {
rangeDecomp = decomposeQueryOperations.decomposeRange(
query.getDataPerDimension(),
compactHilbertCurve,
dimensionDefinitions,
totalPrecision,
maxFilteredIndexedRanges,
REMOVE_VACUUM,
overInclusiveOnEdge);
queryDecompositionCache.put(
key,
rangeDecomp);
}
return rangeDecomp;
}
protected static byte[] fitExpectedByteCount(
final int expectedByteCount,
final byte[] bytes ) {
final int leftPadding = expectedByteCount - bytes.length;
if (leftPadding > 0) {
final byte[] zeroes = new byte[leftPadding];
Arrays.fill(
zeroes,
(byte) 0);
return ByteArrayUtils.combineArrays(
zeroes,
bytes);
}
else if (leftPadding < 0) {
final byte[] truncatedBytes = new byte[expectedByteCount];
if (bytes[0] != 0) {
Arrays.fill(
truncatedBytes,
(byte) 255);
}
else {
System.arraycopy(
bytes,
-leftPadding,
truncatedBytes,
0,
expectedByteCount);
}
return truncatedBytes;
}
return bytes;
}
@Override
public byte[] toBinary() {
final List<byte[]> dimensionDefBinaries = new ArrayList<byte[]>(
dimensionDefinitions.length);
int bufferLength = 4;
for (final SFCDimensionDefinition sfcDimension : dimensionDefinitions) {
final byte[] sfcDimensionBinary = PersistenceUtils.toBinary(sfcDimension);
bufferLength += (sfcDimensionBinary.length + 4);
dimensionDefBinaries.add(sfcDimensionBinary);
}
final ByteBuffer buf = ByteBuffer.allocate(bufferLength);
buf.putInt(dimensionDefinitions.length);
for (final byte[] dimensionDefBinary : dimensionDefBinaries) {
buf.putInt(dimensionDefBinary.length);
buf.put(dimensionDefBinary);
}
return buf.array();
}
@Override
public void fromBinary(
final byte[] bytes ) {
final ByteBuffer buf = ByteBuffer.wrap(bytes);
final int numDimensions = buf.getInt();
dimensionDefinitions = new SFCDimensionDefinition[numDimensions];
for (int i = 0; i < numDimensions; i++) {
final byte[] dim = new byte[buf.getInt()];
buf.get(dim);
dimensionDefinitions[i] = PersistenceUtils.fromBinary(
dim,
SFCDimensionDefinition.class);
}
init(dimensionDefinitions);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
final String className = getClass().getName();
result = (prime * result) + ((className == null) ? 0 : className.hashCode());
result = (prime * result) + Arrays.hashCode(dimensionDefinitions);
return result;
}
@Override
public boolean equals(
final Object obj ) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final HilbertSFC other = (HilbertSFC) obj;
if (!Arrays.equals(
dimensionDefinitions,
other.dimensionDefinitions)) {
return false;
}
return true;
}
@Override
public BigInteger getEstimatedIdCount(
final MultiDimensionalNumericData data ) {
return getIdOperations.getEstimatedIdCount(
data,
dimensionDefinitions);
}
@Override
public MultiDimensionalNumericData getRanges(
final byte[] id ) {
return getIdOperations.convertFromHilbert(
id,
compactHilbertCurve,
dimensionDefinitions);
}
@Override
public long[] normalizeRange(
final double minValue,
final double maxValue,
final int dimension ) {
return getIdOperations.normalizeRange(
minValue,
maxValue,
dimension,
dimensionDefinitions[dimension]);
}
@Override
public long[] getCoordinates(
final byte[] id ) {
return getIdOperations.indicesFromHilbert(
id,
compactHilbertCurve,
dimensionDefinitions);
}
@Override
public double[] getInsertionIdRangePerDimension() {
return getIdOperations.getInsertionIdRangePerDimension(dimensionDefinitions);
}
}