/******************************************************************************* * Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.trend.impl; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Font; import java.awt.FontMetrics; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.font.GlyphVector; import java.awt.font.LineMetrics; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.text.Format; import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; import java.util.List; import org.simantics.databoard.Bindings; import org.simantics.databoard.accessor.error.AccessorException; import org.simantics.databoard.binding.Binding; import org.simantics.databoard.binding.NumberBinding; import org.simantics.databoard.binding.error.BindingException; import org.simantics.g2d.utils.GridSpacing; import org.simantics.g2d.utils.GridUtil; import org.simantics.history.HistoryException; import org.simantics.history.util.Stream; import org.simantics.history.util.ValueBand; import org.simantics.scenegraph.utils.ColorUtil; import org.simantics.trend.configuration.TrendItem.Renderer; import org.simantics.trend.configuration.TrendSpec; import org.simantics.utils.format.TimeFormat; public class Plot extends TrendGraphicalNode { public static final double VALUE_TIP_BOX_PLOT_MARGIN = 7; private static final double VALUE_TIP_BOX_FILM_MARGIN = 5; private static final long serialVersionUID = 6335497685577733932L; public static final BasicStroke BORDER_LINE_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f); public static final BasicStroke MILESTONE_LINE_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f); public static final BasicStroke TREND_LINE_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, null, 0.0f); public static final BasicStroke DASHED_LINE_STROKE = new BasicStroke(2.0f, // Width BasicStroke.CAP_BUTT, // End cap BasicStroke.JOIN_MITER, // Join style 5.0f, // Miter limit new float[] {5.0f,5.0f}, // Dash pattern 0.0f); // Dash phase public static final BasicStroke DASHED_LINE_STROKE_2 = new BasicStroke(2.0f, // Width BasicStroke.CAP_BUTT, // End cap BasicStroke.JOIN_MITER, // Join style 5.0f, // Miter limit new float[] {5.0f,5.0f}, // Dash pattern 5.0f); // Dash phase public static final BasicStroke DASHED_LINE_STROKE_INVERSE = new BasicStroke(1.0f, // Width BasicStroke.CAP_BUTT, // End cap BasicStroke.JOIN_MITER, // Join style 5.0f, // Miter limit new float[] {3.0f,8.0f}, // Dash pattern 0.0f); // Dash phase public static final Color PLOT_AREA_BG_GRADIENT_COLOR_TOP = new Color(228, 228, 248); public static final Color PLOT_AREA_BG_GRADIENT_COLOR_BOTTOM = new Color(250, 250, 250); static final Font MILESTONE_FONT, BASELINE_FONT, TOOLTIP_FONT; static final GridSpacing SOME_SPACING = GridSpacing.makeGridSpacing(100, 100, 15); public static final Color GRID_LINE_COLOR = new Color(190, 190, 220); static final double DIAMOND_SIZE = 7; static final Path2D DIAMOND; double analogAreaHeight; double binaryAreaHeight; Rectangle2D valueTipBoxBounds = new Rectangle2D.Double(); @SuppressWarnings("unused") @Override protected void doRender(Graphics2D g2d) { //long startTime = System.nanoTime(); TrendNode trend = (TrendNode) getParent(); TrendSpec ts = trend.spec; ViewRenderingProfile rprof = trend.renderingProfile; double w = bounds.getWidth(); double h = bounds.getHeight(); double from = trend.horizRuler.from; double end = trend.horizRuler.end; GridSpacing xGrid = trend.horizRuler.spacing; GridSpacing yGrid = trend.vertRuler != null ? trend.vertRuler.spacing : Plot.SOME_SPACING; if (w<1. || h<1.) return; // Prepare data if (trend.shapedirty) { trend.shapedirty = false; for (ItemNode node : trend.analogItems) prepareItem(node, 0, analogAreaHeight); for (ItemNode node : trend.binaryItems) prepareItem(node, 0, analogAreaHeight); } // Fill gradient Gradient: { Paint bgp = rprof.backgroundColor2 == null ? rprof.backgroundColor1 : new GradientPaint( 0.f, (float) h, rprof.backgroundColor1, 0.f, 0.f, rprof.backgroundColor2, false); // g2d.setPaint(Color.white); g2d.setPaint( bgp ); g2d.fill(bounds); } // Draw grid lines GridLines: if (ts.viewProfile.showGrid) { g2d.setPaint( rprof.gridColor ); g2d.setStroke( GridUtil.GRID_LINE_STROKE ); GridUtil.paintGridLines( xGrid, yGrid, g2d, from - trend.horizRuler.basetime, trend.vertRuler != null ? trend.vertRuler.min : 0, w, h, analogAreaHeight); } Rectangle2D oldClip = g2d.getClipBounds(); // Draw analog items AnalogItems: if ( !trend.analogItems.isEmpty() ) { Rectangle2D analogAreaClip = new Rectangle2D.Double(0, 0, getWidth(), analogAreaHeight); g2d.setClip( analogAreaClip ); for (int phase=0; phase<4; phase++) { for (int i=0; i ls = mss.milestones; if ( ls.isEmpty() ) break Milestone; Line2D line = new Line2D.Double(0, 0, 0, h); g2d.setStroke( MILESTONE_LINE_STROKE ); Rectangle2D diamondRegion = new Rectangle2D.Double(0, -DIAMOND_SIZE*2, w, DIAMOND_SIZE*2); for (int i=mss.milestones.size()-1; i>=0; i--) { Milestone ms = mss.milestones.get( i ); if ( ms.hidden ) continue; boolean isBaseline = i == mss.baseline; double time = ms.time; double x = (time-from)*sx; if (x<-DIAMOND_SIZE*2 || x>w+DIAMOND_SIZE*2) continue; x = Math.floor(x); // Diamond g2d.setClip(diamondRegion); g2d.translate( x, 0); g2d.setColor( isBaseline ? Color.LIGHT_GRAY : Color.DARK_GRAY ); g2d.fill( DIAMOND ); g2d.setColor( Color.BLACK ); g2d.draw( DIAMOND ); // Text Font f = isBaseline ? BASELINE_FONT : MILESTONE_FONT; g2d.setFont( f ); g2d.setColor( isBaseline ? Color.black : Color.ORANGE ); GlyphVector glyphVector = f.createGlyphVector(g2d.getFontRenderContext(), ms.label); double cx = glyphVector.getVisualBounds().getCenterX(); double cy = glyphVector.getVisualBounds().getHeight(); g2d.drawString(ms.label, (float)(-cx), (float)(-DIAMOND_SIZE+cy/2) ); g2d.setClip( null ); // Line if (x>=0 && x=from && time<=end && !Double.isNaN(time)) { double sx = getWidth() / ( end - from ); double x = (time-from)*sx; Line2D line = new Line2D.Double(x, 0, x, h); g2d.setStroke( DASHED_LINE_STROKE_2 ); g2d.setColor( trend.valueTipHover ? Color.GRAY : Color.WHITE ); g2d.draw( line ); g2d.setStroke( DASHED_LINE_STROKE ); g2d.setColor( Color.BLACK ); g2d.draw( line ); // g2d.setStroke( DASHED_LINE_STROKE_INVERSE ); // g2d.setColor( Color.white ); // g2d.draw( line ); } } // Draw border Border: { g2d.setStroke(BORDER_LINE_STROKE); g2d.setColor( Color.BLACK ); Rectangle2D rect = new Rectangle2D.Double(); rect.setFrame(0, 0, w, h); g2d.draw(rect); } //long endTime = System.nanoTime(); // System.out.println("Plot render: "+((double)(endTime-startTime)/1000000)+" ms"); } public void drawItem(Graphics2D g, ItemNode data, double y, double height, int phase) { TrendNode trend = getTrend(); double from = trend.horizRuler.from; double end = trend.horizRuler.end; // trend.vertRulerIndex // boolean selected = trend.singleAxis ? false : trend.vertRuler // selected &= !trend.printing; VertRuler ruler = data.ruler; AffineTransform at = g.getTransform(); try { //double pixelsPerSecond = (end-from) / getWidth(); g.translate(0, y); g.setStroke(data.stroke); AffineTransform ab = new AffineTransform(); if (data.item.renderer == Renderer.Analog) { ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) ); ab.translate(-from, -ruler.max); // if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab); data.draw(g, phase, false); } if (data.item.renderer == Renderer.Binary) { ab.scale( getWidth()/(end-from), 1/*height*/ ); ab.translate(-from, 0); // if (phase == 0) data.prepareLine(from, end, pixelsPerSecond, ab); data.draw(g, phase, false); } // } catch (HistoryException e) { // e.printStackTrace(); // } catch (AccessorException e) { // e.printStackTrace(); } finally { g.setTransform(at); } } /** * Prepare item for draw. * * @param data * @param y * @param height */ public void prepareItem(ItemNode data, double y, double height) { TrendNode tn = getTrend(); double from = tn.horizRuler.from; double end = tn.horizRuler.end; VertRuler ruler = data.ruler; try { double pixelsPerSecond = (end-from) / getWidth(); AffineTransform ab = new AffineTransform(); if (data.item.renderer == Renderer.Analog) { ab.scale( getWidth()/(end-from), height/(ruler.min-ruler.max) ); ab.translate(-from, -ruler.max); data.prepareLine(from, end, pixelsPerSecond, ab); } if (data.item.renderer == Renderer.Binary) { ab.scale( getWidth()/(end-from), 1/*height*/ ); ab.translate(-from, 0); data.prepareLine(from, end, pixelsPerSecond, ab); } } catch (HistoryException e) { e.printStackTrace(); } catch (AccessorException e) { e.printStackTrace(); } } static { DIAMOND = new Path2D.Double(); DIAMOND.moveTo(0, -DIAMOND_SIZE*2); DIAMOND.lineTo(DIAMOND_SIZE, -DIAMOND_SIZE); DIAMOND.lineTo(0, 0); DIAMOND.lineTo(-DIAMOND_SIZE, -DIAMOND_SIZE); DIAMOND.lineTo(0, -DIAMOND_SIZE*2); MILESTONE_FONT = new Font("Tahoma", 0, (int) (DIAMOND_SIZE*1.2) ); BASELINE_FONT = new Font("Tahoma", Font.BOLD, (int) (DIAMOND_SIZE*1.2) ); TOOLTIP_FONT = new Font("Tahoma", 0, 13 ); } /// ValueTip public static final AlphaComposite composite66 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .80f); void drawValuetip( Graphics2D g, double time ) throws HistoryException, BindingException { TrendNode trend = getTrend(); Font font = TOOLTIP_FONT; FontMetrics fm = g.getFontMetrics( font ); //double from = trend.horizRuler.from; //double end = trend.horizRuler.end; //double pixelsPerSecond = (end-from) / trend.plot.getWidth(); double marginInPlot = VALUE_TIP_BOX_PLOT_MARGIN; double marginOnFilm = VALUE_TIP_BOX_FILM_MARGIN; double marginBetweenNamesAndValues = 16; double quantumWidthOfValueArea = 20; double marginBetweenLines = 5; double textAreaHeight = 0; double textAreaWidth = 0; double valueAreaLeft = 0; double valueAreaRight = 0; double valueAreaWidth = 0; double x, y, w, h; List tipLines = new ArrayList( trend.allItems.size()+1 ); /// Initialize // Add time { TipLine tl = new TipLine(); tipLines.add(tl); tl.label = "Time"; LineMetrics lm = fm.getLineMetrics(tl.label, g); tl.height = lm.getHeight(); tl.labelBaseline = fm.getAscent(); textAreaHeight += marginBetweenLines; textAreaHeight += tl.height; tl.labelWidth = fm.stringWidth( tl.label ); textAreaWidth = Math.max(tl.labelWidth, textAreaWidth); tl.color = Color.WHITE; Format f = NumberFormat.getInstance(); if (trend.timeFormat == org.simantics.trend.configuration.TimeFormat.Time) { f = new TimeFormat(trend.horizRuler.iEnd, 3); } double t = time - trend.horizRuler.basetime; String formattedTime = f.format( time - trend.horizRuler.basetime ); double roundedTime = t; try { roundedTime = (Double) f.parseObject(formattedTime); } catch (ParseException e) { // Should never happen. } boolean actuallyLessThan = t < roundedTime; boolean actuallyMoreThan = t > roundedTime; tl.value = actuallyLessThan ? "< " + formattedTime : actuallyMoreThan ? "> " + formattedTime : formattedTime; tl.valueWidth = fm.stringWidth( tl.value ); valueAreaWidth = Math.max(valueAreaWidth, tl.valueWidth); } // Add items nextItem: for ( ItemNode i : trend.allItems ) { TipLine tl = new TipLine(); tipLines.add(tl); // Get Label tl.label = i.item.label; LineMetrics lm = fm.getLineMetrics(tl.label, g); tl.height = lm.getHeight(); tl.labelBaseline = fm.getAscent(); textAreaHeight += tl.height; textAreaHeight += marginBetweenLines; tl.labelWidth = fm.stringWidth( tl.label ); textAreaWidth = Math.max(tl.labelWidth, textAreaWidth); tl.color = ColorUtil.gamma( i.color, 0.55 ); // Get Value Stream s = i.openStream( /*pixelsPerSecond*/0 ); if ( s!=null ) { int index = s.binarySearch(Bindings.DOUBLE, time); if (index<0) index = -index-2; if ( index<0 || index>=s.count() ) continue nextItem; boolean isLast = index+1>=s.count(); ValueBand vb = new ValueBand(s.sampleBinding); try { vb.setSample( s.accessor.get(index, s.sampleBinding) ); } catch (AccessorException e) { throw new HistoryException(e); } if ( vb.getSample()!=null ) { if (isLast && vb.hasEndTime() && vb.getEndTimeDouble()=0 ) { String beforeDesimal = tl.value.substring(0, desimalPos); String afterDesimal = tl.value.substring(desimalPos, tl.value.length()); tl.valueLeftWidth = fm.stringWidth(beforeDesimal); tl.valueRightWidth = fm.stringWidth(afterDesimal); tl.valueWidth = tl.valueLeftWidth + tl.valueRightWidth; } else { tl.valueWidth = tl.valueLeftWidth = fm.stringWidth(tl.value); } valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth+tl.valueRightWidth); valueAreaLeft = Math.max(valueAreaLeft, tl.valueLeftWidth); valueAreaRight = Math.max(valueAreaRight, tl.valueRightWidth); } else { Object v = vb.getValue(); tl.value = b.toString(v); tl.number = false; tl.valueLeftWidth = tl.valueRightWidth = fm.stringWidth( tl.value ); valueAreaWidth = Math.max(valueAreaWidth, tl.valueLeftWidth); } } } } } // Layout double halfQuantum = quantumWidthOfValueArea/2; valueAreaWidth = Math.ceil( valueAreaWidth / quantumWidthOfValueArea ) * quantumWidthOfValueArea; valueAreaLeft = Math.ceil( valueAreaLeft / halfQuantum ) * halfQuantum; valueAreaRight = Math.ceil( valueAreaRight / halfQuantum ) * halfQuantum; double finalValueAreaWidth = Math.max(valueAreaWidth, valueAreaLeft + valueAreaRight); w = marginOnFilm + textAreaWidth + marginBetweenNamesAndValues + finalValueAreaWidth + marginOnFilm + 0; h = marginOnFilm + textAreaHeight + marginOnFilm; double maxX = trend.plot.getWidth() - marginInPlot - w; double maxY = trend.plot.getHeight() - marginInPlot - h; x = marginInPlot + (maxX - marginInPlot)*trend.spec.viewProfile.valueViewPositionX; y = marginInPlot + (maxY - marginInPlot)*trend.spec.viewProfile.valueViewPositionY; if ( x < TrendLayout.VERT_MARGIN ) x = TrendLayout.VERT_MARGIN; valueTipBoxBounds.setFrame(x, y, w, h); //System.out.println("value tip bounds: " + valueTipBounds); // Draw Rectangle2D rect = new Rectangle2D.Double(0, 0, w, h); Composite oldComposite = g.getComposite(); AffineTransform oldTransform = g.getTransform(); try { g.setComposite(composite66); g.translate(x, y); g.setColor(Color.BLACK); g.fill( rect ); g.setFont( font ); g.setComposite(oldComposite); y = marginInPlot; for (TipLine tl : tipLines) { g.setColor( tl.color ); x = marginInPlot; g.drawString( tl.label, (float)x, (float)(y+tl.labelBaseline)); if ( tl.value!=null ) { x = marginInPlot + textAreaWidth + marginBetweenNamesAndValues; if ( tl.number ) { x += valueAreaLeft - tl.valueLeftWidth; } else { x += (finalValueAreaWidth - tl.valueWidth)/2; } g.drawString(tl.value, (float) x, (float) (y+tl.labelBaseline)); } y+=tl.height; y+=marginBetweenLines; } } finally { g.setTransform( oldTransform ); g.setComposite( oldComposite ); } } static class TipLine { String label, value; Color color; double labelWidth, height, valueLeftWidth, valueRightWidth, valueWidth, labelBaseline; boolean number; @Override public String toString() { return "TipLine[label=" + label + ", value=" + value + ", color=" + color + ", labelWidth=" + labelWidth + ", height=" + height + ", valueLeftWidth=" + valueLeftWidth + ", valueRightWidth=" + valueRightWidth + ", valueWidth=" + valueWidth + ", labelBaseline=" + labelBaseline + ", number=" + number + "]"; } } public void renderValueTip(Graphics2D g2d) { TrendNode trend = getTrend(); if ( trend.valueTipTime != null ) { AffineTransform at = g2d.getTransform(); try { g2d.transform( getTransform() ); drawValuetip( g2d, trend.valueTipTime ); } catch (HistoryException e) { e.printStackTrace(); } catch (BindingException e) { e.printStackTrace(); } finally { g2d.setTransform( at ); } } } /** * Pick item (Binary node) * * @param pt coordinate in trend coordinate system * @return item node */ public ItemNode pickItem(Point2D pt) { TrendNode trend = getTrend(); double y = pt.getY()-getY(); double x = pt.getX()-getX(); if (yanalogAreaHeight+binaryAreaHeight) return null; if (x<0 || x+getX()>trend.getBounds().getWidth()) return null; for (int i=0; i=sy && y