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