/* File: DasPlot.java * Copyright (C) 2002-2003 The University of Iowa * Created by: Jeremy Faden * Jessica Swanner * Edward E. West * * This file is part of the das2 library. * * das2 is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.das2.graph; import java.util.logging.Level; import org.das2.event.MouseModule; import org.das2.event.HorizontalRangeSelectorMouseModule; import org.das2.event.LengthDragRenderer; import org.das2.event.DisplayDataMouseModule; import org.das2.event.CrossHairMouseModule; import org.das2.event.BoxZoomMouseModule; import org.das2.event.VerticalRangeSelectorMouseModule; import org.das2.event.ZoomPanMouseModule; import org.das2.dataset.VectorUtil; import org.das2.dataset.TableDataSet; import org.das2.dataset.DataSet; import org.das2.dataset.TableUtil; import org.das2.dataset.VectorDataSet; import org.das2.DasApplication; import org.das2.CancelledOperationException; import org.das2.DasProperties; import org.das2.util.GrannyTextRenderer; import org.das2.util.DnDSupport; import java.beans.PropertyChangeEvent; import org.das2.util.monitor.NullProgressMonitor; import org.das2.components.propertyeditor.PropertyEditor; import org.das2.datum.Datum; import org.das2.datum.DatumRange; import org.das2.datum.DatumVector; import org.das2.graph.dnd.TransferableRenderer; import java.awt.image.BufferedImage; import javax.swing.event.MouseInputAdapter; import javax.swing.*; import java.awt.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.*; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.*; import java.io.IOException; import java.nio.channels.*; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import javax.swing.filechooser.FileNameExtensionFilter; import org.das2.DasException; import org.das2.dataset.DataSetAdapter; import org.das2.datum.DatumRangeUtil; import org.das2.datum.LoggerManager; import org.das2.event.DasMouseInputAdapter; import org.das2.event.LengthMouseModule; import org.das2.graph.DasAxis.Memento; import org.das2.qds.DataSetUtil; import org.das2.util.ColorUtil; /** * DasPlot is the 2D plot containing a horizontal X axis and vertical Y * axis, and a stack of Renderers that paint data onto the plot. It coordinates * calls to each Renderer's updatePlotImage and render methods, and manages * the rendered stack image to provide previews of new axis settings. * * @author jbf */ public class DasPlot extends DasCanvasComponent { private static final List CUSTOMIZER_KEYS = new ArrayList<>(); private static final Map PLOT_CUSTOMIZERS = new HashMap<>(); private static final boolean DEBUG_GRAPHICS = System.getProperty("das2.graph.dasplot.debuggraphics","false").equals("true"); /** * Return a list of keys of all current customizing objects in the order they would be invoked. * @return the keys */ public static List getCustomizerKeys() { synchronized ( CUSTOMIZER_KEYS ) { // Defensive copy to ensure data structures maintain invariants. return new ArrayList<>(CUSTOMIZER_KEYS); } } /** * Add a new customizer to the collection of customizers being used when creating * new plots. The new customizer will be invoked last. * @param key the new customizer's lookup key * @param customizer the new customizer */ public static void addCustomizer(CustomizerKey key, Customizer customizer) { synchronized ( CUSTOMIZER_KEYS ) { if (PLOT_CUSTOMIZERS.containsKey(key)) { // A customizer with this key is already in the list and map. // Just replace the customizer but don't tamper with list order. PLOT_CUSTOMIZERS.put(key, customizer); } else { // Add the customizer, and add the key at the end of the key list. PLOT_CUSTOMIZERS.put(key, customizer); CUSTOMIZER_KEYS.add(key); } } } /* * Return the customizer that is associated with the given key. * @param key the key for which to find the cutomizer * @returns the customizer, or null if the customizer is not present */ public static Customizer getCustomizer(CustomizerKey key) { synchronized ( CUSTOMIZER_KEYS ) { return PLOT_CUSTOMIZERS.get(key); } } /** * Remove the customizer that is associated with the given key. * @param key the key to the customizer to be removed. */ public static void removeCustomizer(CustomizerKey key) { synchronized ( CUSTOMIZER_KEYS ) { CUSTOMIZER_KEYS.remove(key); PLOT_CUSTOMIZERS.remove(key); } } private int legendWidthLimitPx = 100; public static final String PROP_LEGEND_WIDTH_LIMIT_PX = "legendWidthLimitPx"; /** * width of plot required to show the legend labels. */ public int getLegendWidthLimitPx() { return legendWidthLimitPx; } /** * width of plot required to show the legend labels. * @param legendWidthLimitPx */ public void setLegendWidthLimitPx(int legendWidthLimitPx) { int oldLegendWidthLimitPx = this.legendWidthLimitPx; this.legendWidthLimitPx = legendWidthLimitPx; firePropertyChange(PROP_LEGEND_WIDTH_LIMIT_PX, oldLegendWidthLimitPx, legendWidthLimitPx); repaint(); } /** * title for the plot */ public static final String PROP_TITLE = "title"; private DasAxis xAxis; private DasAxis yAxis; DasAxis.Memento xmemento; DasAxis.Memento ymemento; private boolean reduceOutsideLegendTopMargin = false; //public String debugString = ""; private String plotTitle = ""; /** * true if the plot title should be displayed. */ protected boolean displayTitle= true; /** * listens for property changes and triggers the process of updating the plot image. */ protected RebinListener rebinListener = new RebinListener(); /** * listens for x and y axis tick changes, and repaints the plot when * drawing grid lines at tick positions. */ protected transient PropertyChangeListener ticksListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (drawGrid || drawMinorGrid) { invalidateCacheImage(); } } }; DnDSupport dndSupport; final static Logger logger = LoggerManager.getLogger("das2.graphics.plot"); private JMenuItem editRendererMenuItem; /** * cacheImage is a cached image that all the renderers have drawn on. This * relaxes the need for renderers' render method to execute in * animation-interactive time. */ boolean cacheImageValid = false; /** * the rendered data is stored in the cacheImage. This image may be * rescaled to provide immediate previews when axes are changed. */ BufferedImage cacheImage; /** * bounds of the cache image. */ Rectangle cacheImageBounds; /** * property preview. If set, the cache image may be scaled to reflect * the new axis position in animation-interactive time. */ boolean preview = false; //private int repaintCount = 0; private final AtomicInteger paintComponentCount = new AtomicInteger(0); /** * height of the title in pixels. */ private int titleHeight= 0; private boolean drawInactiveInLegend= false; /** * use this for conditional breakpoints. Set this to non-null to * trigger breakpoint. */ private static String testSentinal= null; private boolean reluctantLegendIcons= "true".equals( System.getProperty("reluctantLegendIcons","false") ); /** * create a new plot with the x and y axes. * @param xAxis the horizontal axis * @param yAxis the vertical axis */ public DasPlot(DasAxis xAxis, DasAxis yAxis) { super(); addMouseListener(new MouseInputAdapter() { @Override public void mousePressed(MouseEvent e) { //if (e.getButton() == MouseEvent.BUTTON3) { Renderer r = null; int ir; synchronized ( DasPlot.this ) { ir = findRendererAt(getX() + e.getX(), getY() + e.getY()); if ( ir>-1 ) { r= (Renderer) renderers.get(ir); } if ( r==null ) { for ( int i=renderers.size()-1; i>=0; i-- ) { if ( renderers.get(i).isActive()==false ) { r= renderers.get(i); break; } } } setFocusRenderer(r); } if (editRendererMenuItem != null) { //TODO: check out SwingUtilities, I think this is wrong: editRendererMenuItem.setText("Renderer Properties"); if ( ir>-1 && r!=null ) { editRendererMenuItem.setEnabled(true); editRendererMenuItem.setIcon(r.getListIcon()); } else { editRendererMenuItem.setEnabled(false); editRendererMenuItem.setIcon(null); } } //} } }); setOpaque(false); this.renderers = new ArrayList(); this.xAxis = xAxis; if (xAxis != null) { if (!xAxis.isHorizontal()) { throw new IllegalArgumentException("xAxis is not horizontal"); } xAxis.addPropertyChangeListener("dataMinimum", rebinListener); xAxis.addPropertyChangeListener("dataMaximum", rebinListener); xAxis.addPropertyChangeListener(DasAxis.PROPERTY_DATUMRANGE, rebinListener); xAxis.addPropertyChangeListener("log", rebinListener); xAxis.addPropertyChangeListener(DasAxis.PROP_FLIPPED,rebinListener); xAxis.addPropertyChangeListener(DasAxis.PROPERTY_TICKS, ticksListener); } this.yAxis = yAxis; if (yAxis != null) { if (yAxis.isHorizontal()) { throw new IllegalArgumentException("yAxis is not vertical"); } yAxis.addPropertyChangeListener("dataMinimum", rebinListener); yAxis.addPropertyChangeListener("dataMaximum", rebinListener); yAxis.addPropertyChangeListener(DasAxis.PROPERTY_DATUMRANGE, rebinListener); yAxis.addPropertyChangeListener("log", rebinListener); yAxis.addPropertyChangeListener(DasAxis.PROP_FLIPPED,rebinListener); yAxis.addPropertyChangeListener(DasAxis.PROPERTY_TICKS, ticksListener); } if (!"true".equals(DasApplication.getProperty("java.awt.headless", "false"))) { addDefaultMouseModules(); } synchronized ( CUSTOMIZER_KEYS ) { for (CustomizerKey k : CUSTOMIZER_KEYS) { PLOT_CUSTOMIZERS.get(k).customize(this); } } } /** * returns the Renderer with the current focus. Clicking on a trace sets the focus. */ protected Renderer focusRenderer = null; /** * property name for the current renderer that the operator has selected. */ public static final String PROP_FOCUSRENDERER = "focusRenderer"; /** * get the current renderer that the operator has selected. * @return the current renderer that the operator has selected. */ public Renderer getFocusRenderer() { return focusRenderer; } /** * set the current renderer that the operator has selected. * @param focusRenderer the current renderer that the operator has selected. */ public void setFocusRenderer(Renderer focusRenderer) { Renderer oldFocusRenderer = this.focusRenderer; this.focusRenderer = focusRenderer; firePropertyChange(PROP_FOCUSRENDERER, null, focusRenderer); //firePropertyChange(PROP_FOCUSRENDERER, oldFocusRenderer, focusRenderer); } /** * for multiline labels, the horizontal alignment, where 0 is left, 0.5 is center, and 1.0 is right. */ private float multiLineTextAlignment = 0.f; /** * property name for multiline labels, the horizontal alignment, where 0 is left, 0.5 is center, and 1.0 is right. */ public static final String PROP_MULTILINETEXTALIGNMENT = "multiLineTextAlignment"; /** * get the horizontal alignment for multiline labels, where 0 is left, 0.5 is center, and 1.0 is right. * @return the alignment */ public float getMultiLineTextAlignment() { return multiLineTextAlignment; } /** * set the horizontal alignment for multiline labels, where 0 is left, 0.5 is center, and 1.0 is right. * @param multiLineTextAlignment the alignment */ public void setMultiLineTextAlignment(float multiLineTextAlignment) { float oldMultiLineTextAlignment = this.multiLineTextAlignment; this.multiLineTextAlignment = multiLineTextAlignment; firePropertyChange(PROP_MULTILINETEXTALIGNMENT, oldMultiLineTextAlignment, multiLineTextAlignment); repaint(); } private static final Icon NULL_ICON= new ImageIcon(new BufferedImage(1,1,BufferedImage.TYPE_INT_ARGB) ); /** * returns the bounds of the legend, or null if there is no legend. * TODO: merge with drawLegend, so there are not two similar codes. * @param graphics graphics context * @param msgx the location of the box * @param msgy the location of the box * @param llegendElements the elements * @return the bounds */ private Rectangle getLegendBounds( Graphics2D graphics, int msgx, int msgy, List llegendElements) { int maxIconWidth = 0; Rectangle mrect; Rectangle boundRect=null; int em = (int) getEmSize(); if ( llegendElements==null ) return null; if ( graphics==null ) return null; DatumRange lcontext= this.context; String contextStr= lcontext==null ? "" : lcontext.toString(); for (LegendElement le : llegendElements) { Renderer r= le.renderer; if ( ( r!=null && r.isActive() ) || le.icon!=null || drawInactiveInLegend ) { Icon icon= le.icon!=null ? le.icon : ( r==null ? null : r.getListIcon() ); if ( icon==null ) icon=NULL_ICON; GrannyTextRenderer gtr = GraphUtil.newGrannyTextRenderer(); String theLabel= String.valueOf(le.label).trim().replaceAll("%\\{CONTEXT\\}",contextStr); gtr.setString(graphics, theLabel); // protect from nulls, which seems to happen mrect = gtr.getBounds(); maxIconWidth = Math.max(maxIconWidth, icon.getIconWidth()); if ( reluctantLegendIcons ) { if ( llegendElements.size()==1 ) { maxIconWidth = 0; } } int theheight = Math.max(mrect.height, icon.getIconHeight()); mrect.translate(msgx, msgy + (int) gtr.getAscent() ); mrect.height= theheight; if (boundRect == null) { boundRect = mrect; } else { boundRect.add(mrect); } msgy += theheight; } } if ( boundRect==null ) return null; int iconColumnWidth = maxIconWidth + em / 4; mrect = new Rectangle(boundRect); mrect.width += iconColumnWidth; if ( null == legendPosition ) { throw new IllegalArgumentException("not supported: "+legendPosition); } else switch (legendPosition) { case NE: case NW: mrect.y= yAxis.getRow().getDMinimum() + em/2; if ( legendPosition==LegendPosition.NE ) { mrect.x = xAxis.getColumn().getDMaximum() - em - mrect.width; } else if ( legendPosition==LegendPosition.NW ) { mrect.x = xAxis.getColumn().getDMinimum() + em ; } break; case SE: case SW: mrect.y= yAxis.getRow().getDMaximum() - boundRect.height - em; // note em not em/2 is intentional if ( legendPosition==LegendPosition.SE ) { mrect.x = xAxis.getColumn().getDMaximum() - em - mrect.width; } else if ( legendPosition==LegendPosition.SW ) { mrect.x = xAxis.getColumn().getDMinimum() + em ; } break; case OutsideNE: mrect.x = xAxis.getColumn().getDMaximum() + em + maxIconWidth; boundRect.x = mrect.x; mrect.y= yAxis.getRow().getDMinimum(); // em/5 determined by experiment. break; case OutsideSE: mrect.x = xAxis.getColumn().getDMaximum() + em + maxIconWidth; boundRect.x = mrect.x; mrect.y= yAxis.getRow().getDMaximum() - boundRect.height; break; default: throw new IllegalArgumentException("not supported: "+legendPosition); } Rectangle axisBounds= DasDevicePosition.toRectangle( getRow(), getColumn() ); axisBounds.width= Math.max( axisBounds.width, mrect.x+mrect.width-axisBounds.x ); // don't limit width because of outside NE Rectangle2D rr= mrect.createIntersection(axisBounds); return new Rectangle( (int)rr.getX(),(int)rr.getY(),(int)rr.getWidth(),(int)rr.getHeight() ); } /** * draw the legend elements, substituting the plot context if the macro %{CONTEXT} is found. * @param g graphics context * @param llegendElements the legend elements * @see #setDisplayLegend(boolean) * @see #setLegendFontSize(java.lang.String) */ private void drawLegend(Graphics2D g, List llegendElements ) { Graphics2D graphics= (Graphics2D) g.create(); double legendFontSizeImpl= GraphUtil.parseLayoutLength( this.legendFontSize, 0.0, getFont().getSize2D() ); if ( legendFontSizeImpl==getFont().getSize2D() ) { graphics.setFont( getFont().deriveFont( getFont().getSize2D() + legendRelativeFontSize ) ); } else { if ( legendRelativeFontSize!=0 ) { logger.warning("legendRelativeFontSize ignored because legendFontSize is set"); } graphics.setFont( getFont().deriveFont( (float)legendFontSizeImpl ) ); } int em; int msgx, msgy; Color backColor = GraphUtil.getRicePaperColor(); Rectangle mrect; em = (int) getEmSize(); msgx = xAxis.getColumn().getDMiddle() + em; msgy = yAxis.getRow().getDMinimum() + em/2; int maxIconWidth= 0; for (LegendElement le : llegendElements) { Icon icon= le.icon!=null ? le.icon : le.renderer.getListIcon(); if ( icon==null ) icon=NULL_ICON; maxIconWidth = Math.max(maxIconWidth, icon.getIconWidth()); if ( reluctantLegendIcons ) { if ( llegendElements.size()==1 ) { maxIconWidth = 0; } } } mrect= getLegendBounds(graphics,msgx,msgy, llegendElements); if ( mrect==null ) return; // nothing is active msgx= mrect.x; msgy= mrect.y; if ( legendPosition!=LegendPosition.OutsideNE && legendPosition!=LegendPosition.OutsideSE ) { msgx+= maxIconWidth + em/4; Rectangle legendBounds= new Rectangle( mrect.x - em / 4, mrect.y - em/4, mrect.width + em / 2, mrect.height + em/2 ); int canvasWidth= getParent().getWidth(); Rectangle clip= legendBounds.intersection( new Rectangle( 0, getRow().getDMinimum(), 2*canvasWidth, getRow().getHeight() ) ); clip.height+= 1; //TODO lineThickness clip.width+= 1; graphics.clip( clip ); graphics.setColor(backColor); graphics.fillRoundRect( legendBounds.x, legendBounds.y, legendBounds.width, legendBounds.height, 5, 5); graphics.setColor(getForeground()); graphics.drawRoundRect( legendBounds.x, legendBounds.y, legendBounds.width, legendBounds.height, 5, 5); } String contextStr= this.context==null ? "" : this.context.toString(); for (LegendElement le : llegendElements) { if ( ( le.renderer!=null && le.renderer.isActive() ) || le.icon!=null || drawInactiveInLegend ) { Icon icon= le.icon!=null ? le.icon : le.renderer.getListIcon(); if ( icon==null ) icon=NULL_ICON; if ( llegendElements.size()==1 && reluctantLegendIcons ) { icon = NULL_ICON; } GrannyTextRenderer gtr = GraphUtil.newGrannyTextRenderer(); gtr.setAlignment( GrannyTextRenderer.LEFT_ALIGNMENT ); String theLabel= String.valueOf(le.label).trim().replaceAll("%\\{CONTEXT\\}",contextStr); gtr.setString(graphics, theLabel); // protect from nulls, which seems to happen mrect = gtr.getBounds(); mrect.translate(msgx, msgy + (int) gtr.getAscent()); int theheight= Math.max(mrect.height, icon.getIconHeight()); int icony= theheight/2 - icon.getIconHeight() / 2; // from top of rectangle int texty= theheight/2 - (int)gtr.getHeight() / 2 + (int) gtr.getAscent(); if ( reduceOutsideLegendTopMargin ) texty = theheight/2; gtr.draw( graphics, msgx, msgy + texty ); mrect.height = theheight; Rectangle imgBounds = new Rectangle( msgx - (icon.getIconWidth() + em / 4), msgy + icony, icon.getIconWidth(), icon.getIconHeight() ); if ( le.icon!=null ) { graphics.drawImage( ((ImageIcon)icon).getImage(), imgBounds.x, imgBounds.y, null ); } else { if ( llegendElements.size()==1 && reluctantLegendIcons ) { } else { le.drawIcon( graphics, msgx - (icon.getIconWidth() + em / 4), msgy + icony ); } } msgy += mrect.getHeight(); mrect.add(imgBounds); if ( msgy > getRow().bottom() ) break; le.bounds = mrect; } } graphics.dispose(); } /** * draw the message bubbles. For the printing thread, we no * longer display the bubbles, unless the have Long.MAX_VALUE for the * birthmilli. * @param g the graphics context. */ private void drawMessages(Graphics2D g, List lmessages ) { Graphics2D graphics= (Graphics2D) g.create(); graphics.clip( DasDevicePosition.toRectangle( getRow(), getColumn() ) ); boolean isPrint= getCanvas().isPrintingThread(); Font font0 = graphics.getFont(); int msgem = (int) Math.max(8, font0.getSize2D() / 2); graphics.setFont(font0.deriveFont((float) msgem)); int em = (int) getEmSize(); boolean rightJustify= false; int msgx = xAxis.getColumn().getDMinimum() + em; int msgy = yAxis.getRow().getDMinimum() + em; if ( legendPosition==LegendPosition.NW ) { rightJustify= true; msgx= xAxis.getColumn().getDMaximum() - em; } Color warnColor = new Color(255, 255, 100, 200); Color severeColor = new Color(255, 140, 140, 200); List renderers1= Arrays.asList( getRenderers() ); long tnow= System.currentTimeMillis(); boolean needRepaintSoon= false; long repaintDelay= 0; for (MessageDescriptor lmessage : lmessages) { MessageDescriptor message = (MessageDescriptor) lmessage; if ( message.messageType