package org.autoplot.dom; import java.awt.Rectangle; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.das2.graph.DasRow; import org.das2.util.LoggerManager; import org.autoplot.datasource.DataSourceUtil; /** * Many operations are defined within the DOM object controllers that needn't * be. This class is a place for operations that are performed on the DOM * independent of the controllers. For example, the operation to swap the * position of two plots is easily implemented by changing the rowid and columnid * properties of the two plots. * * @author jbf */ public class DomOps { private static final Logger logger = LoggerManager.getLogger("autoplot.dom"); /** * swap the position of the two plots. If one plot has its tick labels hidden, * then this is swapped as well. * @param a * @param b */ public static void swapPosition( Plot a, Plot b ) { if ( a==b ) return; if ( a.controller!=null ) { a.controller.dom.options.setAutolayout( false ); } String trowid= a.getRowId(); String tcolumnid= a.getColumnId(); boolean txtv= a.getXaxis().isDrawTickLabels(); boolean tytv= a.getYaxis().isDrawTickLabels(); String ticksUriA= a.getTicksURI(); String ticksUriB= b.getTicksURI(); a.setTicksURI( ticksUriB ); b.setTicksURI( ticksUriA ); a.setRowId(b.getRowId()); a.setColumnId(b.getColumnId()); a.getXaxis().setDrawTickLabels(b.getXaxis().isDrawTickLabels()); a.getYaxis().setDrawTickLabels(b.getYaxis().isDrawTickLabels()); b.setRowId(trowid); b.setColumnId(tcolumnid); b.getXaxis().setDrawTickLabels(txtv); b.getYaxis().setDrawTickLabels(tytv); if ( a.controller!=null ) { a.controller.dom.controller.waitUntilIdle(); a.controller.dom.options.setAutolayout( true ); } } /** * Copy the plot and its axis settings, optionally binding the axes. Whether * the axes are bound or not, the duplicate plot is initially synchronized to * the source plot. * See {@link org.autoplot.dom.ApplicationController#copyPlot(org.autoplot.dom.Plot, boolean, boolean, boolean) copyPlot} * @param srcPlot * @param bindx * @param bindy * @param direction * @return the new plot * */ public static Plot copyPlot(Plot srcPlot, boolean bindx, boolean bindy, Object direction ) { Application application= srcPlot.getController().getApplication(); ApplicationController ac= application.getController(); Plot that = ac.addPlot( direction ); that.getController().setAutoBinding(false); that.syncTo( srcPlot, Arrays.asList( DomNode.PROP_ID, Plot.PROP_ROWID, Plot.PROP_COLUMNID ) ); if (bindx) { BindingModel bb = ac.findBinding(application, Application.PROP_TIMERANGE, srcPlot.getXaxis(), Axis.PROP_RANGE); if (bb == null) { ac.bind(srcPlot.getXaxis(), Axis.PROP_RANGE, that.getXaxis(), Axis.PROP_RANGE); } else { ac.bind(application, Application.PROP_TIMERANGE, that.getXaxis(), Axis.PROP_RANGE); } } if (bindy) { ac.bind(srcPlot.getYaxis(), Axis.PROP_RANGE, that.getYaxis(), Axis.PROP_RANGE); } return that; } /** * copy the plot elements from srcPlot to dstPlot. This does not appear * to be used. * See {@link org.autoplot.dom.ApplicationController#copyPlotElement(org.autoplot.dom.PlotElement, org.autoplot.dom.Plot, org.autoplot.dom.DataSourceFilter) copyPlotElement} * @param srcPlot plot containing zero or more plotElements. * @param dstPlot destination for the plotElements. * @return */ public static List<PlotElement> copyPlotElements( Plot srcPlot, Plot dstPlot ) { ApplicationController ac= srcPlot.getController().getApplication().getController(); List<PlotElement> srcElements = ac.getPlotElementsFor(srcPlot); List<PlotElement> newElements = new ArrayList<>(); for (PlotElement srcElement : srcElements) { if (!srcElement.getComponent().equals("")) { if ( srcElement.getController().getParentPlotElement()==null ) { PlotElement newp = ac.copyPlotElement(srcElement, dstPlot, null); newElements.add(newp); } } else { PlotElement newp = ac.copyPlotElement(srcElement, dstPlot, null); newElements.add(newp); List<PlotElement> srcKids = srcElement.controller.getChildPlotElements(); DataSourceFilter dsf1 = ac.getDataSourceFilterFor(newp); for (PlotElement k : srcKids) { if (srcElements.contains(k)) { PlotElement kidp = ac.copyPlotElement(k, dstPlot, dsf1); kidp.getController().setParentPlotElement(newp); newElements.add(kidp); } } } } return newElements; } /** * copyPlotAndPlotElements. This does not appear to be used. * See {@link org.autoplot.dom.ApplicationController#copyPlotAndPlotElements(org.autoplot.dom.Plot, org.autoplot.dom.DataSourceFilter, boolean, boolean) copyPlotAndPlotElements} * @param srcPlot * @param copyPlotElements * @param bindx * @param bindy * @param direction * @return */ public static Plot copyPlotAndPlotElements( Plot srcPlot, boolean copyPlotElements, boolean bindx, boolean bindy, Object direction ) { Plot dstPlot= copyPlot( srcPlot, bindx, bindy, direction ); if ( copyPlotElements ) copyPlotElements( srcPlot, dstPlot ); return dstPlot; } /** * Used in the LayoutPanel's add hidden plot, get the column of * the selected plot or create a new column if several plots are * selected. * @param dom the application. * @param selectedPlots the selected plots. * @param create allow a new column to be created. * @return */ public static Column getOrCreateSelectedColumn( Application dom, List<Plot> selectedPlots, boolean create ) { Set<String> n= new HashSet<>(); for ( Plot p: selectedPlots ) { n.add( p.getColumnId() ); } if ( n.size()==1 ) { return (Column) DomUtil.getElementById(dom,n.iterator().next()); } else { if ( create ) { Canvas c= dom.getCanvases(0); //TODO: do this Column col= c.getController().addColumn(); col.setLeft("0%"); col.setRight("100%"); return col; } else { return null; } } } /** * Used in the LayoutPanel's add hidden plot, get the row of * the selected plot or create a new row if several plots are * selected. * @param dom the application. * @param selectedPlots the selected plots. * @param create allow a new column to be created. * @return */ public static Row getOrCreateSelectedRow( Application dom, List<Plot> selectedPlots, boolean create ) { Set<String> n= new HashSet<>(); for ( Plot p: selectedPlots ) { if ( !n.contains(p.getRowId()) ) n.add( p.getRowId() ); } if ( n.size()==1 ) { return (Row) DomUtil.getElementById(dom,n.iterator().next()); } else { if ( create ) { Iterator<String> iter= n.iterator(); Row r= (Row) DomUtil.getElementById( dom.getCanvases(0), iter.next() ); Row rmax= r; Row rmin= r; for ( int i=1; iter.hasNext(); i++ ) { r= (Row) DomUtil.getElementById( dom.getCanvases(0), iter.next() ); if ( r.getController().getDasRow().getDMaximum()>rmax.getController().getDasRow().getDMaximum() ) { rmax= r; } if ( r.getController().getDasRow().getDMinimum()<rmin.getController().getDasRow().getDMinimum() ) { rmin= r; } } Canvas c= dom.getCanvases(0); Row row= c.getController().addRow(); row.setTop(rmin.getTop()); row.setBottom(rmax.getBottom()); return row; } else { return null; } } } /** * return the bottom and top most plot of a list of plots. * This does use controllers. * @param dom * @param plots * @return */ public static Plot[] bottomAndTopMostPlot( Application dom, List<Plot> plots ) { Plot pmax=plots.get(0); Plot pmin=plots.get(0); Row r= (Row) DomUtil.getElementById( dom.getCanvases(0), pmax.getRowId() ); Row rmax= r; Row rmin= r; for ( Plot p: plots ) { r= (Row) DomUtil.getElementById( dom.getCanvases(0), p.getRowId() ); if ( r.getController().getDasRow().getDMaximum()>rmax.getController().getDasRow().getDMaximum() ) { rmax= r; pmax= p; } if ( r.getController().getDasRow().getDMinimum()<rmin.getController().getDasRow().getDMinimum() ) { rmin= r; pmin= p; } } return new Plot[] { pmax, pmin }; } /** * return a list of the plots using the given row. * This does not use controllers. * @param dom a dom * @param row the row to search for. * @param visible if true, then the plot must also be visible. (Note its colorbar visible is ignored.) * @return a list of plots. */ public static List<Plot> getPlotsFor( Application dom, Row row, boolean visible ) { ArrayList<Plot> result= new ArrayList(); for ( Plot p: dom.getPlots() ) { if ( p.getRowId().equals(row.getId()) ) { if ( visible ) { if ( p.isVisible() ) result.add(p); } else { result.add(p); } } } return result; } /** * count the number of lines in the string, breaking on "!c" * @param s * @return */ private static int lineCount( String s ) { String[] ss= s.split("(\\!c|\\!C|\\<br\\>)"); int emptyLines=0; while ( emptyLines<ss.length && ss[emptyLines].trim().length()==0 ) { emptyLines++; } return ss.length - emptyLines; } /** * play with new canvas layout. This started as a Jython Script, but it's faster to implement here. * See http://autoplot.org/developer.autolayout#Algorithm * @param dom */ public static void newCanvasLayout( Application dom ) { fixLayout( dom ); } /** * New layout mechanism which fixes a number of shortcomings of the old layout mechanism, * newCanvasLayout. This one:<ul> * <li> Removes extra whitespace * <li> Preserves relative size weights. * <li> Preserves em heights, to support components which should not be rescaled. (Not yet supported.) * <li> Preserves space taken by strange objects, to support future canvas components. * <li> Renormalizes the margin row, so it is nice. (Not yet supported. This should consider font size, where large fonts don't need so much space.) * </ul> * This should also be idempotent, where calling this a second time should have no effect. * @param dom an application state, with controller nodes. TODO: remove dependence on controller nodes. */ public static void fixLayout( Application dom ) { Logger logger= LoggerManager.getLogger("autoplot.dom.layout"); logger.fine( "enter fixLayout" ); Canvas canvas= dom.getCanvases(0); double emToPixels= java.awt.Font.decode(dom.getCanvases(0).font).getSize(); double pixelsToEm= 1/emToPixels; Row[] rows= canvas.getRows(); int nrow= rows.length; //kludge: check for duplicate names of rows. Use the first one found. Map<String,Row> rowsCheck= new HashMap(); List<Row> rm= new ArrayList<>(); for ( int i=0; i<nrow; i++ ) { List<Plot> plots= DomOps.getPlotsFor( dom, rows[i], true ); if ( plots.size()>0 ) { if ( rowsCheck.containsKey(rows[i].getId()) ) { logger.log(Level.FINE, "duplicate row id: {0}", rows[i].getId()); rm.add( rows[i] ); } else { rowsCheck.put( rows[i].getId(), rows[i] ); } } else { logger.log(Level.FINE, "unused row: {0}", rows[i]); rm.add( rows[i] ); } } for ( Row r : rm ) { canvas.getController().deleteRow(r); } rows= canvas.getRows(); nrow= rows.length; // sort rows, which is a refactoring. Arrays.sort( rows, (Row r1, Row r2) -> { int d1= r1.getController().getDasRow().getDMinimum(); int d2= r2.getController().getDasRow().getDMinimum(); return d1-d2; }); double totalPlotHeightPixels= 0; for ( int i=0; i<nrow; i++ ) { List<Plot> plots= DomOps.getPlotsFor( dom, rows[i], true ); if ( plots.size()>0 ) { DasRow dasRow= rows[i].getController().dasRow; totalPlotHeightPixels= totalPlotHeightPixels + dasRow.getHeight(); } } double [] MaxUp= new double[ nrow ]; double [] MaxDown= new double[ nrow ]; // double[] emHeight= new double[ nrow ]; // for ( int i=0; i<nrow; i++ ) { // DasRow dasRow= rows[i].getController().dasRow; // emHeight[i]= ( dasRow.getEmMaximum() - dasRow.getEmMinimum() ); // }// I know there's some check we can do with this to preserve 1-em high plots. for ( int i=0; i<nrow; i++ ) { List<Plot> plots= DomOps.getPlotsFor( dom, rows[i], true ); double MaxUpJEm; double MaxDownPx; for ( Plot plotj : plots ) { String title= plotj.getTitle(); String content= title; // title.replaceAll("(\\!c|\\!C|\\<br\\>)", " "); boolean addLines= plotj.isDisplayTitle() && content.trim().length()>0; int lc= lineCount(title); MaxUpJEm= addLines ? Math.max( 2, lc ) : 0.; logger.log(Level.FINE, "{0} addLines: {1} isDiplayTitle: {2} lineCount(title): {3}", new Object[]{plotj.getId(), addLines, plotj.isDisplayTitle(), lc}); //if (MaxUpJEm>0 ) MaxUpJEm= MaxUpJEm+1; MaxUp[i]= Math.max( MaxUp[i], MaxUpJEm*emToPixels ); Rectangle plot= plotj.getController().getDasPlot().getBounds(); Rectangle axis= plotj.getXaxis().getController().getDasAxis().getBounds(); MaxDownPx= ( ( axis.getY() + axis.getHeight() ) - ( plot.getY() + plot.getHeight() ) + 1 * emToPixels ); MaxDown[i]= Math.max( MaxDown[i], MaxDownPx ); } } double [] relativePlotHeight= new double[ nrow ]; for ( int i=0; i<nrow; i++ ) { DasRow dasRow= rows[i].getController().dasRow; relativePlotHeight[i]= 1.0 * dasRow.getHeight() / totalPlotHeightPixels; } double newPlotTotalHeightPixels= canvas.height; for ( int i=0; i<nrow; i++ ) { newPlotTotalHeightPixels = newPlotTotalHeightPixels - MaxUp[i] - MaxDown[i]; } double [] newPlotHeight= new double[ nrow ]; for ( int i=0; i<nrow; i++ ) { newPlotHeight[i]= newPlotTotalHeightPixels * relativePlotHeight[i]; } double[] normalPlotHeight= new double[ nrow ]; double height= dom.getCanvases(0).getMarginRow().getController().getDasRow().getHeight(); double marginHeightPixels= ( dom.getCanvases(0).getMarginRow().getController().getDasRow().getEmMinimum() - dom.getCanvases(0).getMarginRow().getController().getDasRow().getEmMaximum() ) * emToPixels ; if ( nrow==1 ) { normalPlotHeight[0]= ( newPlotHeight[0] + MaxUp[0] + MaxDown[0] ) / ( height + marginHeightPixels ); } else { for ( int i=0; i<nrow; i++ ) { normalPlotHeight[i]= ( newPlotHeight[i] + MaxUp[i] + MaxDown[i] ) / ( height + marginHeightPixels ); } } double position=0; for ( int i=0; i<nrow; i++ ) { String newTop= String.format( Locale.US, "%.2f%%%+.2fem", 100*position, MaxUp[i] * pixelsToEm ); rows[i].setTop( newTop ); position+= normalPlotHeight[i]; String newBottom= String.format( Locale.US, "%.2f%%%+.2fem", 100*position, -1 * MaxDown[i] * pixelsToEm ); rows[i].setBottom( newBottom ); DasRow dasRow= rows[i].getController().dasRow; logger.log(Level.FINE, "row {0}: {1},{2} ({3} pixels)", new Object[]{i, newTop, newBottom, dasRow.getHeight() }); } } /** * aggregate all the URIs within the dom. * @param dom */ public static void aggregateAll( Application dom ) { Application oldDom= (Application) dom.copy(); // axis settings, etc. DataSourceFilter[] dsfs= dom.getDataSourceFilters(); for ( DataSourceFilter dsf: dsfs ) { if ( dsf.uri==null || dsf.uri.length()==0 ) continue; if ( dsf.uri.startsWith("vap+internal:") ) continue; String agg= DataSourceUtil.makeAggregation( dsf.uri ); if ( agg!=null ) { dsf.setUri(agg); } } dom.setDataSourceFilters(dsfs); dom.syncTo( oldDom, Collections.singletonList( "dataSourceFilters" ) ); } }