001: /*
002: * uDig - User Friendly Desktop Internet GIS client http://udig.refractions.net (C) 2004,
003: * Refractions Research Inc. This library is free software; you can redistribute it and/or modify it
004: * under the terms of the GNU Lesser General Public License as published by the Free Software
005: * Foundation; version 2.1 of the License. This library is distributed in the hope that it will be
006: * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
007: * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
008: */
009: package net.refractions.udig.ui.palette;
010:
011: import java.awt.Color;
012: import java.util.HashMap;
013: import java.util.Iterator;
014: import java.util.List;
015: import java.util.Set;
016: import java.util.Map.Entry;
017:
018: import net.refractions.udig.internal.ui.UiPlugin;
019:
020: import org.geotools.brewer.color.BrewerPalette;
021:
022: /**
023: * <p>A colour scheme remaps the colours in a palette.</p>
024: *
025: * @author ptozer
026: * @author chorner
027: */
028: public class ColourScheme {
029: /** the number of items which use this scheme */
030: private int itemCount = 0;
031: /** the number of colours we grab from the palette */
032: private int colourCount = 0;
033: /** should the scheme automatically add/remove available colours? */
034: private boolean canAutoSize = true;
035: private HashMap<String, Integer> idMap; //object identifier, colour index
036: private HashMap<Integer, Integer> colourMap; //colour index, new colour index
037: private BrewerPalette palette;
038:
039: public ColourScheme(BrewerPalette palette, int itemSize) {
040: colourMap = new HashMap<Integer, Integer>();
041: idMap = new HashMap<String, Integer>();
042: setColourPalette(palette);
043: setSizeScheme(itemSize);
044: }
045:
046: public ColourScheme(BrewerPalette palette, int itemSize,
047: int paletteSize) {
048: colourMap = new HashMap<Integer, Integer>();
049: idMap = new HashMap<String, Integer>();
050: setColourPalette(palette);
051: setSizePalette(paletteSize);
052: setSizeScheme(itemSize);
053: }
054:
055: public ColourScheme(BrewerPalette palette,
056: HashMap<Integer, Integer> colourMap,
057: HashMap<String, Integer> idMap, int itemSize,
058: int paletteSize) {
059: this .idMap = idMap;
060: this .colourMap = colourMap;
061: this .palette = palette;
062: this .colourCount = paletteSize;
063: this .itemCount = itemSize;
064: }
065:
066: public static ColourScheme getDefault(final BrewerPalette palette) {
067: return new ColourScheme(palette, 0);
068: }
069:
070: public void setColourPalette(BrewerPalette palette) {
071: this .palette = palette;
072: //TODO: check number of colours in palette has not decreased
073: }
074:
075: public boolean getAutoSizing() {
076: return canAutoSize;
077: }
078:
079: /**
080: * Sets the behaviour of the colour scheme as items are added. If true, the palette will morph
081: * in size as items are added or removed. If false, the palette will remain static and scheme
082: * colours will be repeated even if some are unused.
083: *
084: * @param auto boolean
085: */
086: public void setAutoSizing(boolean auto) {
087: canAutoSize = auto;
088: }
089:
090: /**
091: * Set the number of items this scheme is used by. The size of the palette is automatically adjusted to fit.
092: *
093: * @param numItems the number of items obtaining colours from this scheme
094: */
095: public void setSizeScheme(int numItems) {
096: if (canAutoSize) { // we are allowed to adjust the number of colours from the palette this scheme uses
097: setSizePalette(numItems); //setSizePalette is smart enough not to exceed the palette size
098: }
099:
100: if (numItems > itemCount) { //items were added
101: for (int i = itemCount; i < numItems; i++) {
102: int newColourIndex = getNextColourIndex();
103: colourMap.put(i, newColourIndex);
104: itemCount++;
105: }
106: } else {
107: //items were removed
108: for (int i = numItems; i < itemCount; i++) {
109: if (colourMap.containsKey(i))
110: colourMap.remove(i);
111: }
112: itemCount = numItems;
113: }
114: }
115:
116: /**
117: * Sets the number of colours to use from the current palette. This method checks to ensure the
118: * number of colours does not exceed the size of the palette.
119: *
120: * @param numColours
121: */
122: public void setSizePalette(int numColours) {
123: int minSize = palette.getMinColors();
124: int maxSize = palette.getMaxColors();
125: if (numColours > maxSize) {
126: numColours = maxSize;
127: }
128: if (numColours < minSize) {
129: numColours = minSize;
130: }
131: colourCount = numColours;
132: }
133:
134: private int getLargestColourIndex(int numItems) {
135: int largestIndex = -1;
136: for (int i = 0; i < numItems; i++) {
137: if (colourMap.containsKey(i)) {
138: int this Index = colourMap.get(i);
139: if (this Index > largestIndex)
140: largestIndex = i;
141: }
142: }
143: return largestIndex;
144: }
145:
146: public int getMinColours() {
147: int minColours = palette.getMinColors();
148: int colourWidth = getLargestColourIndex(itemCount) + 1;
149: if (colourWidth > minColours) {
150: return colourWidth;
151: } else {
152: return minColours;
153: }
154: }
155:
156: /**
157: * Obtains a new colour index in the range specified, if unused. Colours are repeated if all are
158: * in use.
159: *
160: * @return colour index of the most appropriate next colour
161: */
162: private int getNextColourIndex() {
163: // find an unused colour
164: for (int i = 0; i < colourCount; i++) {
165: boolean hasColour = false;
166: for (int j = 0; j < itemCount; j++) {
167: if (colourMap.containsKey(j) && colourMap.get(j) == i) {
168: hasColour = true;
169: break;
170: }
171: }
172: if (!hasColour) {
173: return i;
174: }
175: }
176: //we're out of colours, re-use one
177: int[] instances = new int[colourCount];
178: for (int i = 0; i < itemCount; i++) {
179: if (colourMap.containsKey(i)) {
180: instances[colourMap.get(i)]++;
181: }
182: }
183: //find the first colourIndex which is used the least
184: int leastInstances = -1;
185: int index = 0;
186: for (int i = 0; i < colourCount; i++) {
187: if (instances[i] < leastInstances || leastInstances == -1) {
188: leastInstances = instances[i];
189: index = i;
190: }
191: }
192: return index;
193: }
194:
195: /**
196: * Gets the number of colours currently available in the palette.
197: *
198: * @return
199: */
200: public int getSizePalette() {
201: return colourCount;
202: }
203:
204: /**
205: * Gets the number of classes utilizing this scheme.
206: *
207: * @return
208: */
209: public int getSizeScheme() {
210: return itemCount;
211: }
212:
213: public boolean alignScheme(List<Color> colours) {
214: int size = colours.size();
215: if (itemCount < size) {
216: setSizeScheme(size);
217: }
218: for (int i = 0; i < size; i++) {
219: if (!getColour(i).equals(colours.get(i))) {
220: boolean consistent = false;
221: //find the first instance of this colour in the palette
222: Color[] paletteColours = palette.getColors(colourCount);
223: for (int j = 0; j < colourCount; j++) {
224: if (paletteColours[j].equals(colours.get(i))) {
225: consistent = true;
226: colourMap.remove(i);
227: colourMap.put(i, j);
228: break;
229: }
230: }
231: if (!consistent) { //utter failure
232: return false;
233: }
234: }
235: }
236: return true;
237: }
238:
239: /**
240: * Returns the next available colour. Good for comparing reality to what we think we have.
241: *
242: * @param colours
243: * @return
244: */
245: public Color getNextAvailableColour(List<Color> colours) {
246: boolean[] inUse = new boolean[itemCount];
247: for (int i = 0; i < itemCount; i++) {
248: inUse[i] = false;
249: }
250: //for each colour in use
251: for (Color colour : colours) {
252: //check off all instances of it
253: HashMap<Integer, Integer> clrMap = getColourMap();
254: for (int index = 0; index < itemCount; index++) {
255: int i;
256: if (clrMap.containsKey(index)) {
257: i = clrMap.get(index);
258: Color aColour = palette.getColor(i, colourCount);
259: if (aColour.equals(colour)) {
260: inUse[index] = true;
261: }
262: } else {
263: //index is not referenced
264: }
265: }
266: }
267: //find the first unused, yet mapped colour
268: for (int i = 0; i < itemCount; i++) {
269: if (!inUse[i]) {
270: return getColour(i);
271: }
272: }
273: if (palette.getMaxColors() == colourCount
274: && colourCount <= itemCount) { //we're out of colours, so this logic won't work
275: return getColour(itemCount);
276: } else {
277: setSizeScheme(itemCount + 1);
278: return getNextAvailableColour(colours); //recursion! run!
279: }
280: }
281:
282: public Color getColour(int index) {
283: if (index >= itemCount) {
284: setSizeScheme(index + 1);
285: }
286: HashMap<Integer, Integer> clrMap = getColourMap();
287: int i;
288: if (clrMap.containsKey(index)) {
289: i = clrMap.get(index);
290: } else {
291: UiPlugin
292: .log(
293: "ColourScheme getColour(" + index + ") exceeded bounds", null); //$NON-NLS-1$ //$NON-NLS-2$
294: i = 0; //return the first colour, instead of exploding
295: }
296: return palette.getColor(i, colourCount);
297: }
298:
299: /**
300: * @return Returns all the available colours, without duplicates.
301: */
302: public Color[] getAllColours() {
303: return palette.getColors(colourCount);
304: }
305:
306: public boolean equals(Object other) {
307: if (!super .equals(other))
308: return false;
309: if (!(other instanceof ColourScheme))
310: return false;
311:
312: ColourScheme schemeToCompare = (ColourScheme) other;
313: if (schemeToCompare.getSizePalette() != colourCount)
314: return false;
315: if (schemeToCompare.getSizeScheme() != itemCount)
316: return false;
317: if (!schemeToCompare.getColourPalette().getName().equals(
318: palette.getName())) //only compare name for the moment
319: return false;
320: for (int i = 0; i < itemCount; i++) {
321: if (!schemeToCompare.getColourMap().get(i).equals(
322: colourMap.get(i))) {
323: return false;
324: }
325: }
326: return true;
327: }
328:
329: @Override
330: public int hashCode() {
331: final int PRIME = 31;
332: int result = 1;
333: result = PRIME * result + colourCount;
334: result = PRIME * result + itemCount;
335: if (palette != null && palette.getName() != null)
336: result = PRIME * result + palette.hashCode();
337: for (int i = 0; i < itemCount; i++) {
338: Integer integer = colourMap.get(i);
339: result = PRIME * result
340: + ((integer == null) ? 0 : integer.hashCode());
341: }
342: return result;
343: }
344:
345: public BrewerPalette getColourPalette() {
346: return palette;
347: }
348:
349: public HashMap<Integer, Integer> getColourMap() {
350: HashMap<Integer, Integer> colourMapping = new HashMap<Integer, Integer>();
351: for (int i = 0; i < itemCount; i++) {
352: if (colourMap.containsKey(i)) {
353: colourMapping.put(i, colourMap.get(i));
354: } else {
355: if (i > colourCount) {
356: colourMapping.put(i, i % colourCount);
357: } else {
358: colourMapping.put(i, i);
359: }
360: }
361: }
362: return colourMapping;
363: }
364:
365: public HashMap<String, Integer> getIdMap() {
366: return idMap;
367: }
368:
369: public void setColourMap(HashMap<Integer, Integer> colourMap) {
370: this .colourMap = colourMap;
371: //TODO: synchronize size
372: }
373:
374: public void swapColours(int firstIndex, int secondIndex) {
375: if (firstIndex >= colourCount) {
376: setSizeScheme(firstIndex + 1);
377: }
378: if (secondIndex >= colourCount) {
379: setSizeScheme(secondIndex + 1);
380: }
381: int tempVal = colourMap.get(firstIndex);
382: colourMap.put(firstIndex, colourMap.get(secondIndex));
383: colourMap.put(secondIndex, tempVal);
384: }
385:
386: public Color addItem() {
387: int size = getSizeScheme();
388: return getColour(size);
389: }
390:
391: public Color addItem(String id) {
392: int size = getSizeScheme();
393: Color color = getColour(size);
394: int index = indexOf(color);
395: if (index > -1) {
396: idMap.put(id, index);
397: }
398: return color;
399: }
400:
401: /**
402: * Add an item to the scheme, and modify the palette to contain this colour.
403: *
404: * @param color
405: * @return
406: */
407: public void addItem(Color color) {
408: //TODO
409: }
410:
411: public int indexOf(String id) {
412: if (idMap.containsKey(id)) {
413: return idMap.get(id);
414: } else {
415: return -1;
416: }
417: }
418:
419: private int indexOf(Color color) {
420: Iterator<Integer> it = colourMap.keySet().iterator();
421: while (it.hasNext()) {
422: Integer i = it.next();
423: if (i < itemCount) { //don't modify the scheme!
424: if (color.equals(getColour(i))) {
425: return i;
426: }
427: }
428: }
429: return -1;
430: }
431:
432: public boolean removeItem(String id) {
433: if (idMap.containsKey(id)) {
434: int index = indexOf(id);
435: idMap.remove(id);
436: return removeItem(index);
437: }
438: return false;
439: }
440:
441: public boolean removeItem(String id, Color colour) {
442: Set<Entry<String, Integer>> entries = idMap.entrySet();
443: for (Entry<String, Integer> entry : entries) {
444: if (id.equals(entry.getKey())) {
445: if (colour.equals(getColour(entry.getValue()))) {
446: entries.remove(entry);
447: return true;
448: }
449: }
450: }
451: return false;
452: }
453:
454: public boolean removeItem(int index) {
455: if (index < 0) {
456: return false;
457: }
458: if (idMap.containsValue(index)) {
459: //optionally remove target
460: Iterator<Entry<String, Integer>> it = idMap.entrySet()
461: .iterator();
462: while (it.hasNext()) {
463: Entry<String, Integer> entry = it.next();
464: if (entry.getValue().equals(index)) {
465: idMap.remove(entry.getKey());
466: }
467: }
468: }
469: if (colourMap.containsKey(index)) {
470: //remove the entry
471: colourMap.remove(index);
472: itemCount--;
473: }
474:
475: return true;
476: }
477:
478: }
|