X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.trend%2Fsrc%2Forg%2Fsimantics%2Ftrend%2Fimpl%2FPlot.java;fp=bundles%2Forg.simantics.trend%2Fsrc%2Forg%2Fsimantics%2Ftrend%2Fimpl%2FPlot.java;h=bb5de8c0dd22bb56ecaea3646cda27df927cc081;hb=969bd23cab98a79ca9101af33334000879fb60c5;hp=0000000000000000000000000000000000000000;hpb=866dba5cd5a3929bbeae85991796acb212338a08;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.trend/src/org/simantics/trend/impl/Plot.java b/bundles/org.simantics.trend/src/org/simantics/trend/impl/Plot.java new file mode 100644 index 000000000..bb5de8c0d --- /dev/null +++ b/bundles/org.simantics.trend/src/org/simantics/trend/impl/Plot.java @@ -0,0 +1,624 @@ +/******************************************************************************* + * 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