package org.das2.graph; import java.awt.BasicStroke; import javax.swing.ImageIcon; import javax.swing.Icon; import org.das2.qds.DDataSet; import org.das2.qds.JoinDataSet; import org.das2.qds.ops.Ops; import org.das2.qds.SemanticOps; import org.das2.qds.QDataSet; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.Arc2D; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import org.das2.datum.Units; import static java.lang.Math.cos; import static java.lang.Math.sin; import java.util.LinkedHashMap; import java.util.Map; import java.util.logging.Level; import org.das2.datum.Datum; import org.das2.datum.DatumRange; import org.das2.datum.DatumVector; import org.das2.datum.UnitsUtil; import org.das2.datum.format.DatumFormatter; import org.das2.qds.ArrayDataSet; import org.das2.qds.DataSetUtil; /** * PolarPlotRenderer is a refactoring of PitchAngleDistributionRenderer * which will also render rank 1 series. * * @author jbf */ public class PolarPlotRenderer extends Renderer { public PolarPlotRenderer( DasColorBar cb ) { setColorBar(cb); } private GeneralPath path; private Shape _shape; //cache, derived from path private DasAxis tinyX; private DasAxis tinyY; private Icon icon; /** * experiment with drawing the list icon dynamically. * @return */ @Override public Icon getListIcon() { QDataSet dsl= getDataSet(); if ( dsl==null ) { return super.getListIcon(); } else { if ( icon!=null ) { return icon; } else { BufferedImage result= new BufferedImage(64,64,BufferedImage.TYPE_INT_RGB); Graphics2D g= (Graphics2D) result.getGraphics(); g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); g.setColor( Color.WHITE ); g.fillRect( 0, 0, 64, 64 ); QDataSet bounds= doAutorange(dsl); DatumRange xrange= DataSetUtil.asDatumRange( bounds.slice(0) ); DatumRange yrange= DataSetUtil.asDatumRange( bounds.slice(1) ); if ( tinyX==null ) { tinyX= new DasAxis( xrange, DasAxis.HORIZONTAL ); tinyX.setColumn( new DasColumn( getParent().getCanvas(), null, 0, 0, 0, 0, 0, 64 ) ); tinyY= new DasAxis( yrange, DasAxis.VERTICAL ); tinyY.setRow( new DasRow( getParent().getCanvas(), null, 0, 0, 0, 0, 0, 64 ) ); } else { tinyX.setDatumRange(xrange); tinyY.setDatumRange(yrange); } try { render( g, tinyX, tinyY ); } catch ( NullPointerException ex ) { ex.printStackTrace(); g.drawLine(0,0,64,64); } icon = new ImageIcon( result.getScaledInstance(16,16,Image.SCALE_SMOOTH) ); return icon; } } } @Override public boolean acceptContext(int x, int y) { return selectionArea().contains(x,y); } public Shape selectionArea() { if ( path==null ) { DasRow row= getParent().getRow(); DasColumn column= getParent().getColumn(); return DasDevicePosition.toRectangle( row, column ); } else { if ( _shape!=null ) { return _shape; } else { Shape s = new BasicStroke( Math.min(14,1.f+8.f), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND ).createStrokedShape(path); _shape= s; return s; } } } /** * return true if the dataset can be interpreted as radian degrees from 0 to PI or from 0 to 2*PI. * @param ds any QDataSet. * @param scrict return null if it's not clear that the units are degrees. * @return the multiplier to make the dataset into radians, or null. * @see Ops#isAngleRange which is a copy of this code. */ private static Double isAngleRange( QDataSet ds, boolean strict) { return Ops.isAngleRange(ds, strict); } /** * accepts data that is rank 2 and not a timeseries. Angles * may be in radians or in Units.degrees. * ds[Energy,Pitch] or ds[Pitch,Energy] where Pitch is in: * Units.degrees, Units.radians, or dimensionless and -2*PI to 2*PI. * @param ds * @return */ public static boolean acceptsData( QDataSet ds ) { switch (ds.rank()) { case 2: if ( SemanticOps.isTimeSeries(ds) ) return false; if ( SemanticOps.isBundle(ds) ) return false; QDataSet yds= SemanticOps.ytagsDataSet(ds); QDataSet xds= SemanticOps.xtagsDataSet(ds); if ( isAngleRange(xds, true)!=null ) return true; if ( isAngleRange(yds, true)!=null ) return true; return false; case 1: return true; default: return false; } } PropertyChangeListener rebinListener= new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent e) { update(); updateCacheImage(); } }; @Override public final void setColorBar(DasColorBar colorBar) { DasColorBar oldColorBar = this.colorBar; if ( this.colorBar!=null ) { this.colorBar.removePropertyChangeListener("dataMinimum", rebinListener); this.colorBar.removePropertyChangeListener("dataMaximum", rebinListener); this.colorBar.removePropertyChangeListener("log", rebinListener); this.colorBar.removePropertyChangeListener(DasColorBar.PROPERTY_TYPE, rebinListener); this.colorBar.removePropertyChangeListener(DasColorBar.PROPERTY_FILL_COLOR, rebinListener); } this.colorBar = colorBar; if (this.colorBar != null) { colorBar.addPropertyChangeListener("dataMinimum", rebinListener); colorBar.addPropertyChangeListener("dataMaximum", rebinListener); colorBar.addPropertyChangeListener("log", rebinListener); colorBar.addPropertyChangeListener(DasColorBar.PROPERTY_TYPE, rebinListener); colorBar.addPropertyChangeListener(DasColorBar.PROPERTY_FILL_COLOR, rebinListener); } propertyChangeSupport.firePropertyChange(PROP_COLORBAR, oldColorBar, colorBar); } @Override public void update() { super.update(); this.icon= null; } /** * the color for contour lines */ private Color color = Color.BLACK; private double lineWidth = 1.0; public static final String PROP_LINEWIDTH = "lineWidth"; /** * get the width, in pixels of the contour lines. * @return width in pixels */ public double getLineWidth() { return lineWidth; } public void setLineWidth(double lineWidth) { double oldLineWidth = this.lineWidth; this.lineWidth = lineWidth; update(); propertyChangeSupport.firePropertyChange(PROP_LINEWIDTH, oldLineWidth, lineWidth); } /** * Get the color for contour lines * @return the color for contour lines */ public Color getColor() { return this.color; } /** * Set the color for contour lines * @param color the color for contour lines */ public void setColor(Color color) { Color oldColor = this.color; this.color = color; update(); propertyChangeSupport.firePropertyChange("color", oldColor, color); } private static QDataSet doAutorangeRank1( QDataSet rds ) { if ( isAngleRange(rds, true)!=null ) { rds= SemanticOps.xtagsDataSet(rds); } Units yunits= SemanticOps.getUnits(rds); ArrayDataSet xdesc= DDataSet.wrap( new double[] { 0, Ops.extent(rds).value(1) }, yunits ); ArrayDataSet ydesc= xdesc; xdesc= ArrayDataSet.maybeCopy( Ops.rescaleRangeLogLin( xdesc, -1.1, 1.1 ) ); ydesc= ArrayDataSet.maybeCopy( Ops.rescaleRangeLogLin( ydesc, -1.1, 1.1 ) ); JoinDataSet bds= new JoinDataSet(2); bds.join(xdesc); bds.join(ydesc); return bds; } /** * autorange the data set. If a color bar is needed then a 3-D bounding cube will be returned. * @param tds * @return bounding box or cube. */ public static QDataSet doAutorange(QDataSet tds) { if ( tds.rank()==1 ) { return doAutorangeRank1(tds); } QDataSet zdesc; if ( SemanticOps.isBundle(tds) ) { if ( tds.length(0)>2 ) { zdesc= Ops.extent( Ops.unbundle(tds,2) ); } else { zdesc= null; } } else { zdesc= Ops.extent( tds ); } if ( zdesc!=null ) { if ( zdesc.value(0)==zdesc.value(1) ) { if ( zdesc.value(0)>0 ) { zdesc= DDataSet.wrap( new double[] { 0, zdesc.value(1) } ); zdesc= Ops.putProperty( zdesc, QDataSet.UNITS, tds.property(QDataSet.UNITS) ); } else { zdesc= DDataSet.wrap( new double[] { 0, 1 } ); zdesc= Ops.putProperty( zdesc, QDataSet.UNITS, tds.property(QDataSet.UNITS) ); } } zdesc= Ops.putProperty( zdesc, QDataSet.SCALE_TYPE, tds.property(QDataSet.SCALE_TYPE ) ); } QDataSet ads= SemanticOps.xtagsDataSet(tds); QDataSet rds= SemanticOps.ytagsDataSet(tds); // this is why they are semanticOps. ytagsDataSet is just used for convenience even though this is not the y values. if ( isAngleRange(rds, true)!=null && isAngleRange(ads, true)==null ) { // swap em rds= SemanticOps.xtagsDataSet(tds); //ads= SemanticOps.ytagsDataSet(tds); // not used } Units yunits= SemanticOps.getUnits(rds); ArrayDataSet xdesc= DDataSet.wrap( new double[] { 0, Ops.extent(rds).value(1) }, yunits ); ArrayDataSet ydesc= xdesc; xdesc= ArrayDataSet.maybeCopy( Ops.rescaleRangeLogLin( xdesc, -1.1, 1.1 ) ); ydesc= ArrayDataSet.maybeCopy( Ops.rescaleRangeLogLin( ydesc, -1.1, 1.1 ) ); JoinDataSet bds= new JoinDataSet(2); bds.join(xdesc); bds.join(ydesc); if ( zdesc!=null ) { bds.join(zdesc); } return bds; } /** * TODO: experiment with delegating to a rectangular renderer and letting it draw onto special Graphics * object, then mapping the result back. The following render types will have to be supported: