Fix for Bug 372352
[deliverable/tracecompass.git] / org.eclipse.linuxtools.tmf.ui / src / org / eclipse / linuxtools / tmf / ui / views / histogram / Histogram.java
1 /*******************************************************************************
2 * Copyright (c) 2011, 2012 Ericsson
3 *
4 * All rights reserved. This program and the accompanying materials are
5 * made available under the terms of the Eclipse Public License v1.0 which
6 * accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
8 *
9 * Contributors:
10 * Francois Chouinard - Initial API and implementation
11 * Bernd Hufmann - Changed to updated histogram data model
12 * Francois Chouinard - Initial API and implementation
13 *******************************************************************************/
14
15 package org.eclipse.linuxtools.tmf.ui.views.histogram;
16
17 import org.eclipse.linuxtools.tmf.ui.views.TmfView;
18 import org.eclipse.swt.SWT;
19 import org.eclipse.swt.events.ControlEvent;
20 import org.eclipse.swt.events.ControlListener;
21 import org.eclipse.swt.events.KeyEvent;
22 import org.eclipse.swt.events.KeyListener;
23 import org.eclipse.swt.events.MouseEvent;
24 import org.eclipse.swt.events.MouseListener;
25 import org.eclipse.swt.events.MouseTrackListener;
26 import org.eclipse.swt.events.PaintEvent;
27 import org.eclipse.swt.events.PaintListener;
28 import org.eclipse.swt.graphics.Color;
29 import org.eclipse.swt.graphics.Font;
30 import org.eclipse.swt.graphics.FontData;
31 import org.eclipse.swt.graphics.GC;
32 import org.eclipse.swt.graphics.Image;
33 import org.eclipse.swt.layout.GridData;
34 import org.eclipse.swt.layout.GridLayout;
35 import org.eclipse.swt.widgets.Canvas;
36 import org.eclipse.swt.widgets.Composite;
37 import org.eclipse.swt.widgets.Display;
38 import org.eclipse.swt.widgets.Text;
39
40 /**
41 * <b><u>Histogram</u></b>
42 * <p>
43 * Re-usable histogram widget with the following features:
44 * <ul>
45 * <li>Y-axis labels displaying min/max count values
46 * <li>X-axis labels displaying time range
47 * <li>a histogram displaying the distribution of values over time (note that
48 * the histogram might not necessarily fill the whole canvas)
49 * </ul>
50 * The widget also has 2 'markers' to identify:
51 * <ul>
52 * <li>a red dashed line over the bar that contains the currently selected event
53 * <li>a dark red dashed line that delimits the right end of the histogram (if
54 * it doesn't fill the canvas)
55 * </ul>
56 * Clicking on the histogram will select the current event at the mouse
57 * location.
58 * <p>
59 * Once the histogram is selected, there is some limited keyboard support:
60 * <ul>
61 * <li>Home: go to the first histogram bar
62 * <li>End: go to the last histogram bar
63 * <li>Left: go to the previous histogram
64 * <li>Right: go to the next histogram bar
65 * </ul>
66 * Finally, when the mouse hovers over the histogram, a tool tip showing the
67 * following information about the corresponding histogram bar time range:
68 * <ul>
69 * <li>start of the time range
70 * <li>end of the time range
71 * <li>number of events in that time range
72 * </ul>
73 */
74 public abstract class Histogram implements ControlListener, PaintListener, KeyListener, MouseListener, MouseTrackListener, IHistogramModelListener {
75
76 // ------------------------------------------------------------------------
77 // Constants
78 // ------------------------------------------------------------------------
79
80 // Histogram colors
81 private final Color fBackgroundColor = Display.getCurrent().getSystemColor(SWT.COLOR_WHITE);
82 private final Color fCurrentEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_RED);
83 private final Color fLastEventColor = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_RED);
84 private final Color fHistoBarColor = new Color(Display.getDefault(), 74, 112, 139);
85
86 // Timestamp scale (nanosecond)
87 public static final byte TIME_SCALE = -9;
88
89 public static final int HISTOGRAM_BAR_WIDTH = 1;
90
91 // ------------------------------------------------------------------------
92 // Attributes
93 // ------------------------------------------------------------------------
94
95 // Owner view
96 protected TmfView fParentView;
97
98 // Histogram text fields
99 private Text fMaxNbEventsText;
100 private Text fMinNbEventsText;
101 private Text fTimeRangeStartText;
102 private Text fTimeRangeEndText;
103
104 // Histogram drawing area
105 protected Canvas fCanvas;
106
107 // Data model
108 protected final HistogramDataModel fDataModel;
109 protected HistogramScaledData fScaledData;
110
111 protected long fCurrentEventTime = 0;
112
113 // ------------------------------------------------------------------------
114 // Construction
115 // ------------------------------------------------------------------------
116
117 public Histogram(TmfView view, Composite parent) {
118 fParentView = view;
119
120 createWidget(parent);
121 fDataModel = new HistogramDataModel();
122 fDataModel.addHistogramListener(this);
123 clear();
124
125 fCanvas.addControlListener(this);
126 fCanvas.addPaintListener(this);
127 fCanvas.addKeyListener(this);
128 fCanvas.addMouseListener(this);
129 fCanvas.addMouseTrackListener(this);
130 }
131
132 public void dispose() {
133 fHistoBarColor.dispose();
134 fDataModel.removeHistogramListener(this);
135 }
136
137 private void createWidget(Composite parent) {
138
139 final Color labelColor = parent.getBackground();
140 final Font fFont = adjustFont(parent);
141
142 final int initalWidth = 10;
143
144 // --------------------------------------------------------------------
145 // Define the histogram
146 // --------------------------------------------------------------------
147
148 GridLayout gridLayout = new GridLayout();
149 gridLayout.numColumns = 3;
150 gridLayout.marginHeight = 0;
151 gridLayout.marginWidth = 0;
152 gridLayout.marginTop = 0;
153 gridLayout.horizontalSpacing = 0;
154 gridLayout.verticalSpacing = 0;
155 gridLayout.marginLeft = 0;
156 gridLayout.marginRight = 0;
157 Composite composite = new Composite(parent, SWT.FILL);
158 composite.setLayout(gridLayout);
159
160 // Use all the horizontal space
161 GridData gridData = new GridData();
162 gridData.horizontalAlignment = SWT.FILL;
163 gridData.verticalAlignment = SWT.FILL;
164 gridData.grabExcessHorizontalSpace = true;
165 composite.setLayoutData(gridData);
166
167 // Y-axis max event
168 gridData = new GridData();
169 gridData.horizontalAlignment = SWT.RIGHT;
170 gridData.verticalAlignment = SWT.TOP;
171 fMaxNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT);
172 fMaxNbEventsText.setFont(fFont);
173 fMaxNbEventsText.setBackground(labelColor);
174 fMaxNbEventsText.setEditable(false);
175 fMaxNbEventsText.setText("0"); //$NON-NLS-1$
176 fMaxNbEventsText.setLayoutData(gridData);
177
178 // Histogram itself
179 gridData = new GridData();
180 gridData.horizontalSpan = 2;
181 gridData.verticalSpan = 2;
182 gridData.horizontalAlignment = SWT.FILL;
183 gridData.verticalAlignment = SWT.FILL;
184 gridData.grabExcessHorizontalSpace = true;
185 fCanvas = new Canvas(composite, SWT.BORDER | SWT.DOUBLE_BUFFERED);
186 fCanvas.setLayoutData(gridData);
187
188 // Y-axis min event (always 0...)
189 gridData = new GridData();
190 gridData.horizontalAlignment = SWT.RIGHT;
191 gridData.verticalAlignment = SWT.BOTTOM;
192 fMinNbEventsText = new Text(composite, SWT.READ_ONLY | SWT.RIGHT);
193 fMinNbEventsText.setFont(fFont);
194 fMinNbEventsText.setBackground(labelColor);
195 fMinNbEventsText.setEditable(false);
196 fMinNbEventsText.setText("0"); //$NON-NLS-1$
197 fMinNbEventsText.setLayoutData(gridData);
198
199 // Dummy cell
200 gridData = new GridData(initalWidth, SWT.DEFAULT);
201 gridData.horizontalAlignment = SWT.RIGHT;
202 gridData.verticalAlignment = SWT.BOTTOM;
203 Text dummyText = new Text(composite, SWT.READ_ONLY);
204 dummyText.setFont(fFont);
205 dummyText.setBackground(labelColor);
206 dummyText.setEditable(false);
207 dummyText.setText(""); //$NON-NLS-1$
208 dummyText.setLayoutData(gridData);
209
210 // Window range start time
211 gridData = new GridData();
212 gridData.horizontalAlignment = SWT.LEFT;
213 gridData.verticalAlignment = SWT.BOTTOM;
214 fTimeRangeStartText = new Text(composite, SWT.READ_ONLY);
215 fTimeRangeStartText.setFont(fFont);
216 fTimeRangeStartText.setBackground(labelColor);
217 fTimeRangeStartText.setText(HistogramUtils.nanosecondsToString(0));
218 fTimeRangeStartText.setLayoutData(gridData);
219
220 // Window range end time
221 gridData = new GridData();
222 gridData.horizontalAlignment = SWT.RIGHT;
223 gridData.verticalAlignment = SWT.BOTTOM;
224 fTimeRangeEndText = new Text(composite, SWT.READ_ONLY);
225 fTimeRangeEndText.setFont(fFont);
226 fTimeRangeEndText.setBackground(labelColor);
227 fTimeRangeEndText.setText(HistogramUtils.nanosecondsToString(0));
228 fTimeRangeEndText.setLayoutData(gridData);
229 }
230
231 private Font adjustFont(Composite composite) {
232 // Reduce font size for a more pleasing rendering
233 int fontSizeAdjustment = -2;
234 Font font = composite.getFont();
235 FontData fontData = font.getFontData()[0];
236 return new Font(font.getDevice(), fontData.getName(), fontData.getHeight() + fontSizeAdjustment, fontData.getStyle());
237 }
238
239 // ------------------------------------------------------------------------
240 // Accessors
241 // ------------------------------------------------------------------------
242
243 public long getStartTime() {
244 return fDataModel.getFirstBucketTime();
245 }
246
247 public long getEndTime() {
248 return fDataModel.getEndTime();
249 }
250
251 public long getTimeLimit() {
252 return fDataModel.getTimeLimit();
253 }
254
255 public HistogramDataModel getDataModel() {
256 return fDataModel;
257 }
258
259 // ------------------------------------------------------------------------
260 // Operations
261 // ------------------------------------------------------------------------
262 public abstract void setFullRange(long startTime, long endTime);
263 public abstract void updateTimeRange(long startTime, long endTime);
264
265 /**
266 * Clear the histogram and reset the data
267 */
268 public void clear() {
269 fDataModel.clear();
270 fScaledData = null;
271 }
272
273 /**
274 * Increase the histogram bucket corresponding to [timestamp]
275 *
276 * @param timestamp
277 */
278 public void countEvent(long eventCount, long timestamp) {
279 fDataModel.countEvent(eventCount, timestamp);
280 }
281
282 /**
283 * Sets the current event time and refresh the display
284 *
285 * @param timestamp
286 */
287 public void setCurrentEvent(long timestamp) {
288 fCurrentEventTime = (timestamp > 0) ? timestamp : 0;
289 fDataModel.setCurrentEventNotifyListeners(timestamp);
290 }
291
292 /**
293 * Computes the timestamp of the bucket at [offset]
294 *
295 * @param offset offset from the left on the histogram
296 * @return the start timestamp of the corresponding bucket
297 */
298 public synchronized long getTimestamp(int offset) {
299 assert offset > 0 && offset < fScaledData.fWidth;
300 try {
301 return fDataModel.getFirstBucketTime() + fScaledData.fBucketDuration * offset;
302 } catch (Exception e) {
303 return 0; // TODO: Fix that racing condition (NPE)
304 }
305 }
306
307 /**
308 * Computes the offset of the timestamp in the histogram
309 *
310 * @param timestamp the timestamp
311 * @return the offset of the corresponding bucket (-1 if invalid)
312 */
313 public synchronized int getOffset(long timestamp) {
314 if (timestamp < fDataModel.getFirstBucketTime() || timestamp > fDataModel.getEndTime())
315 return -1;
316 return (int) ((timestamp - fDataModel.getFirstBucketTime()) / fScaledData.fBucketDuration);
317 }
318
319 /**
320 * Move the currently selected bar cursor to a non-empty bucket.
321 *
322 * @param keyCode the SWT key code
323 */
324 protected void moveCursor(int keyCode) {
325
326 if (fScaledData.fCurrentBucket == HistogramScaledData.OUT_OF_RANGE_BUCKET)
327 return;
328
329 int index;
330 switch (keyCode) {
331
332 case SWT.HOME:
333 index = 0;
334 while (index < fScaledData.fLastBucket && fScaledData.fData[index] == 0)
335 index++;
336 if (index < fScaledData.fLastBucket)
337 fScaledData.fCurrentBucket = index;
338 break;
339
340 case SWT.ARROW_RIGHT:
341 index = fScaledData.fCurrentBucket + 1;
342 while (index < fScaledData.fWidth && fScaledData.fData[index] == 0)
343 index++;
344 if (index < fScaledData.fLastBucket)
345 fScaledData.fCurrentBucket = index;
346 break;
347
348 case SWT.END:
349 index = fScaledData.fLastBucket;
350 while (index >= 0 && fScaledData.fData[index] == 0)
351 index--;
352 if (index >= 0)
353 fScaledData.fCurrentBucket = index;
354 break;
355
356 case SWT.ARROW_LEFT:
357 index = fScaledData.fCurrentBucket - 1;
358 while (index >= 0 && fScaledData.fData[index] == 0)
359 index--;
360 if (index >= 0)
361 fScaledData.fCurrentBucket = index;
362 break;
363
364 default:
365 return;
366 }
367
368 updateCurrentEventTime();
369 }
370
371 /**
372 * Refresh the histogram display
373 */
374 @Override
375 public void modelUpdated() {
376 if (!fCanvas.isDisposed() && fCanvas.getDisplay() != null) {
377 fCanvas.getDisplay().asyncExec(new Runnable() {
378 @Override
379 public void run() {
380 if (!fCanvas.isDisposed()) {
381 // Retrieve and normalize the data
382 int canvasWidth = fCanvas.getBounds().width;
383 int canvasHeight = fCanvas.getBounds().height;
384 if (canvasWidth <= 0 || canvasHeight <= 0)
385 return;
386 fDataModel.setCurrentEvent(fCurrentEventTime);
387 fScaledData = fDataModel.scaleTo(canvasWidth, canvasHeight, HISTOGRAM_BAR_WIDTH);
388 fCanvas.redraw();
389 // Display histogram and update X-,Y-axis labels
390 fTimeRangeStartText.setText(HistogramUtils.nanosecondsToString(fDataModel.getFirstBucketTime()));
391 fTimeRangeEndText.setText(HistogramUtils.nanosecondsToString(fDataModel.getEndTime()));
392 fMaxNbEventsText.setText(Long.toString(fScaledData.fMaxValue));
393
394 setFullRange(fDataModel.getFirstBucketTime(), fDataModel.getEndTime());
395
396 // The Y-axis area might need to be re-sized
397 fMaxNbEventsText.getParent().layout();
398 }
399 }
400 });
401 }
402 }
403
404 // ------------------------------------------------------------------------
405 // Helper functions
406 // ------------------------------------------------------------------------
407
408 private void updateCurrentEventTime() {
409 long bucketStartTime = getTimestamp(fScaledData.fCurrentBucket);
410 ((HistogramView) fParentView).updateCurrentEventTime(bucketStartTime);
411 }
412
413 // ------------------------------------------------------------------------
414 // PaintListener
415 // ------------------------------------------------------------------------
416
417 protected final String IMAGE_KEY = "double-buffer-image"; //$NON-NLS-1$
418
419 @Override
420 public void paintControl(PaintEvent event) {
421
422 // Get the geometry
423 int canvasWidth = fCanvas.getBounds().width;
424 int canvasHeight = fCanvas.getBounds().height;
425
426 // Make sure we have something to draw upon
427 if (canvasWidth <= 0 || canvasHeight <= 0)
428 return;
429
430 // Retrieve image; re-create only if necessary
431 Image image = (Image) fCanvas.getData(IMAGE_KEY);
432 if (image == null || image.getBounds().width != canvasWidth || image.getBounds().height != canvasHeight) {
433 image = new Image(event.display, canvasWidth, canvasHeight);
434 fCanvas.setData(IMAGE_KEY, image);
435 }
436
437 // Draw the histogram on its canvas
438 GC imageGC = new GC(image);
439 formatImage(imageGC, image);
440 event.gc.drawImage(image, 0, 0);
441 imageGC.dispose();
442 }
443
444 private void formatImage(GC imageGC, Image image) {
445
446 if (fScaledData == null)
447 return;
448
449 HistogramScaledData scaledData = new HistogramScaledData(fScaledData);
450
451 try {
452 // Get drawing boundaries
453 int width = image.getBounds().width;
454 int height = image.getBounds().height;
455
456 // Clear the drawing area
457 imageGC.setBackground(fBackgroundColor);
458 imageGC.fillRectangle(0, 0, image.getBounds().width + 1, image.getBounds().height + 1);
459
460 // Draw the histogram bars
461 imageGC.setBackground(fHistoBarColor);
462 int limit = width < scaledData.fWidth ? width : scaledData.fWidth;
463 for (int i = 1; i < limit; i++) {
464 int value = (int) (scaledData.fData[i] * scaledData.fScalingFactor);
465 imageGC.fillRectangle(i, height - value, 1, value);
466 }
467
468 // Draw the current event bar
469 int currentBucket = scaledData.fCurrentBucket;
470 if (currentBucket >= 0 && currentBucket < limit) {
471 drawDelimiter(imageGC, fCurrentEventColor, height, currentBucket);
472 }
473
474 // Add a dashed line as a delimiter (at the right of the last bar)
475 int lastEventIndex = limit - 1;
476 while (lastEventIndex >= 0 && scaledData.fData[lastEventIndex] == 0)
477 lastEventIndex--;
478 lastEventIndex += (lastEventIndex < limit - 1) ? 1 : 0;
479 drawDelimiter(imageGC, fLastEventColor, height, lastEventIndex);
480 } catch (Exception e) {
481 // Do nothing
482 }
483 }
484
485 private void drawDelimiter(GC imageGC, Color color, int height, int index) {
486 imageGC.setBackground(color);
487 int dash = height / 4;
488 imageGC.fillRectangle(index, 0 * dash, 1, dash - 1);
489 imageGC.fillRectangle(index, 1 * dash, 1, dash - 1);
490 imageGC.fillRectangle(index, 2 * dash, 1, dash - 1);
491 imageGC.fillRectangle(index, 3 * dash, 1, height - 3 * dash);
492 }
493
494 // ------------------------------------------------------------------------
495 // KeyListener
496 // ------------------------------------------------------------------------
497
498 @Override
499 public void keyPressed(KeyEvent event) {
500 moveCursor(event.keyCode);
501 }
502
503 @Override
504 public void keyReleased(KeyEvent event) {
505 }
506
507 // ------------------------------------------------------------------------
508 // MouseListener
509 // ------------------------------------------------------------------------
510
511 @Override
512 public void mouseDoubleClick(MouseEvent event) {
513 }
514
515 @Override
516 public void mouseDown(MouseEvent event) {
517 if (fDataModel.getNbEvents() > 0 && fScaledData.fLastBucket >= event.x) {
518 fScaledData.fCurrentBucket = event.x;
519 updateCurrentEventTime();
520 }
521 }
522
523 @Override
524 public void mouseUp(MouseEvent event) {
525 }
526
527 // ------------------------------------------------------------------------
528 // MouseTrackListener
529 // ------------------------------------------------------------------------
530
531 @Override
532 public void mouseEnter(MouseEvent event) {
533 }
534
535 @Override
536 public void mouseExit(MouseEvent event) {
537 }
538
539 @Override
540 public void mouseHover(MouseEvent event) {
541 if (fDataModel.getNbEvents() > 0 && fScaledData != null && fScaledData.fLastBucket >= event.x) {
542 String tooltip = formatToolTipLabel(event.x);
543 fCanvas.setToolTipText(tooltip);
544 }
545 }
546
547 private String formatToolTipLabel(int index) {
548 long startTime = fScaledData.getBucketStartTime(fScaledData.fCurrentBucket);
549 // negative values are possible if time values came into the model in decreasing order
550 if (startTime < 0) {
551 startTime = 0;
552 }
553 long endTime = fScaledData.getBucketEndTime(fScaledData.fCurrentBucket);
554 int nbEvents = (index >= 0) ? fScaledData.fData[index] : 0;
555
556 StringBuffer buffer = new StringBuffer();
557 buffer.append("Range = ["); //$NON-NLS-1$
558 buffer.append(HistogramUtils.nanosecondsToString(startTime));
559 buffer.append(","); //$NON-NLS-1$
560 buffer.append(HistogramUtils.nanosecondsToString(endTime));
561 buffer.append(")\n"); //$NON-NLS-1$
562 buffer.append("Event count = "); //$NON-NLS-1$
563 buffer.append(nbEvents);
564 return buffer.toString();
565 }
566
567 // ------------------------------------------------------------------------
568 // ControlListener
569 // ------------------------------------------------------------------------
570
571 @Override
572 public void controlMoved(ControlEvent event) {
573 fDataModel.complete();
574 }
575
576 @Override
577 public void controlResized(ControlEvent event) {
578 fDataModel.complete();
579 }
580 }
This page took 0.07489 seconds and 6 git commands to generate.