/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2005-2013, Open Source Geospatial Foundation (OSGeo) * * 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; * version 2.1 of the License. * * 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. */ package org.geotools.image.palette; import static org.geotools.image.palette.ColorUtils.*; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; import java.util.NoSuchElementException; import org.geotools.image.palette.ColorMap.ColorEntry; /** * A {@link HashMap} replacement especially designed to map an (eventually packed) color to a * non negative integer value, which can be in our use cases a count or a palette index. * <p> * It uses significant less resources than a normal {@link HashMap} as it avoids the usage of object * wrappers and other redundant information that we don't need in this particular application * * @author Andrea Aime - GeoSolutions * */ final class ColorMap implements Iterable<ColorEntry> { /** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1024; /** * The load factor */ static final float DEFAULT_LOAD_FACTOR = 0.5f; /** * The bucket array */ ColorEntry[] table; /** * When we reach this entry count the bucket array needs to be expanded */ int threshold; /** * The current amount of values */ int size; /** * Used to check for modifications during iteration */ int modificationCount; /** * Stats */ long accessCount = 0; long scanCount = 0; public ColorMap(int initialCapacity) { // Find a power of 2 >= initialCapacity, if we don't use powers of two the // values in the table might end up being non well distributed int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; table = new ColorEntry[capacity]; threshold = (int) (capacity * DEFAULT_LOAD_FACTOR); this.size = 0; } public ColorMap() { this(DEFAULT_INITIAL_CAPACITY); } /** * Increments the counter associated to the specified color by one, or sets the count * of such color to one if missing */ public void increment(int red, int green, int blue, int alpha) { increment(red, green, blue, alpha, 1); } /** * Increments the counter associated to the specified color by one */ public void increment(int r, int g, int b, int a, int increment) { int color = color(r, g, b, a); int index = indexFor(hash(color), table.length); // see if we already have this color, if so, increment its count accessCount++; for (ColorEntry e = table[index]; e != null; e = e.next) { scanCount++; if (e.color == color) { e.value++; return; } } // nope, we need to add a new one, add at the beginning of the list for that bucket addEntry(color, increment, index); } private void addEntry(int color, int value, int index) { ColorEntry entry = new ColorEntry(color, value, table[index]); table[index] = entry; size++; modificationCount++; // do we need to rehash? if (size > threshold) { rehash(2 * table.length); threshold = (int) (table.length * DEFAULT_LOAD_FACTOR); } } /** * Returns the value for the specified color, or -1 if the color is not found * * @param r * @param g * @param b * @param a * @return */ public int get(int r, int g, int b, int a) { int color = color(r, g, b, a); int index = indexFor(hash(color), table.length); // see if we already have this color, if so, increment its count accessCount++; for (ColorEntry e = table[index]; e != null; e = e.next) { scanCount++; if (e.color == color) { return e.value; } } return -1; } /** * Associates the specified value with a color * * @param r * @param g * @param b * @param a * @return The old value associated with the color, or -1 if no old value was found */ public int put(int r, int g, int b, int a, int value) { if (value < 0) { throw new IllegalArgumentException("By contract only positive numbers can be used"); } int color = color(r, g, b, a); int index = indexFor(hash(color), table.length); // see if we already have this color, if so, replace it accessCount++; for (ColorEntry e = table[index]; e != null; e = e.next) { scanCount++; if (e.color == color) { int oldValue = e.value; e.value = value; return oldValue; } } // nope, we need to add a new one, add at the beginning of the list for that bucket addEntry(color, value, index); return -1; } /** * Removes the specified color from the map * * @param r * @param g * @param b * @param a * @return */ public boolean remove(int r, int g, int b, int a) { int color = color(r, g, b, a); int index = indexFor(hash(color), table.length); ColorEntry prev = null; for (ColorEntry e = table[index]; e != null; e = e.next) { if (e.color == color) { if(prev == null) { table[index] = null; } else { prev.next = e.next; } size--; modificationCount++; return true; } else { prev = e; } } return false; } /** * Builds a new bucket array and redistributes the color entries among it * * @param newLength */ private void rehash(int newLength) { ColorEntry[] oldTable = table; this.table = new ColorEntry[newLength]; for (ColorEntry bucketStart : oldTable) { for (ColorEntry e = bucketStart; e != null; e = e.next) { // no need for fancy checks, we know each color is unique in the table int index = indexFor(hash(e.color), table.length); ColorEntry newEntry = new ColorEntry(e.color, e.value, table[index]); table[index] = newEntry; } } } /** * Returns index for the specified color */ static int indexFor(int h, int length) { return h & (length - 1); } /** * A optimized hash function coming from Java own hash map */ int hash(int color) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). color ^= (color >>> 20) ^ (color >>> 12); return color ^ (color >>> 7) ^ (color >>> 4); } int size() { return size; } @Override public Iterator<ColorEntry> iterator() { return new ColorEntryIterator(modificationCount); } /** * Reset its own status to the one of the other color map. * The {@link ColorEntry} are shared, so the other color map should * not be used anymore after this call */ public void reset(ColorMap other) { this.modificationCount = other.modificationCount; this.size = other.size; this.table = other.table; this.threshold = other.threshold; } /** * Prints out statistics about the color map, number of buckes, empty buckets count, * number of entries per bucket, number of access operations and number of average * color entries accessed each time */ public void printStats() { int empty = 0; int largest = 0; int sum = 0; for (int i = 0; i < table.length; i++) { if(table[i] == null) { empty++; } else { ColorEntry ce = table[i]; int count = 0; while(ce != null) { count++; ce = ce.next; } if(count > largest) { largest = count; } sum += count; } } System.out.println("Bins " + table.length + ", empty: " + empty + " largest: " + largest + " avg: " + sum * 1.0 / (table.length - empty)); System.out.println("Accesses: " + accessCount + ", scans: " + scanCount + ", scan per access: " + (scanCount * 1.0 / accessCount)); accessCount = 0; scanCount = 0; } public static final class ColorEntry { int color; int value; private ColorEntry next; public ColorEntry(int color, int value, ColorEntry next) { this.color = color; this.value = value; this.next = next; } @Override public String toString() { return "ColorEntry [color=" + color + ", value=" + value + "]"; } } final class ColorEntryIterator implements Iterator<ColorEntry> { int idx = 0; ColorEntry current; int reference; public ColorEntryIterator(int reference) { this.reference = reference; } @Override public boolean hasNext() { if (reference != modificationCount) { throw new ConcurrentModificationException( "The map entry count has been modified during the iteration"); } if (current == null) { // move to the next bucket while (idx < table.length && table[idx] == null) { idx++; } if (idx == table.length) { return false; } current = table[idx]; idx++; } return current != null; } @Override public ColorEntry next() { if (!hasNext()) { throw new NoSuchElementException(); } ColorEntry result = current; current = result.next; return result; } @Override public void remove() { throw new UnsupportedOperationException("Removal is not supported in this iterator"); } } }