/* Copyright (c) 2001-2015, David A. Clunie DBA Pixelmed Publishing. All rights reserved. */

package com.pixelmed.display;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.RenderingHints;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;

import java.awt.Font;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.BandCombineOp;
import java.awt.image.ComponentColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferDouble;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;

import javax.swing.JComponent;
import javax.swing.JFrame;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;

import javax.swing.SwingUtilities;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Vector;

import com.pixelmed.event.ApplicationEventDispatcher;
import com.pixelmed.event.Event;
import com.pixelmed.event.EventContext;
import com.pixelmed.event.SelfRegisteringListener;

import com.pixelmed.display.event.ApplyShutterChangeEvent;
import com.pixelmed.display.event.FrameSelectionChangeEvent;
import com.pixelmed.display.event.FrameSortOrderChangeEvent;
import com.pixelmed.display.event.GraphicDisplayChangeEvent;
import com.pixelmed.display.event.StatusChangeEvent;
import com.pixelmed.display.event.VOIFunctionChangeEvent;
import com.pixelmed.display.event.WindowingAccelerationValueChangeEvent;
import com.pixelmed.display.event.WindowLinearCalculationChangeEvent;
import com.pixelmed.display.event.WindowCenterAndWidthChangeEvent;

import com.pixelmed.dicom.AttributeList;
import com.pixelmed.dicom.DicomException;
import com.pixelmed.dicom.DisplayShutter;
import com.pixelmed.dicom.ModalityTransform;
import com.pixelmed.dicom.Overlay;
import com.pixelmed.dicom.RealWorldValueTransform;
import com.pixelmed.dicom.SUVTransform;
import com.pixelmed.dicom.VOITransform;

import com.pixelmed.utils.ColorUtilities;
import com.pixelmed.utils.FloatFormatter;

import com.pixelmed.geometry.GeometryOfSlice;
import com.pixelmed.geometry.GeometryOfVolume;

/**
 * <p>Implements a component that can display a single or multi-frame image in a
 * single panel with window center/width adjustment, scrolling through frames
 * of a multi-frame image, resizing to the size of the panel, annotation
 * of pre-defined text and shapes, feedback of cursor position status (2D and
 * 3D position, pixel value, window).</p>
 *
 * <p>Includes a main() method for testing that will display a single image from
 * a file in a JFrame.</p>
 *
 * <p>About the most minimal code to display a single DICOM image looks like this:</p>
 *
 * <pre>
 * JFrame p = new JFrame();
 * p.add(new SingleImagePanel(new SourceImage(filename)));
 * p.setBackground(Color.BLACK);
 * p.setSize(512,512);
 * p.setVisible(true);
 * </pre>
 *
 * @see com.pixelmed.display.SourceImage
 *
 * @author	dclunie
 */
public class SingleImagePanel extends JComponent implements KeyListener, MouseListener, MouseMotionListener, MouseWheelListener {

	/***/
	private static final String identString = "@(#) $Header: /userland/cvs/pixelmed/imgbook/com/pixelmed/display/SingleImagePanel.java,v 1.197 2015/10/18 21:58:53 dclunie Exp $";

	/***/
	SourceImage sImg;
	/***/
	int currentSrcImageIndex;
	/***/
	int[] currentSrcImageSortOrder;

	/***/
	BufferedImage cachedResizedImage;
	/***/
	BufferedImage cachedResizedSelectedRegionImage;
	/***/
	BufferedImage cachedPreWindowedImage;
	/***/
	int offsetDrawingOfResizedSelectedRegionImageX;
	/***/
	int offsetDrawingOfResizedSelectedRegionImageY;
	/***/
	Rectangle cachedWindowSize;
	
	public void dirty() {
		dirtySource();
	}

	public void dirtySource() {
		cachedResizedImage=null;
		cachedResizedSelectedRegionImage=null;
		cachedPreWindowedImage=null;
	}
	
	public void dirty(SourceImage sImg) {
		dirtySource(sImg);
	}

	public void dirtySource(SourceImage sImg) {
		this.sImg=sImg;
		this.realWorldValueTransform=sImg.getRealWorldValueTransform();
		this.modalityTransform=sImg.getModalityTransform();
		this.voiTransform=sImg.getVOITransform();
		this.displayShutter=sImg.getDisplayShutter();
		this.overlay=sImg.getOverlay();
		cachedResizedImage=null;
		cachedResizedSelectedRegionImage=null;
		cachedPreWindowedImage=null;
	}

	public void dirtyWindowing() {
		cachedPreWindowedImage=null;
	}

	public void dirtyPanned() {
		cachedPreWindowedImage=null;
		cachedResizedSelectedRegionImage=null;
	}

	/**
	 * whether or not to use the supplied VOI LUT, rather than a linear or sigmoid window function
	 */
	protected boolean useVOILUTNotFunction;
	/**
	 * the currently selected, default or user modified window center
	 */
	protected double windowCenter;
	/**
	 * the currently selected, default or user modified window width
	 */
	protected double windowWidth;
	/**
	 * the currently selected VOI LUT window width value that will result in the application of the VOI LUT rescaling the input (index) values
	 */
	protected double voiLUTIdentityWindowWidth;
	/**
	 * the currently selected VOI LUT window center value that will result in the application of the VOI LUT rescaling the input (index) values
	 */
	protected double voiLUTIdentityWindowCenter;
	/**
	 * the currently selected VOI LUT number of entries
	 */
	protected int voiLUTNumberOfEntries;
	/**
	 * the currently selected VOI LUT first value mapped
	 */
	protected int voiLUTFirstValueMapped;
	/**
	 * the currently selected VOI LUT bits per entry
	 */
	protected int voiLUTBitsPerEntry;
	/**
	 * the currently selected VOI LUT Data
	 */
	protected short[] voiLUTData;
	/**
	 * the currently selected VOI LUT minimum entry value
	 */
	protected int voiLUTEntryMin;
	/**
	 * the currently selected VOI LUT maximum entry value
	 */
	protected int voiLUTEntryMax;
	/**
	 * the currently selected VOI LUT top of entry range (which may be less than (2^voiLUTNumberOfEntries)-1, e.g., in buggy Agfa images)
	 */
	protected int voiLUTTopOfEntryRange;
	
	/**
	 * the value of rescale slope to use for current frame (set when new frame selected)
	 */
	double useSlope;
	/**
	 * the value of rescale intercept to use for current frame (set when new frame selected)
	 */
	double useIntercept;

	/***/
	double imgMin;
	/***/
	double imgMax;

	/***/
	private int largestGray;
	/***/
	private int firstvalueMapped;
	/***/
	private int numberOfEntries;
	/***/
	private int bitsPerEntry;
	/***/
	private short redTable[];
	/***/
	private short greenTable[];
	/***/
	private short blueTable[];

	/***/
	private boolean signed;
	/***/
	private boolean inverted;
	/***/
	private boolean ybr;
	/***/
	private int signMask;
	/***/
	private int signBit;
	
	/***/
	int pad;
	/***/
	int padRangeLimit;
	/***/
	boolean hasPad;

	/***/
	SUVTransform suvTransform;

	/***/
	RealWorldValueTransform realWorldValueTransform;

	/***/
	ModalityTransform modalityTransform;

	/***/
	VOITransform voiTransform;

	/***/
	DisplayShutter displayShutter;

	/***/
	Overlay overlay;
	
	/***/
	protected GeometryOfVolume imageGeometry;
	
	/**
	 * <p>Get the geometry of the frames currently loaded in the single image panel.</p>
	 *
	 * @return	the geometry of the frames
	 */
	public GeometryOfVolume getImageGeometry() {
		return imageGeometry;
	}

	protected Vector<Shape> preDefinedShapes;
	protected Vector<Shape> preDefinedText;
	protected Vector<Shape> localizerShapes;
	protected Vector<Shape> volumeLocalizationShapes;
	protected Vector<Shape> interactiveDrawingShapes;
	protected Vector<Shape> persistentDrawingShapes;
	protected Vector<Shape> selectedDrawingShapes;
	protected Vector<TextAnnotation> persistentDrawingText;
	protected Vector<Shape>[] perFrameDrawingShapes;	// array size will be number of frames; will be indexed by currentSrcImageIndex just like the image frames
	protected Vector<SuperimposedImage> superimposedImages;

	/**
	 * <p>Set the predefined shapes to to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	shapes	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final void setPreDefinedShapes(Vector shapes) {
//System.err.println("SingleImagePanel.setPreDefinedShapes(): preDefinedShapes was:");
//com.pixelmed.geometry.LocalizerPosterFactory.dumpShapes(preDefinedShapes);
//System.err.println("SingleImagePanel.setPreDefinedShapes(): setting preDefinedShapes to:");
//com.pixelmed.geometry.LocalizerPosterFactory.dumpShapes(shapes);
		this.preDefinedShapes=shapes;
	}

	/**
	 * <p>Set the shapes of any localizer postings to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	shapes	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final void setLocalizerShapes(Vector shapes) { this.localizerShapes=shapes; }

	/**
	 * <p>Set the shapes of any volume localization postings to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	shapes	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final void setVolumeLocalizationShapes(Vector shapes) { this.volumeLocalizationShapes=shapes; }
	
	/**
	 * <p>Set the unselected region shapes to to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	shapes	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final void setPersistentDrawingShapes(Vector shapes) { persistentDrawingShapes=shapes; }
	
	/**
	 * <p>Get the unselected region shapes to to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @return	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final Vector getPersistentDrawingShapes() { return persistentDrawingShapes; }
	
	/**
	 * <p>Set the selected region shapes to to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	shapes	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final void setSelectedDrawingShapes(Vector shapes) { selectedDrawingShapes=shapes; }
	
	/**
	 * <p>Get the selected region shapes to to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @return	a {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}, may be null or empty
	 */
	public final Vector getSelectedDrawingShapes() { return selectedDrawingShapes; }

	/**
	 * <p>Set the array of per-frame shapes to be displayed on the respective frame when it is selected and displayed.</p>
	 *
	 * @param	arrayOfShapes	an array of {@link java.util.Vector java.util.Vector} of {@link java.awt.Shape java.awt.Shape}
	 */
	public final void setPerFrameDrawingShapes(Vector<Shape>[] arrayOfShapes) { this.perFrameDrawingShapes=arrayOfShapes; }
	
	/**
	 * <p>Set the superimposed images to to be displayed on the appropriate frames.</p>
	 *
	 * @param	superimposedImages	a {@link java.util.Vector java.util.Vector} of {@link com.pixelmed.display.SuperimposedImage com.pixelmed.display.SuperimposedImage}, may be null or empty
	 */
	public final void setSuperimposedImages(Vector<SuperimposedImage> superimposedImages) { this.superimposedImages = superimposedImages; }
	
	/**
	 * <p>Get the superimposed images to to be displayed on the appropriate frames.</p>
	 *
	 * @return	a {@link java.util.Vector java.util.Vector} of {@link com.pixelmed.display.SuperimposedImage com.pixelmed.display.SuperimposedImage}, may be null or empty
	 */
	public final Vector<SuperimposedImage> getSuperimposedImages() { return superimposedImages; }

	/***/
	private DemographicAndTechniqueAnnotations demographicAndTechniqueAnnotations = null;
	/***/
	private Color demographicAndTechniqueColor;
	/***/
	private Font demographicAndTechniqueFont;

	/**
	 * <p>Set annotative attribute values to be displayed on the currently selected and displayed frame at defined positions.</p>
	 *
	 * @param	demographicAndTechniqueAnnotations	the set of annotations, indexed per frame
	 * @param	demographicAndTechniqueFontName		the name of the font, e.g. "SansSerif"
	 * @param	demographicAndTechniqueFontStyle	the style of the font, e.g. Font.PLAIN
	 * @param	demographicAndTechniqueFontSize		the size of the font in points, e.g. 10
	 * @param	demographicAndTechniqueColor		the color to draw, e.g. Color.pink
	 */
	public final void setDemographicAndTechniqueAnnotations(DemographicAndTechniqueAnnotations demographicAndTechniqueAnnotations,
			String demographicAndTechniqueFontName,int demographicAndTechniqueFontStyle,int demographicAndTechniqueFontSize,Color demographicAndTechniqueColor) {
		this.demographicAndTechniqueAnnotations=demographicAndTechniqueAnnotations;
		this.demographicAndTechniqueFont = new Font(demographicAndTechniqueFontName,demographicAndTechniqueFontStyle,demographicAndTechniqueFontSize);
		this.demographicAndTechniqueColor=demographicAndTechniqueColor;
	}

	/***/
	protected OrientationAnnotations orientationAnnotations = null;
	/***/
	protected Color orientationColor;
	/***/
	protected Font orientationFont;
	/***/
	protected boolean showOrientationsLeftSide = false;

	/**
	 * <p>Set orientation values to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	orientationAnnotations	the set of orientations, indexed per frame
	 * @param	orientationFontName		the name of the font, e.g. "SansSerif"
	 * @param	orientationFontStyle	the style of the font, e.g. Font.PLAIN
	 * @param	orientationFontSize		the size of the font in points, e.g. 20
	 * @param	orientationColor		the color to draw, e.g. Color.pink
	 */
	public final void setOrientationAnnotations(OrientationAnnotations orientationAnnotations,
			String orientationFontName,int orientationFontStyle,int orientationFontSize,Color orientationColor) {
		setOrientationAnnotations(orientationAnnotations,orientationFontName,orientationFontStyle,orientationFontSize,orientationColor,false);
	}
	
	/**
	 * <p>Set orientation values to be displayed on the currently selected and displayed frame.</p>
	 *
	 * @param	orientationAnnotations	the set of orientations, indexed per frame
	 * @param	orientationFontName		the name of the font, e.g. "SansSerif"
	 * @param	orientationFontStyle	the style of the font, e.g. Font.PLAIN
	 * @param	orientationFontSize		the size of the font in points, e.g. 20
	 * @param	orientationColor		the color to draw, e.g. Color.pink
	 * @param	leftSide				show row orientation on left (true) or right (false) side of view port
	 */
	public final void setOrientationAnnotations(OrientationAnnotations orientationAnnotations,
			String orientationFontName,int orientationFontStyle,int orientationFontSize,Color orientationColor,boolean leftSide) {
		this.orientationAnnotations=orientationAnnotations;
		this.orientationFont = new Font(orientationFontName,orientationFontStyle,orientationFontSize);
		this.orientationColor=orientationColor;
		this.showOrientationsLeftSide=leftSide;
	}
	
	/***/
	protected String sideAndViewAnnotationString;
	/***/
	protected int sideAndViewAnnotationVerticalOffset;
	/***/
	protected Color sideAndViewAnnotationColor;
	/***/
	protected Font sideAndViewAnnotationFont;
	/***/
	protected boolean showSideAndViewAnnotationLeftSide = false;

	/**
	 * <p>Set side and view annotation string.</p>
	 *
	 * @param	annotationString		additional side (image laterality) and view (e.g., mammo CC) string to show on same side
	 * @param	verticalOffset
	 * @param	annotationFontName		the name of the font, e.g. "SansSerif"
	 * @param	annotationFontStyle		the style of the font, e.g. Font.PLAIN
	 * @param	annotationFontSize		the size of the font in points, e.g. 20
	 * @param	annotationColor			the color to draw, e.g. Color.pink
	 * @param	leftSide				show row orientation on left (true) or right (false) side of view port
	 */
	public final void setSideAndViewAnnotationString(String annotationString,int verticalOffset,
			String annotationFontName,int annotationFontStyle,
			int annotationFontSize,Color annotationColor,boolean leftSide) {
		this.sideAndViewAnnotationString = annotationString;
		this.sideAndViewAnnotationVerticalOffset = verticalOffset;
		this.sideAndViewAnnotationFont = new Font(annotationFontName,annotationFontStyle,annotationFontSize);
		this.sideAndViewAnnotationColor = annotationColor;
		this.showSideAndViewAnnotationLeftSide=leftSide;
	}
	
	/***/
	protected DisplayedAreaSelection originalDisplayedAreaSelection = null;
	/***/
	private DisplayedAreaSelection useDisplayedAreaSelection = null;

	/**
	 * <p>Select the sub-region of the image to display.</p>
	 *
	 * @param	displayedAreaSelection	the selection, or null to reset to using the entire image
	 */
	public final void setDisplayedAreaSelection(DisplayedAreaSelection displayedAreaSelection) {
		originalDisplayedAreaSelection=displayedAreaSelection;
		useDisplayedAreaSelection=null;							// defer fitting until window size is known in paintComponent()
	}
	
	public void displayReset() {
//System.err.println("SingleImagePanel.displayReset()");
		dirtySource();
		useDisplayedAreaSelection=null;
		establishInitialWindowOrVOILUT();
	}

	public void displaySelectedMagnificationRatio(double ratio) {
//System.err.println("SingleImagePanel.displaySelectedMagnificationRatio(): ratio ="+ratio);
		dirtySource();
		// originalDisplayedAreaSelection should never be null ... would have been initialized with default on first paintComponent();
		try {
			useDisplayedAreaSelection = (DisplayedAreaSelection)originalDisplayedAreaSelection.clone();
		}
		catch (Exception e) {				// e.g., CloneNotSupportedException ... should never happen
			e.printStackTrace(System.err);
			useDisplayedAreaSelection = null;
		}
		if (useDisplayedAreaSelection != null) {
			useDisplayedAreaSelection.setPixelMagnificationRatio(ratio);
			useDisplayedAreaSelection = useDisplayedAreaSelection.shapeSelectionToMatchAvailableWindow(cachedWindowSize);
			// leave window or VOI LUT alone
		}
	}

	/***/
	private AffineTransform preTransformImageRelativeCoordinates = null;

	/**
	 * <p>Select the AffineTransform to apply to image-relative coordinates.</p>
	 *
	 * <p>Used in cases where the supplied image has already been flipped or rotated
	 * but the coordinates relative to the original image have not.</p>
	 *
	 * @param	transform	the transform, or null to reset to no transform
	 */
	public final void setPreTransformImageRelativeCoordinates(AffineTransform transform) {
		this.preTransformImageRelativeCoordinates=transform;
	}

	/***/
	protected boolean showZoomFactor = false;
	/***/
	protected boolean showZoomFactorLeftSide = false;
	/***/
	protected double pixelSpacingInSourceImage = 0;
	/***/
	protected String typeOfPixelSpacing;

	/**
	 * <p>Select whether or not to annotate displayed image with zoom factor.</p>
	 *
	 * <p>Uses same font parameters as set for orientation annotations.</p>
	 *
	 * <p>Also implicitly effects setPixelSpacingInSourceImage().</p>
	 *
	 * @param	showZoomFactor				true or false to activate annotation of zoom factor
	 * @param	leftSide					show zoom factor on left (true) or right (false) side of view port
	 * @param	pixelSpacingInSourceImage	a single value that is the (square) row and column pixel spacing, or 0 if not known
	 */
	public final void setShowZoomFactor(boolean showZoomFactor,boolean leftSide,double pixelSpacingInSourceImage) {
		this.showZoomFactor=showZoomFactor;
		this.showZoomFactorLeftSide=leftSide;
		this.pixelSpacingInSourceImage=pixelSpacingInSourceImage;
	}

	/**
	 * <p>Select whether or not to annotate displayed image with zoom factor.</p>
	 *
	 * <p>Uses same font parameters as set for orientation annotations.</p>
	 *
	 * <p>Also implicitly effects setPixelSpacingInSourceImage().</p>
	 *
	 * @param	showZoomFactor				true or false to activate annotation of zoom factor
	 * @param	leftSide					show zoom factor on left (true) or right (false) side of view port
	 * @param	pixelSpacingInSourceImage	a single value that is the (square) row and column pixel spacing, or 0 if not known
	 * @param	typeOfPixelSpacing       	a String that describes the type of pixel spacing (e.g., detector plane, calibrated, accounting for geometric magnification, etc.), or null if not to be described when making measurements
	 */
	public final void setShowZoomFactor(boolean showZoomFactor,boolean leftSide,double pixelSpacingInSourceImage,String typeOfPixelSpacing) {
		this.showZoomFactor=showZoomFactor;
		this.showZoomFactorLeftSide=leftSide;
		this.pixelSpacingInSourceImage=pixelSpacingInSourceImage;
		this.typeOfPixelSpacing=typeOfPixelSpacing;
	}

	/**
	 * <p>Set pixel spacing in source image.</p>
	 *
	 * <p>Used for displaying zoom factor and making measurements, therefore should be appropriate choice of Pixel Spacing or Imager Pixel Spacing (appropriately corrected for radiographic magnification factor, if any), etc.</p>
	 *
	 * @param	pixelSpacingInSourceImage	a single value that is the (square) row and column pixel spacing, or 0 if not known
	 */
	public final void setPixelSpacingInSourceImage(double pixelSpacingInSourceImage) {
		this.pixelSpacingInSourceImage=pixelSpacingInSourceImage;
	}

	/**
	 * <p>Set pixel spacing in source image.</p>
	 *
	 * <p>Used for displaying zoom factor and making measurements, therefore should be appropriate choice of Pixel Spacing or Imager Pixel Spacing (appropriately corrected for radiographic magnification factor, if any), etc.</p>
	 *
	 * @param	pixelSpacingInSourceImage	a single value that is the (square) row and column pixel spacing, or 0 if not known
	 * @param	typeOfPixelSpacing       	a String that describes the type of pixel spacing (e.g., detector plane, calibrated, accounting for geometric magnification, etc.), or null if not to be described when making measurements
	 */
	public final void setPixelSpacingInSourceImage(double pixelSpacingInSourceImage,String typeOfPixelSpacing) {
		this.pixelSpacingInSourceImage=pixelSpacingInSourceImage;
		this.typeOfPixelSpacing=typeOfPixelSpacing;
	}
	
	/***/
	private AffineTransform imageToWindowCoordinateTransform = null;
	/***/
	private AffineTransform windowToImageCoordinateTransform = null;
	
	/***/
	private int useVOIFunction = 0;		// 0 is linear, 1 is logistic
	
	/**
	 * <p>Set the VOI function to the (default) window center/width linear transformation.</p>
	 */
	public final void setVOIFunctionToLinear() {
//System.err.println("SingleImagePanel.setVOIFunctionToLinear()");
		useVOIFunction = 0;
	}

	/**
	 * <p>Set the VOI function to a non-linear transformation using a logistic (sigmoid) curve with window center and width as parameters.</p>
	 */
	public final void setVOIFunctionToLogistic() {
//System.err.println("SingleImagePanel.setVOIFunctionToLogistic()");
		useVOIFunction = 1;
	}
	
	/***/
	private boolean useWindowLinearExactCalculationInsteadOfDICOMStandardMethod = false;
	
	/**
	 * <p>Set the VOI linear function to use the exact window center/width linear transformation when applying to rescaled pixels.</p>
	 */
	public final void setWindowLinearCalculationToExact() {
//System.err.println("SingleImagePanel.setWindowLinearCalculationToExact()");
		useWindowLinearExactCalculationInsteadOfDICOMStandardMethod = true;
	}

	/**
	 * <p>Set the VOI linear function to use the DICOM offset window center/width linear transformation when applying to rescaled pixels.</p>
	 *
	 * <p>The DICOM offset subtracts 0.5 from the window center and subtracts 1.0 from the window width before applying to rescaled pixels.</p>
	 */
	public final void setWindowLinearCalculationToDicom() {
//System.err.println("SingleImagePanel.setWindowLinearCalculationToDicom()");
		useWindowLinearExactCalculationInsteadOfDICOMStandardMethod = false;
	}
	
	private double windowingAccelerationValue = 1;

	/**
	 * <p>Set the windowing acceleration value to use.</p>
	 */
	public final void setWindowingAccelerationValue(double value) {
//System.err.println("SingleImagePanel.setWindowingAccelerationValue(): to "+value);
		windowingAccelerationValue = value;
	}


	private boolean showOverlays = true;
	private boolean applyShutter = true;
	private boolean showSuperimposedImages = true;

	/**
	 * <p>Set whether or not to show graphics such as overlays.</p>
	 */
	public final void setShowOverlays(boolean showOverlays) {
//System.err.println("SingleImagePanel.setShowOverlays(): "+showOverlays);
		this.showOverlays = showOverlays;
	}

	/**
	 * <p>Set whether or not to apply shutter.</p>
	 */
	public final void setApplyShutter(boolean applyShutter) {
//System.err.println("SingleImagePanel.setApplyShutter(): "+applyShutter);
		this.applyShutter = applyShutter;
	}

	/**
	 * <p>Set whether or not to show superimposed images.</p>
	 */
	public final void setShowSuperimposedImages(boolean showSuperimposedImages) {
//System.err.println("SingleImagePanel.setShowSuperimposedImages(): "+showSuperimposedImages);
		this.showSuperimposedImages = showSuperimposedImages;
	}


	// Event stuff ...

	/***/
	EventContext typeOfPanelEventContext;
	
	/***/
	int lastx;
	/***/
	int lasty;
	
	int currentx;	// these are used to cache current mouse position in case we need to update status bar values when frame changed but mouse location not moved (since RWVM may be different per frame) (000840)
	int currenty;

	/***/
	int lastmiddley;
	
	/***/
	double windowingMultiplier = 1;
	/***/
	double panningMultiplier = 1;
	
	protected class LeftMouseMode {
		public static final int WINDOWING = 1;
		public static final int PANNING = 2;
		protected int mode;
		
		LeftMouseMode() {
			mode = WINDOWING;
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
		}
		
		public void setWindowing() {
			mode = WINDOWING;
			setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
		}
		public void setPanning() {
			mode = PANNING;
			setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
		}
		public boolean isWindowing() { return mode == WINDOWING; }
		public boolean isPanning() { return mode == PANNING; }
	}
	
	protected LeftMouseMode leftMouseMode = new LeftMouseMode();
	
	/**
	 * if -1, then use statistical value first time then user adjusted values subsequently
	 * if greater than or equal to 0, then use selected VOI transform (if there is one for that frame)
	 */
	int currentVOITransformInUse;

	/**
	 * @param	e
	 */
	public void keyPressed(KeyEvent e) {
//System.err.println("Key pressed event"+e);
		if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
//System.err.println("Shift pressed");
			//windowingMultiplier=50;
			//windowingMultiplier=0.001;
			windowingMultiplier=windowingAccelerationValue;
			panningMultiplier=5;
		}
		else if (e.getKeyCode() == KeyEvent.VK_ALT) {
			leftMouseMode.setPanning();
		}
	}

	/**
	 * @param	e
	 */
	public void keyReleased(KeyEvent e) {
//System.err.println("Key released event"+e);
		if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
//System.err.println("Shift released");
			windowingMultiplier=1;
			panningMultiplier=1;
		}
		else if (e.getKeyCode() == KeyEvent.VK_ALT) {
			leftMouseMode.setWindowing();
		}
		else if (e.getKeyCode() == KeyEvent.VK_R) {			// reset display to original displayed area selection, and reset windowing
			displayReset();
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_S) {			// reset display to original displayed area selection, and reset windowing
			dirtySource();
			establishStatisticalWindow();
			repaint();
		}

		else if (e.getKeyCode() == KeyEvent.VK_1) {			// reset display to original displayed area selection but with 1:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(1);
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_2) {			// reset display to original displayed area selection but with 2:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(2);
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_3) {			// reset display to original displayed area selection but with 2:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(4);
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_4) {			// reset display to original displayed area selection but with 2:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(8);
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_5) {			// reset display to original displayed area selection but with 2:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(16);
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_6) {			// reset display to original displayed area selection but with 2:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(32);
			repaint();
		}
		else if (e.getKeyCode() == KeyEvent.VK_7) {			// reset display to original displayed area selection but with 2:1 display:image pixel magnification ratio
			displaySelectedMagnificationRatio(64);
			repaint();
		}
	}

	/**
	 * @param	e
	 */
	public void keyTyped(KeyEvent e) {
//System.err.println("Key typed event"+e);
	}

	/**
	 * @param	e
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {
		int delta = e.getWheelRotation();
//System.err.println("Mouse wheel delta = "+delta);
//System.err.println("Mouse wheel currentSrcImageIndex = "+currentSrcImageIndex);
		int newSrcImageIndex = currentSrcImageIndex + delta;
//System.err.println("Mouse wheel newSrcImageIndex = "+newSrcImageIndex);
		if (newSrcImageIndex >= sImg.getNumberOfBufferedImages()) newSrcImageIndex=sImg.getNumberOfBufferedImages()-1;
		if (newSrcImageIndex < 0) newSrcImageIndex=0;
//System.err.println("Mouse wheel newSrcImageIndex clipped = "+newSrcImageIndex);
		// don't send an event unless it is actually necessary ...
		if (newSrcImageIndex != currentSrcImageIndex) {
			ApplicationEventDispatcher.getApplicationEventDispatcher().processEvent(new FrameSelectionChangeEvent(typeOfPanelEventContext,newSrcImageIndex));
		}
	}

	/**
	 * @param	e
	 */
	public void mouseClicked(MouseEvent e) {}

	/**
	 * @param	e
	 */
	public void mouseEntered(MouseEvent e) {
		requestFocus();		// In order to allow us to receive KeyEvents
	}

	/**
	 * @param	e
	 */
	public void mouseExited(MouseEvent e) {}

	/**
	 * @param	e
	 */
	public void mouseDragged(MouseEvent e) {
//System.err.println("SingleImagePanel.mouseDragged(): "+e);
		if (SwingUtilities.isLeftMouseButton(e)) {
//System.err.println("Left dragged "+e.getX()+" "+e.getY());
			int newx=e.getX();
			int newy=e.getY();
			int deltax=newx-lastx;
			int deltay=newy-lasty;

			if (deltax != 0 || deltay != 0) {
				lastx=newx;
				lasty=newy;
				if (leftMouseMode.isWindowing()) {
					double newWindowWidth=windowWidth+deltax*windowingMultiplier;
					if (windowingMultiplier >= 1 && newWindowWidth < 1.0) newWindowWidth=1.0;
					if (newWindowWidth < 0.0) newWindowWidth=0.0;
					double newWindowCenter=windowCenter+deltay*windowingMultiplier;
					ApplicationEventDispatcher.getApplicationEventDispatcher().processEvent(new WindowCenterAndWidthChangeEvent(typeOfPanelEventContext,newWindowCenter,newWindowWidth));
				}
				else if (leftMouseMode.isPanning()) {
					if (useDisplayedAreaSelection != null) {
						int panX = (int)(-deltax*panningMultiplier);
						int panY = (int)(-deltay*panningMultiplier);
//System.err.println("Translate displayed area selection by ("+panX+","+panY+")");
						useDisplayedAreaSelection.translate(panX,panY);
						dirtyPanned();
						repaint();
					}
				}
			}
		}
		else if (SwingUtilities.isMiddleMouseButton(e)) {
//System.err.println("Middle dragged "+e.getX()+" "+e.getY());
			//selectNewSrcImage(e.getY()-lastmiddley);
			int delta = e.getY()-lastmiddley;
//System.err.println("Middle dragged delta = "+delta);
//System.err.println("Middle dragged currentSrcImageIndex = "+currentSrcImageIndex);
			int newSrcImageIndex = currentSrcImageIndex + delta;
//System.err.println("Middle dragged newSrcImageIndex = "+newSrcImageIndex);
			if (newSrcImageIndex >= sImg.getNumberOfBufferedImages()) newSrcImageIndex=sImg.getNumberOfBufferedImages()-1;
			if (newSrcImageIndex < 0) newSrcImageIndex=0;
//System.err.println("Middle dragged newSrcImageIndex clipped = "+newSrcImageIndex);
			// don't send an event unless it is actually necessary ...
			if (newSrcImageIndex != currentSrcImageIndex)
				ApplicationEventDispatcher.getApplicationEventDispatcher().processEvent(new FrameSelectionChangeEvent(typeOfPanelEventContext,newSrcImageIndex));
			lastmiddley=e.getY();		// helps a lot when clipped at top or bottom of range
		}
		else if (SwingUtilities.isRightMouseButton(e)) {
//System.err.println("Right dragged "+e.getX()+" "+e.getY());
		}
	}


//	private void selectNewSrcImage(int delta) {
//		int newSrcImageIndex = currentSrcImageIndex + delta;
//		if (newSrcImageIndex >= sImg.getNumberOfBufferedImages()) newSrcImageIndex=sImg.getNumberOfBufferedImages()-1;
//		if (newSrcImageIndex < 0) newSrcImageIndex=0;
//		if (newSrcImageIndex != currentSrcImageIndex) {
//			currentSrcImageIndex=newSrcImageIndex;
//			cachedPreWindowedImage=null;		// forces application of new values on repaint 
//			repaint();
//		}
//	}

	protected int getSourceImageHeight() {
		return sImg.getHeight();
	}
	
	protected int getSourceImageWidth() {
		return sImg.getWidth();
	}
	
	/**
	 *
	 * Get location on source image from window relative location.
	 *
	 * @param	xw	x coordinate in AWT window as returned by MouseEvent.getX()
	 * @param	yw	y coordinate in AWT window as returned by MouseEvent.getY()
	 * @return		source image-relative coordinates with sub-pixel resolution clamped to image size but including BLHC of BLHC pixel (per DICOM PS 3.3 Figure C.10.5-1) 
	 */
	protected Point2D getImageCoordinateFromWindowCoordinate(double xw,double yw) {
		double xi = 0;
		double yi = 0;
		if (windowToImageCoordinateTransform != null) {
			Point2D pointd = new Point2D.Double(xw,yw);
			pointd = windowToImageCoordinateTransform.transform(pointd,pointd);	// overwrites self rather than allocating a new point
			xi = pointd.getX();
			yi = pointd.getY();
//System.err.println("SingeImagePanel.getImageCoordinateFromWindowCoordinate(): window ("+xw+","+yw+") -> ("+xi+","+yi+")");
			if (xi < 0) {
				xi=0;
			}
			else {
				int width = getSourceImageWidth();
				if (xi > width) {
					xi=width;
				}
			}
			if (yi < 0) {
				yi=0;
			}
			else {
				int height = getSourceImageHeight();
				if (yi > height) {
					yi=height;
				}
			}
//System.err.println("SingeImagePanel.getImageCoordinateFromWindowCoordinate(): clamped to image size ("+xi+","+yi+")");
		}
		return new Point2D.Double(xi,yi);
	}

	/**
	 * This value is outside mouseMoved() only so that it doesn't need to be constantly reallocated - it is not used by any other method
	 */
	protected double[] currentLocationIn3DSpace = new double[3];

	/**
	 * @param	e
	 */
	public void mouseMoved(MouseEvent e) {
//System.err.println(e.getX()+" "+e.getY());
		currentx = e.getX();	// keep track of these in case needed by OurFrameSelectionChangeListener.changed() (000840)
		currenty = e.getY();
		updateStatusBarValues(currentx,currenty);
	}
	
	/**
	 * @param	x
	 * @param	y
	 */
	public void updateStatusBarValues(int x,int y) {	// called by mouseMoved() or OurFrameSelectionChangeListener.changed() since mapping may be different for different frames (000840)
//System.err.println(x+" "+y);
		{
			Point2D point = getImageCoordinateFromWindowCoordinate(x,y);
			double subPixelX = point.getX();
			double subPixelY = point.getY();		
//System.err.println("X: "+subPixelX+" ("+x+") Y: "+subPixelX+" ("+y+")");
			int useSrcImageIndex = currentSrcImageSortOrder == null ? currentSrcImageIndex : currentSrcImageSortOrder[currentSrcImageIndex];

			StringBuffer sbuf = new StringBuffer();
			sbuf.append("(");
			sbuf.append(FloatFormatter.toString(subPixelX,Locale.US));
			sbuf.append(",");
			sbuf.append(FloatFormatter.toString(subPixelY,Locale.US));
			if (imageGeometry != null) {
				imageGeometry.lookupImageCoordinate(currentLocationIn3DSpace,subPixelX,subPixelY,useSrcImageIndex);
				{
					// check round trip
					GeometryOfSlice sliceGeometry = imageGeometry.getGeometryOfSlice(useSrcImageIndex);
					if (sliceGeometry != null) {
						double[] roundtrip = sliceGeometry.lookupImageCoordinate(currentLocationIn3DSpace);
//System.err.println("Round trip imageGeometry.getGeometryOfSlice().lookupImageCoordinate(): ("+subPixelX+","+subPixelY+") -> ("+roundtrip[0]+","+roundtrip[1]+") for 3D ("+currentLocationIn3DSpace[0]+","+currentLocationIn3DSpace[1]+","+currentLocationIn3DSpace[2]+")");
						if (Double.isNaN(roundtrip[0]) || Double.isInfinite(roundtrip[0]) || Math.abs(roundtrip[0] - subPixelX) > 0.01
						 || Double.isNaN(roundtrip[1]) || Double.isInfinite(roundtrip[1]) || Math.abs(roundtrip[1] - subPixelY) > 0.01) {
System.err.println("Round trip imageGeometry.getGeometryOfSlice().lookupImageCoordinate(): failed ("+subPixelX+","+subPixelY+") != ("+roundtrip[0]+","+roundtrip[1]+") for 3D ("+currentLocationIn3DSpace[0]+","+currentLocationIn3DSpace[1]+","+currentLocationIn3DSpace[2]+")");
						}
					}
				}
				{
					sbuf.append(": ");
					sbuf.append(FloatFormatter.toString(currentLocationIn3DSpace[0],Locale.US));
					sbuf.append(",");
					sbuf.append(FloatFormatter.toString(currentLocationIn3DSpace[1],Locale.US));
					sbuf.append(",");
					sbuf.append(FloatFormatter.toString(currentLocationIn3DSpace[2],Locale.US));
				}
			}
			sbuf.append(")");
		
			BufferedImage src = sImg.getBufferedImage(useSrcImageIndex);
			int wholePixelXFromZero = (int)subPixelX;	// just truncate (not round) ... 0 becomes 0, 0.5 becomes 0, 1.0 is next pixel and becomes 1, only extreme BLHC of BLHC pixel goes out of bounds
			int wholePixelYFromZero = (int)subPixelY;		
			if (wholePixelXFromZero >= 0 && wholePixelXFromZero < src.getWidth() && wholePixelYFromZero >= 0 && wholePixelYFromZero < src.getHeight()) {	// check avoids occasional ArrayIndexOutOfBoundsException exception
				double storedPixelValue;
				if (src.getRaster().getDataBuffer() instanceof DataBufferFloat) {
					float[] storedPixelValues  = src.getSampleModel().getPixel(wholePixelXFromZero,wholePixelYFromZero,(float[])null,src.getRaster().getDataBuffer());
					storedPixelValue=storedPixelValues[0];
				}
				else if (src.getRaster().getDataBuffer() instanceof DataBufferDouble) {
					double[] storedPixelValues  = src.getSampleModel().getPixel(wholePixelXFromZero,wholePixelYFromZero,(double[])null,src.getRaster().getDataBuffer());
					storedPixelValue=storedPixelValues[0];
				}
				else {
					int[] storedPixelValues  = src.getSampleModel().getPixel(wholePixelXFromZero,wholePixelYFromZero,(int[])null,src.getRaster().getDataBuffer());
					int storedPixelValueInt=storedPixelValues[0];
//System.err.println("storedPixelValue as stored = 0x"+Integer.toHexString(storedPixelValueInt)+" "+storedPixelValueInt+" dec");
					if (signed && (storedPixelValueInt&signBit) != 0) {
						storedPixelValueInt|=signMask;	// sign extend
//System.err.println("storedPixelValue extended  = 0x"+Integer.toHexString(storedPixelValueInt)+" "+storedPixelValueInt+" dec");
					}
					storedPixelValue=storedPixelValueInt;
				}
				if (realWorldValueTransform != null) {
//System.err.println("mouseMoved(): Have realWorldValueTransform");
					sbuf.append(" = ");
					sbuf.append(realWorldValueTransform.toString(useSrcImageIndex,storedPixelValue));
					sbuf.append(" [");
					sbuf.append(FloatFormatter.toString(storedPixelValue,Locale.US));	// this will not append spurious trailing "0." as default toString() method would
					sbuf.append("]");
				}

				if (suvTransform != null) {
					sbuf.append(" ");
					sbuf.append(suvTransform.toString(useSrcImageIndex,storedPixelValue));
				}
			}
			
			ApplicationEventDispatcher.getApplicationEventDispatcher().processEvent(new StatusChangeEvent(sbuf.toString()));
		}
	}

	/**
	 * @param	e
	 */
	public void mousePressed(MouseEvent e) {
		if (SwingUtilities.isLeftMouseButton(e)) {
//System.err.println("Left pressed "+e.getX()+" "+e.getY());
			lastx=e.getX();
			lasty=e.getY();
		}
		else if (SwingUtilities.isMiddleMouseButton(e)) {
//System.err.println("Middle pressed "+e.getX()+" "+e.getY());
			lastmiddley=e.getY();
		}
		else if (SwingUtilities.isRightMouseButton(e)) {
//System.err.println("Right pressed "+e.getX()+" "+e.getY());
		}
	}

	/**
	 * @param	e
	 */
	public void mouseReleased(MouseEvent e) {
		if (SwingUtilities.isLeftMouseButton(e)) {
//System.err.println("Left released "+e.getX()+" "+e.getY());
			// on button release (but not during drag) propagate changed window values to self (a nop) and other registered SingleImagePanels ...
			if (leftMouseMode.isWindowing()) {
				ApplicationEventDispatcher.getApplicationEventDispatcher().processEvent(new WindowCenterAndWidthChangeEvent(typeOfPanelEventContext,windowCenter,windowWidth));
			}
		}
		else if (SwingUtilities.isMiddleMouseButton(e)) {
//System.err.println("Middle released "+e.getX()+" "+e.getY());
		}
		else if (SwingUtilities.isRightMouseButton(e)) {
//System.err.println("Right released "+e.getX()+" "+e.getY());
		}
	}

	// implement WindowCenterAndWidthChangeListener to respond to events from self or other SingleImagePanel or elsewhere ...
	
	private OurWindowCenterAndWidthChangeListener ourWindowCenterAndWidthChangeListener;

	class OurWindowCenterAndWidthChangeListener extends SelfRegisteringListener {
	
		public OurWindowCenterAndWidthChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.WindowCenterAndWidthChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurWindowCenterAndWidthChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			WindowCenterAndWidthChangeEvent wcwe = (WindowCenterAndWidthChangeEvent)e;
			double newWindowCenter=wcwe.getWindowCenter();
			double newWindowWidth=wcwe.getWindowWidth();
			if (windowCenter != newWindowCenter || windowWidth != newWindowWidth) {
//System.err.println("SingleImagePanel.OurWindowCenterAndWidthChangeListener.changed(): new values");
				windowCenter=newWindowCenter;
				windowWidth=newWindowWidth;
				cachedPreWindowedImage=null;		// forces application of new values on repaint 
				currentVOITransformInUse=-1;		// override pre-specified VOI tranform with user selection
				repaint();
			}
			else {
//System.err.println("SingleImagePanel.OurWindowCenterAndWidthChangeListener.changed(): same values");
			}
		}
	}
	
	// implement FrameSelectionChangeListener to respond to events from self or elsewhere ...
	
	private OurFrameSelectionChangeListener ourFrameSelectionChangeListener;

	class OurFrameSelectionChangeListener extends SelfRegisteringListener {
	
		public OurFrameSelectionChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.FrameSelectionChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurFrameSelectionChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			FrameSelectionChangeEvent fse = (FrameSelectionChangeEvent)e;
			int newCurrentSrcImageIndex = fse.getIndex();
			if (currentSrcImageIndex != newCurrentSrcImageIndex) {
//System.err.println("SingleImagePanel.OurFrameSelectionChangeListener.changed(): new value = "+currentSrcImageIndex);
				currentSrcImageIndex=newCurrentSrcImageIndex;
				dirtySource(); 
				repaint();
				updateStatusBarValues(currentx,currenty);	// since mapping may be different for different frames (000840)
			}
			else {
//System.err.println("SingleImagePanel.OurFrameSelectionChangeListener.changed(): same value");
			}
		}
	}
	
	// implement FrameSortOrderChangeListener to respond to events from self or other SingleImagePanel or elsewhere ...
	
	private OurFrameSortOrderChangeListener ourFrameSortOrderChangeListener;

	class OurFrameSortOrderChangeListener extends SelfRegisteringListener {
	
		public OurFrameSortOrderChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.FrameSortOrderChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurFrameSortOrderChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			FrameSortOrderChangeEvent fso = (FrameSortOrderChangeEvent)e;
			int[] newSrcImageSortOrder = fso.getSortOrder();
			int newCurrentSrcImageIndex = fso.getIndex();
			if (currentSrcImageIndex != newCurrentSrcImageIndex
			 || currentSrcImageSortOrder != newSrcImageSortOrder
			 || !Arrays.equals(currentSrcImageSortOrder,newSrcImageSortOrder)) {
//System.err.println("SingleImagePanel.OurFrameSortOrderChangeListener.changed(): new values");
				currentSrcImageIndex=newCurrentSrcImageIndex;
				currentSrcImageSortOrder=newSrcImageSortOrder;		// change even if null in event (request to go back to implicit order)
				dirtySource(); 
				repaint();
			}
			else {
//System.err.println("SingleImagePanel.OurFrameSortOrderChangeListener.changed(): same values");
			}
		}
	}
	
	// implement VOIFunctionChangeListener to respond to events from elsewhere ...
	
	private OurVOIFunctionChangeListener ourVOIFunctionChangeListener;

	class OurVOIFunctionChangeListener extends SelfRegisteringListener {
	
		public OurVOIFunctionChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.VOIFunctionChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurVOIFunctionChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			VOIFunctionChangeEvent vfce = (VOIFunctionChangeEvent)e;
			if (vfce.isLinearFunction()) {
				setVOIFunctionToLinear();
			}
			else if (vfce.isLogisticFunction()) {
				setVOIFunctionToLogistic();
			}
			dirtyWindowing();
			repaint();				// force repaint
		}
	}
	
	// implement WindowLinearCalculationChangeListener to respond to events from elsewhere ...
	
	private OurWindowLinearCalculationChangeListener ourWindowLinearCalculationChangeListener;

	class OurWindowLinearCalculationChangeListener extends SelfRegisteringListener {
	
		public OurWindowLinearCalculationChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.WindowLinearCalculationChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurWindowLinearCalculationChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			WindowLinearCalculationChangeEvent vlce = (WindowLinearCalculationChangeEvent)e;
			if (vlce.isExactCalculation()) {
				setWindowLinearCalculationToExact();
			}
			else if (vlce.isDicomCalculation()) {
				setWindowLinearCalculationToDicom();
			}
			dirtyWindowing();
			repaint();				// force repaint
		}
	}
	
	// implement WindowingAccelerationValueChangeListener to respond to events from elsewhere ...
	
	private OurWindowingAccelerationValueChangeListener ourWindowingAccelerationValueChangeListener;

	class OurWindowingAccelerationValueChangeListener extends SelfRegisteringListener {
	
		public OurWindowingAccelerationValueChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.WindowingAccelerationValueChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurWindowingAccelerationValueChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			WindowingAccelerationValueChangeEvent wavce = (WindowingAccelerationValueChangeEvent)e;
			setWindowingAccelerationValue(wavce.getValue());
			dirtyWindowing();
			repaint();				// force repaint
		}
	}
	
	// implement GraphicDisplayChangeListener to respond to events from elsewhere ...
	
	private OurGraphicDisplayChangeListener ourGraphicDisplayChangeListener;

	class OurGraphicDisplayChangeListener extends SelfRegisteringListener {
	
		public OurGraphicDisplayChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.GraphicDisplayChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurGraphicDisplayChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			GraphicDisplayChangeEvent gdce = (GraphicDisplayChangeEvent)e;
			setShowOverlays(gdce.showOverlays());
			dirtyWindowing();
			repaint();				// force repaint
		}
	}
	
	// implement ApplyShutterChangeListener to respond to events from elsewhere ...
	
	private OurApplyShutterChangeListener ourApplyShutterChangeListener;

	class OurApplyShutterChangeListener extends SelfRegisteringListener {
	
		public OurApplyShutterChangeListener(EventContext eventContext) {
			super("com.pixelmed.display.event.ApplyShutterChangeEvent",eventContext);
//System.err.println("SingleImagePanel.OurApplyShutterChangeListener():");
		}
		
		/**
		 * @param	e
		 */
		public void changed(Event e) {
			ApplyShutterChangeEvent asce = (ApplyShutterChangeEvent)e;
			setApplyShutter(asce.applyShutter());
			dirtyWindowing();
			repaint();				// force repaint
		}
	}
	
	// called by paintComponent() first time or after change create image with window values applied ...
	
	/**
	 * @param	src
	 * @param	center
	 * @param	width
	 * @param	identityCenter
	 * @param	identityWidth
	 * @param	signed
	 * @param	inverted
	 * @param	useSlope
	 * @param	useIntercept
	 * @param	hasPad
	 * @param	pad
	 * @param	padRangeLimit
	 * @param	numberOfEntries
	 * @param	bitsPerEntry
	 * @param	grayTable
	 * @param	entryMin
	 * @param	entryMax
	 * @param	topOfEntryRange
	 */
	public static final BufferedImage applyVOILUT(BufferedImage src,double center,double width,double identityCenter,double identityWidth,
			boolean signed,boolean inverted,double useSlope,double useIntercept,
			boolean hasPad,int pad,int padRangeLimit,
			int numberOfEntries,int firstValueMapped,int bitsPerEntry,short[] grayTable,int entryMin,int entryMax,int topOfEntryRange) {
		return WindowCenterAndWidth.applyVOILUT(src,center,width,identityCenter,identityWidth,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit,numberOfEntries,firstValueMapped,bitsPerEntry,grayTable,entryMin,entryMax,topOfEntryRange);
	}
	
	/**
	 * @param	src
	 * @param	center
	 * @param	width
	 * @param	signed
	 * @param	inverted
	 * @param	useSlope
	 * @param	useIntercept
	 * @param	hasPad
	 * @param	pad
	 * @param	padRangeLimit
	 */
	public static final BufferedImage applyWindowCenterAndWidthLogistic(BufferedImage src,double center,double width,
			boolean signed,boolean inverted,double useSlope,double useIntercept,boolean hasPad,int pad,int padRangeLimit) {
		return WindowCenterAndWidth.applyWindowCenterAndWidthLogistic(src,center,width,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit);
	}

	/**
	 * @param	src
	 * @param	center
	 * @param	width
	 * @param	signed
	 * @param	inverted
	 * @param	useSlope
	 * @param	useIntercept
	 * @param	hasPad
	 * @param	pad
	 * @param	padRangeLimit
	 */
	public static final BufferedImage applyWindowCenterAndWidthLinear(BufferedImage src,double center,double width,
			boolean signed,boolean inverted,double useSlope,double useIntercept,boolean hasPad,int pad,int padRangeLimit) {
		return WindowCenterAndWidth.applyWindowCenterAndWidthLinear(src,center,width,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit);
	}

	/**
	 * @param	src
	 * @param	center
	 * @param	width
	 * @param	signed
	 * @param	inverted
	 * @param	useSlope
	 * @param	useIntercept
	 * @param	hasPad
	 * @param	pad
	 * @param	padRangeLimit
	 * @param	useExactCalculationInsteadOfDICOMStandardMethod
	 */
	public static final BufferedImage applyWindowCenterAndWidthLinear(BufferedImage src,double center,double width,
			boolean signed,boolean inverted,double useSlope,double useIntercept,boolean hasPad,int pad,int padRangeLimit,boolean useExactCalculationInsteadOfDICOMStandardMethod) {
		return WindowCenterAndWidth.applyWindowCenterAndWidthLinear(src,center,width,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit,useExactCalculationInsteadOfDICOMStandardMethod);
	}

	/**
	 * @param	src
	 * @param	center
	 * @param	width
	 * @param	signed
	 * @param	inverted
	 * @param	useSlope
	 * @param	useIntercept
	 * @param	hasPad
	 * @param	pad
	 * @param	padRangeLimit
	 * @param	largestGray
	 * @param	bitsPerEntry
	 * @param	numberOfEntries
	 * @param	redTable
	 * @param	greenTable
	 * @param	blueTable
	 */
	public static final BufferedImage applyWindowCenterAndWidthWithPaletteColor(BufferedImage src,double center,double width,
			boolean signed,boolean inverted,double useSlope,double useIntercept,boolean hasPad,int pad,int padRangeLimit,
			int largestGray,int bitsPerEntry,int numberOfEntries,
			short[] redTable,short[] greenTable,short[] blueTable) {
//System.err.println("SingleImagePanel.applyWindowCenterAndWidthWithPaletteColor():");
		return WindowCenterAndWidth.applyWindowCenterAndWidthWithPaletteColor(src,center,width,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit,
			largestGray,bitsPerEntry,numberOfEntries,redTable,greenTable,blueTable);
	}
	
	/**
	 * @param	src
	 * @param	center
	 * @param	width
	 */
	public static final BufferedImage applyWindowCenterAndWidthLinearToColorImage(BufferedImage src,double center,double width) {
		return WindowCenterAndWidth.applyWindowCenterAndWidthLinearToColorImage(src,center,width);
	}
	
	// Common constructor support ...

	protected void establishStatisticalWindow() {
//System.err.println("SingleImagePanel.establishStatisticalWindow(): Deriving window statistically");
		//if (iMean != 0.0 && iSD != 0.0) {
//System.err.println("Using mean and SD");
		//	windowWidth=iSD*2.0;
		//	windowCenter=iMean;
		//}
		//else {
//System.err.println("useSlope "+useSlope);
//System.err.println("useIntercept "+useIntercept);
//System.err.println("imgMin "+imgMin);
//System.err.println("imgMax "+imgMax);
			double ourMin = imgMin*useSlope+useIntercept;
			double ourMax = imgMax*useSlope+useIntercept;
//System.err.println("ourMin "+ourMin);
//System.err.println("ourMax "+ourMax);
			//windowWidth=(ourMax-ourMin)/2.0;
			windowWidth=(ourMax-ourMin);
			windowCenter=(ourMax+ourMin)/2.0;
			currentVOITransformInUse=-1;		// flag not to mess with values when scrolling through frames
//System.err.println("SingleImagePanel.establishStatisticalWindow(): Initially using statistically derived center "+windowCenter+" and width "+windowWidth);
		//}
	}

	protected void establishInitialWindowOrVOILUT() {
		// choose the initial window center and width or VOI LUT ...
				
		useVOILUTNotFunction=false;
		windowWidth=0;
		windowCenter=0;
		voiLUTIdentityWindowCenter=0;
		voiLUTIdentityWindowWidth=0;
		voiLUTNumberOfEntries=0;
		voiLUTFirstValueMapped=0;
		voiLUTBitsPerEntry=0;
		voiLUTData=null;
		voiLUTEntryMin=0;
		voiLUTEntryMax=0;
		voiLUTTopOfEntryRange=0;
		
//System.err.println("SingleImagePanel.establishInitialWindowOrVOILUT(): Looking at voiTransform "+voiTransform);
		if (voiTransform != null) {
			final int nTransforms = voiTransform.getNumberOfTransforms(currentSrcImageIndex);
			
			// first look for actual LUT, and prefer LUT over window values
			
			currentVOITransformInUse=0;
			while (currentVOITransformInUse < nTransforms) {
				if (voiTransform.isLUTTransform(currentSrcImageIndex,currentVOITransformInUse)) {
//System.err.println("SingleImagePanel.doCommonConstructorStuff(): found possible LUT "+currentVOITransformInUse);
					 voiLUTNumberOfEntries=voiTransform.getNumberOfEntries (currentSrcImageIndex,currentVOITransformInUse);
					voiLUTFirstValueMapped=voiTransform.getFirstValueMapped(currentSrcImageIndex,currentVOITransformInUse);
					    voiLUTBitsPerEntry=voiTransform.getBitsPerEntry    (currentSrcImageIndex,currentVOITransformInUse);
					            voiLUTData=voiTransform.getLUTData         (currentSrcImageIndex,currentVOITransformInUse);
					        voiLUTEntryMin=voiTransform.getEntryMinimum    (currentSrcImageIndex,currentVOITransformInUse);
					        voiLUTEntryMax=voiTransform.getEntryMaximum    (currentSrcImageIndex,currentVOITransformInUse);
					 voiLUTTopOfEntryRange=voiTransform.getTopOfEntryRange (currentSrcImageIndex,currentVOITransformInUse);
					if (voiLUTData != null && voiLUTData.length == voiLUTNumberOfEntries) {
						useVOILUTNotFunction=true;	// only if LUT is "good"
//System.err.println("SingleImagePanel.establishInitialWindowOrVOILUT(): using good LUT "+currentVOITransformInUse);
						// initialize "pseudo-window" to scale input values used to index LUT to identity transformation (i.e., apply LUT exactly as supplied)
						// note that the choice of identity values is arbitrary, but is chosen this way so that the numerical values "make sense" to the user
						// must be consistent with identity values specified in applyVOILUT() invocation
						voiLUTIdentityWindowWidth  = voiLUTNumberOfEntries;
						voiLUTIdentityWindowCenter = voiLUTFirstValueMapped + voiLUTNumberOfEntries/2;
						windowWidth  = voiLUTIdentityWindowWidth;
						windowCenter = voiLUTIdentityWindowCenter;
					}
					break;
				}
				++currentVOITransformInUse;
			}

			if (!useVOILUTNotFunction) {		// no LUT, so search transforms again for window values
				currentVOITransformInUse=0;
				while (currentVOITransformInUse < nTransforms) {
					if (voiTransform.isWindowTransform(currentSrcImageIndex,currentVOITransformInUse)) {
						useVOILUTNotFunction=false;
						windowWidth=voiTransform.getWidth(currentSrcImageIndex,currentVOITransformInUse);
						windowCenter=voiTransform.getCenter(currentSrcImageIndex,currentVOITransformInUse);
//System.err.println("SingleImagePanel.establishInitialWindowOrVOILUT(): Initially using preselected center "+windowCenter+" and width "+windowWidth);
						break;
					}
					++currentVOITransformInUse;
				}
			}
		}
		if (!useVOILUTNotFunction && windowWidth <= 0) {			// if no LUT, use supplied window only if there was one, and if its width was not zero or negative (center may legitimately be zero)
			establishStatisticalWindow();
		}
	}	

	/***/
	private boolean useConvertToMostFavorableImageType;	// used in paintComponent()

	/**
	 * @param	sImg
	 * @param	typeOfPanelEventContext
	 * @param	sortOrder
	 * @param	preDefinedShapes
	 * @param	preDefinedText
	 * @param	imageGeometry
	 */
	private void doCommonConstructorStuff(SourceImage sImg,
			EventContext typeOfPanelEventContext,
			int[] sortOrder,
			Vector preDefinedShapes,Vector preDefinedText,
			GeometryOfVolume imageGeometry) {
//System.err.println("SingleImagePanel.doCommonConstructorStuff():");
		this.sImg = sImg;
		boolean convertNonGrayscale = false;
		if (sImg != null && sImg.getNumberOfBufferedImages() > 0) {
//System.err.println("SingleImagePanel.doCommonConstructorStuff(): sImg != null");
			BufferedImage img=sImg.getBufferedImage(0);
		//	SampleModel sampleModel = img.getSampleModel();
			if (img != null && img.getRaster().getNumBands() > 1) {
		//		if (sampleModel instanceof ComponentSampleModel && ((ComponentSampleModel)sampleModel).getPixelStride() != 1
		//		 || sampleModel instanceof PixelInterleavedSampleModel) {
					convertNonGrayscale = true;
		//		}
			}
		}
//System.err.println("SingleImagePanel.doCommonConstructorStuff(): convertNonGrayscale = "+convertNonGrayscale);
		try {
			useConvertToMostFavorableImageType =
			//	   (System.getProperty("mrj.version") != null && Double.parseDouble(System.getProperty("mrj.version")) < 4)	|| // because slow otherwise
				convertNonGrayscale == true;
			//useConvertToMostFavorableImageType = true;
			//useConvertToMostFavorableImageType = false;
		}
		catch (NumberFormatException e) {
		}
//System.err.println("SingleImagePanel.doCommonConstructorStuff(): useConvertToMostFavorableImageType = "+useConvertToMostFavorableImageType);
		
		addKeyListener(this);
		addMouseListener(this);
		addMouseMotionListener(this);
		addMouseWheelListener(this);

		currentSrcImageIndex=0;
		
		currentSrcImageSortOrder=sortOrder;	// may be null ... that is OK ... paintComponent() handles null as implicit frame order

		dirtySource();

		this.typeOfPanelEventContext=typeOfPanelEventContext;
		ourWindowCenterAndWidthChangeListener = new OurWindowCenterAndWidthChangeListener(typeOfPanelEventContext);
		ourFrameSelectionChangeListener = new OurFrameSelectionChangeListener(typeOfPanelEventContext);
		ourFrameSortOrderChangeListener = new OurFrameSortOrderChangeListener(typeOfPanelEventContext);
		ourVOIFunctionChangeListener = new OurVOIFunctionChangeListener(typeOfPanelEventContext);
		ourWindowLinearCalculationChangeListener = new OurWindowLinearCalculationChangeListener(typeOfPanelEventContext);
		ourWindowingAccelerationValueChangeListener = new OurWindowingAccelerationValueChangeListener(typeOfPanelEventContext);
		ourGraphicDisplayChangeListener = new OurGraphicDisplayChangeListener(typeOfPanelEventContext);
		ourApplyShutterChangeListener = new OurApplyShutterChangeListener(typeOfPanelEventContext);
	
		this.largestGray=sImg.getPaletteColorLargestGray();
		this.firstvalueMapped=sImg.getPaletteColorFirstValueMapped();
		this.numberOfEntries=sImg.getPaletteColorNumberOfEntries();
		this.bitsPerEntry=sImg.getPaletteColorBitsPerEntry();
		this.redTable=sImg.getPaletteColorRedTable();
		this.greenTable=sImg.getPaletteColorGreenTable();
		this.blueTable=sImg.getPaletteColorBlueTable();

		this.signed=sImg.isSigned();
		this.inverted=sImg.isInverted();
		this.ybr=sImg.isYBR();
		
		this.hasPad=sImg.isPadded();
		this.pad=sImg.getPadValue();
		this.padRangeLimit=sImg.getPadRangeLimit();
		
		if (sImg != null && sImg.getNumberOfBufferedImages() > 0) {
			BufferedImage img=sImg.getBufferedImage(0);
			imgMin=sImg.getMinimum();					// defer until have fetched BufferedImage, else may be invalid if in memoery mapped file
			imgMax=sImg.getMaximum();
			signBit=0;
			signMask=0;
			if (signed) {
//System.err.println("signed="+signed);
				// the source image will already have been sign extended to the data type size
				// so we don't need to worry about other than exactly 8 and 16 bits
				if (img.getSampleModel().getDataType() == DataBuffer.TYPE_BYTE) {
					signBit=0x0080;
					signMask=0xffffff80;
				}
				else {	// assume short or ushort
					signBit=0x8000;
					signMask=0xffff8000;
				}
			}
//System.err.println("signMask=0x"+Integer.toHexString(signMask));
//System.err.println("signBit=0x"+Integer.toHexString(signBit));
		}
		
		this.suvTransform=sImg.getSUVTransform();
		this.realWorldValueTransform=sImg.getRealWorldValueTransform();
		this.modalityTransform=sImg.getModalityTransform();
		this.voiTransform=sImg.getVOITransform();
		this.displayShutter=sImg.getDisplayShutter();
		this.overlay=sImg.getOverlay();
		
		// slope & intercept get set on 1st paintComponent() anyway, but
		// we need to get them now to statistically derive the default
		// window center and width ...
		
		if (modalityTransform != null) {
			    useSlope = modalityTransform.getRescaleSlope    (currentSrcImageIndex);
			useIntercept = modalityTransform.getRescaleIntercept(currentSrcImageIndex);
//System.err.println("Initially using preselected rescale slope "+useSlope+" and intercept "+useIntercept);
		}
		else {
			useSlope=1.0;
			useIntercept=0.0;
//System.err.println("Initially using default rescale slope "+useSlope+" and intercept "+useIntercept);
		}
		
		establishInitialWindowOrVOILUT();
		
//System.err.println("SingleImagePanel.doCommonConstructorStuff(): setting preDefinedShapes");
		this.preDefinedShapes=preDefinedShapes;
		this.preDefinedText=preDefinedText;
		
		this.imageGeometry=imageGeometry;
	}
	
	// Actual constructors ...

	/**
	 * @param	sImg
	 * @param	typeOfPanelEventContext
	 * @param	sortOrder
	 * @param	preDefinedShapes
	 * @param	preDefinedText
	 * @param	imageGeometry
	 */
	public SingleImagePanel(SourceImage sImg,
			EventContext typeOfPanelEventContext,
			int[] sortOrder,
			Vector preDefinedShapes,Vector preDefinedText,
			GeometryOfVolume imageGeometry) {
		doCommonConstructorStuff(sImg,
			typeOfPanelEventContext,
			sortOrder,
			preDefinedShapes,preDefinedText,
			imageGeometry);
	}

	/**
	 * @param	sImg
	 * @param	typeOfPanelEventContext
	 * @param	imageGeometry
	 */
	public SingleImagePanel(SourceImage sImg,
			EventContext typeOfPanelEventContext,
			GeometryOfVolume imageGeometry) {
		doCommonConstructorStuff(sImg,
			typeOfPanelEventContext,
			null,
			null,null,
			imageGeometry);
	}

	/**
	 * @param	sImg
	 * @param	typeOfPanelEventContext
	 */
	public SingleImagePanel(SourceImage sImg,
			EventContext typeOfPanelEventContext) {
		doCommonConstructorStuff(sImg,
			typeOfPanelEventContext,
			null,
			null,null,
			null);
	}

	/**
	 * @param	sImg
	 */
	public SingleImagePanel(SourceImage sImg) {
		doCommonConstructorStuff(sImg,
			null,
			null,
			null,null,
			null);
	}

	public void deconstruct() {
//System.err.println("SingleImagePanel.deconstruct()");
		// avoid "listener leak"
		if (ourWindowCenterAndWidthChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourWindowCenterAndWidthChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourWindowCenterAndWidthChangeListener);
			ourWindowCenterAndWidthChangeListener=null;
		}
		if (ourFrameSelectionChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourFrameSelectionChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourFrameSelectionChangeListener);
			ourFrameSelectionChangeListener=null;
		}
		if (ourFrameSortOrderChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourFrameSortOrderChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourFrameSortOrderChangeListener);
			ourFrameSortOrderChangeListener=null;
		}
		if (ourVOIFunctionChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourVOIFunctionChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourVOIFunctionChangeListener);
			ourVOIFunctionChangeListener=null;
		}
		if (ourWindowLinearCalculationChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourWindowLinearCalculationChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourWindowLinearCalculationChangeListener);
			ourWindowLinearCalculationChangeListener=null;
		}
		if (ourWindowingAccelerationValueChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourWindowingAccelerationValueChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourWindowingAccelerationValueChangeListener);
			ourWindowingAccelerationValueChangeListener=null;
		}
		if (ourGraphicDisplayChangeListener != null) {
//System.err.println("SingleImagePanel.deconstruct(): removing ourGraphicDisplayChangeListener");
			ApplicationEventDispatcher.getApplicationEventDispatcher().removeListener(ourGraphicDisplayChangeListener);
			ourGraphicDisplayChangeListener=null;
		}
	}
	
	/*
	 * @param container
	 */
	 public static void deconstructAllSingleImagePanelsInContainer(Container container) {
		Component[] components = container.getComponents();				
//System.err.println("SingleImagePanel.deconstructAllSingleImagePanelsInContainer(): deconstructing old SingleImagePanels components.length="+components.length);
		for (int i=0; i<components.length; ++i) {
			Component component = components[i];
			if (component instanceof SingleImagePanel) {
				((SingleImagePanel)component).deconstruct();
			}
		}
	}

	protected void finalize() throws Throwable {
//System.err.println("SingleImagePanel.finalize()");
		deconstruct();		// just in case wasn't already called, and garbage collection occurs
		super.finalize();
	}

	// stuff to handle drawing ...

	// override methods for JComponent ...
        
	private BufferedImageUtilities resampler = null;
	
//	public void paint(Graphics g) {
//System.err.println("SingleImagePanel.paint(): start");
//		try { throw new Exception(); } catch (Exception e) { e.printStackTrace(System.err); }
//		super.paint(g);
//System.err.println("SingleImagePanel.paint(): end");
//	}
	
//	public void update(Graphics g) {
//System.err.println("SingleImagePanel.update(): start");
//		try { throw new Exception(); } catch (Exception e) { e.printStackTrace(System.err); }
//		super.update(g);
//System.err.println("SingleImagePanel.update(): end");
//	}

//	public void repaint() {
//System.err.println("SingleImagePanel.repaint(): start");
//		try { throw new Exception(); } catch (Exception e) { e.printStackTrace(System.err); }
//		super.repaint();
//System.err.println("SingleImagePanel.repaint(): end");
//	}

//	public void repaint(Rectangle r) {
//System.err.println("SingleImagePanel.repaint(): start");
//		try { throw new Exception(); } catch (Exception e) { e.printStackTrace(System.err); }
//		super.repaint(r);
//System.err.println("SingleImagePanel.repaint(): end");
//	}
	
//	public void repaint(long tm, int x, int y, int width, int height) {
//System.err.println("SingleImagePanel.repaint(): start");
//		try { throw new Exception(); } catch (Exception e) { e.printStackTrace(System.err); }
//		super.repaint(tm,x,y,width,height);
//System.err.println("SingleImagePanel.repaint(): end");
//	}
	

	/**
	 * @param	g
	 */
	public void paintComponent(Graphics g) {
//System.err.println("SingleImagePanel.paintComponent(): start");
//try { throw new Exception(); } catch (Exception e) { e.printStackTrace(System.err); }
//long startTime = System.currentTimeMillis();
		Cursor was = getCursor();
		setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
		
		int useSrcImageIndex = currentSrcImageSortOrder == null ? currentSrcImageIndex : currentSrcImageSortOrder[currentSrcImageIndex];
//System.err.println("SingleImagePanel.paintComponent() useSrcImageIndex: "+useSrcImageIndex);
		BufferedImage useSrcImage = sImg.getBufferedImage(useSrcImageIndex);
//System.err.println("SingleImagePanel.paintComponent() useSrcImage is BufferedImage as follows:"); BufferedImageUtilities.describeImage(useSrcImage,System.err);

		if (ybr) {
			// do this BEFORE any interpolation, else get artifacts at edges
//System.err.println("SingleImagePanel.paintComponent(): attempting to convert YBR to RGB for display");
			BufferedImage convertedImage = BufferedImageUtilities.convertYBRToRGB(useSrcImage);
			if (convertedImage != null) {
				useSrcImage = convertedImage;
				dirtySource();
			}
			else {
//System.err.println("SingleImagePanel.paintComponent(): YBR to RGB conversion failed - just use what we have");
			}
		}

		Rectangle windowSize = this.getBounds();
//System.err.println("SingleImagePanel.paintComponent():windowSize = "+windowSize);

		// the following is not sensitive to selection area change ... in future may need to make this explicit on resize event ... :(
		if (cachedWindowSize != null && (windowSize.width != cachedWindowSize.width && windowSize.height != cachedWindowSize.height)) {		// do not care about position
//System.err.println("SingleImagePanel.paintComponent(): window size changed, so flushing cache and resetting selection to original");
			dirtySource();
			useDisplayedAreaSelection=null;
		}
		cachedWindowSize = windowSize;

		if (useDisplayedAreaSelection == null) {
			if (originalDisplayedAreaSelection == null) {
				originalDisplayedAreaSelection = new DisplayedAreaSelection(useSrcImage.getWidth(),useSrcImage.getHeight());		// default is entire image
			}
//System.err.println("SingleImagePanel.setDisplayedAreaSelection(): originalDisplayedAreaSelection = "+originalDisplayedAreaSelection);
			useDisplayedAreaSelection = originalDisplayedAreaSelection.shapeSelectionToMatchAvailableWindow(windowSize);
//System.err.println("SingleImagePanel.paintComponent(): useDisplayedAreaSelection (after shapping to match available window size) = "+useDisplayedAreaSelection);
		}

		double useScaleFactor = ((double)windowSize.width)/useDisplayedAreaSelection.getSelectionWidth();
//System.err.println("SingleImagePanel.paintComponent(): useScaleFactor = "+useScaleFactor);

		imageToWindowCoordinateTransform = AffineTransform.getTranslateInstance(-useDisplayedAreaSelection.getXOffset()*useScaleFactor,-useDisplayedAreaSelection.getYOffset()*useScaleFactor);
		imageToWindowCoordinateTransform.concatenate(AffineTransform.getScaleInstance(useScaleFactor,useScaleFactor));
		if (preTransformImageRelativeCoordinates != null) {
			imageToWindowCoordinateTransform.concatenate(preTransformImageRelativeCoordinates);
		}
//System.err.println("SingleImagePanel.paintComponent(): imageToWindowCoordinateTransform = "+imageToWindowCoordinateTransform);
		try {
			windowToImageCoordinateTransform = imageToWindowCoordinateTransform.createInverse();
		}
		catch (NoninvertibleTransformException e) {
			e.printStackTrace(System.err);
			windowToImageCoordinateTransform=null;
		}
//System.err.println("SingleImagePanel.paintComponent(): windowToImageCoordinateTransform = "+windowToImageCoordinateTransform);
		
		if (cachedPreWindowedImage == null) {
//System.err.println("SingleImagePanel.paintComponent(): cachedPreWindowedImage is null");
			if (cachedResizedSelectedRegionImage == null) {
//System.err.println("SingleImagePanel.paintComponent(): resizing prior to windowing");
				if (cachedResizedImage == null) {
					if (resampler == null) {
						resampler = new BufferedImageUtilities();
					}
//long resampleTime = System.currentTimeMillis();
					int backgroundValuePriorToWindowing = sImg.getBackgroundValue();

					int srcWidth = useSrcImage.getWidth();
					int srcHeight = useSrcImage.getHeight();
					cachedResizedImage=resampler.resample(useSrcImage,
						srcWidth,
						srcHeight,
						0,
						0,
						(int)Math.round(srcWidth*useScaleFactor),
						(int)Math.round(srcHeight*useScaleFactor),
						signed,
						backgroundValuePriorToWindowing);
//System.err.println("SingleImagePanel.paintComponent(): resizing with resampler - done in "+(System.currentTimeMillis()-resampleTime)+" ms");
//System.err.println("SingleImagePanel.paintComponent(): resampled BufferedImage is as follows:"); BufferedImageUtilities.describeImage(cachedResizedImage,System.err);
				}
//long getSubimageTime = System.currentTimeMillis();
				int scaledSelectionX = (int)Math.round(useDisplayedAreaSelection.getXOffset()*useScaleFactor);
				int scaledSelectionY = (int)Math.round(useDisplayedAreaSelection.getYOffset()*useScaleFactor);
				int scaledSelectionWidth = (int)Math.round(useDisplayedAreaSelection.getSelectionWidth()*useScaleFactor);
				int scaledSelectionHeight = (int)Math.round(useDisplayedAreaSelection.getSelectionHeight()*useScaleFactor);
//System.err.println("SingleImagePanel.paintComponent(): before clipping to image area - scaledSelectionX = "+scaledSelectionX);
//System.err.println("SingleImagePanel.paintComponent(): before clipping to image area - scaledSelectionY = "+scaledSelectionY);
//System.err.println("SingleImagePanel.paintComponent(): before clipping to image area - scaledSelectionWidth = "+scaledSelectionWidth);
//System.err.println("SingleImagePanel.paintComponent(): before clipping to image area - scaledSelectionHeight = "+scaledSelectionHeight);

				if (scaledSelectionX < 0) {
					offsetDrawingOfResizedSelectedRegionImageX = - scaledSelectionX;
					//scaledSelectionWidth -= offsetDrawingOfResizedSelectedRegionImageX;
					scaledSelectionX = 0;
				}
				else {
					offsetDrawingOfResizedSelectedRegionImageX = 0;
				}
					
				if (scaledSelectionY < 0) {
					offsetDrawingOfResizedSelectedRegionImageY = - scaledSelectionY;
					//scaledSelectionHeight -= offsetDrawingOfResizedSelectedRegionImageY;
					scaledSelectionY = 0;
				}
				else {
					offsetDrawingOfResizedSelectedRegionImageY = 0;
				}

				int cachedResizedImageWidth = cachedResizedImage.getWidth();
//System.err.println("SingleImagePanel.paintComponent(): cachedResizedImageWidth = "+cachedResizedImageWidth);
				if (scaledSelectionX + scaledSelectionWidth > cachedResizedImageWidth) {
					scaledSelectionWidth = cachedResizedImageWidth - scaledSelectionX;
				}
				else if (scaledSelectionX + scaledSelectionWidth < cachedResizedImageWidth) {	// use all available image to the right, i.e., if panning beyond original selection
					scaledSelectionWidth = cachedResizedImageWidth - scaledSelectionX;
				}
				if (scaledSelectionWidth < 0) {
					scaledSelectionWidth = 0;
				}

				int cachedResizedImageHeight = cachedResizedImage.getHeight();
//System.err.println("SingleImagePanel.paintComponent(): cachedResizedImageHeight = "+cachedResizedImageHeight);
				if (scaledSelectionY + scaledSelectionHeight > cachedResizedImageHeight) {
					scaledSelectionHeight = cachedResizedImageHeight - scaledSelectionY;
				}
				else if (scaledSelectionY + scaledSelectionHeight < cachedResizedImageHeight) {	// use all available image to the bottom, i.e., if panning beyond original selection
					scaledSelectionHeight = cachedResizedImageHeight - scaledSelectionY;
				}
				if (scaledSelectionHeight < 0) {
					scaledSelectionHeight = 0;
				}

//System.err.println("SingleImagePanel.paintComponent(): after clipping to image area - scaledSelectionX = "+scaledSelectionX);
//System.err.println("SingleImagePanel.paintComponent(): after clipping to image area - scaledSelectionY = "+scaledSelectionY);
//System.err.println("SingleImagePanel.paintComponent(): after clipping to image area - scaledSelectionWidth = "+scaledSelectionWidth);
//System.err.println("SingleImagePanel.paintComponent(): after clipping to image area - scaledSelectionHeight = "+scaledSelectionHeight);
//System.err.println("SingleImagePanel.paintComponent(): offsetDrawingOfResizedSelectedRegionImageX = "+offsetDrawingOfResizedSelectedRegionImageX);
//System.err.println("SingleImagePanel.paintComponent(): offsetDrawingOfResizedSelectedRegionImageY = "+offsetDrawingOfResizedSelectedRegionImageY);

				cachedResizedSelectedRegionImage = 
					scaledSelectionHeight > 0 && scaledSelectionWidth > 0
					? cachedResizedImage.getSubimage(scaledSelectionX,scaledSelectionY,scaledSelectionWidth,scaledSelectionHeight)
					: null;
//System.err.println("SingleImagePanel.paintComponent(): selecting subimage from resized image - done in "+(System.currentTimeMillis()-getSubimageTime)+" ms");
			}
			useSrcImage=cachedResizedSelectedRegionImage;
			
//System.err.println("SingleImagePanel.paintComponent(): Before windowing (if needed), getNumComponents() == "+useSrcImage.getColorModel().getNumComponents());

			if (useSrcImage == null) {		// e.g., nothing left to draw since panned offscreen
				cachedPreWindowedImage = null;
			}
			else  if (useSrcImage.getColorModel().getNumComponents() == 1) {
			
				// First, find pre-selected VOI for this frame, if any
				
//System.err.println("currentVOITransformInUse "+currentVOITransformInUse);
				if (currentVOITransformInUse != -1
				 && !useVOILUTNotFunction
				 && voiTransform != null
				 && voiTransform.getNumberOfTransforms(useSrcImageIndex) > currentVOITransformInUse) {
					windowWidth=voiTransform.getWidth(useSrcImageIndex,currentVOITransformInUse);
					windowCenter=voiTransform.getCenter(useSrcImageIndex,currentVOITransformInUse);
//System.err.println("For new frame "+useSrcImageIndex+" using preselected center "+windowCenter+" and width "+windowWidth);
				}
				else {
					//currentVOITransformInUse=-1;
					// Just leave it alone (but don't disable use of selection for other frames)
//System.err.println("For new frame "+useSrcImageIndex+" using user selected center "+windowCenter+" and width "+windowWidth);
				}
				
				// Second, find rescale attributes for this frame
				
				if (modalityTransform != null) {
					    useSlope = modalityTransform.getRescaleSlope    (useSrcImageIndex);
					useIntercept = modalityTransform.getRescaleIntercept(useSrcImageIndex);
//System.err.println("For new frame "+useSrcImageIndex+" using preselected rescale slope "+useSlope+" and intercept "+useIntercept);
				}
				else {
					useSlope=1.0;
					useIntercept=0.0;
//System.err.println("For new frame "+useSrcImageIndex+" using default rescale slope "+useSlope+" and intercept "+useIntercept);
				}
				
				// Finally, actually build the destination image
//System.err.println("SingleImagePanel.paintComponent() useVOILUTNotFunction = "+useVOILUTNotFunction);
//System.err.println("SingleImagePanel.paintComponent() numberOfEntries = "+numberOfEntries);
//System.err.println("SingleImagePanel.paintComponent() redTable = "+redTable);
//System.err.println("SingleImagePanel.paintComponent() useVOIFunction = "+useVOIFunction);
//long applyVOITime = System.currentTimeMillis();
				if (useVOILUTNotFunction) {
					cachedPreWindowedImage = applyVOILUT(useSrcImage,windowCenter,windowWidth,voiLUTIdentityWindowCenter,voiLUTIdentityWindowWidth,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit,
						voiLUTNumberOfEntries,voiLUTFirstValueMapped,voiLUTBitsPerEntry,voiLUTData,voiLUTEntryMin,voiLUTEntryMax,voiLUTTopOfEntryRange);
				}
				else if (numberOfEntries != 0 && redTable != null) {
					cachedPreWindowedImage = applyWindowCenterAndWidthWithPaletteColor(useSrcImage,windowCenter,windowWidth,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit,
						largestGray,bitsPerEntry,numberOfEntries,redTable,greenTable,blueTable);
				}
				else if (useVOIFunction == 1) {
					cachedPreWindowedImage = applyWindowCenterAndWidthLogistic(useSrcImage,windowCenter,windowWidth,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit);
				}
				else {
					cachedPreWindowedImage = applyWindowCenterAndWidthLinear(useSrcImage,windowCenter,windowWidth,signed,inverted,useSlope,useIntercept,hasPad,pad,padRangeLimit,useWindowLinearExactCalculationInsteadOfDICOMStandardMethod);
				}
//System.err.println("SingleImagePanel.paintComponent(): VOI/window applied in "+(System.currentTimeMillis()-applyVOITime)+" ms");
			}
			else  if (useSrcImage.getColorModel().getNumComponents() == 3) {
//System.err.println("SingleImagePanel.paintComponent(): is 3 component");
//System.err.println("currentVOITransformInUse "+currentVOITransformInUse);
				if (currentVOITransformInUse != -1 && voiTransform.getNumberOfTransforms(useSrcImageIndex) > currentVOITransformInUse) {
					windowWidth=voiTransform.getWidth(useSrcImageIndex,currentVOITransformInUse);
					windowCenter=voiTransform.getCenter(useSrcImageIndex,currentVOITransformInUse);
//System.err.println("For new frame "+useSrcImageIndex+" using preselected center "+windowCenter+" and width "+windowWidth);
				}
				else {
					//currentVOITransformInUse=-1;
					// Just leave it alone (but don't disable use of selection for other frames)
//System.err.println("For new frame "+useSrcImageIndex+" using user selected center "+windowCenter+" and width "+windowWidth);
				}
				
				// No rescaling for color images
								
				// Finally, actually build the destination image
//long applyVOITime = System.currentTimeMillis();
				// use only linear voiTransform ... and no rescaling, no sign, no inversion (for now), no padding
				{
					cachedPreWindowedImage = applyWindowCenterAndWidthLinearToColorImage(useSrcImage,windowCenter,windowWidth);
				}
//System.err.println("SingleImagePanel.paintComponent(): VOI/window applied in "+(System.currentTimeMillis()-applyVOITime)+" ms");
			}
			else {
//System.err.println("SingleImagePanel.paintComponent(): Not windowing, getNumComponents() = "+useSrcImage.getColorModel().getNumComponents()+", currentVOITransformInUse = "+currentVOITransformInUse+", useVOILUTNotFunction = "+useVOILUTNotFunction);
//System.err.println("SingleImagePanel.paintComponent(): Just using unwindowed useSrcImage as cachedPreWindowedImage="+useSrcImage);
				cachedPreWindowedImage=useSrcImage;
			}
//System.err.println("SingleImagePanel.paintComponent() mrj.version="+System.getProperty("mrj.version"));
			if (cachedPreWindowedImage != null && useConvertToMostFavorableImageType) {
//long conversionTime = System.currentTimeMillis();
				cachedPreWindowedImage=BufferedImageUtilities.convertToMostFavorableImageType(cachedPreWindowedImage);
//System.err.println("SingleImagePanel.paintComponent(): converted to most favorable done in "+(System.currentTimeMillis()-conversionTime)+" ms");
			}
		}
		else {
//System.err.println("SingleImagePanel.paintComponent(): using cachedPreWindowedImage");
		}
				
//long drawImageTime = System.currentTimeMillis();
		Graphics2D g2d=(Graphics2D)g;
		if (cachedPreWindowedImage != null) {
//System.err.println("SingleImagePanel.paintComponent(): same size draw");
//System.err.println("SingleImagePanel.paintComponent(): applyShutter = "+applyShutter);
			Shape holdClip = null;
			if (applyShutter && displayShutter != null) {
				holdClip = g2d.getClip();
				if (displayShutter.isCircularShutter()) {
					Point2D tlhc = imageToWindowCoordinateTransform.transform(displayShutter.getCircularShutterTLHC(),null);
					double tlhcX = tlhc.getX();
					double tlhcY = tlhc.getY();
					Point2D brhc = imageToWindowCoordinateTransform.transform(displayShutter.getCircularShutterBRHC(),null);
					double brhcX = brhc.getX();
					double brhcY = brhc.getY();
					g2d.clip(new Ellipse2D.Double(tlhcX,tlhcY,brhcX-tlhcX,brhcY-tlhcY));
				}
				if (displayShutter.isRectangularShutter()) {
					Point2D tlhc = imageToWindowCoordinateTransform.transform(displayShutter.getRectangularShutterTLHC(),null);
					double tlhcX = tlhc.getX();
					double tlhcY = tlhc.getY();
					Point2D brhc = imageToWindowCoordinateTransform.transform(displayShutter.getRectangularShutterBRHC(),null);
					double brhcX = brhc.getX();
					double brhcY = brhc.getY();
					g2d.clip(new Rectangle2D.Double(tlhcX,tlhcY,brhcX-tlhcX,brhcY-tlhcY));
				}
				if (displayShutter.isPolygonalShutter()) {
					Point2D[] points = displayShutter.getVerticesOfPolygonalShutterAsPoint2D();
					Polygon polygon = new Polygon();
					for (int i=0; i<points.length; ++i) {
						Point2D point = imageToWindowCoordinateTransform.transform(points[i],null);
						polygon.addPoint((int)(point.getX()),(int)(point.getY()));
					}
					g2d.clip(polygon);
				}
			}
			g2d.drawImage(cachedPreWindowedImage,offsetDrawingOfResizedSelectedRegionImageX,offsetDrawingOfResizedSelectedRegionImageY,this);
			if (holdClip != null) {
				g2d.setClip(holdClip);
			}
		}
//System.err.println("SingleImagePanel.paintComponent(): draw cachedPreWindowedImage into dst image done in "+(System.currentTimeMillis()-drawImageTime)+" ms");

//long drawAnnotationsTime = System.currentTimeMillis();
		Color interactiveColor = Color.red;
		Color persistentColor = Color.green;
		Color selectedColor = Color.red;
		Color localizerColor = Color.green;
		Color volumeLocalizationColor = Color.blue;
		Color preDefinedColor = Color.yellow;
		Color perFrameDrawingColor = Color.red;
		int lineWidth = 2;	// in display (not source image) pixels
		Font preDefinedFont = new Font("SansSerif",Font.BOLD,14);
		Font persistentFont = new Font("SansSerif",Font.BOLD,10);
		
		// draw any graphics that are displayed area relative ...
				
		if (demographicAndTechniqueAnnotations != null) {
			g2d.setColor(demographicAndTechniqueColor);
			g2d.setFont(demographicAndTechniqueFont);

			Iterator i = demographicAndTechniqueAnnotations.iterator(useSrcImageIndex);
			while (i.hasNext()) {
				TextAnnotationPositioned.drawPositionedString((TextAnnotationPositioned)i.next(),g2d,this,5,5);
			}

		}
		
		{
			g2d.setColor(orientationColor);
			g2d.setFont(orientationFont);
			// draw orientation
			if (orientationAnnotations != null) {
				String rowOrientation = orientationAnnotations.getRowOrientation(useSrcImageIndex);
				if (rowOrientation != null && rowOrientation.length() > 0) {
					TextAnnotationPositioned.drawVerticallyCenteredString(rowOrientation,showOrientationsLeftSide,g2d,this,5);
				}
				String columnOrientation = orientationAnnotations.getColumnOrientation(useSrcImageIndex);
				if (columnOrientation != null && columnOrientation.length() > 0) {
					TextAnnotationPositioned.drawHorizontallyCenteredString(columnOrientation,false/*isTop*/,g2d,this,5);
				}
			}
			
			if (showZoomFactor) {
				double scaleFactor = Math.abs(imageToWindowCoordinateTransform.getScaleX());		// -ve if flipped; should check same in both axes ? :(
				String sZoomFactor = "1:" + FloatFormatter.toString(scaleFactor,Locale.US);
//System.err.println("SingleImagePanel.paintComponent(): sZoomFactor = "+sZoomFactor);
				TextAnnotationPositioned.drawVerticallyCenteredString(sZoomFactor,showZoomFactorLeftSide,g2d,this,2,5);
				String sDisplayPixelSize = "[" + FloatFormatter.toString(pixelSpacingInSourceImage/scaleFactor,Locale.US) + "mm]";
				TextAnnotationPositioned.drawVerticallyCenteredString(sDisplayPixelSize,showZoomFactorLeftSide,g2d,this,3,5);
			}
		}

		if (sideAndViewAnnotationString != null) {
			g2d.setColor(sideAndViewAnnotationColor);
			g2d.setFont(sideAndViewAnnotationFont);
			TextAnnotationPositioned.drawPositionedString(
				new TextAnnotationPositioned(sideAndViewAnnotationString,showSideAndViewAnnotationLeftSide,true/*fromTop*/,0/*row*/),
					g2d,this,sideAndViewAnnotationVerticalOffset,5
			);
		}

		// draw any text that are image relative, but using display relative font size
		// by transforming point BEFORE changing graphics context to image relative
		
		if (persistentDrawingText != null) {
			g2d.setColor(persistentColor);
			g2d.setFont(persistentFont);
			Iterator i = persistentDrawingText.iterator();
			while (i.hasNext()) {
				TextAnnotation text = (TextAnnotation)i.next();
				Point2D imageRelativeAnchorPoint = text.getAnchorPoint();
				Point2D windowRelativeAnchorPoint = imageToWindowCoordinateTransform.transform(imageRelativeAnchorPoint,null);
				String string = text.getString();
				int margin = 5;
				int stringWidth  = (int)(g2d.getFontMetrics().getStringBounds(string,g2d).getWidth());
				DrawingUtilities.drawShadowedString(string,
					showOrientationsLeftSide ? margin : getBounds().width - margin - stringWidth,
					(int)windowRelativeAnchorPoint.getY(),			// always left or right since sometimes doesn't fit (clipped on right) if drawn at anchor point:(
					g2d);
			}
		}

		// draw any graphics that are image relative, by applying a scaled AffineTransform to the graphics context ...

//System.err.println("SingleImagePanel.paintComponent(): draw any graphics");
		
		g2d.transform(imageToWindowCoordinateTransform);
		// want the stroke line width specified in display (not source image) pixels
		g2d.setStroke(new BasicStroke((float)(lineWidth/useScaleFactor)));

		if (showSuperimposedImages && superimposedImages != null) {
			for (SuperimposedImage superimposedImage : superimposedImages) {
//System.err.println("SingleImagePanel.paintComponent(): Drawing superimposed image "+superimposedImage);
				SuperimposedImage.AppliedToUnderlyingImage superimposedImageAppliedToUnderlyingImage = superimposedImage.getAppliedToUnderlyingImage(imageGeometry,useSrcImageIndex);
				if (superimposedImageAppliedToUnderlyingImage != null) {
//System.err.println("SingleImagePanel.paintComponent(): Drawing superimposed image applied to underlying image "+superimposedImageAppliedToUnderlyingImage);
					BufferedImage bufferedImage = superimposedImageAppliedToUnderlyingImage.getBufferedImage();
					if (bufferedImage != null) {
						double rowOrigin = superimposedImageAppliedToUnderlyingImage.getRowOrigin();
						double columnOrigin = superimposedImageAppliedToUnderlyingImage.getColumnOrigin();
						
						java.awt.image.RescaleOp applyColor = null;
						int bufferedImageType = bufferedImage.getType();
						if (bufferedImageType == BufferedImage.TYPE_INT_ARGB) {
//System.err.println("SingleImagePanel.paintComponent(): have ARGB superimposed image so can change color and use transparency");
							// BufferedImageOp ... use this for transparency with ARGB BufferedImage and RescaleOp to mess with alpha channel (see "http://docs.oracle.com/javase/tutorial/2d/images/drawimage.html")
							int[] cieLabScaled = superimposedImage.getIntegerScaledCIELabPCS();
							float redScale = 1f;
							float greenScale = 1f;
							float blueScale = 0f;
							if (cieLabScaled != null) {
								int[] srgb = ColorUtilities.getSRGBFromIntegerScaledCIELabPCS(cieLabScaled);
								redScale = srgb[0] / 255f;
								greenScale = srgb[1] / 255f;
								blueScale = srgb[2] / 255f;
							}
							float[] scales = { redScale, greenScale, blueScale, 1f /* color and alpha */ };
							float[] offsets = new float[4];
							applyColor = new java.awt.image.RescaleOp(scales,offsets,null/*RenderingHints*/);
						}
						else {
System.err.println("SingleImagePanel.paintComponent(): not ARGB superimposed image so cannot change color and use transparency");
						}
						g2d.setComposite(java.awt.AlphaComposite.getInstance(java.awt.AlphaComposite.SRC_OVER,0.5f));	// set transparency
						g2d.drawImage(
							bufferedImage,
							applyColor,
							(int)columnOrigin,	// aaargh ! ... really need to indirect the original doubles through the affine transform in case upsampled for displayed, since usually are +0.5 sub-pixel resolution :(
							(int)rowOrigin
						);
					}
					//else {
					//	System.err.println("SingleImagePanel.paintComponent(): could not apply superimposed image to underlying image because no BufferedImage returned");
					//}
				}
				//else {
				//	System.err.println("SingleImagePanel.paintComponent(): could not apply superimposed image to underlying image because no SuperimposedImage.AppliedToUnderlyingImage returned");
				//}
			}
		}
		
		if (showOverlays && overlay != null && overlay.getNumberOfOverlays(useSrcImageIndex) > 0) {
//System.err.println("SingleImagePanel.paintComponent(): Drawing overlays for frame "+useSrcImageIndex);
			for (int o=0; o<16; ++o) {
				BufferedImage overlayImage = overlay.getOverlayAsBinaryBufferedImage(useSrcImageIndex,o);
				if (overlayImage != null) {
//System.err.println("SingleImagePanel.paintComponent(): Drawing overlay number "+o);
					int rowOrigin = overlay.getRowOrigin(useSrcImageIndex,o);
					int columnOrigin = overlay.getColumnOrigin(useSrcImageIndex,o);
					{
						byte[] red = { (byte)0, (byte)0 };
						byte[] green = { (byte)0, (byte)0 };
						byte[] blue = { (byte)0, (byte)0 };
						IndexColorModel colorModel = new IndexColorModel(
							1 /* bits */,
							2 /* size */,
							red,green,blue,
							0 /* the first index (black value) is to be transparent */);
						BufferedImage shadowImage = new BufferedImage(colorModel,overlayImage.getRaster(),false,null);
						g2d.drawImage(shadowImage,null,columnOrigin+1,rowOrigin+1);
					}
					g2d.drawImage(overlayImage,null,columnOrigin,rowOrigin);
				}
			}
		}
		
		if (interactiveDrawingShapes != null) {
//System.err.println("SingleImagePanel.paintComponent(): Draw interactive shapes:");
//LocalizerPosterFactory.dumpShapes(interactiveDrawingShapes);
			g2d.setColor(interactiveColor);
			Iterator i = interactiveDrawingShapes.iterator();
			while (i.hasNext()) {
				DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
			}
		}

		if (persistentDrawingShapes != null) {
//System.err.println("SingleImagePanel.paintComponent(): Draw persistent shapes:");
//LocalizerPosterFactory.dumpShapes(persistentDrawingShapes);
			g2d.setColor(persistentColor);
			Iterator i = persistentDrawingShapes.iterator();
			while (i.hasNext()) {
				DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
			}
		}

		if (selectedDrawingShapes != null) {
//System.err.println("SingleImagePanel.paintComponent(): Draw selected shapes:");
//LocalizerPosterFactory.dumpShapes(persistentDrawingShapes);
			g2d.setColor(selectedColor);
			Iterator i = selectedDrawingShapes.iterator();
			while (i.hasNext()) {
				DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
			}
		}

		if (volumeLocalizationShapes != null) {
//System.err.println("SingleImagePanel.paintComponent(): draw volume localization shapes");
//LocalizerPosterFactory.dumpShapes(volumeLocalizationShapes);
			g2d.setColor(volumeLocalizationColor);
			Iterator i = volumeLocalizationShapes.iterator();
			while (i.hasNext()) {
				DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
			}
		}
		
		if (localizerShapes != null) {			// do this after volumeLocalizationShapes, in case need to draw on top
//System.err.println("SingleImagePanel.paintComponent(): draw localizer shapes");
//LocalizerPosterFactory.dumpShapes(localizerShapes);
			g2d.setColor(localizerColor);
			Iterator i = localizerShapes.iterator();
			while (i.hasNext()) {
				DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
			}
		}
		
		if (preDefinedShapes != null) {
//System.err.println("SingleImagePanel.paintComponent(): draw pre-defined shapes");
//com.pixelmed.geometry.LocalizerPosterFactory.dumpShapes(preDefinedShapes);
			g2d.setColor(preDefinedColor);
			Iterator i = preDefinedShapes.iterator();
			while (i.hasNext()) {
				DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
			}
		}
		
		if (perFrameDrawingShapes != null && perFrameDrawingShapes.length > currentSrcImageIndex && perFrameDrawingShapes[currentSrcImageIndex] != null) {
//System.err.println("SingleImagePanel.paintComponent(): draw per-frame shapes");
			Vector<Shape> shapes = perFrameDrawingShapes[currentSrcImageIndex];
//com.pixelmed.geometry.LocalizerPosterFactory.dumpShapes(shapes);
			g2d.setColor(perFrameDrawingColor);
			Iterator i = shapes.iterator();
			while (i.hasNext()) {
				//DrawingUtilities.drawShadowedShape((Shape)i.next(),g2d);
				g2d.draw((Shape)i.next());
			}
		}

		if (preDefinedText != null) {
			g2d.setColor(preDefinedColor);
			g2d.setFont(preDefinedFont);
			Iterator i = preDefinedText.iterator();
			while (i.hasNext()) {
				TextAnnotation text = (TextAnnotation)i.next();
				DrawingUtilities.drawShadowedString(text.getString(),text.getAnchorPointXAsInt(),text.getAnchorPointYAsInt(),g2d);
			}
		}
//System.err.println("SingleImagePanel.paintComponent(): draw annotations done in "+(System.currentTimeMillis()-drawAnnotationsTime)+" ms");
		
		setCursor(was);
//System.err.println("paintComponent() elapsed: "+(System.currentTimeMillis()-startTime)+" ms");
//System.err.println("SingleImagePanel.paintComponent(): end");
	}

	/**
	 * @param	arg
	 */
	public static void main(String arg[]) {
		try {
			if (arg.length == 1) {
				JFrame p = new JFrame();
				SourceImage sImg = new SourceImage(arg[0]);
				//AttributeList list = new AttributeList();
				//list.read(arg[0]);
				if (sImg.isImage()) {
					p.add(new SingleImagePanel(sImg));
					//p.add(new SingleImagePanel(new SourceImage(list)));
					p.setBackground(Color.BLACK);
					p.setSize(512,512);
					p.setVisible(true);
				}
				else {
					throw new DicomException("Is not an image or is not an image that can be displayed");
				}
			}
		} catch (Exception e) {
			e.printStackTrace(System.err);
			System.exit(0);
		}
	}
}





