View Javadoc

1   package de.desy.acop.video.displayer;
2   
3   //big memory for testing:
4   //-Xms512m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=128m
5   
6   import java.awt.AlphaComposite;
7   import java.awt.BorderLayout;
8   import java.awt.Color;
9   import java.awt.Dimension;
10  import java.awt.Font;
11  import java.awt.FontMetrics;
12  import java.awt.Graphics;
13  import java.awt.Graphics2D;
14  import java.awt.Point;
15  import java.awt.Rectangle;
16  import java.awt.event.ActionEvent;
17  import java.awt.event.ActionListener;
18  import java.awt.event.MouseEvent;
19  import java.awt.event.MouseListener;
20  import java.awt.event.MouseMotionListener;
21  import java.awt.image.BufferedImage;
22  import java.beans.ExceptionListener;
23  import java.beans.PropertyChangeEvent;
24  import java.beans.PropertyChangeListener;
25  import java.io.File;
26  import java.net.MalformedURLException;
27  import java.net.URL;
28  import java.text.SimpleDateFormat;
29  import java.util.Calendar;
30  import java.util.Date;
31  
32  import javax.swing.JPanel;
33  import javax.swing.SwingUtilities;
34  import javax.swing.Timer;
35  
36  import de.desy.acop.video.timageio.TBufferedImage;
37  import de.desy.acop.video.timageio.TImageIO;
38  import de.desy.acop.video.timageio.TImageMetadata;
39  import de.desy.tine.types.IMAGE;
40  import de.desy.tine.types.IMAGE.FrameHeader;
41  
42  /**
43   * <code>ImageDisplayer</code> implements basic, but versatile display of video
44   * images. Five guidelines were set at development startup:
45   * <ul>
46   * <li>pure Java-code, no Java native interface (JNI, which might improve speed
47   * at cost of compatibility)
48   * <li>fast (above average performance) of Java live image display
49   * <li>providing of additional "image enhancement" methods like false color
50   * mode(s), normalization
51   * <li>support of JPEG, RGB raw, Grayscale and HuffYUV-compressed grayscale as
52   * input data
53   * <li>robustness (malformed data should do no harm and should be recognizable)
54   * <li>easiness, intuitiveness of use (keep aspect ratio, overlaying of image
55   * meta-data, obtain pixel value under cursor, save still image to PNG file,
56   * facility-overridable splash and error screens)
57   * </ul>
58   * The component was designed that it can either be used inside a so-called ACOP
59   * Video Bean as well as inside a stand alone application.
60   * 
61   * @author sweisse, mdavid
62   * @version $Id: Templates.xml,v 1.10 2008/06/23 14:30:13 sweisse Exp $
63   * 
64   */
65  public class ImageDisplayer extends JPanel implements TineImageReceiver {
66  
67  	private static final long serialVersionUID = 310376L;
68  
69  	public static final String INVALID_CAMERA_PORT_NAME = "invalid";
70  
71  	public static final String PROPERTY_KEEP_ASPECT_RATIO = "keepAspectRatio";
72  	public static final String PROPERTY_AOI_ZOOM = "AOIZoom";
73  	public static final String PROPERTY_OVERLAY_STATE = "overlayState";
74  	public static final String PROPERTY_IMAGE_ZOOM = "imageZoom";
75  	public static final String PROPERTY_HISTOGRAM_EQUALISATION = "histogramEqualisation";
76  	public static final String PROPERTY_HISTOGRAM_MIN = "histogramMin";
77  	public static final String PROPERTY_HISTOGRAM_MAX = "histogramMax";
78  	public static final String PROPERTY_COLOR_MAP = "colorMap";
79  
80  	private static final int TIMER_DELAY = 250;
81  
82  	/**
83  	 * Currently used color lookup table.
84  	 */
85  	private ImageCLUT _clut;
86  
87  	/**
88  	 * Resizing an image in the ImageDisplayer pane (see description of
89  	 * ImageZoom enumeration)
90  	 */
91  	private ImageZoom _imageZoom = ImageZoom.AUTO;
92  
93  	/**
94  	 * see description of OverlayState enumeration
95  	 */
96  	private OverlayState _overlayState = OverlayState.AUTO;
97  
98  	/**
99  	 * zooming out of area of interest (zoom area of interest to full rectangle
100 	 * of component, but still do take account of aspect ratio if it is on)
101 	 */
102 	private boolean _aoiZoom;
103 
104 	/**
105 	 * set to true if aspect ratio should be kept. At the moment, a pixel is
106 	 * known to be square so aspect ratio takes into account the ratio of width
107 	 * vs. height. This might change in future because VSv3 allows to detect and
108 	 * take into account non- squared pixels.
109 	 */
110 	private boolean _keepAspectRatio;
111 
112 	/**
113 	 * set to true if histogram equalization (aka. normalization, aka. contrast
114 	 * enhancement) should be applied to any image before displayed. Makes sense
115 	 * for dark scenes, very bright scenes or more in general any scene with low
116 	 * contrast. Color histogram equalization is numerically well but is
117 	 * outperformed by professional algorithms. This might be improved in
118 	 * future. Same but weaker effect is known for luminosity (grayscale)
119 	 * normalization.
120 	 */
121 	private boolean _histogramEqualisation;
122 
123 	/**
124 	 * Histogram data min value
125 	 */
126 	private int _histogramMin = UNDEFINED_CONDITION;
127 
128 	/**
129 	 * Histogram data max value
130 	 */
131 	private int _histogramMax = UNDEFINED_CONDITION;
132 
133 	/**
134 	 * Set to true if live transfer is taking place and is not timed out. Set to
135 	 * false if either live transfer is running but timed out or not running
136 	 * (stop mode) at all.
137 	 */
138 	private boolean _isLiveTransfer;
139 
140 	/** dedicated synchronization (thread lock) object */
141 	private Object _lockHeaderUpdate = new Object();
142 
143 	/** is the mouse arrow inside the video canvas? */
144 	private boolean _isMouseInCanvas;
145 
146 	/** is the cursor inside the video canvas and is the left/right button down */
147 	private boolean _isMousePressed;
148 
149 	/**
150 	 * instance that will buffer the special error screen about to be displayed
151 	 * if video image data could not properly be decoded or displayed
152 	 */
153 	private IMAGE _errorScreen;
154 
155 	/**
156 	 * extracted but volatile video header out of current "about-to-be"
157 	 * displayed frame. Values are altered if header data is preprocessed and
158 	 * changed during image prepare inside this class.
159 	 */
160 	private IMAGE _timage;
161 
162 	/**
163 	 * Image counter
164 	 */
165 	private ImageCounter _imageCounter = new ImageCounter();
166 
167 	/**
168 	 * Image instance buffer for rendered video image contents ready for drawing
169 	 */
170 	private BufferedImage _image;
171 
172 	/** Camera port ID for last received frame */
173 	private long _lastCameraPortId = UNDEFINED_CONDITION;
174 
175 	/**
176 	 * the timer manually repainting the images
177 	 */
178 	private Timer _timer;
179 
180 	/**
181 	 * Exception Listener
182 	 * 
183 	 * @see ImageDisplayer#setExceptionListener(ExceptionListener)
184 	 */
185 	private ExceptionListener _exceptionListener;
186 
187 	/** Constructs class */
188 	public ImageDisplayer() {
189 		initialize();
190 	}
191 
192 	@Override
193 	protected void paintComponent(Graphics g) {
194 		// long t = System.currentTimeMillis();
195 
196 		super.paintComponents(g);
197 
198 		if (_timer != null && _timer.isRunning())
199 			_timer.stop();
200 
201 		// for (int i = 0; i < 10; i++) {
202 		BufferedImage bi = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
203 		Graphics2D g2 = bi.createGraphics();
204 		try {
205 			drawFrame(g2, _image);
206 		} finally {
207 			g2.dispose();
208 		}
209 		g.drawImage(bi, 0, 0, null);
210 		// }
211 		// System.out.println("Rendered:  " + (System.currentTimeMillis() - t));
212 	}
213 
214 	/**
215 	 * Returns current Tine image
216 	 * 
217 	 * @return TINE image
218 	 */
219 	public IMAGE getIMAGE() {
220 		return _timage;
221 	}
222 
223 	/**
224 	 * returns the currently used color lookup table.
225 	 * 
226 	 * @return currently used color lookup table
227 	 */
228 	public ImageCLUT getCLUT() {
229 		return _clut;
230 	}
231 
232 	/**
233 	 * Sets color lookup table.
234 	 * 
235 	 * @param newValue
236 	 *            new color lookup table
237 	 */
238 	public void setCLUT(ImageCLUT newValue) {
239 		ImageCLUT oldValue = _clut;
240 		_clut = newValue;
241 		firePropertyChange(PROPERTY_COLOR_MAP, oldValue, newValue);
242 	}
243 
244 	/**
245 	 * Updates the currently used false color map.
246 	 * 
247 	 * @param newValue
248 	 *            - new color map to use
249 	 */
250 	public void setColorMap(ColorMap newValue) {
251 		System.out.println("ImageDisplayer.setColorMap()");
252 		setCLUT(new ImageCLUT(newValue, _clut.getComponentSize()));
253 	}
254 
255 	/**
256 	 * returns the currently used false color map to outside world.
257 	 * 
258 	 * @return currently used false color map
259 	 */
260 	public ColorMap getColorMap() {
261 		if (_clut == null)
262 			throw null;
263 		return _clut.getColorMap();
264 	}
265 
266 	/**
267 	 * Sets image zoom
268 	 * 
269 	 * @param newValue
270 	 *            - new image zoom value
271 	 */
272 	public void setImageZoom(ImageZoom newValue) {
273 		ImageZoom oldValue = _imageZoom;
274 		_imageZoom = newValue;
275 		firePropertyChange(PROPERTY_IMAGE_ZOOM, oldValue, newValue);
276 	}
277 
278 	/**
279 	 * Returns image zoom.
280 	 */
281 	public ImageZoom getImageZoom() {
282 		return _imageZoom;
283 	}
284 
285 	/**
286 	 * Sets overlay state mode
287 	 * 
288 	 * @param newValue
289 	 *            - new overlay state value
290 	 */
291 	public void setOverlayState(OverlayState newValue) {
292 		OverlayState oldValue = _overlayState;
293 		_overlayState = newValue;
294 		firePropertyChange(PROPERTY_OVERLAY_STATE, oldValue, newValue);
295 	}
296 
297 	/**
298 	 * Returns overlay information state.
299 	 */
300 	public OverlayState getOverlayState() {
301 		return _overlayState;
302 	}
303 
304 	/**
305 	 * Sets whether or not the histogram equalization (normalization) is
306 	 * enabled.<br>
307 	 * After new assignment, drawing as well as repainting is enforced to update
308 	 * all involved parts of code so that Histogram Equalization is immediately
309 	 * applied to image.
310 	 * 
311 	 * @param newValue
312 	 *            - switch histogram equalization on (true) or off (false)
313 	 */
314 	public void setHistogramEqualisation(boolean newValue) {
315 		boolean oldValue = _histogramEqualisation;
316 		_histogramEqualisation = newValue;
317 		firePropertyChange(PROPERTY_HISTOGRAM_EQUALISATION, oldValue, newValue);
318 	}
319 
320 	/**
321 	 * returns whether histogram equalization (normalization) is currently
322 	 * enabled.
323 	 * 
324 	 * @return <code>true</code> if histogram equalization is enabled, otherwise
325 	 *         <code>false</code>
326 	 */
327 	public boolean isHistogramEqualisation() {
328 		return _histogramEqualisation;
329 	}
330 
331 	/**
332 	 * Sets histogram min value
333 	 * 
334 	 * @param newValue
335 	 *            - histogram min value
336 	 */
337 	public void setHistogramMin(int newValue) {
338 		int max = getHistogramMax();
339 		validateMinMax(newValue, _clut.getMinValue(), (max == UNDEFINED_CONDITION ? _clut.getMaxValue() : max) - 1);
340 		int oldValue = _histogramMin;
341 		_histogramMin = newValue;
342 		firePropertyChange(PROPERTY_HISTOGRAM_MIN, oldValue, newValue);
343 	}
344 
345 	/**
346 	 * returns histogram min value
347 	 * 
348 	 * @return histogram min value
349 	 */
350 	public int getHistogramMin() {
351 		return _histogramMin;
352 	}
353 
354 	/**
355 	 * Sets histogram max value
356 	 * 
357 	 * @param newValue
358 	 *            - histogram max value
359 	 */
360 	public void setHistogramMax(int newValue) {
361 		int min = getHistogramMin();
362 		validateMinMax(newValue, (min == UNDEFINED_CONDITION ? _clut.getMinValue() : min) + 1, _clut.getMaxValue());
363 		int oldValue = _histogramMax;
364 		_histogramMax = newValue;
365 		firePropertyChange(PROPERTY_HISTOGRAM_MAX, oldValue, newValue);
366 	}
367 
368 	private void validateMinMax(int value, int min, int max) {
369 		if (value < min || value > max)
370 			throw new IllegalArgumentException(value + " is out of range [" // 
371 					+ min + ", " + max + "]");
372 	}
373 
374 	/**
375 	 * returns histogram max value
376 	 * 
377 	 * @return histogram max value
378 	 */
379 	public int getHistogramMax() {
380 		return _histogramMax;
381 	}
382 
383 	/**
384 	 * Switches 'AOI zoom' on or off. This method will do nothing if the AOI
385 	 * zoom mode is already in the state about to be set. Repainting is enforced
386 	 * to have an immediate update.
387 	 * 
388 	 * @param aoiZoom
389 	 *            - enable (true) or disable (false) AOI zooming
390 	 */
391 	public void setAOIZoom(boolean aoiZoom) {
392 		boolean oldValue = _aoiZoom;
393 		_aoiZoom = aoiZoom;
394 		firePropertyChange(PROPERTY_AOI_ZOOM, oldValue, aoiZoom);
395 	}
396 
397 	/**
398 	 * returns whether out zooming of AOI (no black border around AOI which
399 	 * depicts full frame) is currently enabled.
400 	 * 
401 	 * @return true - AOI Zoom is switched on<br>
402 	 *         false - AOI Zoom is switched off
403 	 */
404 	public boolean isAOIZoom() {
405 		return _aoiZoom;
406 	}
407 
408 	/**
409 	 * Switches 'aspect ratio is kept' on or off. This method will do nothing if
410 	 * the aspect ratio mode is already in the state about to be set. Repainting
411 	 * is enforced to have an immediate update.
412 	 * 
413 	 * @param keepAspectRatio
414 	 *            - enable (true) or disable (false) keeping of aspect ratio
415 	 */
416 	public void setKeepAspectRatio(boolean keepAspectRatio) {
417 		boolean oldValue = _keepAspectRatio;
418 		_keepAspectRatio = keepAspectRatio;
419 		firePropertyChange(PROPERTY_KEEP_ASPECT_RATIO, oldValue, keepAspectRatio);
420 	}
421 
422 	/**
423 	 * returns whether 'aspect ratio is kept' is currently enabled.
424 	 * 
425 	 * @return true - 'aspect ratio is kept' is switched on<br>
426 	 *         false - 'aspect ratio is kept' is switched off
427 	 */
428 	public boolean isKeepAspectRatio() {
429 		return _keepAspectRatio;
430 	}
431 
432 	/**
433 	 * informs the displayer that a lot of frames are constantly to be
434 	 * drawn(true) or it is not very likely that a lot of frames are drawn
435 	 * (timeout on transfer or stop mode).
436 	 * 
437 	 * The method was invented for performance reasons. When a lot of frames are
438 	 * coming in each second it can be a very time-consuming process for Java.
439 	 * In order to off-load some performance, certain Java-scheduled redrawing
440 	 * of the component is disabled because it is redrawn with each new video
441 	 * image coming in anyhow.
442 	 * 
443 	 * @param aLive
444 	 *            <ul>
445 	 *            <li>true: live mode is set, a lot of frames are expected
446 	 *            <li>false: either live mode was stopped or timeout happened
447 	 *            </ul>
448 	 */
449 	public void setLiveTransfer(boolean aLive) {
450 		_isLiveTransfer = aLive;
451 	}
452 
453 	/**
454 	 * resets important internal parameters. This method must be called from
455 	 * outside (e.g. image provider like TineHandler) in order to prepare the
456 	 * displayer for a new chunk of subsequent images.
457 	 * 
458 	 */
459 	public void resetForReceiving() {
460 		_imageCounter.reset();
461 	}
462 
463 	/**
464 	 * passes/injects a new image into image processing, drawing and redrawing
465 	 * pipeline.
466 	 * 
467 	 * Uses thread locking for proper synchronization. Drawing and Displaying is
468 	 * done inside invoked Swing thread.
469 	 * 
470 	 * @param ti
471 	 *            new TINE image
472 	 */
473 	public void updateValue(IMAGE ti) {
474 		_timage = ti;
475 		long cameraPortId = ti.getSourceHeader().cameraPortId;
476 		if (cameraPortId != _lastCameraPortId) {
477 			if (cameraPortId != IMAGE.DEFAULT_CAMERA_PORT_ID) {
478 				if (_lastCameraPortId != UNDEFINED_CONDITION)
479 					_imageCounter.reset();
480 			} else
481 				System.out.println("WARN: value of camera port ID: " + cameraPortId);
482 			_lastCameraPortId = cameraPortId;
483 		}
484 
485 		FrameHeader frmHdr = ti.getFrameHeader();
486 
487 		// update image frame counter
488 		_imageCounter.calculate(frmHdr.frameNumber);
489 
490 		// update CLUT and Histogram min/max values
491 		boolean isGrayscale = (frmHdr.imageFormat == 0 || frmHdr.imageFormat == 10 || (frmHdr.imageFormat == 5 && frmHdr.bytesPerPixel == 1));
492 		if (isGrayscale) {
493 			if (_clut == null || frmHdr.effectiveBitsPerPixel != _clut.getPixelSize()) {
494 				ImageCLUT clut = new ImageCLUT(_clut == null ? ColorMap.GRAYSCALE : _clut.getColorMap(),
495 						new int[] { frmHdr.effectiveBitsPerPixel });
496 				setCLUT(clut);
497 				setHistogramMin(0);
498 				setHistogramMax(clut.getMaxValue());
499 
500 			} else if (ColorMap.NONE.equals(_clut.getColorMap()))
501 				setCLUT(new ImageCLUT(ColorMap.GRAYSCALE, new int[] { frmHdr.effectiveBitsPerPixel }));
502 
503 		} else if (_clut == null || !ColorMap.NONE.equals(_clut.getColorMap()) || !_clut.isRGB()) {
504 			ImageCLUT clut = new ImageCLUT(ColorMap.NONE, new int[] { 8, 8, 8 });
505 			setCLUT(clut);
506 			setHistogramMin(0);
507 			setHistogramMax(clut.getMaxValue());
508 		}
509 
510 		// create BufferedImage for rendering
511 		Runnable r = new Runnable() {
512 			public void run() {
513 				createBufferedImage();
514 			}
515 		};
516 
517 		if (SwingUtilities.isEventDispatchThread()) {
518 			r.run();
519 		} else {
520 			SwingUtilities.invokeLater(r);
521 		}
522 	}
523 
524 	// private void updateHistogramMinMax(int maxValue) {
525 	// int max = getHistogramMax();
526 	// int min = getHistogramMin();
527 	// max = Math.min(max == UNDEFINED_CONDITION ? maxValue : max, maxValue);
528 	// min = Math.min(min == UNDEFINED_CONDITION ? 0 : min, max - 1);
529 	//
530 	// System.out.println("ImageDisplayer.updateHistogramMinMax(" + min + ", " +
531 	// max + ")");
532 	//
533 	// setHistogramMin(min);
534 	// setHistogramMax(max);
535 	// }
536 
537 	/**
538 	 * Takes a snapshot of the currently displayed image. TODO: mdavid
539 	 * 
540 	 * @return a snapshot of the current image
541 	 */
542 	public BufferedImage getSnapshotImage() {
543 		return _image;
544 	}
545 
546 	/**
547 	 * Saves the currently displayed image as archival PNG file.
548 	 * 
549 	 * @param fileNamePath
550 	 *            Filename of PNG file with extension, may include path
551 	 * @return <code>false</code> if an error occurs during writing, otherwise
552 	 *         <code>true</code>
553 	 */
554 	public boolean saveAsPNGImage(String fileNamePath) {
555 		synchronized (_lockHeaderUpdate) {
556 			try {
557 				return TImageIO.write(new TBufferedImage(_timage), new File(fileNamePath));
558 
559 			} catch (Exception e) {
560 				getExceptionListener().exceptionThrown(e);
561 			}
562 			return false;
563 		}
564 	}
565 
566 	/**
567 	 * Exports the currently displayed image as exported PNG file.
568 	 * 
569 	 * @param fileNamePath
570 	 *            Filename of PNG file with extension, may include path
571 	 * @return <code>false</code> if an error occurs during writing, otherwise
572 	 *         <code>true</code>
573 	 */
574 	public boolean exportToPNG(String fileNamePath) {
575 		synchronized (_lockHeaderUpdate) {
576 			try {
577 
578 				// long t = System.currentTimeMillis();
579 				// BufferedImage rgbImage = TBufferedImage.toImageRGB(_image);
580 				// System.out.println("toImageRGB: " +
581 				// (System.currentTimeMillis() - t));
582 
583 				return TImageIO.write(new TBufferedImage(TBufferedImage.toImageRGB(_image), null), //
584 						new File(fileNamePath));
585 
586 				// return TImageIO.write(new TBufferedImage(rgbImage, new
587 				// TImageMetadata(_image)), new File(fileNamePath));
588 
589 				// return TImageIO.write(new TBufferedImage(_timage,
590 				// _clut.getColorMap(), _histogramEqualisation, true),
591 				// new File(fileNamePath));
592 
593 			} catch (Exception e) {
594 				getExceptionListener().exceptionThrown(e);
595 			}
596 			return false;
597 		}
598 	}
599 
600 	// //////////////////////////////////////////////////////////////////////////
601 	// /
602 	//
603 	// private methods
604 	//
605 	// //////////////////////////////////////////////////////////////////////////
606 	// /
607 
608 	/**
609 	 * This method initializes this current instance in order to display video
610 	 * images. Splash screen will be shown if the component is drawn after
611 	 * initialize() was finished.
612 	 * 
613 	 * @return void
614 	 */
615 	private void initialize() {
616 
617 		setLayout(new BorderLayout());
618 		// setBackground(Color.darkGray);
619 
620 		// create mouse listeners
621 		addMouseListener(new MouseListener() {
622 			@Override
623 			public void mouseClicked(MouseEvent e) {
624 				// TODO Auto-generated method stub
625 			}
626 
627 			@Override
628 			public void mouseEntered(MouseEvent e) {
629 				_isMouseInCanvas = true;
630 				if (_overlayState == OverlayState.AUTO)
631 					repaintImage();
632 			}
633 
634 			@Override
635 			public void mouseExited(MouseEvent e) {
636 				_isMouseInCanvas = false;
637 				if (_overlayState == OverlayState.AUTO)
638 					repaintImage();
639 			}
640 
641 			@Override
642 			public void mousePressed(MouseEvent e) {
643 				_isMousePressed = true;
644 				if (_overlayState == OverlayState.ON || _overlayState == OverlayState.AUTO)
645 					repaintImage();
646 			}
647 
648 			@Override
649 			public void mouseReleased(MouseEvent e) {
650 				_isMousePressed = false;
651 				if (_overlayState == OverlayState.ON || _overlayState == OverlayState.AUTO)
652 					repaintImage();
653 			}
654 
655 		});
656 
657 		addMouseMotionListener(new MouseMotionListener() {
658 			@Override
659 			public void mouseDragged(MouseEvent e) {
660 				if (_overlayState == OverlayState.ON || _overlayState == OverlayState.AUTO)
661 					repaintImage();
662 			}
663 
664 			@Override
665 			public void mouseMoved(MouseEvent e) {
666 			}
667 		});
668 
669 		addPropertyChangeListener(new PropertyChangeListener() {
670 			public void propertyChange(PropertyChangeEvent e) {
671 				String propertyChanged = e.getPropertyName();
672 				if (propertyChanged.equals(PROPERTY_KEEP_ASPECT_RATIO) //
673 						|| propertyChanged.equals(PROPERTY_OVERLAY_STATE)) {
674 					repaintImage();
675 
676 				} else if (propertyChanged.equals(PROPERTY_IMAGE_ZOOM)) {
677 					setPreferredSize(ImageZoom.AUTO.equals(_imageZoom) ? new Dimension(0, 0) : getZoomedSize(_image
678 							.getWidth(), _image.getHeight(), getWidth(), getHeight()));
679 					revalidate();
680 					repaintImage();
681 
682 				} else if (propertyChanged.equals(PROPERTY_AOI_ZOOM) //
683 						|| propertyChanged.equals(PROPERTY_HISTOGRAM_EQUALISATION) //
684 						|| propertyChanged.equals(PROPERTY_HISTOGRAM_MIN) //
685 						|| propertyChanged.equals(PROPERTY_HISTOGRAM_MAX) //
686 						|| propertyChanged.equals(PROPERTY_COLOR_MAP)) {
687 					createBufferedImage();
688 				}
689 			}
690 		});
691 
692 		createSplashScreen(_timage = new IMAGE(0));
693 		updateValue(_timage);
694 	}
695 
696 	/**
697 	 * loads or creates a splash screen. If a file "splash.png" can be found in
698 	 * the current directory on disk and can be loaded, it is used as splash
699 	 * screen. Otherwise, a basic splash screen is generated which is just a
700 	 * pure black video image.<br>
701 	 * <br>
702 	 * The splash screen is shown when the ImageDisplayer is shown or drawn and
703 	 * updateValue was not called a single time from outside before.
704 	 * 
705 	 * @param destImage
706 	 *            reference to the IMAGE instance that will store the splash
707 	 *            screen
708 	 */
709 	@SuppressWarnings("deprecation")
710 	private void createSplashScreen(IMAGE destImage) {
711 		if (destImage == null) {
712 			getExceptionListener().exceptionThrown(new NullPointerException("destImage == null!"));
713 			return;
714 		}
715 
716 		String strURL = System.getProperty("splashImage.url");
717 		URL splashURL = null;
718 		try {
719 			if (strURL == null || (strURL = strURL.trim()).isEmpty())
720 				splashURL = (new File("splash.png")).toURI().toURL();
721 			else
722 				splashURL = new URL(strURL);
723 
724 		} catch (MalformedURLException e) {
725 			getExceptionListener().exceptionThrown(e);
726 			return;
727 		}
728 
729 		// TODO: remove loading IMM splash (!?)
730 		if (!ImageParser.loadImageFile(splashURL, destImage, false) && !ImageParser.loadIMM("splash.imm", destImage)) {
731 			// obtain references:
732 			IMAGE.FrameHeader frameHeader = destImage.getFrameHeader();
733 			IMAGE.SourceHeader sourceHeader = destImage.getSourceHeader();
734 
735 			// set data of frameHeader
736 			frameHeader.bytesPerPixel = 1;
737 			frameHeader.appendedFrameSize = 768 * 576 * frameHeader.bytesPerPixel;
738 			frameHeader.effectiveBitsPerPixel = 8;
739 
740 			// mdavid: changed
741 			frameHeader.imageFlags |= ImageFlag.IMAGE_LOSSLESS.getId();
742 
743 			frameHeader.imageFormat = frameHeader.sourceFormat = ImageFormat.IMAGE_FORMAT_GRAY.getId();
744 			frameHeader.sourceHeight = 576;
745 			frameHeader.sourceWidth = 768;
746 			frameHeader.xScale = frameHeader.yScale = 1F;
747 
748 			// set data of sourceHeader
749 			sourceHeader.cameraPortName = "Internal Backup Splashscreen";
750 			java.util.Calendar c = java.util.Calendar.getInstance();
751 			sourceHeader.timestampMicroseconds = (int) (((c.getTimeInMillis() % ((long) 1000)) * ((long) 1000)));
752 			sourceHeader.timestampSeconds = (int) (((c.getTimeInMillis()) / ((long) 1000)));
753 			sourceHeader.totalLength = IMAGE.HEADER_SIZE + frameHeader.appendedFrameSize;
754 
755 			byte[] buf = new byte[frameHeader.appendedFrameSize];
756 
757 			// imperformant, but easy solution:
758 			for (int i = 0; i < frameHeader.sourceWidth * frameHeader.sourceHeight;)
759 				buf[i++] = (byte) 0;
760 
761 			destImage.setImageFrameBuffer(buf);
762 		}
763 
764 	}
765 
766 	/**
767 	 * loads or creates an error screen. If a file "error.png" can be found in
768 	 * the current directory on disk and can be loaded, it is used as error
769 	 * screen. Otherwise, a basic error screen is generated which is a random
770 	 * noise screen (should look like _static_ (paused) white noise in analog
771 	 * TV.<br>
772 	 * <br>
773 	 * The error screen is shown in case the current video image could not be
774 	 * decoded, displayed or is generally malformed (header bad, ...). A special
775 	 * textual region will be embedded in the noisy error screen stating that
776 	 * the current frame could not be displayed.
777 	 * 
778 	 * @param destImage
779 	 *            reference to the IMAGE instance that will store the error
780 	 *            screen in memory
781 	 */
782 	@SuppressWarnings("deprecation")
783 	private void createErrorScreen(IMAGE destImage) {
784 		if (destImage == null) {
785 			getExceptionListener().exceptionThrown(new NullPointerException("destImage == null!"));
786 			return;
787 		}
788 
789 		String strURL = System.getProperty("errorImage.url");
790 		URL splashURL = null;
791 		try {
792 			if (strURL == null || (strURL = strURL.trim()).isEmpty())
793 				splashURL = (new File("error.png")).toURI().toURL();
794 			else
795 				splashURL = new URL(strURL);
796 
797 		} catch (MalformedURLException e) {
798 			getExceptionListener().exceptionThrown(e);
799 			return;
800 		}
801 
802 		if (!ImageParser.loadImageFile(splashURL, destImage, false)) {
803 
804 			// obtain references:
805 			IMAGE.FrameHeader frameHeader = destImage.getFrameHeader();
806 			IMAGE.SourceHeader sourceHeader = destImage.getSourceHeader();
807 
808 			// set data of frameHeader
809 			frameHeader.appendedFrameSize = 768 * 576;
810 			frameHeader.bytesPerPixel = 1;
811 			frameHeader.effectiveBitsPerPixel = 8;
812 
813 			// mdavid: changed
814 			frameHeader.imageFlags |= ImageFlag.IMAGE_LOSSLESS.getId();
815 
816 			frameHeader.imageFormat = ImageFormat.IMAGE_FORMAT_GRAY.getId();
817 			frameHeader.sourceFormat = ImageFormat.IMAGE_FORMAT_GRAY.getId();
818 			frameHeader.sourceHeight = 576;
819 			frameHeader.sourceWidth = 768;
820 			frameHeader.xScale = frameHeader.yScale = 1F;
821 
822 			// set data of sourceHeader
823 			sourceHeader.cameraPortName = INVALID_CAMERA_PORT_NAME;
824 			java.util.Calendar c = java.util.Calendar.getInstance();
825 			sourceHeader.timestampMicroseconds = (int) (((c.getTimeInMillis() % ((long) 1000)) * ((long) 1000)));
826 			sourceHeader.timestampSeconds = (int) (((c.getTimeInMillis()) / ((long) 1000)));
827 			sourceHeader.totalLength = IMAGE.HEADER_SIZE + frameHeader.appendedFrameSize;
828 
829 			// create, generate and set video frame buffer
830 
831 			byte[] buf = new byte[frameHeader.appendedFrameSize];
832 
833 			// imperformant, but easy solution:
834 			for (int i = 0; i < frameHeader.sourceWidth * frameHeader.sourceHeight;)
835 				buf[i++] = (byte) (Math.random() * 256.0); // grayscale noise
836 
837 			destImage.setImageFrameBuffer(buf);
838 		}
839 	}
840 
841 	/**
842 	 * Calculates zoomed image dimension.
843 	 * 
844 	 * @param srcW
845 	 *            - source image width
846 	 * @param srcH
847 	 *            - source image height
848 	 * @param destW
849 	 *            - destination image width
850 	 * @param destH
851 	 *            - destination image height
852 	 * @return zoomed image dimension
853 	 */
854 	private Dimension getZoomedSize(int srcW, int srcH, int destW, int destH) {
855 		switch (_imageZoom) {
856 		case AUTO:
857 			return new Dimension(destW, destH);
858 
859 		case HALF:
860 			return new Dimension(srcW / 2, srcH / 2);
861 
862 		case NORMAL:
863 			return new Dimension(srcW, srcH);
864 
865 		case DOUBLE:
866 			return new Dimension(srcW * 2, srcH * 2);
867 
868 		default:
869 			throw new IllegalStateException("unsupported image zoom: " + _imageZoom);
870 		}
871 	}
872 
873 	/**
874 	 * Calculates destination image dimension.
875 	 * 
876 	 * @param srcW
877 	 *            - source image width
878 	 * @param srcH
879 	 *            - source image height
880 	 * @param destW
881 	 *            - destination image width
882 	 * @param destH
883 	 *            - destination image height
884 	 * @return calculated destination image dimension
885 	 */
886 	private Dimension getKeepedRatioSize(int srcW, int srcH, int destW, int destH) {
887 		float ratio = (float) srcW / (float) srcH;
888 		if (destW > destH) {
889 			int tmp = Math.round(ratio * destH);
890 			if (tmp > destW)
891 				return new Dimension(destW, Math.round(destW / ratio));
892 			return new Dimension(tmp, destH);
893 		}
894 		return new Dimension(destW, Math.round(destW / ratio));
895 	}
896 
897 	/**
898 	 * Drawing of image as well as full overlay information inside the video
899 	 * canvas. Takes into account AOI zooming and aspect ratio keeping. Reads
900 	 * out video buffer for "value under cursor" function.
901 	 * 
902 	 * @author mdavid
903 	 * @param g2d
904 	 *            - Graphics context
905 	 * @param image
906 	 *            - Image to draw context
907 	 * @param size
908 	 *            - Frame size
909 	 */
910 	private void drawFrame(Graphics2D g2d, BufferedImage srcImage) {
911 
912 		int srcW = srcImage.getWidth();
913 		int srcH = srcImage.getHeight();
914 		int winW = getWidth();
915 		int winH = getHeight();
916 
917 		Dimension destDim = (ImageZoom.AUTO.equals(_imageZoom) && _keepAspectRatio) ? getKeepedRatioSize(srcW, srcH,
918 				winW, winH) : getZoomedSize(srcW, srcH, winW, winH);
919 		Point destPoint = new Point((winW - destDim.width) / 2, (winH - destDim.height) / 2);
920 
921 		g2d.drawImage(srcImage, destPoint.x, destPoint.y, destDim.width, destDim.height, null);
922 
923 		g2d.setColor(Color.gray);
924 		g2d.drawRect(destPoint.x - 1, destPoint.y - 1, destDim.width + 2, destDim.height + 2);
925 
926 		int xStart = (Integer) srcImage.getProperty(TImageMetadata.KEY_XSTART);
927 		int yStart = (Integer) srcImage.getProperty(TImageMetadata.KEY_YSTART);
928 		int aoiW = (Integer) srcImage.getProperty(TImageMetadata.KEY_AOI_WIDTH);
929 		int aoiH = (Integer) srcImage.getProperty(TImageMetadata.KEY_AOI_HEIGHT);
930 
931 		String cameraPortName = (String) srcImage.getProperty(TImageMetadata.KEY_CAMERA_PORT_NAME);
932 		long frameNumber = (Long) srcImage.getProperty(TImageMetadata.KEY_FRAME_NUMBER);
933 		int bits = (Integer) srcImage.getProperty(TImageMetadata.KEY_EFFECTIVE_BITS_PER_PIXEL);
934 		int bpp = (Integer) srcImage.getProperty(TImageMetadata.KEY_BYTES_PER_PIXEL);
935 		int srcFormat = (Integer) srcImage.getProperty(TImageMetadata.KEY_SOURCE_FORMAT);
936 		int imgFormat = (Integer) srcImage.getProperty(TImageMetadata.KEY_IMAGE_FORMAT);
937 		int imgFlags = (Integer) srcImage.getProperty(TImageMetadata.KEY_IMAGE_FLAGS);
938 		float imgRotation = (Float) srcImage.getProperty(TImageMetadata.KEY_IMAGE_ROTATION);
939 		int timestampSeconds = (Integer) srcImage.getProperty(TImageMetadata.KEY_TIMESTAMP_SECONDS);
940 		int timestampMicroseconds = (Integer) srcImage.getProperty(TImageMetadata.KEY_TIMESTAMP_MICROSECONDS);
941 
942 		if (cameraPortName == null || INVALID_CAMERA_PORT_NAME.equals(cameraPortName))
943 			drawErrorString(g2d, frameNumber, winW, winH);
944 
945 		boolean isAoi = (aoiW != UNDEFINED_CONDITION);
946 
947 		// draw overlay information
948 		if (OverlayState.ON.equals(_overlayState) || (_isMouseInCanvas && OverlayState.AUTO.equals(_overlayState))) {
949 			StringBuilder sb = new StringBuilder();
950 			if (_isMousePressed) {
951 				// Rectangle destRect;
952 				// if (isAoi) // AOI is present
953 				// destRect = new Rectangle(destPoint.x + xStart, destPoint.y +
954 				// yStart, aoiW, aoiH);
955 				// else
956 				// destRect = new Rectangle(destPoint.x, destPoint.y,
957 				// destDim.width, destDim.height);
958 
959 				Rectangle destRect = new Rectangle(destPoint.x, destPoint.y, destDim.width, destDim.height);
960 
961 				Point p = getMousePosition();
962 				if (p != null && destRect.contains(p)) {
963 					double scaleX = 1D * srcW / destRect.width;
964 					double scaleY = 1D * srcH / destRect.height;
965 					int pX = (int) (scaleX * (p.x - destRect.x));
966 					int pY = (int) (scaleY * (p.y - destRect.y));
967 					int rgb = ((BufferedImage) _image).getRGB(pX, pY);
968 					sb.append(" px(").append(pX).append(",").append(pY).append(") = ");
969 					sb.append("(").append((rgb >> 16) & 0xff);
970 					sb.append("/").append((rgb >> 8) & 0xff);
971 					sb.append("/").append(rgb & 0xff).append(")");
972 				} else
973 					sb.append(" (out of dimension)");
974 			}
975 
976 			final int ovH = 36;
977 			final int insetX = 10;
978 			final int insetY = 15;
979 
980 			Rectangle r = getVisibleRect();
981 
982 			// Composite orgComposite = g2d.getComposite();
983 			g2d.setColor(Color.lightGray);
984 			g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.85F));
985 			g2d.fillRect(r.x, r.y, r.width, ovH);
986 			g2d.fillRect(r.x, r.y + r.height - ovH, r.width, ovH);
987 			// g2d.setComposite(orgComposite);
988 			g2d.setColor(Color.black);
989 
990 			FontMetrics fm = g2d.getFontMetrics();
991 
992 			String leftStr = "Source: " + cameraPortName + " (" + ImageFormat.valueOf(srcFormat) + ")";
993 			String rightStr = "# " + frameNumber + " - "
994 					+ formatDate(new Date((timestampSeconds * 1000L) + (timestampMicroseconds / 1000L)));
995 
996 			int strW = fm.stringWidth(leftStr) + insetX;
997 			drawOverlayStr(leftStr, g2d, fm, r.x, r.y + insetY, r.x + r.width, ovH, false);
998 			drawOverlayStr(rightStr, g2d, fm, r.x + strW, r.y + insetY, r.x + r.width, ovH, true);
999 
1000 			leftStr = "Size: " + srcW + " px * " + srcH + " px * " + bits + " of " + bpp * 8 + " bpp ";
1001 			if (isAoi)
1002 				leftStr += "(AOI: L " + xStart + ", T " + yStart + ", W " + aoiW + ", H " + aoiH + ")";
1003 			else
1004 				leftStr += "(AOI: none)";
1005 
1006 			rightStr = "Drop: " + _imageCounter.getDroppedFrames() // 
1007 					+ " (" + String.format("%4.3f", _imageCounter.getDroppedPercent() * 100D) + "%)";
1008 
1009 			strW = fm.stringWidth(leftStr) + insetX;
1010 			drawOverlayStr(leftStr, g2d, fm, r.x, r.y + 2 * insetY, r.x + r.width, ovH, false);
1011 			drawOverlayStr(rightStr, g2d, fm, r.x + strW, r.y + 2 * insetY, r.x + r.width, ovH, true);
1012 
1013 			leftStr = "Format: " + ImageFormat.valueOf(imgFormat) + sb.toString();
1014 			strW = fm.stringWidth(leftStr) + insetX;
1015 			drawOverlayStr(leftStr, g2d, fm, r.x, r.y + r.height - ovH + insetY, r.x + r.width, ovH, false);
1016 
1017 			rightStr = "Rotation: " + String.format("%4.2f", Math.abs(imgRotation)) + "°";
1018 			if (imgRotation > 0F)
1019 				rightStr += " cw";
1020 			else if (imgRotation < 0F)
1021 				rightStr += " ccw";
1022 
1023 			leftStr = "Flags: " + ImageFlag.toString(imgFlags);
1024 			strW = fm.stringWidth(leftStr) + insetX;
1025 
1026 			drawOverlayStr(leftStr, g2d, fm, r.x, r.y + r.height - ovH + 2 * insetY, r.x + r.width, ovH, false);
1027 			drawOverlayStr(rightStr, g2d, fm, r.x + strW, r.y + r.height - ovH + 2 * insetY, r.x + r.width, ovH, true);
1028 		}
1029 	}
1030 
1031 	private void drawErrorString(Graphics2D g2d, long frameNumber, int w, int h) {
1032 
1033 		String s1 = "Error: Video Frame #" + frameNumber + " could not be decoded or displayed properly.";
1034 
1035 		Font orgFont = g2d.getFont();
1036 		g2d.setFont(orgFont.deriveFont(Font.BOLD, (float) 12.0));
1037 
1038 		int fontH = g2d.getFontMetrics().getHeight();
1039 		int maxH = fontH + 20;
1040 
1041 		int s1w = g2d.getFontMetrics().stringWidth(s1);
1042 		int maxW = s1w + 40;
1043 
1044 		int y = (h - maxH) / 2;
1045 
1046 		g2d.setColor(Color.black);
1047 		g2d.fillRect((w - maxW) / 2, y, maxW, maxH);
1048 
1049 		g2d.setColor(Color.red);
1050 		g2d.drawString(s1, (w - s1w) / 2, y + 20);
1051 
1052 		g2d.setFont(orgFont);
1053 	}
1054 
1055 	/**
1056 	 * @author mdavid
1057 	 */
1058 	private void drawOverlayStr(String str, //
1059 			Graphics2D g2d, //
1060 			FontMetrics fm, //
1061 			int x, //
1062 			int y, //
1063 			int width, //
1064 			int height, //
1065 			boolean isRight) {
1066 
1067 		if (str == null || str.length() == 0)
1068 			return;
1069 
1070 		int insetX = 10;
1071 
1072 		int area = width - x;
1073 		int stringWidth = fm.stringWidth(str) + insetX;
1074 
1075 		if (stringWidth + insetX > area) {
1076 			String suffix = "...";
1077 			int suffixWidth = fm.stringWidth(suffix) + insetX;
1078 			stringWidth = suffixWidth + insetX;
1079 
1080 			StringBuilder sb = new StringBuilder();
1081 			for (int i = 0; i < str.length(); i++) {
1082 				if (stringWidth >= area)
1083 					break;
1084 				sb.append(str.charAt(i));
1085 				stringWidth = insetX + fm.stringWidth(sb.toString()) + suffixWidth;
1086 			}
1087 			g2d.drawString(sb.append(suffix).toString(), x + insetX, y);
1088 
1089 		} else
1090 			g2d.drawString(str, (isRight ? width - stringWidth : x + insetX), y);
1091 	}
1092 
1093 	/**
1094 	 * redraws the current image. A meaningful display buffer is created or
1095 	 * reused. In case of error, the error image is passed along. This display
1096 	 * buffer is rendered. <br>
1097 	 */
1098 	private void createBufferedImage() {
1099 		// long t = System.currentTimeMillis();
1100 		try {
1101 			// for (int i = 0; i < 10; i++) { // TODO
1102 			_image = TBufferedImage.toBufferedImage(_timage, _clut.getColorMap(), _histogramEqualisation,
1103 					_histogramMin, _histogramMax, !_aoiZoom);
1104 			// }
1105 
1106 			// int type = _image.getType();
1107 			// if (type == BufferedImage.TYPE_BYTE_GRAY //
1108 			// || type == BufferedImage.TYPE_USHORT_GRAY //
1109 			// || type == BufferedImage.TYPE_BYTE_INDEXED //
1110 			// || (type == BufferedImage.TYPE_CUSTOM &&
1111 			// _image.getData().getNumBands() == 1)) { //
1112 			//
1113 			// setCLUT(new TLookUpTable(_timage.getFrameHeader().bytesPerPixel,
1114 			// _timage.getFrameHeader().effectiveBitsPerPixel,
1115 			// ColorMap.GRAYSCALE));
1116 			//
1117 			// } else {
1118 			// // setCLUT(new TLookUpTable(0, 0, null));
1119 			// }
1120 
1121 		} catch (Exception e) {
1122 			_image = TBufferedImage.toBufferedImage(_timage = getErrorScreen());
1123 			getExceptionListener().exceptionThrown(e);
1124 		}
1125 		// System.out.println("CREATED: " + (System.currentTimeMillis() - t));
1126 
1127 		repaint();
1128 	}
1129 
1130 	/**
1131 	 * Reduces number of renders in run-mode
1132 	 */
1133 	private void repaintImage() {
1134 		if (_isLiveTransfer) {
1135 			if (_timer == null) { // Set up timer to drive animation events.
1136 				_timer = new Timer(TIMER_DELAY, new ActionListener() {
1137 					public void actionPerformed(ActionEvent evt) {
1138 						repaint();
1139 					}
1140 				});
1141 				_timer.start();
1142 			} else
1143 				_timer.restart();
1144 		} else
1145 			repaint();
1146 	}
1147 
1148 	/**
1149 	 * @author mdavid
1150 	 * @param date
1151 	 *            - Date to formatted
1152 	 * @return date in full format if date is not today
1153 	 */
1154 	private String formatDate(Date date) {
1155 		java.util.Calendar tmp = java.util.Calendar.getInstance();
1156 		int day = tmp.get(java.util.Calendar.DAY_OF_YEAR);
1157 		int year = tmp.get(java.util.Calendar.YEAR);
1158 		tmp.setTime(date);
1159 		boolean isSameDate = (day == tmp.get(Calendar.DAY_OF_YEAR) && year == tmp.get(Calendar.YEAR));
1160 		return new SimpleDateFormat(isSameDate ? "HH:mm:ss.SSS" : "dd.MM.yy HH:mm:ss.SSS").format(date);
1161 	}
1162 
1163 	/**
1164 	 * Create error screen (TINE IMAGE type) by calling appropriate helper
1165 	 * method
1166 	 * 
1167 	 * @return IMAGE - Error screen image
1168 	 */
1169 	private IMAGE getErrorScreen() {
1170 		if (_errorScreen == null) {
1171 			_errorScreen = new IMAGE(0);
1172 			createErrorScreen(_errorScreen);
1173 		}
1174 		return _errorScreen;
1175 	}
1176 
1177 	/**
1178 	 * Sets an exception listener for this ImageDisplayer.
1179 	 * 
1180 	 * @param exceptionListener
1181 	 *            - the exception listener
1182 	 */
1183 	public void setExceptionListener(ExceptionListener exceptionListener) {
1184 		this._exceptionListener = exceptionListener;
1185 	}
1186 
1187 	/**
1188 	 * Gets the ExceptionListener object for this ImageDisplayer.
1189 	 * 
1190 	 * @return the ExceptionListener for this ImageDisplayer
1191 	 */
1192 	public ExceptionListener getExceptionListener() {
1193 		return (_exceptionListener != null) ? _exceptionListener : defaultExceptionListener;
1194 	}
1195 
1196 	/**
1197 	 * The default ExceptionListener is the default implementation of the
1198 	 * ExceptionListener interface. An instance of this class is used whenever
1199 	 * the user provided no ExceptionListener instance on its own.
1200 	 */
1201 	private static final ExceptionListener defaultExceptionListener = new ExceptionListener() {
1202 		public void exceptionThrown(Exception e) {
1203 			System.err.println(e);
1204 		}
1205 	};
1206 }