001: /**
002: * ===========================================
003: * JFreeReport : a free Java reporting library
004: * ===========================================
005: *
006: * Project Info: http://reporting.pentaho.org/
007: *
008: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
009: *
010: * This library is free software; you can redistribute it and/or modify it under the terms
011: * of the GNU Lesser General Public License as published by the Free Software Foundation;
012: * either version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
015: * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016: * See the GNU Lesser General Public License for more details.
017: *
018: * You should have received a copy of the GNU Lesser General Public License along with this
019: * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
020: * Boston, MA 02111-1307, USA.
021: *
022: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
023: * in the United States and other countries.]
024: *
025: * ------------
026: * ElementTrafficLightFunction.java
027: * ------------
028: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
029: */package org.jfree.report.function;
030:
031: import java.awt.Color;
032: import java.io.Serializable;
033: import java.math.BigDecimal;
034: import java.util.ArrayList;
035: import java.util.Arrays;
036:
037: import org.jfree.report.Band;
038: import org.jfree.report.Element;
039: import org.jfree.report.style.ElementStyleKeys;
040: import org.jfree.util.ObjectUtilities;
041:
042: /**
043: * A function that performs basic traffic lighting based on a range of values and a given set of colors to use. The
044: * default colors are for red for values smaller than 50, yellow for values smaller than 75 and green for all values
045: * greater than 75. The default behaviour will be applied if no other limits and colors are defined. This function
046: * respects absolute values when flagged.
047: * <p/>
048: * By default the given limits specify the lower boundary of an range. That means a value lower than the first given
049: * limit will return the default color, a value lower than the second value will return the first color and so on.
050: * <p/>
051: * That logic can be inversed using the 'useOppositeLogic' flag. In that case the limits specify an upper boundary. If
052: * the value read from the datarow is greater than the last limit specified, the default is returned. If the value is
053: * greater than the second last limit, the last color is used, and so on.
054: *
055: * @author Michael D'Amour
056: * @deprecated This function can be safely replaced by a formula.
057: */
058: public class ElementTrafficLightFunction extends
059: AbstractElementFormatFunction {
060: /**
061: * An internal immutable helper class that bundles the color and limit. This class corresponds to one entry in the
062: * list of colors and limits.
063: */
064: private static class LightDefinition implements Comparable,
065: Serializable, Cloneable {
066: /**
067: * The color of the entry.
068: */
069: private Color color;
070: /**
071: * The numeric limit of the entry.
072: */
073: private Number limit;
074:
075: /**
076: * Creates a definition with the given limit and color.
077: *
078: * @param limit the limit that activates the color.
079: * @param color the color for the limit.
080: */
081: protected LightDefinition(final Number limit, final Color color) {
082: this .limit = limit;
083: this .color = color;
084: }
085:
086: /**
087: * Returns the color for the entry.
088: *
089: * @return the color.
090: */
091: public Color getColor() {
092: return color;
093: }
094:
095: /**
096: * Returns the numerical limit for the entry.
097: *
098: * @return the limit.
099: */
100: public Number getLimit() {
101: return limit;
102: }
103:
104: /**
105: * Compares the object for equality.
106: *
107: * @return true, if the given object is a LightDefinition where both color and limit match, false otherwise.
108: */
109: public boolean equals(final Object o) {
110: if (this == o) {
111: return true;
112: }
113: if (o == null || getClass() != o.getClass()) {
114: return false;
115: }
116:
117: final LightDefinition that = (LightDefinition) o;
118:
119: if (ObjectUtilities.equal(color, that.color) == false) {
120: return false;
121: }
122: if (ObjectUtilities.equal(limit, that.limit) == false) {
123: return false;
124: }
125:
126: return true;
127: }
128:
129: /**
130: * Computes a hashcode for the LightDefinition object.
131: *
132: * @return the hashcode.
133: */
134: public int hashCode() {
135: int result = 0;
136: if (color != null) {
137: result += color.hashCode();
138: }
139: if (limit != null) {
140: result = 31 * result + limit.hashCode();
141: } else {
142: result = 31 * result;
143: }
144: return result;
145: }
146:
147: /**
148: * Compares this LightDefinition with another LightDefinition. This will happily crash if the given object is no
149: * LightDefinition object.
150: *
151: * @param o the other object.
152: * @return -1, 0 or -1 depending on whether this object is less, equal or greater than the given object.
153: * @throws ClassCastException if the given object is no LightDefinition.
154: */
155: public int compareTo(final Object o) {
156: final LightDefinition ldef = (LightDefinition) o;
157: final Number myLimit = this .getLimit();
158: final Number otherLimit = ldef.getLimit();
159: if (myLimit == null && otherLimit == null) {
160: return 0;
161: }
162: if (myLimit == null) {
163: return +1;
164: }
165: if (otherLimit == null) {
166: return -1;
167: }
168: final double myValue = myLimit.doubleValue();
169: final double otherValue = otherLimit.doubleValue();
170: if (myValue < otherValue) {
171: return -1;
172: }
173: if (myValue > otherValue) {
174: return +1;
175: }
176: return 0;
177: }
178:
179: /**
180: * Creates a copy of this entry.
181: *
182: * @return a copy of the LightDefinition.
183: * @throws CloneNotSupportedException if an error occurs.
184: */
185: public Object clone() throws CloneNotSupportedException {
186: return super .clone();
187: }
188: }
189:
190: /**
191: * A flag indicating whether limits specify the lower or the upper boundary of a range.
192: */
193: private boolean useOppositeLogic;
194: /**
195: * A flag indicating whether the values read from the field should be made absolute before they are compared to the
196: * limits.
197: */
198: private boolean useAbsoluteValue;
199: /**
200: * A flag indicating whether the color is applied to the foreground or background of the element.
201: */
202: private boolean defineBackground;
203: /**
204: * The name of the data-row column from where to read the number that is compared to the limits.
205: */
206: private String field;
207: /**
208: * The default color that is used if none of the limits applies.
209: */
210: private Color defaultColor;
211: /**
212: * The list of limit and color pairs.
213: */
214: private ArrayList limits;
215: /**
216: * A temporary sorted array that speeds up the comparison.
217: */
218: private transient LightDefinition[] lightDefArray;
219:
220: /**
221: * Default constructor.
222: */
223: public ElementTrafficLightFunction() {
224: limits = new ArrayList();
225: defaultColor = Color.red;
226: }
227:
228: /**
229: * Configures the default behaviour. The function will behave like a traffic-light with red for values smaller than
230: * 50, yellow for values smaller than 75 but greater than 50 and green for values greater than 75.
231: */
232: private void configureDefaultBehaviour() {
233: if (limits.isEmpty()) {
234: limits.add(new LightDefinition(new Integer(50),
235: Color.yellow));
236: limits
237: .add(new LightDefinition(new Integer(75),
238: Color.green));
239: lightDefArray = null;
240: }
241: }
242:
243: /**
244: * Defines, whether all negative limits should be made positive, by calling 'Math.abs'.
245: *
246: * @return Returns the useAbsoluteValue.
247: */
248: public boolean isUseAbsoluteValue() {
249: return useAbsoluteValue;
250: }
251:
252: /**
253: * Defines, whether all negative limits should be made positive, by calling 'Math.abs'.
254: *
255: * @param useAbsoluteValue The useAbsoluteValue to set.
256: */
257: public void setUseAbsoluteValue(final boolean useAbsoluteValue) {
258: this .useAbsoluteValue = useAbsoluteValue;
259: }
260:
261: /**
262: * Returns whether limits specify the lower or the upper boundary of a range.
263: *
264: * @return true, if limits specify the upper boundaries, false otherwise.
265: */
266: public boolean isUseOppositeLogic() {
267: return useOppositeLogic;
268: }
269:
270: /**
271: * Defines whether limits specify the lower or the upper boundary of a range. This property defaults to false, making
272: * limits define the lower boundary.
273: *
274: * @param useOppositeLogic true, if limits specify the upper boundaries, false otherwise.
275: */
276: public void setUseOppositeLogic(final boolean useOppositeLogic) {
277: this .useOppositeLogic = useOppositeLogic;
278: }
279:
280: /**
281: * Updates the color at the given index in the list of LightDefinition entries.
282: *
283: * @param index the position of the entry that should be updated.
284: * @param color the new color.
285: */
286: public void setColor(final int index, final Color color) {
287: if (limits.size() == index) {
288: final LightDefinition ldef = new LightDefinition(null,
289: color);
290: limits.add(ldef);
291: lightDefArray = null;
292: } else {
293: final LightDefinition ldef = (LightDefinition) limits
294: .get(index);
295: if (ldef == null) {
296: final LightDefinition newdef = new LightDefinition(
297: null, color);
298: limits.set(index, newdef);
299: lightDefArray = null;
300: } else {
301: final LightDefinition newdef = new LightDefinition(ldef
302: .getLimit(), color);
303: limits.set(index, newdef);
304: lightDefArray = null;
305: }
306: }
307: }
308:
309: /**
310: * Returns the color at the given index in the list of LightDefinition entries.
311: *
312: * @param index the position of the entry that should be queried.
313: * @return the color at the given position.
314: */
315: public Color getColor(final int index) {
316: final LightDefinition ldef = (LightDefinition) limits
317: .get(index);
318: if (ldef == null) {
319: return null;
320: }
321: return ldef.getColor();
322: }
323:
324: /**
325: * Returns the number of LightDefinitions defined in this function.
326: *
327: * @return the number of entries.
328: */
329: public int getColorCount() {
330: return limits.size();
331: }
332:
333: /**
334: * Returns all colors defined in this function mapped to their respective position.
335: *
336: * @return the colors as array.
337: */
338: public Color[] getColor() {
339: final Color[] retval = new Color[limits.size()];
340: for (int i = 0; i < limits.size(); i++) {
341: final LightDefinition definition = (LightDefinition) limits
342: .get(i);
343: retval[i] = definition.getColor();
344: }
345: return retval;
346: }
347:
348: /**
349: * Updates all colors defined in this function mapped to their respective position. If the color-array contains more
350: * entries than the function has, new LightDefinitions will be added. If the given array contains fewer entries, the
351: * extra LightDefinitions will be deleted.
352: *
353: * @param colors the colors as array.
354: */
355: public void setColor(final Color[] colors) {
356: for (int i = 0; i < colors.length; i++) {
357: final Color color = colors[i];
358: setColor(i, color);
359: }
360: final int size = this .limits.size();
361: if (size > colors.length) {
362: for (int i = size - 1; i >= colors.length; i--) {
363: limits.remove(i);
364: }
365: }
366: lightDefArray = null;
367: }
368:
369: /**
370: * Updates the numerical limit at the given index in the list of LightDefinition entries.
371: *
372: * @param index the position of the entry that should be updated.
373: * @param value the new numerical limit.
374: */
375: public void setLimit(final int index, final Number value) {
376: if (limits.size() == index) {
377: final LightDefinition ldef = new LightDefinition(value,
378: null);
379: limits.add(ldef);
380: } else {
381: final LightDefinition ldef = (LightDefinition) limits
382: .get(index);
383: if (ldef == null) {
384: final LightDefinition newdef = new LightDefinition(
385: value, null);
386: limits.set(index, newdef);
387: } else {
388: final LightDefinition newdef = new LightDefinition(
389: value, ldef.getColor());
390: limits.set(index, newdef);
391: }
392: }
393: lightDefArray = null;
394: }
395:
396: /**
397: * Returns the numerical limit at the given index in the list of LightDefinition entries.
398: *
399: * @param index the position of the entry that should be queried.
400: * @return the numerical limit at the given position.
401: */
402: public Number getLimit(final int index) {
403: final LightDefinition ldef = (LightDefinition) limits
404: .get(index);
405: if (ldef == null) {
406: return null;
407: }
408: return ldef.getLimit();
409: }
410:
411: /**
412: * Returns the number of LightDefinitions defined in this function.
413: *
414: * @return the number of entries.
415: */
416: public int getLimitCount() {
417: return limits.size();
418: }
419:
420: /**
421: * Returns all numerical limits defined in this function mapped to their respective position.
422: *
423: * @return the numerical limits as array.
424: */
425: public Number[] getLimit() {
426: final Number[] retval = new Number[limits.size()];
427: for (int i = 0; i < limits.size(); i++) {
428: final LightDefinition definition = (LightDefinition) limits
429: .get(i);
430: retval[i] = definition.getLimit();
431: }
432: return retval;
433: }
434:
435: /**
436: * Updates all numerical limits defined in this function mapped to their respective position. If the numerical
437: * limits-array contains more entries than the function has, new LightDefinitions will be added. If the given array
438: * contains fewer entries, the extra LightDefinitions will be deleted.
439: *
440: * @param limits the numerical limits as array.
441: */
442: public void setLimit(final Number[] limits) {
443: for (int i = 0; i < limits.length; i++) {
444: final Number limit = limits[i];
445: setLimit(i, limit);
446: }
447: final int size = this .limits.size();
448: if (size > limits.length) {
449: for (int i = size - 1; i >= limits.length; i--) {
450: this .limits.remove(i);
451: }
452: }
453: lightDefArray = null;
454: }
455:
456: /**
457: * Returns the default color that is used if none of the limits applies.
458: *
459: * @return the default color.
460: */
461: public Color getDefaultColor() {
462: return defaultColor;
463: }
464:
465: /**
466: * Defines the default color that is used if none of the limits applies.
467: *
468: * @param defaultColor the default color.
469: */
470: public void setDefaultColor(final Color defaultColor) {
471: this .defaultColor = defaultColor;
472: }
473:
474: /**
475: * Returns whether the computed color is applied to the foreground or background of the element.
476: *
477: * @return true, if the color is applied as background, false if the color is applied as foreground.
478: */
479: public boolean isDefineBackground() {
480: return defineBackground;
481: }
482:
483: /**
484: * Defines whether the computed color is applied to the foreground or background of the element.
485: *
486: * @param defineBackground true, if the color is applied as background, false if the color is applied as foreground.
487: */
488: public void setDefineBackground(final boolean defineBackground) {
489: this .defineBackground = defineBackground;
490: }
491:
492: /**
493: * Returns the field used by the function.
494: * <p/>
495: * The field name corresponds to a column name in the data-row.
496: *
497: * @return The field name.
498: */
499: public String getField() {
500: return field;
501: }
502:
503: /**
504: * Sets the field name for the function.
505: * <p/>
506: * The field name corresponds to a column name in the data-row.
507: *
508: * @param field the field name.
509: */
510: public void setField(final String field) {
511: this .field = field;
512: }
513:
514: /**
515: * Process the given band and color the named element of that band if it exists.
516: *
517: * @param b the band that should be colored.
518: */
519: protected void processRootBand(final Band b) {
520: // only if needed ...
521: configureDefaultBehaviour();
522:
523: final Element[] elements = FunctionUtilities.findAllElements(b,
524: getElement());
525: if (elements.length == 0) {
526: // there is no such element ! (we searched it by its name)
527: return;
528: }
529:
530: final Color color = computeColor();
531: for (int i = 0; i < elements.length; i++) {
532: if (defineBackground) {
533: elements[i].getStyle().setStyleProperty(
534: ElementStyleKeys.BACKGROUND_COLOR, color);
535: } else {
536: elements[i].getStyle().setStyleProperty(
537: ElementStyleKeys.PAINT, color);
538: }
539: }
540: }
541:
542: /**
543: * Computes the color that corresponds to the LightDefinition entry for which the limits match the value read from
544: * field.
545: *
546: * @return the computed color.
547: */
548: private Color computeColor() {
549: if (field == null) {
550: return defaultColor;
551: }
552:
553: final Object o = getDataRow().get(field);
554: if (o instanceof Number == false) {
555: return defaultColor;
556: }
557:
558: final Number n = (Number) o;
559: final Number value;
560: if (useAbsoluteValue) {
561: if (n instanceof BigDecimal) {
562: final BigDecimal td = (BigDecimal) n;
563: value = td.abs();
564: } else {
565: final BigDecimal td = new BigDecimal(n.toString());
566: value = td.abs();
567: }
568: } else {
569: value = n;
570: }
571:
572: if (lightDefArray == null) {
573: lightDefArray = (LightDefinition[]) limits
574: .toArray(new LightDefinition[limits.size()]);
575: Arrays.sort(lightDefArray);
576: }
577:
578: if (useOppositeLogic) {
579: // Inverse logic. The first interval ranging from '-INF' to the first limit will use the
580: // first color. If the value is in the range 'limit[i]' and 'limit[i+1]', the color[i+1]
581: // will be used. If the value is greater than the last limit, the default color is used.
582:
583: if (limits.isEmpty()) {
584: return defaultColor;
585: }
586:
587: Color returnColor = defaultColor;
588: for (int i = lightDefArray.length - 1; i >= 0; i--) {
589: final LightDefinition definition = lightDefArray[i];
590: if (definition == null) {
591: continue;
592: }
593: final Number limit = definition.getLimit();
594: if (limit == null) {
595: continue;
596: }
597: if (value.doubleValue() < limit.doubleValue()) {
598: returnColor = definition.getColor();
599: }
600: }
601: if (returnColor == null) {
602: return defaultColor;
603: }
604: return returnColor;
605: } else {
606: // Standard logic. The first interval from '-INF' to the first limit uses the default color.
607: // from there, the color for the first limit that is greater than the given value is used.
608: // For the interval ranging from the last limit to '+INF', the last color is used.
609: // If there are no limits defined, the default color is always used.
610:
611: Color returnColor = defaultColor;
612: for (int i = 0; i < lightDefArray.length; i++) {
613: final LightDefinition definition = lightDefArray[i];
614: if (definition == null) {
615: continue;
616: }
617: final Number limit = definition.getLimit();
618: if (limit == null) {
619: continue;
620: }
621: if (value.doubleValue() >= limit.doubleValue()) {
622: returnColor = definition.getColor();
623: }
624: }
625: if (returnColor == null) {
626: return defaultColor;
627: }
628: return returnColor;
629: }
630: }
631:
632: /**
633: * Return a completly separated copy of this function. The copy does no longer share any changeable objects with the
634: * original function.
635: *
636: * @return a copy of this function.
637: */
638: public Expression getInstance() {
639: try {
640: final ElementTrafficLightFunction elf = (ElementTrafficLightFunction) super
641: .getInstance();
642: elf.limits = (ArrayList) limits.clone();
643: for (int i = 0; i < limits.size(); i++) {
644: final LightDefinition definition = (LightDefinition) limits
645: .get(i);
646: elf.limits.set(i, definition.clone());
647: }
648: if (lightDefArray != null) {
649: elf.lightDefArray = (LightDefinition[]) lightDefArray
650: .clone();
651: }
652: return elf;
653: } catch (CloneNotSupportedException cne) {
654: throw new IllegalStateException(
655: "Clone must always be supported.");
656: }
657: }
658: }
|