package org.signalml.app.view.book; import static org.signalml.app.util.i18n.SvarogI18n._; import static org.signalml.app.util.i18n.SvarogI18n._R; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.font.FontRenderContext; import java.awt.geom.GeneralPath; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DecimalFormat; import javax.swing.JComponent; import javax.swing.JPopupMenu; import javax.swing.border.EmptyBorder; import org.apache.log4j.Logger; import org.signalml.app.SvarogApplication; import org.signalml.app.config.ApplicationConfiguration; import org.signalml.app.view.book.wignermap.WignerMapImageProvider; import org.signalml.app.view.book.wignermap.WignerMapPalette; import org.signalml.app.view.common.dialogs.PleaseWaitDialog; import org.signalml.app.view.common.dialogs.errors.Dialogs; import org.signalml.domain.book.SegmentReconstructionProvider; import org.signalml.domain.book.StandardBook; import org.signalml.domain.book.StandardBookAtom; import org.signalml.domain.book.StandardBookSegment; import org.signalml.domain.book.WignerMapProvider; import org.signalml.domain.book.WignerMapScaleType; import org.signalml.plugin.export.SignalMLException; /** BookPlot * * * @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o. */ public class BookPlot extends JComponent implements PropertyChangeListener { private static final long serialVersionUID = 1L; protected static final Logger logger = Logger.getLogger(BookPlot.class); public static final int RECONSTRUCTION_GAP = 5; public static final int RECONSTRUCTION_HEIGHT = 40; public static final int MIN_RECONSTRUCTION_HEIGHT = 20; public static final int MAX_RECONSTRUCTION_HEIGHT = 100; public static final int LEGEND_WIDTH = 75; public static final int SCALE_WIDTH = 15; private static final Dimension MINIMUM_SIZE = new Dimension(300, 3 *(RECONSTRUCTION_HEIGHT+RECONSTRUCTION_GAP) + 200); // 10 ranges + marginal public static final int Y_TICK_COUNT = 11; public static final int X_TICK_COUNT = 11; public static final int X_AXIS_HEIGHT = 6; public static final int Y_AXIS_WIDTH = 8; public static final int MAX_ASPECT_RATIO_SCALE = 4; private DecimalFormat axisFormat = new DecimalFormat("0.00"); private DecimalFormat toolTipFormat = new DecimalFormat("0.00"); private GeneralPath generalPath = new GeneralPath(GeneralPath.WIND_EVEN_ODD,50000); private BookPlotPopupProvider popupMenuProvider; private BookView view; private PleaseWaitDialog pleaseWaitDialog; private StandardBookSegment segment; private WignerMapProvider wignerMapProvider; private SegmentReconstructionProvider reconstructionProvider; private WignerMapImageProvider imageProvider; private WignerMapPalette palette; private boolean signalAntialiased; private boolean originalSignalVisible; private boolean fullReconstructionVisible; private boolean reconstructionVisible; private boolean legendVisible; private boolean scaleVisible; private boolean axesVisible; private boolean atomToolTipsVisible; private int mapAspectRatioUp = 2; private int mapAspectRatioDown = 1; private double mapAspectRatio; // width/height private int reconstructionHeight = RECONSTRUCTION_HEIGHT; private double reconstructionPixelPerSample; private double reconstructionPixelPerValue; private double mapPixelPerSecond; private double mapPixelPerHz; private double mapPixelPerPoint; private double mapPixelPerNaturalFreq; private Rectangle mapRectangle; private Rectangle originalSignalRectangle; private Rectangle reconstructionRectangle; private Rectangle fullReconstructionRectangle; private Rectangle legendRectangle; private Rectangle scaleRectangle; private Rectangle xAxisRectangle; private Rectangle yAxisRectangle; private String[] yLabels; private Rectangle[] yLabelRectangles; private int[] yTickOffsets; private String[] xLabels; private Rectangle[] xLabelRectangles; private int[] xTickOffsets; private float samplingFrequency; private double maxPosition; private double minPosition; private double minFrequency; private double maxFrequency; private int reconstructionSampleCount; private BufferedImage cachedImage = null; private boolean calculated = false; private String cachedToolTipText; private StandardBookAtom cachedToolTipAtom; private StandardBookAtom outlinedAtom; private int segmentLength; private int naturalMinFrequency; private int naturalMaxFrequency; private int pointMinPosition; private int pointMaxPosition; /** * True, if this book plot already shown a message explaining * that an out of memory error had happened. In that case * this book shouldn't be redrawn. */ private boolean outOfMemoryErrorShown; /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* Initialization & setup */ public BookPlot(BookView view) throws SignalMLException { super(); this.view = view; setBorder(new EmptyBorder(5,5,5,5)); setFont(new Font(Font.DIALOG, Font.PLAIN, 10)); view.addPropertyChangeListener(this); StandardBook book = view.getDocument().getBook(); int segmentCount = book.getSegmentCount(); samplingFrequency = book.getSamplingFrequency(); wignerMapProvider = new WignerMapProvider(samplingFrequency); StandardBookSegment firstSegment = (segmentCount > 0 ? book.getSegmentAt(0,0) : null); wignerMapProvider.setRange(0, samplingFrequency/2, 0, firstSegment != null ? firstSegment.getSegmentTimeLength() : 20.0); reconstructionProvider = new SegmentReconstructionProvider(); setBackground(Color.WHITE); setFocusable(true); imageProvider = new WignerMapImageProvider(); addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { maybeShowPopupMenu(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopupMenu(e); } private void maybeShowPopupMenu(MouseEvent e) { if (e.isPopupTrigger()) { JPopupMenu popupMenu = getPlotPopupMenu(); if (popupMenu != null) { popupMenu.show(e.getComponent(),e.getX(),e.getY()); } } } }); addMouseMotionListener(new MouseAdapter() { @Override public void mouseMoved(MouseEvent e) { Point point = e.getPoint(); Rectangle outline; if (mapRectangle != null && mapRectangle.contains(point)) { StandardBookAtom atom = getNearestAtom(point, 20); if (outlinedAtom != atom) { if (outlinedAtom != null) { outline = getOutlineRectangle(outlinedAtom); if (outline != null) { outline.grow(3,3); repaint(outline); } } outlinedAtom = atom; if (atom != null) { outline = getOutlineRectangle(atom); if (outline != null) { outline.grow(3,3); repaint(outline); } } } } } }); } /** * Loads all plot settings from the {@link ApplicationConfiguration}. */ public void loadSettingsFromApplicationConfiguration() { ApplicationConfiguration config = SvarogApplication.getApplicationConfiguration(); signalAntialiased = config.isSignalInBookAntialiased(); reconstructionVisible = config.isReconstructionVisible(); fullReconstructionVisible = config.isFullReconstructionVisible(); originalSignalVisible = config.isOriginalSignalVisible(); legendVisible = config.isLegendVisible(); scaleVisible = config.isScaleVisible(); axesVisible = config.isAxesVisible(); atomToolTipsVisible = config.isAtomToolTipsVisible(); mapAspectRatioUp = config.getMapAspectRatioUp(); mapAspectRatioDown = config.getMapAspectRatioDown(); mapAspectRatio = ((double) mapAspectRatioUp) / ((double) mapAspectRatioDown); setReconstructionHeight(config.getReconstructionHeight()); setPalette(config.getPalette()); setScaleType(config.getScaleType()); } @Override public void setBounds(int x, int y, int width, int height) { super.setBounds(x, y, width, height); calculated = false; } @Override public void setSize(Dimension d) { super.setSize(d); calculated = false; } @Override public void setSize(int width, int height) { super.setSize(width, height); calculated = false; } public void initialize() throws SignalMLException { reset(); } private void calculateParameters() { if (calculated) { return; } if (segment == null) { return; } Graphics2D g = (Graphics2D) getGraphics(); FontRenderContext fontRenderContext = g.getFontRenderContext(); Font font = getFont(); FontMetrics fontMetrics = g.getFontMetrics(font); int ascent = fontMetrics.getAscent(); Dimension size = getSize(); if (size == null || size.height == 0 || size.width == 0) { return; } float segmentTimeLength = segment.getSegmentTimeLength(); maxPosition = wignerMapProvider.getMaxPosition(); minPosition = wignerMapProvider.getMinPosition(); minFrequency = wignerMapProvider.getMinFrequency(); maxFrequency = wignerMapProvider.getMaxFrequency(); if (maxPosition > segmentTimeLength) { maxPosition = segmentTimeLength; wignerMapProvider.setMaxPosition(segmentTimeLength); } Insets insets = getInsets(); size = new Dimension(size.width-(insets.left+insets.right), size.height-(insets.top+insets.bottom)); int availableWidth = size.width; int availableHeight = size.height; int axesWidth = 0; int axesHeight = 0; if (axesVisible) { // setup y axis; axesWidth = Y_AXIS_WIDTH; // for the axis itself; int maxLabelWidth = 0; double span = maxFrequency - minFrequency; double tick = span / (Y_TICK_COUNT-1); int i; Rectangle2D stringBounds; yLabels = new String[Y_TICK_COUNT]; yLabelRectangles = new Rectangle[Y_TICK_COUNT]; for (i=0; i<Y_TICK_COUNT; i++) { yLabels[i] = axisFormat.format(minFrequency+tick*i); stringBounds = font.getStringBounds(yLabels[i], fontRenderContext); yLabelRectangles[i] = new Rectangle(); yLabelRectangles[i].width = (int) stringBounds.getWidth(); yLabelRectangles[i].height = (int) stringBounds.getHeight(); if (maxLabelWidth < yLabelRectangles[i].width) { maxLabelWidth = yLabelRectangles[i].width; } } axesWidth += maxLabelWidth; // setup x axis axesHeight = X_AXIS_HEIGHT; int maxLabelHeight = 0; span = maxPosition - minPosition; tick = span / (X_TICK_COUNT-1); xLabels = new String[X_TICK_COUNT]; xLabelRectangles = new Rectangle[X_TICK_COUNT]; for (i=0; i<X_TICK_COUNT; i++) { xLabels[i] = axisFormat.format(minPosition+tick*i); stringBounds = font.getStringBounds(xLabels[i], fontRenderContext); xLabelRectangles[i] = new Rectangle(); xLabelRectangles[i].width = (int) stringBounds.getWidth(); xLabelRectangles[i].height = (int) stringBounds.getHeight(); if (maxLabelHeight < xLabelRectangles[i].height) { maxLabelHeight = xLabelRectangles[i].height; } } axesHeight += maxLabelHeight; } availableHeight -= axesHeight; int legendWidth = 0; if (legendVisible) { legendWidth = LEGEND_WIDTH; } int reservedLeftWidth = Math.max(axesWidth, legendWidth); if (axesVisible) { if (reservedLeftWidth < xLabelRectangles[0].width/2) { reservedLeftWidth = xLabelRectangles[0].width/2; } } availableWidth -= reservedLeftWidth; int scaleWidth = 0; if (scaleVisible) { scaleWidth = SCALE_WIDTH; } int reservedRightWidth = scaleWidth; if (axesVisible) { if (reservedRightWidth < xLabelRectangles[X_TICK_COUNT-1].width/2) { reservedRightWidth = xLabelRectangles[X_TICK_COUNT-1].width/2; } } availableWidth -= reservedRightWidth; int reconstructionTop = -1; int usedReconstructionHeight = 0; if (reconstructionVisible) { reconstructionRectangle = new Rectangle(); reconstructionRectangle.x = insets.left + reservedLeftWidth; reconstructionRectangle.width = availableWidth; reconstructionRectangle.y = insets.top + availableHeight-reconstructionHeight; reconstructionRectangle.height = reconstructionHeight; availableHeight -= (reconstructionHeight + RECONSTRUCTION_GAP); reconstructionTop = reconstructionRectangle.y; usedReconstructionHeight += (reconstructionHeight + RECONSTRUCTION_GAP); } else { reconstructionRectangle = null; } if (fullReconstructionVisible) { fullReconstructionRectangle = new Rectangle(); fullReconstructionRectangle.x = insets.left + reservedLeftWidth; fullReconstructionRectangle.width = availableWidth; fullReconstructionRectangle.y = insets.top + availableHeight-reconstructionHeight; fullReconstructionRectangle.height = reconstructionHeight; availableHeight -= (reconstructionHeight + RECONSTRUCTION_GAP); reconstructionTop = fullReconstructionRectangle.y; usedReconstructionHeight += (reconstructionHeight + RECONSTRUCTION_GAP); } else { fullReconstructionRectangle = null; } if (originalSignalVisible) { originalSignalRectangle = new Rectangle(); originalSignalRectangle.x = insets.left + reservedLeftWidth; originalSignalRectangle.width = availableWidth; originalSignalRectangle.y = insets.top + availableHeight-reconstructionHeight; originalSignalRectangle.height = reconstructionHeight; availableHeight -= (reconstructionHeight + RECONSTRUCTION_GAP); reconstructionTop = originalSignalRectangle.y; usedReconstructionHeight += (reconstructionHeight + RECONSTRUCTION_GAP); } else { originalSignalRectangle = null; } // the map must vertically fit into availableHeight int width = (int) Math.round((availableHeight) * mapAspectRatio); int height = availableHeight; int paddingY = 0; int paddingX = 0; if (width > availableWidth) { width = availableWidth; height = (int) Math.round((width) / mapAspectRatio); paddingY = (availableHeight-height) / 2; } else if (width < size.width) { paddingX = (availableWidth-width) / 2; } int paddingXLeft; if (reservedLeftWidth > reservedRightWidth) { paddingXLeft = paddingX - Math.max(0, paddingX - reservedLeftWidth); } else if (reservedLeftWidth < reservedRightWidth) { paddingXLeft = paddingX + Math.max(0, paddingX - reservedRightWidth); } else { paddingXLeft = paddingX; } mapRectangle = new Rectangle(insets.left+reservedLeftWidth+paddingXLeft, insets.top+paddingY, width, height); if (legendVisible && reconstructionTop >= 0) { legendRectangle = new Rectangle(insets.left + paddingXLeft, reconstructionTop, reservedLeftWidth, usedReconstructionHeight); } else { legendRectangle = null; } if (scaleVisible) { scaleRectangle = new Rectangle(mapRectangle.x + mapRectangle.width, mapRectangle.y, scaleWidth, mapRectangle.height); } else { scaleRectangle = null; } if (axesVisible) { xAxisRectangle = new Rectangle(mapRectangle.x, mapRectangle.y+mapRectangle.height, mapRectangle.width, axesHeight); yAxisRectangle = new Rectangle(mapRectangle.x - axesWidth, mapRectangle.y, axesWidth, mapRectangle.height); } else { xAxisRectangle = null; yAxisRectangle = null; } if (reconstructionRectangle != null) { reconstructionRectangle.x += paddingXLeft; reconstructionRectangle.width -= (2 * paddingX); reconstructionRectangle.y += (axesHeight-paddingY); } if (fullReconstructionRectangle != null) { fullReconstructionRectangle.x += paddingXLeft; fullReconstructionRectangle.width -= (2 * paddingX); fullReconstructionRectangle.y += (axesHeight-paddingY); } if (originalSignalRectangle != null) { originalSignalRectangle.x += paddingXLeft; originalSignalRectangle.width -= (2 * paddingX); originalSignalRectangle.y += (axesHeight-paddingY); } // position labels if (axesVisible) { double tickSize = ((double)(width-1)) / (X_TICK_COUNT-1); int i; xTickOffsets = new int[X_TICK_COUNT]; for (i=0; i<X_TICK_COUNT; i++) { xTickOffsets[i] = (int) Math.round((tickSize*i)); xLabelRectangles[i].x = mapRectangle.x + xTickOffsets[i] - xLabelRectangles[i].width / 2; xLabelRectangles[i].y = mapRectangle.y + mapRectangle.height + X_AXIS_HEIGHT + ascent; } tickSize = ((double)(height-1)) / (Y_TICK_COUNT-1); yTickOffsets = new int[Y_TICK_COUNT]; for (i=0; i<Y_TICK_COUNT; i++) { yTickOffsets[i] = (int) Math.round((tickSize*i)); yLabelRectangles[i].x = mapRectangle.x - (Y_AXIS_WIDTH + yLabelRectangles[i].width); yLabelRectangles[i].y = (mapRectangle.y + mapRectangle.height - 1) + ascent/2 - yTickOffsets[i]; } } // determine scales double maxDeflection = 0; reconstructionSampleCount = (int)((maxPosition-minPosition) * samplingFrequency); reconstructionPixelPerSample = ((double)(width-1)) / ((double)(reconstructionSampleCount-1)); int i; double value; if (segment.hasSignal()) { float[] signalSamples = segment.getSignalSamples(); for (i=0; i<signalSamples.length; i++) { value = Math.abs(signalSamples[i]); if (maxDeflection < value) { maxDeflection = value; } } } reconstructionProvider.setSegmentWithNaturalWidth(segment, samplingFrequency); double[] fullReconstruction = reconstructionProvider.getFullReconstruction(); for (i=0; i<fullReconstruction.length; i++) { value = Math.abs(fullReconstruction[i]); if (maxDeflection < value) { maxDeflection = value; } } reconstructionPixelPerValue = (reconstructionHeight/2-1) / maxDeflection; wignerMapProvider.setSize(width, height); if (wignerMapProvider.isDirty()) { cachedImage = null; } mapPixelPerSecond = (width-1) / (maxPosition-minPosition); mapPixelPerHz = (height-1) / (maxFrequency-minFrequency); segmentLength = segment.getSegmentLength(); naturalMinFrequency = (int) Math.round((minFrequency / samplingFrequency) * segmentLength); naturalMaxFrequency = (int) Math.round((maxFrequency / samplingFrequency) * segmentLength); pointMinPosition = (int) Math.round(minPosition * samplingFrequency); pointMaxPosition = (int) Math.round(maxPosition * samplingFrequency); mapPixelPerPoint = ((double)(width-1)) / (pointMaxPosition-pointMinPosition); mapPixelPerNaturalFreq = ((double)(height-1)) / (naturalMaxFrequency-naturalMinFrequency); calculated = true; } public StandardBookSegment getSegment() { return segment; } public void setSegment(StandardBookSegment segment) { if (this.segment != segment) { this.segment = segment; wignerMapProvider.setSegment(segment); outlinedAtom = null; reset(); } } public boolean isOriginalSignalVisible() { return originalSignalVisible; } public void setOriginalSignalVisible(boolean originalSignalVisible) { if (this.originalSignalVisible != originalSignalVisible) { this.originalSignalVisible = originalSignalVisible; reset(); } } public boolean isReconstructionVisible() { return reconstructionVisible; } public void setReconstructionVisible(boolean reconstructionVisible) { if (this.reconstructionVisible != reconstructionVisible) { this.reconstructionVisible = reconstructionVisible; reset(); } } public boolean isFullReconstructionVisible() { return fullReconstructionVisible; } public void setFullReconstructionVisible(boolean fullReconstructionVisible) { if (this.fullReconstructionVisible != fullReconstructionVisible) { this.fullReconstructionVisible = fullReconstructionVisible; reset(); } } public boolean isLegendVisible() { return legendVisible; } public void setLegendVisible(boolean legendVisible) { if (this.legendVisible != legendVisible) { this.legendVisible = legendVisible; reset(); } } public boolean isScaleVisible() { return scaleVisible; } public void setScaleVisible(boolean scaleVisible) { if (this.scaleVisible != scaleVisible) { this.scaleVisible = scaleVisible; reset(); } } public boolean isAxesVisible() { return axesVisible; } public void setAxesVisible(boolean axesVisible) { if (this.axesVisible != axesVisible) { this.axesVisible = axesVisible; reset(); } } public boolean isSignalAntialiased() { return signalAntialiased; } public void setSignalAntialiased(boolean signalAntialiased) { if (this.signalAntialiased != signalAntialiased) { this.signalAntialiased = signalAntialiased; if (originalSignalRectangle != null) { repaint(originalSignalRectangle); } if (fullReconstructionRectangle != null) { repaint(fullReconstructionRectangle); } if (reconstructionRectangle != null) { repaint(reconstructionRectangle); } } } public boolean isAtomToolTipsVisible() { return atomToolTipsVisible; } public void setAtomToolTipsVisible(boolean atomToolTipsVisible) { if (this.atomToolTipsVisible != atomToolTipsVisible) { this.atomToolTipsVisible = atomToolTipsVisible; if (atomToolTipsVisible) { setToolTipText(""); } else { setToolTipText(null); } } } public int getMapAspectRatioUp() { return mapAspectRatioUp; } public void setMapAspectRatioUp(int mapAspectRatioUp) { if (mapAspectRatioUp < 1) { mapAspectRatioUp = 1; } else if (mapAspectRatioUp > MAX_ASPECT_RATIO_SCALE) { mapAspectRatioUp = MAX_ASPECT_RATIO_SCALE; } this.mapAspectRatioUp = mapAspectRatioUp; setMapAspectRatio(((double) mapAspectRatioUp) / ((double) mapAspectRatioDown)); } public int getMapAspectRatioDown() { return mapAspectRatioDown; } public void setMapAspectRatioDown(int mapAspectRatioDown) { if (mapAspectRatioDown < 1) { mapAspectRatioDown = 1; } else if (mapAspectRatioDown > MAX_ASPECT_RATIO_SCALE) { mapAspectRatioDown = MAX_ASPECT_RATIO_SCALE; } this.mapAspectRatioDown = mapAspectRatioDown; setMapAspectRatio(((double) mapAspectRatioUp) / ((double) mapAspectRatioDown)); } public double getMapAspectRatio() { return mapAspectRatio; } protected void setMapAspectRatio(double mapAspectRatio) { if (this.mapAspectRatio != mapAspectRatio) { this.mapAspectRatio = mapAspectRatio; reset(); } } public WignerMapPalette getPalette() { return palette; } public void setPalette(WignerMapPalette palette) { if (this.palette != palette) { this.palette = palette; cachedImage = null; if (mapRectangle != null) { repaint(mapRectangle); } if (scaleRectangle != null) { repaint(scaleRectangle); } view.getPaletteComboBox().setSelectedItem(palette); } } public WignerMapScaleType getScaleType() { return wignerMapProvider.getScaleType(); } public void setScaleType(WignerMapScaleType type) { if (wignerMapProvider.getScaleType() != type) { wignerMapProvider.setScaleType(type); cachedImage = null; if (mapRectangle != null) { repaint(mapRectangle); } view.getScaleComboBox().setSelectedItem(type); } } public int getReconstructionHeight() { return reconstructionHeight; } public void setReconstructionHeight(int reconstructionHeight) { if (this.reconstructionHeight != reconstructionHeight) { this.reconstructionHeight = reconstructionHeight; view.getReconstructionHeightSlider().setValue(reconstructionHeight); reset(); } } public void setZoom(double minPosition, double maxPosition, double minFrequency, double maxFrequency) { this.reconstructionPixelPerSample *= (maxPosition - minPosition) / (this.maxPosition - this.minPosition); this.minFrequency = minFrequency; this.maxFrequency = maxFrequency; this.minPosition = minPosition; this.maxPosition = maxPosition; wignerMapProvider.setRange(minFrequency, maxFrequency, minPosition, maxPosition); if (wignerMapProvider.isDirty()) { calculated = false; repaint(); } } public void destroy() { view.removePropertyChangeListener(this); setVisible(false); segment = null; view = null; } /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* JComponent implementation */ @Override protected void paintComponent(Graphics gOrig) { Graphics2D g = (Graphics2D)gOrig; Rectangle clip = g.getClipBounds(); g.setColor(getBackground()); g.fillRect(clip.x,clip.y,clip.width,clip.height); if (!calculated) { calculateParameters(); } if (segment == null) { return; } if (mapRectangle != null) { Rectangle mapToRepaint = clip.intersection(mapRectangle); if (!mapToRepaint.isEmpty()) { paintWignerMapAndCatchOutOfMemory(g, mapToRepaint); } } if (legendRectangle != null) { if (legendRectangle.intersects(clip)) { paintLegend(g); } } if (scaleRectangle != null) { if (scaleRectangle.intersects(clip)) { paintScale(g); } } if (xAxisRectangle != null) { if (xAxisRectangle.intersects(clip)) { paintXAxis(g); } } if (yAxisRectangle != null) { if (yAxisRectangle.intersects(clip)) { paintYAxis(g); } } if (reconstructionRectangle != null) { double[] selectiveReconstruction = reconstructionProvider.getSelectiveReconstruction(); paintReconstruction(g, selectiveReconstruction, reconstructionRectangle); } if (fullReconstructionRectangle != null) { double[] reconstruction = reconstructionProvider.getFullReconstruction(); paintReconstruction(g, reconstruction, fullReconstructionRectangle); } if (originalSignalRectangle != null) { float[] signal = segment.getSignalSamples(); // XXX not optimal double[] signalD = null; if (signal != null) { signalD = new double[signal.length]; for (int i=0; i<signal.length; i++) { signalD[i] = signal[i]; } } paintReconstruction(g, signalD, originalSignalRectangle); } } private void paintYAxis(Graphics2D g) { g.setColor(Color.BLACK); int axisLevel = mapRectangle.x-3; g.drawLine(axisLevel, mapRectangle.y, axisLevel, mapRectangle.y + mapRectangle.height - 1); for (int i=0; i<Y_TICK_COUNT; i++) { g.drawLine(axisLevel-2, mapRectangle.y + yTickOffsets[i], axisLevel+2, mapRectangle.y + yTickOffsets[i]); g.drawString(yLabels[i], yLabelRectangles[i].x, yLabelRectangles[i].y); } } private void paintXAxis(Graphics2D g) { g.setColor(Color.BLACK); int axisLevel = mapRectangle.y + mapRectangle.height + 2; g.drawLine(mapRectangle.x, axisLevel, mapRectangle.x + mapRectangle.width - 1, axisLevel); for (int i=0; i<X_TICK_COUNT; i++) { g.drawLine(mapRectangle.x + xTickOffsets[i], axisLevel-2, mapRectangle.x + xTickOffsets[i], axisLevel+2); g.drawString(xLabels[i], xLabelRectangles[i].x, xLabelRectangles[i].y); } } private void paintScale(Graphics2D g) { int[] pal = palette.getPalette(); int height = Math.min(scaleRectangle.height, pal.length); double factor = 1; if (height < pal.length) { factor = ((double)(pal.length-1)) / (height-1); } int index; Color color; for (int i=0; i<height; i++) { if (factor != 1) { index = (int)(i * factor); } else { index = i; } color = new Color(pal[index], false); g.setColor(color); g.drawLine(scaleRectangle.x+5, scaleRectangle.y + (scaleRectangle.height-i), scaleRectangle.x + scaleRectangle.width-1, scaleRectangle.y + (scaleRectangle.height-i)); } } private void paintReconstruction(Graphics2D gOrig, double[] samples, Rectangle area) { Graphics2D g = (Graphics2D) gOrig.create(); g.clip(area); if (samples == null) { String label = _("(no signal to paint)"); Rectangle2D stringBounds; int width; int height; int level; Font font = g.getFont(); FontRenderContext fontRenderContext = g.getFontRenderContext(); FontMetrics fontMetrics = g.getFontMetrics(font); g.setColor(Color.BLACK); level = area.y + (area.height) / 2; stringBounds = font.getStringBounds(label, fontRenderContext); width = (int) stringBounds.getWidth(); height = (int) stringBounds.getHeight(); g.drawString(label, area.x + (area.width - width)/2, level + fontMetrics.getAscent() - height/2); return; } if (signalAntialiased) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); } if (signalAntialiased) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON); } int level = area.y + area.height / 2; g.setColor(Color.BLUE); g.drawLine(area.x, level, area.x+area.width-1, level); g.setColor(Color.BLACK); int firstSample = (int)((minPosition * samplingFrequency / segment.getSegmentLength()) * samples.length); int lastSample = (int) Math.min(samples.length - 1, (maxPosition * samplingFrequency / segment.getSegmentLength()) * samples.length); if (lastSample < firstSample) { return; } int length = 1 + lastSample - firstSample; double realX = area.x; double y = level - (samples[firstSample] * reconstructionPixelPerValue); generalPath.reset(); double x; double lastX = 0; double lastY = 0; if (!signalAntialiased) { x = StrictMath.floor(realX + 0.5); y = StrictMath.floor(y + 0.5); generalPath.moveTo(x, y); lastX = x; lastY = y; } else { generalPath.moveTo(realX, y); } for (int i=0; i<length; i++) { y = level - (samples[firstSample+i] * reconstructionPixelPerValue); realX += reconstructionPixelPerSample; if (signalAntialiased) { generalPath.lineTo(realX, y); } else { // if not antialiased then round to integer in order to prevent aliasing affects // (which cause slave plots to display the signal slightly differently) // expand Math.round for performance, StrictMath.floor is native x = StrictMath.floor(realX + 0.5); y = StrictMath.floor(y + 0.5); if (x != lastX || y != lastY) { generalPath.lineTo(x, y); } lastX = x; lastY = y; } } g.draw(generalPath); } private void paintLegend(Graphics2D g) { String label; Rectangle2D stringBounds; int width; int height; int level; Font font = g.getFont(); FontRenderContext fontRenderContext = g.getFontRenderContext(); FontMetrics fontMetrics = g.getFontMetrics(font); g.setColor(Color.BLACK); if (originalSignalRectangle != null) { level = originalSignalRectangle.y + (originalSignalRectangle.height) / 2; label = _("Original"); stringBounds = font.getStringBounds(label, fontRenderContext); width = (int) stringBounds.getWidth(); height = (int) stringBounds.getHeight(); g.drawString(label, originalSignalRectangle.x - (width + 5), level + fontMetrics.getAscent() - height/2); } if (fullReconstructionRectangle != null) { level = fullReconstructionRectangle.y + (fullReconstructionRectangle.height) / 2; label = _("Reconstruction"); stringBounds = font.getStringBounds(label, fontRenderContext); width = (int) stringBounds.getWidth(); height = (int) stringBounds.getHeight(); g.drawString(label, fullReconstructionRectangle.x - (width + 5), level + fontMetrics.getAscent() - height/2); } if (reconstructionRectangle != null) { level = reconstructionRectangle.y + (reconstructionRectangle.height) / 2; label = _("Chosen"); stringBounds = font.getStringBounds(label, fontRenderContext); width = (int) stringBounds.getWidth(); height = (int) stringBounds.getHeight(); g.drawString(label, reconstructionRectangle.x - (width + 5), level + fontMetrics.getAscent() - height/2); } } private void paintWignerMapAndCatchOutOfMemory(Graphics2D gOrig, Rectangle mapToRepaint) { try { paintWignerMap(gOrig, mapToRepaint); outOfMemoryErrorShown = false; } catch (OutOfMemoryError error) { if (!outOfMemoryErrorShown) { logger.error("", error); Dialogs.showError(_("This book cannot be rendered because of lack of memory. Please close other books to free some memory.")); outOfMemoryErrorShown = true; // this error should be shown only once } } } private void paintWignerMap(Graphics2D gOrig, Rectangle mapToRepaint) { // limit clipping to the map to prevent elements from protruding outside the map Graphics2D g = (Graphics2D) gOrig.create(); g.clip(new Rectangle(mapRectangle.x, mapRectangle.y, mapRectangle.width, mapRectangle.height)); if (cachedImage == null) { double[][] map = null; map = wignerMapProvider.getMap(); cachedImage = imageProvider.getImage(map, mapRectangle.width, mapRectangle.height, palette); } int imgX = mapToRepaint.x - mapRectangle.x; int imgY = mapToRepaint.y - mapRectangle.y; g.drawImage( cachedImage, mapToRepaint.x, mapToRepaint.y, mapToRepaint.x+mapToRepaint.width, mapToRepaint.y+mapToRepaint.height, imgX, imgY, imgX+mapToRepaint.width, imgY+mapToRepaint.height, null ); Rectangle markCaptureRectangle = new Rectangle(mapToRepaint); markCaptureRectangle.grow(3,3); g.setColor(Color.WHITE); int atomCount = segment.getAtomCount(); Point atomPoint; StandardBookAtom atom; for (int i=0; i<atomCount; i++) { atom = segment.getAtomAt(i); atomPoint = getAtomLocation(atom); if (atomPoint != null && markCaptureRectangle.contains(atomPoint)) { g.drawLine(atomPoint.x-2, atomPoint.y, atomPoint.x+2, atomPoint.y); g.drawLine(atomPoint.x, atomPoint.y-2, atomPoint.x, atomPoint.y+2); if (reconstructionProvider.isAtomInSelectiveReconstruction(atom)) { g.drawOval(atomPoint.x-3, atomPoint.y-3, 6, 6); } } } if (outlinedAtom != null) { Rectangle outline = getOutlineRectangle(outlinedAtom); if (outline != null) { Stroke stroke = g.getStroke(); try { g.setStroke(new BasicStroke(1F, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10F, new float[] { 1, 3 }, 0F)); g.draw(outline); } finally { g.setStroke(stroke); } } } } @Override public Dimension getMinimumSize() { return MINIMUM_SIZE; } @Override public Dimension getMaximumSize() { return getPreferredSize(); } public JPopupMenu getPlotPopupMenu() { if (view.isToolEngaged()) { return null; } if (popupMenuProvider == null) { return null; } return popupMenuProvider.getPlotPopupMenu(); } @Override public boolean isDoubleBuffered() { return true; } @Override public String getToolTipText(MouseEvent event) { Point point = event.getPoint(); StandardBookAtom nearestAtom = getNearestAtom(point, 20); if (nearestAtom != null) { if (cachedToolTipText == null || cachedToolTipAtom != nearestAtom) { StringBuilder sb = new StringBuilder("<html><body>"); sb.append("<b>") .append(_R("Atom {0}", segment.indexOfAtom(nearestAtom)+1)) .append("</b>"); sb.append("<p><table cellpadding=\"0\">"); sb.append("<tr><td>") .append(_("Position")) .append("</td><td> </td><td>") .append(toolTipFormat.format(nearestAtom.getTimePosition())) .append("</td></tr>"); sb.append("<tr><td>") .append(_("Frequency")) .append("</td><td>    </td><td>") .append(toolTipFormat.format(nearestAtom.getHzFrequency())) .append("</td></tr>"); sb.append("<tr><td>") .append(_("Modulus")) .append("</td><td>    </td><td>") .append(toolTipFormat.format(nearestAtom.getModulus())) .append("</td></tr>"); sb.append("<tr><td>") .append(_("Amplitude")) .append("</td><td>    </td><td>") .append(toolTipFormat.format(nearestAtom.getAmplitude())) .append("</td></tr>"); sb.append("<tr><td>") .append(_("Scale")) .append("</td><td>    </td><td>") .append(toolTipFormat.format(nearestAtom.getScale())) .append("</td></tr>"); sb.append("<tr><td>") .append(_("Phase")) .append("</td><td>    </td><td>") .append(toolTipFormat.format(nearestAtom.getPhase())) .append("</td></tr>"); sb.append("</table>"); sb.append("</body></html>"); cachedToolTipAtom = nearestAtom; cachedToolTipText = sb.toString(); } return cachedToolTipText; } else { return _("No nearby atom"); } } /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* Listener implementations */ public void reset() { calculated = false; repaint(); } @Override public void propertyChange(PropertyChangeEvent evt) { } /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* Conversions */ public StandardBookAtom getNearestAtom(Point point, int maxRadius) { if (mapRectangle == null || !mapRectangle.contains(point)) { return null; } if (segment == null) { return null; } int atomCount = segment.getAtomCount(); double minDistance = Double.MAX_VALUE; double distance; Point atomPoint; StandardBookAtom atom; StandardBookAtom nearestAtom = null; for (int i=0; i<atomCount; i++) { atom = segment.getAtomAt(i); atomPoint = getAtomLocation(atom); if (atomPoint != null) { distance = point.distance(atomPoint); if (maxRadius == 0 || maxRadius >= distance) { if (distance < minDistance) { minDistance = distance; nearestAtom = atom; } } } } return nearestAtom; } public Point getAtomLocation(StandardBookAtom atom) { if (mapRectangle == null || segment == null) { return null; } int frequency = atom.getNaturalFrequency(); if (frequency < naturalMinFrequency || frequency > naturalMaxFrequency) { return null; } int position = atom.getPosition(); if (position < pointMinPosition || position > pointMaxPosition) { return null; } int x = mapRectangle.x + ((int) Math.round((position-pointMinPosition) * mapPixelPerPoint)); int y = mapRectangle.y + ((mapRectangle.height-1) - (int) Math.round((frequency-naturalMinFrequency) * mapPixelPerNaturalFreq)); return new Point(x, y); } public Rectangle getOutlineRectangle(StandardBookAtom atom) { // TODO maybe do this in a more intelligent way // can we compute a "width" in frequency Point atomPoint = getAtomLocation(atom); if (atomPoint != null) { return new Rectangle(atomPoint.x-20, atomPoint.y-20, 40, 40); } else { return null; } } // the following 4 functions use coordinates relative to map origin public double toPosition(int x) { return minPosition + (x / mapPixelPerSecond); } public double toFrequency(int y) { return minFrequency + (((mapRectangle.height-1)-y) / mapPixelPerHz); } // in time domain, seconds public int toX(double position) { return (int) Math.round((position-minPosition) * mapPixelPerSecond); } // in frequency domain, seconds public int toY(double frequency) { return (mapRectangle.height-1) - (int) Math.round((frequency-minFrequency) * mapPixelPerHz); } /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* ***************** ***************** ***************** */ /* Other getters and setters */ public BookView getView() { return view; } public SegmentReconstructionProvider getReconstructionProvider() { return reconstructionProvider; } public BookPlotPopupProvider getPopupMenuProvider() { return popupMenuProvider; } public void setPopupMenuProvider(BookPlotPopupProvider popupMenuProvider) { this.popupMenuProvider = popupMenuProvider; } public Rectangle getMapRectangle() { return mapRectangle; } public Rectangle getReconstructionRectangle() { return reconstructionRectangle; } public WignerMapProvider getWignerMapProvider() { return wignerMapProvider; } public PleaseWaitDialog getPleaseWaitDialog() { return pleaseWaitDialog; } public void setPleaseWaitDialog(PleaseWaitDialog pleaseWaitDialog) { this.pleaseWaitDialog = pleaseWaitDialog; } public double getMinPosition() { return minPosition; } public double getMaxPosition() { return maxPosition; } public double getMinFrequency() { return minFrequency; } public double getMaxFrequency() { return maxFrequency; } }