/* File: Renderer.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 org.das2.util.ColorUtil; import java.awt.Color; import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.HeadlessException; import java.awt.Toolkit; import java.awt.geom.AffineTransform; import java.util.logging.Level; import org.das2.dataset.NoDataInIntervalException; import org.das2.dataset.DataSetConsumer; import org.das2.dataset.DataSetDescriptor; import org.das2.dataset.VectorUtil; import org.das2.dataset.TableDataSet; import org.das2.dataset.TableUtil; import org.das2.dataset.VectorDataSet; import org.das2.DasApplication; import org.das2.DasException; import org.das2.graph.DasAxis.Memento; import java.beans.PropertyChangeListener; import org.das2.util.monitor.ProgressMonitor; import org.das2.components.propertyeditor.Editable; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintStream; import java.text.ParseException; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Logger; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JFileChooser; import org.das2.CancelledOperationException; import org.das2.components.propertyeditor.Displayable; import org.das2.dataset.DataSetAdapter; import org.das2.datum.Datum; import org.das2.datum.DatumUtil; import static org.das2.graph.DasPlot.INFO; import static org.das2.graph.DasPlot.SEVERE; import org.das2.util.LoggerManager; import org.das2.qds.DataSetUtil; import org.das2.qds.QDataSet; import org.das2.qds.SemanticOps; public abstract class Renderer implements DataSetConsumer, Editable, Displayable { protected static final Logger logger= LoggerManager.getLogger("das2.graphics.renderer"); /** * identifies the dataset (in the DataSetDescriptor sense) being plotted * by the Renderer. May be null if no such identifier exists. See * DataSetDescriptor.create( String id ). */ String dataSetId; /** * The dataset that is being plotted by the Renderer. */ protected QDataSet ds; /** * Memento for x axis state last time updatePlotImage was called. */ private DasAxis.Memento xmemento; /** * Memento for y axis state last time updatePlotImage was called. */ private DasAxis.Memento ymemento; /** * plot containing this renderer */ private DasPlot parent; //DasPlot2 parent2; /** * the responsibility of keeping a relevant dataset loaded. Can be null * if a loading mechanism is not used. The DataLoader will be calling * setDataSet and setException. */ DataLoader loader; /** * When a dataset cannot be loaded, the exception causing the failure * will be rendered instead. */ protected Exception lastException; /** * This is the exception to be rendered. This is so if an exception occurs during drawing, then this will be drawn instead. */ protected Exception renderException; /** * keep track of first and last valid points of the dataset to simplify * subclass code and allow them to check if there are any valid points. */ protected int firstValidIndex=-1; protected int lastValidIndex=-1; private static final String PROPERTY_ACTIVE = "active"; private static final String PROPERTY_DATASET = "dataSet"; protected Renderer(DataSetDescriptor dsd) { this.loader = new XAxisDataLoader(this, dsd); } protected Renderer(QDataSet ds) { this.ds = ds; this.loader = null; } protected Renderer() { this((DataSetDescriptor) null); } public DasPlot getParent() { return this.parent; } public void setParent( DasPlot parent ) { this.parent= parent; } public Memento getXmemento() { return xmemento; } public Memento getYmemento() { return ymemento; } /** * post the message, checking to see that there is a parent first. * @param message the message * @param messageType the message type, DasPlot.INFO, DasPlot.WARNING, or DasPlot.SEVERE. * @param x the X position or null * @param y the Y position or null */ public void postMessage( String message, int messageType, Datum x, Datum y) { DasPlot lparent= this.parent; if ( lparent==null ) return; lparent.postMessage( this, message, messageType, x, y); } /** * Notify user of an exception, in the context of the plot. A position in * the data space may be specified to locate the text within the data context. * Note either or both x or y may be null. Messages must only be posted while the * Renderer's render method is called, not during updatePlotImage. All messages are * cleared before the render step. (TODO:check on this) * * @param message the text to be displayed, may contain granny text. * @param messageLevel allows java.util.logging.Level to be used, for example Level.INFO, Level.WARNING, and Level.SEVERE * @param x if non-null, the location on the x axis giving context for the text. * @param y if non-null, the location on the y axis giving context for the text. */ public void postMessage(String message, Level messageLevel, Datum x, Datum y) { DasPlot lparent= this.parent; if ( lparent==null ) return; lparent.postMessage(this, message, messageLevel, x, y); } /** * notify user of an exception, in the context of the plot. This is similar * to postMessage(renderer, exception.getMessage(), DasPlot.SEVERE, null, null ) * except that it does catch CancelledOperationExceptions and reduced the * severity since the user probably initiated the condition. * @param exception the exception to post. */ public void postException( Exception exception ) { DasPlot lparent= this.parent; if ( lparent==null ) return; lparent.postException( this, exception ); } public static boolean isTableDataSet( QDataSet ds ) { return SemanticOps.isTableDataSet(ds); } protected Set needWorkMarkers= Collections.synchronizedSet( new HashSet() ); protected final String MARKER_DATASET= "dataset"; protected final String MARKER_X_AXIS_RANGE= "xaxisRange"; protected final String MARKER_Y_AXIS_RANGE= "yaxisRange"; /** * find the first and last valid data points. This is an inexpensive * calculation which is only done when the dataset changes. It improves * update and render codes by allowing them to skip initial fill data * and more accurately report the presence of off-screen valid data. * preconditions: setDataSet is called with null or non-null dataset. * postconditions: firstValid and lastValid are set. In the case of a * null dataset, firstValid and lastValid are set to 0. */ private void updateFirstLastValid() { if ( ds==null || ds.rank()==0 ) { firstValidIndex=0; lastValidIndex=0; } else { if ( SemanticOps.isTableDataSet(ds) ) { firstValidIndex= 0; lastValidIndex= ds.length(); } else if ( SemanticOps.isSimpleBundleDataSet(ds) ) { firstValidIndex= 0; lastValidIndex= ds.length(); } else { firstValidIndex= -1; lastValidIndex= -1; QDataSet wds= DataSetUtil.weightsDataSet(ds); if ( wds.rank()==1 ) { for ( int i=0; firstValidIndex==-1 && i0 ) firstValidIndex=i; } for ( int i=ds.length()-1; lastValidIndex==-1 && i>=0; i-- ) { if ( wds.value(i)>0 ) lastValidIndex=i+1; } } else { firstValidIndex= 0; lastValidIndex= wds.length(); } } } } protected void invalidateParentCacheImage() { DasPlot lparent= parent; if (lparent != null) lparent.invalidateCacheImage(); } /** * returns the current dataset being displayed. * @return */ public QDataSet getDataSet() { return this.ds; } /** * Renderers should use this internally instead of getDataSet() to support * subclasses preprocessing datasets * @return */ protected QDataSet getInternalDataSet(){ return getDataSet(); } /** * return the data for DataSetConsumer, which might be rebinned. * @return */ @Override public QDataSet getConsumedDataSet() { return this.ds; } private boolean dumpDataSet; /** Getter for property dumpDataSet. * @return Value of property dumpDataSet. * */ public boolean isDumpDataSet() { return this.dumpDataSet; } /** Setter for property dumpDataSet setting this to * true causes the dataSet to be dumped. * @param dumpDataSet New value of property dumpDataSet. * */ public void setDumpDataSet(boolean dumpDataSet) { this.dumpDataSet = dumpDataSet; if (dumpDataSet == true) { try { if (ds == null) { setDumpDataSet(false); throw new DasException("data set is null"); } else { JFileChooser chooser = new JFileChooser(); int xx = chooser.showSaveDialog(this.getParent()); if (xx == JFileChooser.APPROVE_OPTION) { File file = chooser.getSelectedFile(); if ( isTableDataSet(ds) ) { TableUtil.dumpToAsciiStream( (TableDataSet) DataSetAdapter.createLegacyDataSet(ds), new FileOutputStream(file)); } else if (ds instanceof VectorDataSet) { VectorUtil.dumpToAsciiStream((VectorDataSet) DataSetAdapter.createLegacyDataSet(ds), new FileOutputStream(file)); } else { throw new DasException("don't know how to serialize data set: " + ds); } } setDumpDataSet(false); } } catch (HeadlessException | FileNotFoundException | DasException e) { DasApplication.getDefaultApplication().getExceptionHandler().handle(e); } this.dumpDataSet = dumpDataSet; } } protected Painter bottomDecorator = null; public static final String PROP_BOTTOMDECORATOR = "bottomDecorator"; public Painter getBottomDecorator() { return bottomDecorator; } /** * add additional painting code to the renderer, which is called before * the renderer is called. * @param bottomDecorator the Painter to call, or null to clear. */ public void setBottomDecorator(Painter bottomDecorator) { Painter oldBottomDecorator = this.topDecorator; this.bottomDecorator = bottomDecorator; updateCacheImage(); propertyChangeSupport.firePropertyChange(PROP_BOTTOMDECORATOR, oldBottomDecorator, bottomDecorator); } protected Painter topDecorator = null; public static final String PROP_TOPDECORATOR = "topDecorator"; public Painter getTopDecorator() { return topDecorator; } /** * add additional painting code to the renderer, which is called after * the renderer is called. * @param topDecorator the Painter to call, or null to clear. */ public void setTopDecorator(Painter topDecorator) { Painter oldTopDecorator = this.topDecorator; this.topDecorator = topDecorator; updateCacheImage(); propertyChangeSupport.firePropertyChange(PROP_TOPDECORATOR, oldTopDecorator, topDecorator); } /** * TODO: what is the difference between lastException and exception? * @param e */ public void setLastException(Exception e) { logger.log(Level.FINE, "Renderer.setLastException {0}: {1}", new Object[] { id, String.valueOf(e) }); this.lastException = e; this.renderException = lastException; } public Exception getLastException() { return this.lastException; } /** * return true if the dataset appears to be in a scheme accepted by this renderer. This should assume that axis * units can be reset, etc. * @param ds * @return true if the dataset appears to be acceptable. */ public boolean acceptsDataSet( QDataSet ds ) { return true; } /** * Set the dataset to be plotted. Different renderers accept QDataSets with * different schemes. For example SeriesRenderer takes: * ds[t] rank 1 dataset with rank 1 DEPEND_0 or * ds[t,n] rank 2 bundle dataset with X in ds[t,0] and Y in ds[t,n-1] * and SpectrogramRenderer takes: * ds[t,y] rank 2 table dataset * ds[n,t,y] rank 3 dataset with the first dimension join * See each renderer's documentation for the schemes it takes. * Note the lastException property is cleared, even when the dataset is null. * @param ds */ public void setDataSet(QDataSet ds) { logger.log(Level.FINE, "Renderer.setDataSet {0}: {1}", new Object[]{id, String.valueOf(ds) }); QDataSet oldDs = this.ds; boolean update= lastException!=null || oldDs!=ds ; this.lastException = null; this.renderException = null; if ( update ) { synchronized(this) { updateFirstLastValid(); this.ds = ds; } needWorkMarkers.add( MARKER_DATASET ); //refresh(); update(); invalidateParentCacheImage(); propertyChangeSupport.firePropertyChange(PROPERTY_DATASET, oldDs, ds); } } /** * set the exception to be rendered instead of the dataset. * @param e */ public void setException(Exception e) { logger.log(Level.FINE, "Renderer.setException {0}: {1}", new Object[] { id, String.valueOf(e) }); Exception oldException = this.lastException; this.lastException = e; this.renderException = lastException; if ( parent != null && oldException != e) { //parent.markDirty(); //parent.update(); update(); //refresh(); invalidateParentCacheImage(); } //refresh(); } public void setDataSetID(String id) throws org.das2.DasException { if (id == null) throw new NullPointerException("Null dataPath not allowed"); if (id.equals("")) { setDataSetDescriptor(null); return; } try { DataSetDescriptor dsd = DataSetDescriptor.create(id); setDataSetDescriptor(dsd); } catch (DasException ex) { ex.printStackTrace(); throw ex; } } public String getDataSetID() { if (getDataSetDescriptor() == null) { return ""; } else { return getDataSetDescriptor().getDataSetID(); } } /** * allocate a bunch of canonical properties. See http://autoplot.org/developer.guessRenderType#Proposed_extensions */ public static final String CONTROL_KEY_COLOR= "color"; public static final String CONTROL_KEY_FILL_COLOR= "fillColor"; public static final String CONTROL_KEY_FILL_DIRECTION= "fillDirection"; // "above" "below" "none" "both" public static final String CONTROL_KEY_COLOR_TABLE= "colorTable"; public static final String CONTROL_KEY_LINE_THICK= "lineThick"; /** * used in ContoursRenderer and EventsRenderer, values like DotDashes and Solid */ public static final String CONTROL_KEY_LINE_STYLE= "lineStyle"; public static final String CONTROL_KEY_SYMBOL= "symbol"; public static final String CONTROL_KEY_SYMBOL_SIZE= "symbolSize"; /** * mapping from one double value to color, like:
    *
  • "0.0:white" *
* This might be expanded to support "ge(100.0):red;lt(0):gray;within(0to100);green" and nominal values. */ public static final String CONTROL_KEY_SPECIAL_COLORS="specialColors"; /** * when filling a region, use this texture (solid,hash,backhash,crosshash) to fill. * @see GraphUtil#fillWithTexture(java.awt.Graphics2D, java.awt.geom.GeneralPath, java.awt.Color, java.lang.String) */ public static final String CONTROL_KEY_FILL_TEXTURE="fillTexture"; /** * font size relative to the parent, so "" or "1em" is the same size. */ public static final String CONTROL_KEY_FONT_SIZE= "fontSize"; public static final String CONTROL_KEY_REFERENCE= "reference"; public static final String CONTROL_KEY_DRAW_ERROR= "drawError"; /** * modulo Y indicates that a difference in 23 hours is the same as -1 hours. */ public static final String CONTROL_KEY_MODULO_Y= "moduloY"; public static final String CONTROL_KEY_MODULO_X= "moduloX"; public static final String PROP_CONTROL= "control"; /** * generic control string, that is handled by the renderer. In general, this should be * a ampersand-delimited string of name=value pairs. This may return values that * are represented as a separate control, such as color. * {@code fill=red,above,5.0;grey,below,0.0&ref=2.5} * (Note these are example controls which are not implemented.) */ protected String control=""; private Map controls= Collections.emptyMap(); /** * set the control string which contains a number of properties. These are defined for * each renderer, but they should try to be consistent. See http://autoplot.org/developer.guessRenderType#Proposed_extensions * When overriding this, be sure to call super. * @see #CONTROL_KEY_COLOR etc * @param s the control */ public void setControl( String s ) { String oldValue= this.control; this.control= s; if ( oldValue==s || ( oldValue != null && oldValue.equals(s) ) ) { return; } this.controls= parseControl(s); update(); propertyChangeSupport.firePropertyChange(PROP_CONTROL, oldValue, control ); } /** * get the string which summarizes the state of the renderer. These allow a compact string to contain the renderer settings. * This should be an ampersand-delimited list of name=value pairs. * @return */ public String getControl() { return this.control; } /** * convenient and official location for method that formats control string. * @param c * @return */ public static String formatControl( Map c ) { StringBuilder result= new StringBuilder(50); String ampstr= "&"; for ( Entry ee: c.entrySet() ) { if ( ee.getKey().contains("&") ) throw new IllegalArgumentException("keys must be java identifiers"); if ( ee.getValue().contains("&") ) ampstr= "&"; } boolean amp= false; for ( Entry ee: c.entrySet() ) { if ( amp ) result.append(ampstr); else amp=true; result.append( ee.getKey() ) .append("=").append(ee.getValue() ); } return result.toString(); } /** * convenient and official location for method that parses control string. * This will split on ampersand, and when no ampersands are found then it will * try semicolons. This is to support embedding the control string in * other control strings (like Autoplot URIs) which use ampersands. * @param c the control string or null. * @return the control string, parsed. */ public static Map parseControl( String c ) { Map result= new LinkedHashMap(); if ( c==null ) { return result; } String ampstr= "&"; if ( c.contains("&") ) { ampstr= "&"; } if ( c.trim().length()==0 ) return result; String[] ss= c.split(ampstr); if ( ss.length==1 ) { ss= c.split(";"); } for (String s : ss) { if (s.trim().length() == 0) continue; String[] ss2 = s.split("=", 2); if ( ss2.length==1 ) { result.put( ss2[0].trim(), "T" ); // true } else { String k= ss2[0].trim(); String v= ss2[1].trim(); result.put( k,v ); } } return result; } /** * Get the control. This provides an easy way for renderers to have controls in a compact string. * @param key the key name. * @param deft the default value (or null) * @return the string value of the control. * @see #getDoubleControl(java.lang.String, double) * @see #getBooleanControl(java.lang.String, boolean) */ public String getControl( String key, String deft ) { if ( this.control.trim().length()==0 ) return deft; String v= controls.get(key); if ( v!=null ) return v; else return deft; } /** * return true if the control is specified. * @param key the key name * @return true if the control is specified. */ public boolean hasControl( String key ) { if ( this.control.trim().length()==0 ) return false; return controls.containsKey(key); } /** * get the boolean control. * @param key the key name. * @param deft the default value. * @return the boolean value, where "T" is true, false otherwise; or the default when the value is not found. */ public boolean getBooleanControl( String key, boolean deft ) { String v= controls.get(key); if ( v!=null ) return v.equalsIgnoreCase("T"); else return deft; } /** * return the encoding for the boolean value. * @param v the boolean value. * @return "T" or "F" */ public static String encodeBooleanControl( boolean v ) { return v ? "T" : "F"; } /** * get the double control. * @param key the key name. * @param deft the default value. * @return the double, parsed with Double.parseDouble; or the default when the value is not found. */ public double getDoubleControl( String key, double deft ) { String v= controls.get(key); if ( v!=null ) { try { return Double.parseDouble(v); } catch ( NumberFormatException ex ) { logger.log( Level.WARNING, "Unable to parse as double: {0}", key); return deft; } } else { return deft; } } /** * get the integer control. * @param key the key name. * @param deft the default value. * @return the int, parsed with Integer.parseInt; or the default when the value is not found. */ public int getIntegerControl( String key, int deft ) { String v= controls.get(key); if ( v!=null ) { try { return Integer.parseInt(v); } catch ( NumberFormatException ex ) { logger.log( Level.WARNING, "Unable to parse as int: {0}", key); return deft; } } else { return deft; } } /** * get the double array control. These should be encoded on a string * with commas delimiting values. * @param key the key name. * @param deft the default value. * @return the double array, each element parsed with Double.parseDouble or the default when the value is not found. */ public double[] getDoubleArrayControl( String key, double[] deft ) { String v= controls.get(key); if ( v!=null ) { try { String[] ss= v.split(","); double[] result= new double[ss.length]; for ( int i=0; i