1/*******************************************************************************
2 * Copyright (c) 2000, 2009 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors:
9 *     IBM Corporation - initial API and implementation
10 *******************************************************************************/
11package org.eclipse.test.performance.ui;
12
13import java.io.File;
14import java.io.PrintStream;
15import java.text.NumberFormat;
16import java.util.ArrayList;
17import java.util.HashMap;
18import java.util.Iterator;
19import java.util.List;
20import java.util.Map;
21
22import org.eclipse.swt.SWT;
23import org.eclipse.swt.graphics.Color;
24import org.eclipse.swt.graphics.Font;
25import org.eclipse.swt.graphics.FontData;
26import org.eclipse.swt.graphics.GC;
27import org.eclipse.swt.graphics.Image;
28import org.eclipse.swt.graphics.ImageData;
29import org.eclipse.swt.graphics.Point;
30import org.eclipse.swt.graphics.Rectangle;
31import org.eclipse.swt.graphics.Resource;
32import org.eclipse.swt.widgets.Display;
33import org.eclipse.test.internal.performance.results.db.BuildResults;
34import org.eclipse.test.internal.performance.results.db.ConfigResults;
35import org.eclipse.test.internal.performance.results.db.DB_Results;
36import org.eclipse.test.internal.performance.results.db.ScenarioResults;
37import org.eclipse.test.internal.performance.results.utils.Util;
38
39/**
40 * Abstract class to build graph with bars
41 */
42public class FingerPrintGraph {
43
44	// Sizes
45	static final int MARGIN= 5; // margin on all four sides
46	static final int BAR_HEIGHT= 6; // height of bar
47	static final int GAP= 10; // gap between bars
48	static final int TGAP= 5; // gap between lines and labels
49	static final int LINE_HEIGHT = 2*BAR_HEIGHT + GAP;
50
51	// fraction of width reserved for bar graph
52	static final double RATIO= 0.6;
53
54	// Formatting constants
55	static final NumberFormat NUMBER_FORMAT;
56	static {
57		NUMBER_FORMAT = NumberFormat.getInstance();
58		NUMBER_FORMAT.setMaximumFractionDigits(1);
59	}
60
61	// Graphic constants
62	static final Display DEFAULT_DISPLAY = Display.getDefault();
63	static final Color BLACK= DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_BLACK);
64	static final Color BLUE= DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_BLUE);
65	static final Color GREEN= DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_GREEN);
66	static final Color RED = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_RED);
67	static final Color GRAY = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_GRAY);
68	static final Color DARK_GRAY = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_DARK_GRAY);
69	static final Color YELLOW = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_YELLOW);
70	static final Color WHITE = DEFAULT_DISPLAY.getSystemColor(SWT.COLOR_WHITE);
71
72	// Bar graph kinds
73	static final int NO_TIME = 0; // i.e. percentage
74	static final int TIME_LINEAR = 1;
75	static final int TIME_LOG = 2;
76	static final int[] SUPPORTED_GRAPHS = {
77//		NO_TIME,
78		TIME_LINEAR,
79		TIME_LOG,
80	};
81
82	// Graphic fields
83	GC gc;
84	Image image;
85	int imageWidth;
86	int imageHeight;
87	int graphWidth;
88	int graphHeight;
89	Map resources = new HashMap();
90
91	// Data fields
92	int count = 0;
93	ConfigResults[] results = new ConfigResults[10];
94	BarGraphArea[] areas;
95
96	// Values
97	double maxValue = 0.0;
98	double minValue = Double.MAX_VALUE;
99
100	// File info
101	File outputDir;
102	String imageName;
103	private final String defaultDimName = DB_Results.getDefaultDimension().getName();
104
105	/*
106	 * Member class defining a bar graph area.
107	 * This area applies to a configuration results and is made of several zones.
108	 */
109	class BarGraphArea {
110		List zones;
111		private ConfigResults configResults;
112
113		/*
114		 * Member class defining a zone inside a bar graph area.
115		 * Typically made of a rectangle and an associated text used as tooltip.
116		 */
117		class AreaZone {
118			Rectangle zone;
119			String title;
120
121			AreaZone(Rectangle zone, String tooltip) {
122	            super();
123	            this.zone = zone;
124	            this.title = tooltip;
125            }
126
127			void print(String url, PrintStream stream) {
128				stream.print("		echo '<area shape=\"RECT\"");
129				if (this.title != null) {
130					stream.print(" title=\""+this.title+"\"");
131				}
132				stream.print("coords=\"");
133				stream.print(this.zone.x);
134				stream.print(',');
135				stream.print(this.zone.y);
136				stream.print(',');
137				stream.print(this.zone.x+this.zone.width);
138				stream.print(',');
139				stream.print(this.zone.y+this.zone.height);
140				stream.print('"');
141				if (url != null) {
142					stream.print(" href=\"");
143					stream.print(url);
144					stream.print('"');
145				}
146				stream.print(">';\n");
147			}
148		}
149
150		 BarGraphArea(ConfigResults results) {
151			this.configResults = results;
152			this.zones = new ArrayList();
153        }
154
155		void print(PrintStream stream) {
156			String url = this.configResults.getName() + "/" + ((ScenarioResults) this.configResults.getParent()).getFileName() + ".html";
157			int size = this.zones.size();
158			for (int i=0; i<size; i++) {
159				AreaZone zone = (AreaZone) this.zones.get(i);
160				zone.print(url, stream);
161			}
162		}
163
164		void addArea(Rectangle rec, String tooltip) {
165			AreaZone zone = new AreaZone(rec, tooltip);
166			this.zones.add(zone);
167		}
168
169	}
170
171
172FingerPrintGraph(File dir, String fileName, int width, List results) {
173    super();
174    this.imageWidth = width;
175    this.count = results.size();
176    this.results = new ConfigResults[this.count];
177    results.toArray(this.results);
178    this.outputDir = dir;
179    this.imageName = fileName;
180}
181
182/**
183 */
184void drawBars(int kind) {
185
186	// Get/Set graphical resources
187	Font italicFont = (Font) this.resources.get("italicFont");
188	if (italicFont == null) {
189		String fontDataName = this.gc.getFont().getFontData()[0].toString();
190		FontData fdItalic = new FontData(fontDataName);
191		fdItalic.setStyle(SWT.ITALIC);
192		italicFont = new Font(DEFAULT_DISPLAY, fdItalic);
193		this.resources.put("italicFont", italicFont);
194	}
195	Color blueref = (Color) this.resources.get("blueref");
196	if (blueref == null) {
197		blueref = new Color(DEFAULT_DISPLAY, 200, 200, 255);
198		this.resources.put("blueref", blueref);
199	}
200	Color lightyellow= (Color) this.resources.get("lightyellow");
201	if (lightyellow == null) {
202		lightyellow = new Color(DEFAULT_DISPLAY, 255, 255, 160);
203		this.resources.put("lightyellow", lightyellow);
204	}
205	Color darkyellow= (Color) this.resources.get("darkyellow");
206	if (darkyellow == null) {
207		darkyellow = new Color(DEFAULT_DISPLAY, 160, 160, 0);
208		this.resources.put("darkyellow", darkyellow);
209	}
210	Color okColor= (Color) this.resources.get("lightgreen");
211	if (okColor == null) {
212		okColor = new Color(DEFAULT_DISPLAY, 95, 191, 95);
213		this.resources.put("lightgreen", okColor);
214	}
215	Color failureColor = (Color) this.resources.get("lightred");
216	if (failureColor == null) {
217		failureColor = new Color(DEFAULT_DISPLAY, 220, 50, 50);
218		this.resources.put("lightred", failureColor);
219	}
220
221	// Build each scenario bar graph
222	this.areas = new BarGraphArea[this.count];
223	double max = kind == TIME_LOG ? Math.log(this.maxValue) : this.maxValue;
224	for (int i=0, y=MARGIN; i < this.count; i++, y+=LINE_HEIGHT) {
225
226		// get builds info
227		ConfigResults configResults = this.results[i];
228		this.areas[i] = new BarGraphArea(configResults);
229		BarGraphArea graphArea = this.areas[i];
230		BuildResults currentBuildResults = configResults.getCurrentBuildResults();
231		double currentValue = currentBuildResults.getValue();
232		double currentError = currentBuildResults.getError();
233		double error = configResults.getError();
234		boolean singleTest = Double.isNaN(error);
235		boolean isSignificant = singleTest || error < Utils.STANDARD_ERROR_THRESHOLD;
236		boolean isCommented = currentBuildResults.getComment() != null;
237		BuildResults baselineBuildResults = configResults.getBaselineBuildResults();
238		double baselineValue = baselineBuildResults.getValue();
239		double baselineError = baselineBuildResults.getError();
240
241		// draw baseline build bar
242		Color whiteref = (Color) this.resources.get("whiteref");
243		if (whiteref == null) {
244			whiteref = new Color(DEFAULT_DISPLAY, 240, 240, 248);
245			this.resources.put("whiteref", whiteref);
246		}
247		this.gc.setBackground(whiteref);
248		double baselineGraphValue = kind == TIME_LOG ? Math.log(baselineValue) : baselineValue;
249		int baselineBarLength= (int) (baselineGraphValue / max * this.graphWidth);
250		int baselineErrorLength= (int) (baselineError / max * this.graphWidth / 2);
251		int labelxpos = MARGIN + baselineBarLength;
252		if (kind == TIME_LOG || baselineErrorLength <= 1) {
253			this.gc.fillRectangle(MARGIN, y + (GAP/2), baselineBarLength, BAR_HEIGHT);
254			Rectangle rec = new Rectangle(MARGIN, y + (GAP/2), baselineBarLength, BAR_HEIGHT);
255			this.gc.drawRectangle(rec);
256			graphArea.addArea(rec, "Time for baseline build "+baselineBuildResults.getName()+": "+Util.timeString((long)baselineValue));
257		} else {
258			int wr = baselineBarLength - baselineErrorLength;
259			Rectangle recValue = new Rectangle(MARGIN, y + (GAP/2), wr, BAR_HEIGHT);
260			this.gc.fillRectangle(recValue);
261			this.gc.setBackground(YELLOW);
262			Rectangle recError = new Rectangle(MARGIN+wr, y + (GAP/2), baselineErrorLength*2, BAR_HEIGHT);
263			this.gc.fillRectangle(recError);
264			Rectangle rec = new Rectangle(MARGIN, y + (GAP/2), baselineBarLength+baselineErrorLength, BAR_HEIGHT);
265			this.gc.drawRectangle(rec);
266			StringBuffer tooltip = new StringBuffer("Time for baseline build ");
267			tooltip.append(baselineBuildResults.getName());
268			tooltip.append(": ");
269			tooltip.append(Util.timeString((long)baselineValue));
270			tooltip.append(" [&#177;");
271			tooltip.append(Util.timeString((long)baselineError));
272			tooltip.append(']');
273			graphArea.addArea(rec, tooltip.toString());
274			labelxpos += baselineErrorLength;
275		}
276
277		// set current build bar color
278		if (baselineValue < currentValue) {
279			if (isCommented) {
280				this.gc.setBackground(GRAY);
281			} else  {
282				this.gc.setBackground(failureColor);
283			}
284		} else {
285			this.gc.setBackground(okColor);
286		}
287
288		// draw current build bar
289		double currentGraphValue = kind == TIME_LOG ? Math.log(currentValue) : currentValue;
290		int currentBarLength= (int) (currentGraphValue / max * this.graphWidth);
291		int currentErrorLength= (int) (currentError / max * this.graphWidth / 2);
292		if (kind == TIME_LOG || currentErrorLength <= 1) {
293			this.gc.fillRectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, currentBarLength, BAR_HEIGHT);
294			Rectangle rec = new Rectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, currentBarLength, BAR_HEIGHT);
295			this.gc.drawRectangle(rec);
296			String tooltip = "Time for current build "+currentBuildResults.getName()+": "+Util.timeString((long)currentValue);
297			if (isCommented) {
298				tooltip += ".		" + currentBuildResults.getComment();
299			}
300			graphArea.addArea(rec, tooltip);
301			if (labelxpos < (MARGIN+currentBarLength)) {
302				labelxpos = MARGIN + currentBarLength;
303			}
304		} else {
305			int wr = currentBarLength - currentErrorLength;
306			Rectangle recValue = new Rectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, wr, BAR_HEIGHT);
307			this.gc.fillRectangle(recValue);
308			this.gc.setBackground(YELLOW);
309			Rectangle recError = new Rectangle(MARGIN+wr, y + (GAP/2) + BAR_HEIGHT, currentErrorLength*2, BAR_HEIGHT);
310			this.gc.fillRectangle(recError);
311			Rectangle rec = new Rectangle(MARGIN, y + (GAP/2) + BAR_HEIGHT, currentBarLength+currentErrorLength, BAR_HEIGHT);
312			this.gc.drawRectangle(rec);
313			StringBuffer tooltip = new StringBuffer("Time for current build ");
314			tooltip.append(currentBuildResults.getName());
315			tooltip.append(": ");
316			tooltip.append(Util.timeString((long)currentValue));
317			tooltip.append(" [&#177;");
318			tooltip.append(Util.timeString((long)currentError));
319			tooltip.append(']');
320			if (isCommented) {
321				tooltip.append(".		");
322				tooltip.append(currentBuildResults.getComment());
323			}
324			graphArea.addArea(rec, tooltip.toString());
325			if (labelxpos < (MARGIN+currentBarLength+currentErrorLength)) {
326				labelxpos = MARGIN + currentBarLength+currentErrorLength;
327			}
328		}
329
330		// set delta value style and color
331		boolean hasFailure = currentBuildResults.getFailure() != null;
332		if (hasFailure) {
333			if (isCommented) {
334				this.gc.setForeground(DARK_GRAY);
335			} else  {
336				this.gc.setForeground(RED);
337			}
338		} else {
339			this.gc.setForeground(BLACK);
340		}
341
342		// draw delta value
343		double delta = -configResults.getDelta();
344		String label = delta > 0 ? "+" : "";
345		label += NUMBER_FORMAT.format(delta*100) + "%";
346		Point labelExtent= this.gc.stringExtent(label);
347		int labelvpos= y + (LINE_HEIGHT - labelExtent.y) / 2;
348		this.gc.drawString(label, labelxpos+TGAP, labelvpos, true);
349		this.gc.setForeground(BLACK);
350		this.gc.setFont(null);
351		int titleStart = (int) (RATIO * this.imageWidth);
352		if (singleTest || !isSignificant) {
353			String deltaTooltip = null;
354			if (singleTest) {
355				deltaTooltip = "This test performed only one iteration; hence its reliability cannot be assessed";
356			} else if (!isSignificant) {
357				deltaTooltip = "This test has a bad reliability: error is "+NUMBER_FORMAT.format(error*100)+"% (> 3%)!";
358			}
359			Image warning = (Image) this.resources.get("warning");
360			int xi = labelxpos+TGAP+labelExtent.x;
361			this.gc.drawImage(warning, xi, labelvpos);
362			ImageData imageData = warning.getImageData();
363			// Set zones
364			// - first one is between end of bar and warning image beginning
365			Rectangle deltaZone = new Rectangle(labelxpos, labelvpos-2, xi-labelxpos, labelExtent.y+4);
366			graphArea.addArea(deltaZone, null);
367			// - second one is the warning image
368			Rectangle warningZone = new Rectangle(xi, labelvpos, imageData.width, imageData.height);
369			graphArea.addArea(warningZone, deltaTooltip);
370			// - last one is between end of the warning image and the scenario title beginning
371			int warningImageEnd = xi+imageData.width;
372			Rectangle emptyZone = new Rectangle(warningImageEnd, labelvpos, titleStart-warningImageEnd, imageData.height);
373			graphArea.addArea(emptyZone, deltaTooltip);
374		} else {
375			// No tooltip => delta zone is between end of bar and the scenario title beginning
376			Rectangle deltaZone = new Rectangle(labelxpos, labelvpos-2, titleStart-labelxpos, labelExtent.y+4);
377			graphArea.addArea(deltaZone, null);
378		}
379
380		// set title style
381		Color oldfg= this.gc.getForeground();
382		this.gc.setForeground(BLUE);
383
384		// draw scenario title
385		int x= titleStart;
386		ScenarioResults scenarioResults = (ScenarioResults) configResults.getParent();
387		String title = scenarioResults.getLabel() + " (" + this.defaultDimName + ")";
388		Point e= this.gc.stringExtent(title);
389		this.gc.drawLine(x, labelvpos + e.y - 1, x + e.x, labelvpos + e.y - 1);
390		this.gc.drawString(title, x, labelvpos, true);
391		this.gc.setForeground(oldfg);
392		this.gc.setFont(null);
393		Rectangle titleZone = new Rectangle(x, labelvpos, e.x, e.y);
394		graphArea.addArea(titleZone, null/*no tooltip*/);
395		if (!configResults.isBaselined()) {
396			Image warning = (Image) this.resources.get("warning");
397			this.gc.drawImage(warning, x+e.x, labelvpos);
398			ImageData imageData = warning.getImageData();
399			Rectangle warningZone = new Rectangle(x+e.x, labelvpos, imageData.width, imageData.height);
400			String titleTooltip =  "This test has no baseline result, hence use build "+configResults.getBaselineBuildName()+" for reference!";
401			graphArea.addArea(warningZone, titleTooltip);
402		}
403	}
404}
405
406void drawLinearScale() {
407
408	// Draw scale background
409	drawScaleBackground();
410
411	// Draw scale grid lines
412	int gridValue = 100;
413	int n = (int) (this.maxValue / gridValue);
414	while (n > 10) {
415		switch (gridValue) {
416			case 100:
417				gridValue = 200;
418				break;
419			case 200:
420				gridValue = 500;
421				break;
422			case 500:
423				gridValue = 1000;
424				break;
425			default:
426				gridValue += 1000;
427				break;
428		}
429		n = (int) (this.maxValue / gridValue);
430	}
431	int gridWidth = (int) (this.graphWidth * gridValue / this.maxValue);
432	int x = MARGIN;
433	long value = 0; // TODO use minValue instead
434	while (x < this.graphWidth) {
435
436		// draw line
437		this.gc.setForeground(GRAY);
438		if (x > 0) {
439			this.gc.setLineStyle(SWT.LINE_DOT);
440			this.gc.drawLine(x, MARGIN, x, this.graphHeight + TGAP);
441		}
442
443		// draw value
444		this.gc.setForeground(BLACK);
445		String val= Util.timeString(value);
446		Point point= this.gc.stringExtent(val);
447		this.gc.drawString(val, x - point.x / 2, this.graphHeight + TGAP, true);
448
449		// compute next grid position
450		x += gridWidth;
451		value += gridValue; // value is expressed in seconds
452	}
453	this.gc.setLineStyle(SWT.LINE_SOLID);
454	this.gc.drawLine(0, this.graphHeight, this.graphWidth, this.graphHeight);
455}
456
457void drawLogarithmScale() {
458
459	// Draw scale background
460	drawScaleBackground();
461
462	// Draw scale grid lines
463	double max = Math.log(this.maxValue);
464	int gridValue = 100;
465	int x = MARGIN;
466	long value = 0; // TODO use minValue instead
467	while (x < this.graphWidth) {
468
469		// draw line
470		this.gc.setForeground(GRAY);
471		if (x > MARGIN) {
472			this.gc.setLineStyle(SWT.LINE_DOT);
473			this.gc.drawLine(x, MARGIN, x, this.graphHeight + TGAP);
474		}
475
476		// draw value
477		this.gc.setForeground(BLACK);
478		String str = Util.timeString(value);
479		Point point= this.gc.stringExtent(str);
480		this.gc.drawString(str, x - point.x / 2, this.graphHeight + TGAP, true);
481
482		// compute next grid position
483		value += gridValue;
484		int v = (int) (value / 100);
485		int c = 1;
486		while (v > 10) {
487			v = v / 10;
488			c *= 10;
489		}
490		switch (v) {
491			case 3:
492				gridValue = 200*c;
493				break;
494			case 5:
495				gridValue = 500*c;
496				break;
497			case 10:
498				gridValue = 1000*c;
499				break;
500		}
501		x = MARGIN + (int) (this.graphWidth * Math.log(value) / max);
502	}
503	this.gc.setLineStyle(SWT.LINE_SOLID);
504	this.gc.drawLine(0, this.graphHeight, this.graphWidth, this.graphHeight);
505}
506
507/**
508 * Draw the scale depending on the bar time graph kind.
509 */
510void drawScale(int kind) {
511	switch (kind) {
512		case TIME_LINEAR:
513			drawLinearScale();
514			break;
515		case TIME_LOG:
516			drawLogarithmScale();
517			break;
518	}
519}
520
521private void drawScaleBackground() {
522
523	// Draw striped background
524	Color lightblue = (Color) this.resources.get("lightblue");
525	if (lightblue == null) {
526		lightblue = new Color(DEFAULT_DISPLAY, 237, 243, 254);
527		this.resources.put("lightblue", lightblue);
528	}
529	this.gc.setBackground(lightblue);
530	for (int i= 0; i<this.count; i++) {
531		if (i % 2 == 0) {
532	        this.gc.fillRectangle(0, MARGIN + i * LINE_HEIGHT, this.imageWidth, LINE_HEIGHT);
533        }
534	}
535
536	// Draw bottom vertical line
537	int yy= MARGIN + this.count * LINE_HEIGHT;
538	this.gc.drawLine(MARGIN, MARGIN, MARGIN, yy + TGAP);
539}
540
541String getImageName(int kind) {
542	switch (kind) {
543		case TIME_LINEAR:
544			return this.imageName+"_linear";
545		case TIME_LOG:
546			return this.imageName+"_log";
547	}
548	return this.imageName;
549}
550
551void paint(int kind) {
552
553	// Set image
554	this.graphHeight = MARGIN + this.count * LINE_HEIGHT;
555	this.imageHeight = this.graphHeight + GAP + 16 + MARGIN;
556	this.image = new Image(DEFAULT_DISPLAY, this.imageWidth, this.imageHeight);
557	this.gc = new GC(this.image);
558
559	// draw white background
560	this.gc.setBackground(WHITE);
561	this.gc.fillRectangle(0, 0, this.imageWidth, this.imageHeight);
562
563	// Set widths and heights
564	int width= (int) (RATIO * this.imageWidth); // width for results bar
565	this.graphWidth= width - this.gc.stringExtent("-999.9%").x - TGAP - MARGIN; // reserve space //$NON-NLS-1$
566
567	// Get warning image width
568	Image warning = (Image) this.resources.get("warning");
569	if (warning == null) {
570		warning = new Image(this.gc.getDevice(), new File(this.outputDir, Utils.WARNING_OBJ).toString());
571		this.resources.put("warning", warning);
572	}
573	this.graphWidth -= warning.getImageData().width;
574
575	// Set maximum of values
576	this.maxValue = 0.0;
577	this.minValue = Double.MAX_VALUE;
578	for (int i= 0; i<this.count; i++) {
579		BuildResults baselineBuildResults = this.results[i].getBaselineBuildResults();
580		double value = baselineBuildResults.getValue();
581		double error = baselineBuildResults.getError();
582		if (!Double.isNaN(error)) value += Math.abs(error);
583		if (value < 1000000 && value > this.maxValue) {
584			this.maxValue = value;
585		}
586		if (value < this.minValue) {
587			this.minValue = value;
588		}
589		BuildResults currentBuildResults = this.results[i].getCurrentBuildResults();
590		value = currentBuildResults.getValue();
591		error = currentBuildResults.getError();
592		if (!Double.isNaN(error)) value += Math.abs(error);
593		if (value < 1000000 && value > this.maxValue) {
594			this.maxValue = value;
595		}
596		if (value < this.minValue) {
597			this.minValue = value;
598		}
599	}
600	this.minValue = 0; // do not use minValue for now...
601
602	// Draw the scale
603	drawScale(kind);
604
605	// Draw the bars
606	drawBars(kind);
607
608	// Dispose
609	this.gc.dispose();
610}
611
612/**
613 * Create, paint and save all supported bar graphs and add the corresponding
614 * image and map references in the given stream.
615 *
616 * @param stream
617 */
618final public void paint(PrintStream stream) {
619
620	// Paint supported graphs
621	int length = SUPPORTED_GRAPHS.length;
622	for (int i=0; i<length; i++) {
623		int kind = SUPPORTED_GRAPHS[i];
624		paint(kind);
625		save(kind, stream);
626	}
627
628	// Dispose created graphic resources
629	Iterator iterator = this.resources.values().iterator();
630	while (iterator.hasNext()) {
631		Resource resource = (Resource) iterator.next();
632		resource.dispose();
633	}
634	this.resources.clear();
635}
636
637void print(int kind, PrintStream stream) {
638	String imgName = getImageName(kind);
639	stream.print("	if ($type==\"fp_type="+kind+"\") {\n");
640	stream.print("		echo '<img src=\"");
641	stream.print(imgName);
642	stream.print(".gif\" usemap=\"#");
643	stream.print(imgName);
644	stream.print("\" name=\"");
645	stream.print(imgName.substring(imgName.lastIndexOf('.')));
646	stream.print("\">';\n");
647	stream.print("		echo '<map name=\"");
648	stream.print(imgName);
649	stream.print("\">';\n");
650	if (this.areas != null) {
651		for (int i=0; i<this.count; i++) {
652			this.areas[i].print(stream);
653		}
654	}
655	stream.print("		echo '</map>';\n");
656	stream.print("	}\n");
657}
658
659void save(int kind, PrintStream stream) {
660	File file = new File(this.outputDir, getImageName(kind)+".gif");
661	Utils.saveImage(file, this.image);
662	if (file.exists()) {
663		print(kind, stream);
664	} else {
665		stream.print("<br><br>There is no fingerprint for ");
666		stream.print(this.imageName);
667		stream.print(" (kind=");
668		stream.print(kind);
669		stream.print(")<br><br>\n");
670	}
671}
672}
673