/*========================================================================= Program: FusionViewer Module: $RCSfile: WindowLevelControl.java,v $ Language: Java Date: $Date: 2007/02/02 19:27:57 $ Version: $Revision: 1.2 $ Copyright (c) Insightful Corporation. All rights reserved. See Copyright.txt for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notice for more information. =========================================================================*/ package org.fusionviewer.ui; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Point; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.nio.ByteBuffer; import java.text.NumberFormat; import javax.swing.JComponent; import javax.swing.Popup; import javax.swing.PopupFactory; import org.fusionviewer.display.Colormap; import org.fusionviewer.model.ImageDisplayModel; import org.fusionviewer.model.ImageDisplayModelListener; /**\class WindowLevelControl *\brief Widget to adjust window and level. * * Displays the current color map as a rectangle with handles on the left and right sides. * Dragging the handles adjusts the window width as well as the level. Dragging the entire * rectangle adjusts the level. */ public class WindowLevelControl extends JComponent implements MouseListener, MouseMotionListener, ImageDisplayModelListener, ComponentListener { private static final int WIDTH = 257; private static final int HEIGHT = 24; private static final int MOUSE_REGION_TOLERANCE = 4; private ImageDisplayModel m_model; // Window and level data container private int m_idx; // Index in m_model to display/alter private int m_lowerBound, m_upperBound; // Upper and lower bounds for the window private Color[] m_colormap = new Color[Colormap.NUM_ENTRIES]; private Colormap m_map; private Popup m_popup; private WindowLevelStatus m_windowLevelStatus = new WindowLevelStatus(); private Font m_smallFont; // The following members are used while the left mouse button is down over the main // window level widget private int m_offset; // Offset from a hot spot the user clicked on private int m_anchor; // The anchor point used to interpret mouse drags private static final int ANCHOR_NONE = 0; private static final int ANCHOR_TOP = 1; private static final int ANCHOR_BOTTOM = 2; private static final int ANCHOR_MIDDLE = 3; public WindowLevelControl(ImageDisplayModel model, int idx) { m_idx = idx; m_model = model; model.addListener(this); syncColormap(); syncWindowLevel(); addMouseListener(this); addMouseMotionListener(this); addComponentListener(this); } /* * Cache the model's color map */ private void syncColormap() { if (m_model == null) return; Colormap newMap = m_model.getColormap(m_idx); if (m_map == newMap) return; m_map = newMap; ByteBuffer map = m_map.getColormap(); for (int i = 0; i < Colormap.NUM_ENTRIES; i++) m_colormap[i] = new Color(getColor(map.get()), getColor(map.get()), getColor(map.get())); } public Dimension getPreferredSize() { return new Dimension(WIDTH, HEIGHT); } public Dimension getMinimumSize() { return new Dimension(WIDTH, HEIGHT); } protected void paintComponent(Graphics g) { final int TICKMARK_SIZE = 2; g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); // Draw the color map float scale = 255.0f / (float) (m_upperBound - m_lowerBound); int controlHeight = getHeight(); for (int i = 0; i < getWidth(); i++) { int colorIdx; if (i < m_lowerBound) colorIdx = 0; else if (i > m_upperBound) colorIdx = Colormap.NUM_ENTRIES - 1; else colorIdx = (int) ((i - m_lowerBound) * scale); if (m_model.isInverted(m_idx)) colorIdx = 255 - colorIdx; Color color = m_colormap[colorIdx]; if ((i == m_lowerBound) || (i == m_lowerBound + 1) || (i == m_upperBound) || (i == m_upperBound - 1)) color = getInverseColor(color); g.setColor(color); g.drawLine(i, TICKMARK_SIZE + 1, i, controlHeight - TICKMARK_SIZE - 2); } g.setColor(getForeground()); // Upper left tickmark g.drawLine(m_lowerBound, 0, m_lowerBound, TICKMARK_SIZE - 1); g.drawLine(m_lowerBound + 1, 0, m_lowerBound + 1, TICKMARK_SIZE - 1); // Lower left tickmark g.drawLine(m_lowerBound, controlHeight - TICKMARK_SIZE, m_lowerBound, controlHeight - 1); g.drawLine(m_lowerBound + 1, controlHeight - TICKMARK_SIZE, m_lowerBound + 1, controlHeight - 1); // Upper right tickmark g.drawLine(m_upperBound - 1, 0, m_upperBound - 1, TICKMARK_SIZE - 1); g.drawLine(m_upperBound, 0, m_upperBound, TICKMARK_SIZE - 1); // Bottom left tickmark g.drawLine(m_upperBound - 1, controlHeight - TICKMARK_SIZE, m_upperBound - 1, controlHeight - 1); g.drawLine(m_upperBound, controlHeight - TICKMARK_SIZE, m_upperBound, controlHeight - 1); // Frame the colorbar area g.drawRect(0, TICKMARK_SIZE, getWidth() - 1, getHeight() - TICKMARK_SIZE * 2 - 1); } private int getColor(byte val) { if (val < 0) return val + 256; else return val; } private Color getInverseColor(Color color) { return new Color(255 - color.getRed(), 255 - color.getGreen(), 255 - color.getBlue()); } /* (non-Javadoc) * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent) */ public void mouseClicked(MouseEvent e) { } /* (non-Javadoc) * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent) */ public void mousePressed(MouseEvent e) { m_anchor = findAnchor(e.getPoint()); if (m_anchor != ANCHOR_NONE) { int coord = e.getX(); switch (m_anchor) { case ANCHOR_MIDDLE: m_offset = coord - (m_lowerBound + ((m_upperBound - m_lowerBound) / 2)); break; case ANCHOR_TOP: m_offset = coord - m_upperBound; break; case ANCHOR_BOTTOM: m_offset = coord - m_lowerBound; break; } setCursor(m_anchor); Dimension size = m_windowLevelStatus.getPreferredSize(); Point screenLocation = getLocationOnScreen(); m_popup = PopupFactory.getSharedInstance().getPopup(this, m_windowLevelStatus, screenLocation.x + getWidth() / 2 - size.width / 2, screenLocation.y + HEIGHT - 4); m_popup.show(); } } private int findAnchor(Point pt) { int sizeControl = getWidth(); int coord = pt.x; int anchor; // See which end of the window the mouse is at or if it is in the middle if ((coord >= m_lowerBound - MOUSE_REGION_TOLERANCE) && (coord <= m_lowerBound + MOUSE_REGION_TOLERANCE)) anchor = ANCHOR_BOTTOM; else if ((coord >= m_upperBound - MOUSE_REGION_TOLERANCE) && (coord <= m_upperBound + MOUSE_REGION_TOLERANCE)) anchor = ANCHOR_TOP; else if ((coord > m_lowerBound + MOUSE_REGION_TOLERANCE) && (coord < m_upperBound - MOUSE_REGION_TOLERANCE) && (m_upperBound - m_lowerBound != sizeControl - 1)) anchor = ANCHOR_MIDDLE; else anchor = ANCHOR_NONE; // If the window becomes too small to grab either end, then go through the following // algorithm to prevent users from not being able to adjust the window at all. if ((anchor != ANCHOR_NONE) && (m_upperBound - m_lowerBound < MOUSE_REGION_TOLERANCE)) { // If the mouse is near the top of the range, then grab the bottom if (m_lowerBound < MOUSE_REGION_TOLERANCE) anchor = ANCHOR_TOP; // If the mouse is at the bottom of the range, then grab the top if (m_upperBound > sizeControl - MOUSE_REGION_TOLERANCE) anchor = ANCHOR_BOTTOM; // Otherwise, see if the window is closer to the top or bottom // and grab the opposite end if (m_lowerBound + ((m_upperBound - m_lowerBound) / 2) < (sizeControl / 2)) anchor = ANCHOR_TOP; else anchor = ANCHOR_BOTTOM; } return anchor; } /* * Update the cursor to reflect the state of the user interaction. */ private void setCursor(int anchor) { switch (anchor) { case ANCHOR_TOP: setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)); break; case ANCHOR_BOTTOM: setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR)); break; case ANCHOR_MIDDLE: setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); break; default: setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); break; } } /* (non-Javadoc) * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent) */ public void mouseReleased(MouseEvent e) { m_anchor = findAnchor(e.getPoint()); setCursor(m_anchor); if (m_popup != null) { m_popup.hide(); m_popup = null; } } /* (non-Javadoc) * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent) */ public void mouseEntered(MouseEvent e) { } /* (non-Javadoc) * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent) */ public void mouseExited(MouseEvent e) { } /* (non-Javadoc) * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent) */ public void mouseDragged(MouseEvent e) { adjustBounds(e); } /* (non-Javadoc) * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent) */ public void mouseMoved(MouseEvent e) { setCursor(findAnchor(e.getPoint())); } private void adjustBounds(MouseEvent e) { int coord = e.getX() - m_offset; int sizeControl = getWidth(); if (coord < 0) coord = 0; if (coord >= sizeControl) coord = sizeControl - 1; switch (m_anchor) { case ANCHOR_TOP: m_upperBound = coord; // Check for out of range and make sure the window is at least 1 if (m_upperBound <= m_lowerBound) m_upperBound = m_lowerBound + 1; break; case ANCHOR_BOTTOM: m_lowerBound = coord; // Check for out of range and make sure the window is at least 1 if (m_lowerBound >= m_upperBound) m_lowerBound = m_upperBound - 1; break; case ANCHOR_MIDDLE: int window = m_upperBound - m_lowerBound; if (window == sizeControl - 1) return; int halfWindow = window / 2; if (coord - halfWindow < 0) coord = halfWindow; if (coord + halfWindow >= sizeControl) coord = sizeControl - 1 - halfWindow; m_lowerBound = coord - halfWindow; m_upperBound = m_lowerBound + window; break; } if (m_lowerBound < 0) m_lowerBound = 0; if (m_upperBound >= sizeControl) m_upperBound = sizeControl - 1; paintImmediately(0, 0, getWidth(), getHeight()); m_windowLevelStatus.paintImmediately(0, 0, m_windowLevelStatus.getWidth(), m_windowLevelStatus.getHeight()); setWindowLevel(m_lowerBound, m_upperBound); } private void setWindowLevel(int lowerBound, int upperBound) { m_upperBound = upperBound; m_lowerBound = lowerBound; int sizeControl = getWidth() - 1; int windowSize = m_upperBound - m_lowerBound; double window = (double) (windowSize) / (double) sizeControl; double level = (double) ((m_lowerBound + (double) windowSize / 2.0)) / (double) sizeControl; float maxWindow = m_model.getPixelRange(m_idx); if (window <= 0.0) window = 1.0 / maxWindow; m_model.setWindowLevel(m_idx, (float) window * maxWindow, (float) level * maxWindow + m_model.getMinValue(m_idx)); } /* * Update to match the display model's window and level. */ public void syncWindowLevel() { float maxWindow = m_model.getPixelRange(m_idx); float window = m_model.getWindow(m_idx) / maxWindow; float level = (m_model.getLevel(m_idx) - m_model.getMinValue(m_idx)) / maxWindow; float halfWindow = window / 2.0f; if (window <= 0.0f) window = 1.0f / maxWindow; float sizeControl = getWidth(); m_lowerBound = (int) ((level - halfWindow) * sizeControl); m_upperBound = m_lowerBound + (int) (window * sizeControl); if (m_lowerBound < 0) m_lowerBound = 0; if (m_upperBound >= sizeControl) m_upperBound = (int) sizeControl - 1; repaint(); } /* (non-Javadoc) * @see com.insightful.jimage.core.ImageDisplayModelListener#modelChanged(int, com.insightful.jimage.core.ImageDisplayModel) */ public void modelChanged(int type, int imageSet, ImageDisplayModel source) { if (source.setIncludesImage(m_idx, imageSet)) { if (type == ImageDisplayModel.COLORMAP_CHANGED) syncColormap(); else if (type == ImageDisplayModel.WINDOWLEVEL_CHANGED) syncWindowLevel(); repaint(); } } /* (non-Javadoc) * @see java.awt.event.ComponentListener#componentResized(java.awt.event.ComponentEvent) */ public void componentResized(ComponentEvent evt) { syncWindowLevel(); if (m_smallFont == null) { Font font = getFont(); m_smallFont = new Font(font.getName(), font.getStyle(), font.getSize() - 2); } } /* (non-Javadoc) * @see java.awt.event.ComponentListener#componentHidden(java.awt.event.ComponentEvent) */ public void componentHidden(ComponentEvent evt) { } /* (non-Javadoc) * @see java.awt.event.ComponentListener#componentMoved(java.awt.event.ComponentEvent) */ public void componentMoved(ComponentEvent evt) { } /* (non-Javadoc) * @see java.awt.event.ComponentListener#componentShown(java.awt.event.ComponentEvent) */ public void componentShown(ComponentEvent evt) { } /* * Helper class that shows the exact window and level values. */ private class WindowLevelStatus extends JComponent { private static final int GAP = 5; protected void paintComponent(Graphics g) { if (m_smallFont == null) return; Color background, foreground; background = javax.swing.UIManager.getColor("ToolTip.background"); if (background == null) background = Color.WHITE; foreground = javax.swing.UIManager.getColor("ToolTip.foreground"); if (foreground == null) foreground = Color.BLACK; g.setColor(background); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(foreground); String windowString = "Window: " + formatFloat(m_model.getWindow(m_idx)); String levelString = "Level: " + formatFloat(m_model.getLevel(m_idx)); FontMetrics metrics = getFontMetrics(m_smallFont); g.setFont(m_smallFont); // Split the display into 2 equal sized areas to display the window // and level values int width = getWidth() / 2; int height = getHeight(); g.setClip(null); g.setClip(0, 0, width, height); g.drawString(windowString, width / 2 - metrics.stringWidth(windowString) / 2, GAP / 2 + metrics.getAscent()); g.setClip(width, 0, width, height); g.drawString(levelString, width + width / 2 - metrics.stringWidth(levelString) / 2, GAP / 2 + metrics.getAscent()); g.setClip(null); g.drawRect(0, 0, getWidth() - 1, getHeight() - 1); } /* * Format a floating point value for display */ private String formatFloat(float value) { NumberFormat format = NumberFormat.getNumberInstance(); format.setGroupingUsed(false); value = Math.abs(value); if (value > 10) format.setMaximumFractionDigits(0); else if (value > 0.1) format.setMaximumFractionDigits(2); else if (value > 0.01) format.setMaximumFractionDigits(3); else format.setMaximumFractionDigits(4); return format.format(value); } /* (non-Javadoc) * @see java.awt.Component#getPreferredSize() */ public Dimension getPreferredSize() { if (m_smallFont == null) return super.getPreferredSize(); FontMetrics metrics = getFontMetrics(m_smallFont); return new Dimension(metrics.stringWidth("Window: 1234567890 Level: 1234567890"), metrics.getHeight() + GAP); } } }