/*
* This file is part of Cubic Chunks Mod, licensed under the MIT License (MIT).
*
* Copyright (c) 2015 contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package cubicchunks.util;
import java.util.Iterator;
/**
* Hash table implementation for objects in a 2-dimensional cartesian coordinate system.
*
* @param <T> class of the objects to be contained in this map
*
* @see XZAddressable
*/
public class XZMap<T extends XZAddressable> implements Iterable<T> {
/**
* A larger prime number used as seed for hash calculation.
*/
private static final int HASH_SEED = 1183822147;
/**
* backing array containing all elements of this map
*/
private XZAddressable[] buckets;
/**
* the current number of elements in this map
*/
private int size;
/**
* the maximum permissible load of the backing array, after reaching it the array will be resized
*/
private float loadFactor;
/**
* the load threshold of the backing array, after reaching it the array will be resized
*/
private int loadThreshold;
/**
* binary mask used to wrap indices
*/
private int mask;
/**
* Creates a new XZMap with the given load factor and initial capacity. The map will automatically grow if
* the specified load is surpassed.
*
* @param loadFactor the load factor
* @param capacity the initial capacity
*/
public XZMap(float loadFactor, int capacity) {
if (loadFactor > 1.0) {
throw new IllegalArgumentException("You really dont want to be using a " + loadFactor + " load loadFactor with this hash table!");
}
this.loadFactor = loadFactor;
int tCapacity = 1;
while (tCapacity < capacity) {
tCapacity <<= 1;
}
this.buckets = new XZAddressable[tCapacity];
this.refreshFields();
}
/**
* Returns the number of elements in this map
*
* @return the number of elements in this map
*/
public int getSize() {
return this.size;
}
/**
* Computes a 32b hash based on the given coordinates.
*
* @param x the x-coordinate
* @param z the z-coordinate
*
* @return a 32b hash based on the given coordinates
*/
private static int hash(int x, int z) {
int hash = HASH_SEED;
hash += x;
hash *= HASH_SEED;
hash += z;
hash *= HASH_SEED;
return hash;
}
/**
* Computes the desired bucket's index for the given coordinates, based on the map's current capacity.
*
* @param x the x-coordinate
* @param z the z-coordinate
*
* @return the desired bucket's index for the given coordinates
*/
private int getIndex(int x, int z) {
return hash(x, z) & this.mask;
}
/**
* Computes the next index to the right of the given index, wrapping around if necessary.
*
* @param index the previous index
*
* @return the next index
*/
private int getNextIndex(int index) {
return (index + 1) & this.mask;
}
/**
* Associates the given value with its xz-coordinates. If the map previously contained a mapping for these
* coordinates, the old value is replaced.
*
* @param value value to be associated with its coordinates
*
* @return the previous value associated with the given value's coordinates or null if no such value exists
*/
@SuppressWarnings("unchecked")
public T put(T value) {
int x = value.getX();
int z = value.getZ();
int index = getIndex(x, z);
// find the closest empty space or the element to be replaced
XZAddressable bucket = this.buckets[index];
while (bucket != null) {
// If there exists an element at the given element's position, overwrite it.
if (bucket.getX() == x && bucket.getZ() == z) {
this.buckets[index] = value;
return (T) bucket;
}
index = getNextIndex(index);
bucket = this.buckets[index];
}
// Insert the element into the empty bucket.
this.buckets[index] = value;
// If the load threshold has been reached, increase the map's size.
++this.size;
if (this.size > this.loadThreshold) {
grow();
}
return null;
}
/**
* Removes and returns the entry associated with the given coordinates.
*
* @param x the x-coordinate
* @param z the z-coordinate
*
* @return the entry associated with the specified coordinates or null if no such value exists
*/
@SuppressWarnings("unchecked")
public T remove(int x, int z) {
int index = getIndex(x, z);
// Search for the element. Only the buckets from the element's supposed index up to the next free slot must
// be checked.
XZAddressable bucket = this.buckets[index];
while (bucket != null) {
// If the correct bucket was found, remove it.
if (bucket.getX() == x && bucket.getZ() == z) {
this.collapseBucket(index);
return (T) bucket;
}
index = getNextIndex(index);
bucket = this.buckets[index];
}
// nothing was removed
return null;
}
/**
* Removes and returns the given value from this map. More specifically, removes the entry whose xz-coordinates
* equal the given value's coordinates.
*
* @param value the value to be removed
*
* @return the entry associated with the given value's coordinates or null if no such entry exists
*/
public T remove(T value) {
return this.remove(value.getX(), value.getZ());
}
/**
* Returns the value associated with the given coordinates or null if no such value exists.
*
* @param x the x-coordinate
* @param z the z-coordinate
*
* @return the entry associated with the specified coordinates or null if no such value exists
*/
@SuppressWarnings("unchecked")
public T get(int x, int z) {
int index = getIndex(x, z);
XZAddressable bucket = this.buckets[index];
while (bucket != null) {
// If the correct bucket was found, return it.
if (bucket.getX() == x && bucket.getZ() == z) {
return (T) bucket;
}
index = getNextIndex(index);
bucket = this.buckets[index];
}
// nothing was found
return null;
}
/**
* Returns true if there exists an entry associated with the given xz-coordinates in this map.
*
* @param x the x-coordinate
* @param z the y-coordinate
*
* @return true if there exists an entry associated with the given coordinates in this map
*/
public boolean contains(int x, int z) {
int index = getIndex(x, z);
XZAddressable bucket = this.buckets[index];
while (bucket != null) {
// If the correct bucket was found, return true.
if (bucket.getX() == x && bucket.getZ() == z) {
return true;
}
index = getNextIndex(index);
bucket = this.buckets[index];
}
// nothing was found
return false;
}
/**
* Returns true if the given value is contained within this map. More specifically, returns true if there exists
* an entry in this map whose xz-coordinates equal the given value's coordinates.
*
* @param value the value
*
* @return true if the given value is contained within this map
*/
public boolean contains(T value) {
return this.contains(value.getX(), value.getZ());
}
/**
* Doubles the size of the backing array and redistributes all contained values accordingly.
*/
private void grow() {
XZAddressable[] oldBuckets = this.buckets;
// double the size!
this.buckets = new XZAddressable[this.buckets.length*2];
this.refreshFields();
// Move the old entries to the new array.
for (XZAddressable oldBucket : oldBuckets) {
// Skip empty buckets.
if (oldBucket == null) {
continue;
}
// Get the desired index of the old bucket and insert it into the first available slot.
int index = getIndex(oldBucket.getX(), oldBucket.getZ());
XZAddressable bucket = this.buckets[index];
while (bucket != null) {
bucket = this.buckets[index = getNextIndex(index)];
}
this.buckets[index] = oldBucket;
}
}
/**
* Removes the value contained at the given index by shifting suitable values on its right to the left.
*
* @param hole the index of the bucket to be collapsed
*/
private void collapseBucket(int hole) {
// This method must not be called on empty buckets.
assert this.buckets[hole] != null;
--this.size;
int currentIndex = hole;
while (true) {
currentIndex = getNextIndex(currentIndex);
// If there exists no element at the given index, there is nothing to fill the hole with.
XZAddressable bucket = this.buckets[currentIndex];
if (bucket == null) {
this.buckets[hole] = null;
return;
}
// If the hole lies to the left of the currentIndex and to the right of the targetIndex, move the current
// element. These if conditions are necessary due to the bucket array wrapping around.
int targetIndex = getIndex(bucket.getX(), bucket.getZ());
// normal
if (hole < currentIndex) {
if (targetIndex <= hole || currentIndex < targetIndex) {
this.buckets[hole] = bucket;
hole = currentIndex;
}
}
// wrap around!
else {
if (hole >= targetIndex && targetIndex > currentIndex) {
this.buckets[hole] = bucket;
hole = currentIndex;
}
}
}
}
/**
* Updates the load threshold and the index mask based on the backing array's current size.
*/
private void refreshFields() {
// we need that 1 extra space, make shore it will be there
this.loadThreshold = Math.min(this.buckets.length - 1, (int) (this.buckets.length*this.loadFactor));
this.mask = this.buckets.length - 1;
}
// Interface: Iterable<T> ------------------------------------------------------------------------------------------
public Iterator<T> iterator() {
return new Iterator<T>() {
int at = -1;
int next = -1;
@Override
public boolean hasNext() {
if (next > at) {
return true;
}
for (next++; next < buckets.length; next++) {
if (buckets[next] != null) {
return true;
}
}
return false;
}
@Override
@SuppressWarnings("unchecked")
public T next() {
if (next > at) {
at = next;
return (T) buckets[at];
}
for (next++; next < buckets.length; next++) {
if (buckets[next] != null) {
at = next;
return (T) buckets[at];
}
}
return null;
}
//TODO: WARNING: risk of iterating over the same item more than once if this is used
// do to items wrapping back around form the front of the buckets array
@Override
public void remove() {
collapseBucket(at);
next = at = at - 1; // There could be a new item in the removed bucket
}
};
}
}