]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendNode.java
Fixed all line endings of the repository
[simantics/platform.git] / bundles / org.simantics.trend / src / org / simantics / trend / impl / TrendNode.java
index dba76ccad2afdc13565cdc1412a62ab20a358883..575af21f5aa77265d0ee80298ceb1e8c3eb791dd 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2011 Association for Decentralized Information Management in\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.BasicStroke;\r
-import java.awt.Color;\r
-import java.awt.Font;\r
-import java.awt.Graphics2D;\r
-import java.awt.Rectangle;\r
-import java.awt.RenderingHints;\r
-import java.awt.font.GlyphVector;\r
-import java.awt.geom.Rectangle2D;\r
-import java.util.ArrayList;\r
-import java.util.Collections;\r
-import java.util.Comparator;\r
-import java.util.HashMap;\r
-import java.util.HashSet;\r
-import java.util.List;\r
-import java.util.Map;\r
-import java.util.Set;\r
-import java.util.TreeSet;\r
-\r
-import org.simantics.databoard.Bindings;\r
-import org.simantics.databoard.accessor.error.AccessorException;\r
-import org.simantics.databoard.binding.error.BindingException;\r
-import org.simantics.databoard.util.Bean;\r
-import org.simantics.g2d.utils.GridUtil;\r
-import org.simantics.history.Collector;\r
-import org.simantics.history.History;\r
-import org.simantics.history.HistoryException;\r
-import org.simantics.history.HistoryManager;\r
-import org.simantics.history.ItemManager;\r
-import org.simantics.history.impl.CollectorImpl;\r
-import org.simantics.history.util.Stream;\r
-import org.simantics.history.util.ValueBand;\r
-import org.simantics.scenegraph.g2d.G2DParentNode;\r
-import org.simantics.scenegraph.utils.QualityHints;\r
-import org.simantics.trend.configuration.ItemPlacement;\r
-import org.simantics.trend.configuration.LineQuality;\r
-import org.simantics.trend.configuration.Scale;\r
-import org.simantics.trend.configuration.TimeFormat;\r
-import org.simantics.trend.configuration.TimeWindow;\r
-import org.simantics.trend.configuration.TrendItem;\r
-import org.simantics.trend.configuration.TrendItem.Renderer;\r
-import org.simantics.trend.configuration.TrendQualitySpec;\r
-import org.simantics.trend.configuration.TrendSpec;\r
-import org.simantics.trend.configuration.Viewport;\r
-import org.simantics.trend.configuration.Viewport.AxisViewport;\r
-import org.simantics.trend.configuration.YAxisMode;\r
-import org.simantics.utils.format.ValueFormat;\r
-\r
-import gnu.trove.map.TObjectIntMap;\r
-import gnu.trove.map.hash.TObjectIntHashMap;\r
-import gnu.trove.procedure.TObjectProcedure;\r
-\r
-public class TrendNode extends G2DParentNode implements TrendLayout {\r
-\r
-       private static final long serialVersionUID = -8339696405893626168L;\r
-\r
-       /** Title node */\r
-       public TextNode titleNode;\r
-\r
-       /** Plot node */\r
-       public Plot plot;\r
-       public HorizRuler horizRuler;\r
-       VertRuler vertRuler;   // Selected vertical ruler, null if there are no analog items\r
-       int vertRulerIndex;    // Index of the selected vertical ruler\r
-       List<VertRuler> vertRulers = new ArrayList<VertRuler>();\r
-       SelectionNode selection;\r
-       public MilestoneSpec milestones;\r
-       \r
-       /** Control bounds */\r
-       Rectangle2D bounds = new Rectangle2D.Double();\r
-       \r
-       /** Trend spec */\r
-       public TrendSpec spec;\r
-       public ViewRenderingProfile renderingProfile = new ViewRenderingProfile();\r
-       public TrendQualitySpec quality = TrendQualitySpec.DEFAULT;\r
-       public boolean printing = false;\r
-       boolean singleAxis;\r
-       \r
-       // Data nodes\r
-       List<ItemNode> analogItems = new ArrayList<ItemNode>();\r
-       List<ItemNode> binaryItems = new ArrayList<ItemNode>();\r
-       List<ItemNode> allItems = new ArrayList<ItemNode>();\r
-\r
-       // History\r
-       static HistoryManager DUMMY_HISTORY = History.createMemoryHistory();\r
-       public HistoryManager historian = DUMMY_HISTORY;\r
-       \r
-       // Collector - set for flushing the stream, right before drawing\r
-       public Collector collector = null;\r
-       Set<String> itemIds = new HashSet<String>();\r
-       \r
-       // Signal to indicate history has changed.\r
-       public boolean datadirty = false;\r
-       // Signal to indicate the cached shapes are dirty. This will cause reloading of data.\r
-       public boolean shapedirty = false;\r
-       \r
-       public boolean autoscaletime = true;\r
-       \r
-       public TimeFormat timeFormat = TimeFormat.Time;\r
-       public ValueFormat valueFormat = ValueFormat.Default;\r
-       public boolean drawSamples = false;\r
-       \r
-       /** Time at mouse, when mouse hovers over trend. This value is set by TrendParticipant. NaN is mouse is not hovering */\r
-       public double mouseHoverTime = Double.NaN, lastMouseHoverTime = 0.0;\r
-       \r
-       /** If set, valueTip is drawn at the time */\r
-       public Double valueTipTime = null, prevValueTipTime = null;\r
-       public boolean valueTipHover = false;\r
-\r
-       public ItemPlacement itemPlacement = ItemPlacement.Overlapping;\r
-       \r
-       @Override\r
-       public void init() {\r
-               spec = new TrendSpec();\r
-               spec.init();\r
-               \r
-               milestones = new MilestoneSpec();\r
-               milestones.init();\r
-               \r
-               //// Create title node\r
-               titleNode = addNode( "Title", TextNode.class );\r
-               titleNode.setFont( new Font("Arial", 0, 30) );\r
-               titleNode.setColor( Color.BLACK );\r
-               titleNode.setText( "<title here>");\r
-               titleNode.setSize(300, 40);\r
-               \r
-               plot = addNode( "Plot", Plot.class );\r
-               \r
-               horizRuler = addNode("HorizRuler", HorizRuler.class);\r
-               vertRuler = addNode("VertRuler", VertRuler.class);\r
-               vertRulers.add( vertRuler );\r
-               \r
-               /// Set some bounds\r
-               horizRuler.setFromEnd(0, 100);\r
-               vertRuler.setMinMax(0, 100);\r
-               setSize(480, 320);\r
-       }\r
-       \r
-       /**\r
-        * Set source of data\r
-        * @param historian\r
-        * @param collector (Optional) Used for flushing collectors \r
-        */\r
-       public void setHistorian(HistoryManager historian, Collector collector) {\r
-               this.historian = historian==null?DUMMY_HISTORY:historian;\r
-               this.collector = collector;\r
-               itemIds.clear();\r
-               ItemManager allFiles = ItemManager.createUnchecked( getHistoryItems() );\r
-               for (ItemNode item : allItems) {\r
-                       item.setTrendItem(item.item, allFiles);\r
-                       for (Bean historyItem : item.historyItems ) {\r
-                               try {\r
-                                       itemIds.add( (String) historyItem.getField("id") );\r
-                               } catch (BindingException e) {\r
-                               }\r
-                       }\r
-               }\r
-       }\r
-       \r
-       /**\r
-        * Get info of all history items.\r
-        * This util is created for polling strategy.  \r
-        * \r
-        * @return\r
-        */\r
-       Bean[] getHistoryItems() {\r
-               Bean[] result = null;\r
-               HistoryException e = null;\r
-               for (int attempt=0; attempt<10; attempt++) {\r
-                       try {\r
-                               result = historian.getItems();\r
-                               break;\r
-                       } catch (HistoryException e2) {\r
-                               if (e==null) e = e2;\r
-                               try {\r
-                                       Thread.sleep(1);\r
-                               } catch (InterruptedException e1) {\r
-                               }\r
-                       }\r
-               }\r
-               if (result!=null) return result;\r
-               //throw new RuntimeException(e);\r
-               return new Bean[0];\r
-       }\r
-       \r
-       public void setMilestones(Bean milestones) {\r
-               if (this.milestones.equals(milestones)) return;\r
-               this.milestones.readFrom( milestones );         \r
-               boolean hasBaseline = this.milestones.baseline>=0;\r
-\r
-               // Sort by first, put baseline on top\r
-               final Milestone baseline = this.milestones.baseline>=0 ? this.milestones.milestones.get( this.milestones.baseline ) : null;\r
-               Collections.sort(this.milestones.milestones, new Comparator<Milestone>() {\r
-                       public int compare(Milestone o1, Milestone o2) {\r
-                               if (o1==baseline) return -1;\r
-                               if (o2==baseline) return 1;\r
-                               return Double.compare(o1.time, o1.time);\r
-                       }});\r
-               \r
-               this.milestones.baseline = hasBaseline ? 0 : -1;                \r
-               double newBasetime = hasBaseline ? this.milestones.milestones.get(this.milestones.baseline).time : 0.;\r
-               if (newBasetime != horizRuler.basetime) {                       \r
-                       horizRuler.basetime = newBasetime;\r
-                       horizRuler.layout();\r
-               }\r
-               shapedirty = true;\r
-       }\r
-       \r
-       public void selectVertRuler(int index) {\r
-               vertRulerIndex = index;\r
-               if (index<0 || index>=vertRulers.size()) {\r
-                       vertRuler = vertRulers.get(0);\r
-               } else {\r
-                       vertRuler = vertRulers.get(index);\r
-               }\r
-               shapedirty = true;\r
-       }\r
-       \r
-       @Override\r
-       public void cleanup() {\r
-               spec = new TrendSpec();\r
-               spec.init();\r
-               analogItems.clear();\r
-               binaryItems.clear();\r
-               allItems.clear();\r
-               historian = DUMMY_HISTORY;\r
-               super.cleanup();\r
-       }\r
-\r
-       private static TObjectIntMap<String> itemIndexMap(List<TrendItem> items) {\r
-               TObjectIntMap<String> map = new TObjectIntHashMap<>(items.size(), 0.5f, -1);\r
-               for (int i = 0; i < items.size(); ++i) {\r
-                       TrendItem it = items.get(i);\r
-                       map.put(it.groupItemId, i);\r
-               }\r
-               return map;\r
-       }\r
-\r
-       private static <T> TObjectIntMap<T> subtract(TObjectIntMap<T> a, TObjectIntMap<T> b) {\r
-               TObjectIntMap<T> r = new TObjectIntHashMap<>(a);\r
-               b.forEachKey(new TObjectProcedure<T>() {\r
-                       @Override\r
-                       public boolean execute(T key) {\r
-                               r.remove(key);\r
-                               return true;\r
-                       }\r
-               });\r
-               return r;\r
-       }\r
-\r
-       /**\r
-        * @param newSpec\r
-        *            new trending specification, cannot not be <code>null</code>.\r
-        *            Use {@link TrendSpec#EMPTY} instead of <code>null</code>.\r
-        */\r
-       public void setTrendSpec(TrendSpec newSpec) {\r
-               //System.out.println(newSpec);\r
-               // Check if equal & Read spec\r
-               if (newSpec.equals(this.spec)) return;\r
-\r
-               boolean timeWindowChange = !this.spec.viewProfile.timeWindow.equals( newSpec.viewProfile.timeWindow );\r
-               boolean yaxisModeChanged = this.spec.axisMode != newSpec.axisMode;\r
-\r
-               TObjectIntMap<String> newItemMap = itemIndexMap(newSpec.items);\r
-               TObjectIntMap<String> currentItemMap = itemIndexMap(spec.items);\r
-               TObjectIntMap<String> removedItemMap = subtract(currentItemMap, newItemMap);\r
-               Map<String, VertRuler> existingRulers = new HashMap<>();\r
-               if (this.spec.axisMode == YAxisMode.MultiAxis) {\r
-                       for (ItemNode item : analogItems)\r
-                               if (item.ruler != null)\r
-                                       existingRulers.put(item.item.groupItemId, item.ruler);\r
-               }\r
-\r
-               this.spec.readFrom( newSpec );\r
-               this.spec.sortItems();\r
-               this.renderingProfile.read(this.spec.viewProfile);\r
-\r
-               // Set title\r
-               if (titleNode != null) titleNode.setText( spec.name );\r
-\r
-               // Setup trend item nodes\r
-               itemIds.clear();\r
-               for (ItemNode item : allItems) removeNode(item);\r
-               analogItems.clear();\r
-               binaryItems.clear();\r
-               allItems.clear();\r
-               \r
-               ItemManager itemManager = ItemManager.createUnchecked( getHistoryItems() );\r
-               for (int i = 0; i<spec.items.size(); i++) {\r
-                       TrendItem item = spec.items.get(i);\r
-                       if (item.hidden)\r
-                               continue;\r
-                       \r
-                       ItemNode node = createItemNode(item, itemManager);\r
-                       for (Bean historyItem : node.historyItems) {\r
-                               try {\r
-                                       itemIds.add( (String) historyItem.getField("id") );\r
-                               } catch (BindingException e) {\r
-                               }\r
-                       }\r
-                       \r
-                       if (item.renderer == Renderer.Analog) {\r
-                               analogItems.add(node);\r
-                       } else {\r
-                               binaryItems.add(node);\r
-                       }\r
-                       allItems.add(node);\r
-               }\r
-\r
-               // Setup vertical ruler nodes\r
-               singleAxis = spec.axisMode == YAxisMode.SingleAxis;\r
-               if (singleAxis) {\r
-                       if (yaxisModeChanged || vertRulers.size() != 1 || vertRuler == null) {\r
-                               for (VertRuler vr : vertRulers) removeNode(vr);\r
-                               vertRulers.clear();\r
-\r
-                               vertRuler = addNode("VertRuler", VertRuler.class);\r
-                               vertRulers.add( vertRuler );\r
-                       }\r
-\r
-                       vertRuler.manualscale = true;\r
-                       for (int i=0; i<analogItems.size(); i++) {\r
-                               ItemNode item = analogItems.get(i);\r
-                               item.ruler = vertRuler;\r
-                               item.trendNode = this;\r
-                               if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false;\r
-                       }\r
-               } else {\r
-                       if (yaxisModeChanged) {\r
-                               // Recreate all rulers\r
-                               for (VertRuler vr : vertRulers) removeNode(vr);\r
-                               vertRulers.clear();\r
-                               for (int i=0; i<analogItems.size(); i++)\r
-                                       vertRulers.add( addNode(VertRuler.class) );\r
-                       } else {\r
-                               // Remove rulers of the items that were removed\r
-                               // and add new rulers to have enough of them for\r
-                               // each separate analog signal.\r
-                               removedItemMap.forEachKey(new TObjectProcedure<String>() {\r
-                                       @Override\r
-                                       public boolean execute(String id) {\r
-                                               VertRuler vr = existingRulers.get(id);\r
-                                               if (vr != null) {\r
-                                                       removeNode(vr);\r
-                                                       vertRulers.remove(vr);\r
-                                               }\r
-                                               return true;\r
-                                       }\r
-                               });\r
-                               for (int i = vertRulers.size(); i < analogItems.size(); ++i) {\r
-                                       VertRuler ruler = addNode(VertRuler.class);\r
-                                       vertRulers.add(ruler);\r
-                               }\r
-                       }\r
-\r
-                       for (int i = 0; i < analogItems.size(); i++) {\r
-                               ItemNode item = analogItems.get(i);\r
-                               VertRuler vr = vertRulers.get(i);\r
-                               vr.setZIndex(1000 + i);\r
-                               vr.color = item.color;\r
-                               vr.label = item.item.label;\r
-                               vr.manualscale = item.item.scale instanceof Scale.Manual;\r
-                               item.ruler = vr;\r
-                               item.trendNode = this;\r
-                       }\r
-                       // Select vert ruler\r
-                       vertRuler = vertRulers.isEmpty() ? null : vertRulers.get( vertRulerIndex <= 0 || vertRulerIndex >= vertRulers.size() ? 0 : vertRulerIndex );\r
-               }\r
-               \r
-               // Locked\r
-               TimeWindow tw = spec.viewProfile.timeWindow;\r
-               horizRuler.manualscale = tw.timeWindowLength!=null && tw.timeWindowStart!=null;\r
-               \r
-               if (timeWindowChange) {\r
-                       horizRuler.autoscale();\r
-               }\r
-               shapedirty = true;\r
-       }\r
-\r
-    private ItemNode createItemNode(TrendItem item, ItemManager itemManager) {\r
-        ItemNode node = addNode( ItemNode.class );\r
-        //node.trendNode = this;\r
-        node.setTrendItem(item, itemManager);\r
-        node.color = toColor(item.customColor);\r
-        if (node.color == null)\r
-            node.color = JarisPaints.getColor( item.index );\r
-        node.stroke = item.customStrokeWidth != null\r
-                ? withStrokeWidth(item.customStrokeWidth, Plot.TREND_LINE_STROKE)\r
-                : Plot.TREND_LINE_STROKE;\r
-        return node;\r
-    }\r
-\r
-    private static BasicStroke withStrokeWidth(float width, BasicStroke stroke) {\r
-        return new BasicStroke(width,\r
-                stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),\r
-                stroke.getDashArray(), stroke.getDashPhase());\r
-    }\r
-\r
-    private static Color toColor(float[] components) {\r
-        if (components == null)\r
-            return null;\r
-        switch (components.length) {\r
-        case 3: return new Color(components[0], components[1], components[2]);\r
-        case 4: return new Color(components[0], components[1], components[2], components[3]);\r
-        default: return null;\r
-        }\r
-    }\r
-\r
-       /**\r
-        * Layout graphical nodes based on bounds\r
-        */\r
-       public void layout() {\r
-               double w = bounds.getWidth();\r
-               double h = bounds.getHeight();\r
-               if ( titleNode != null ) {\r
-                       titleNode.setSize(w, h * 0.02);\r
-                       titleNode.setTranslate(0, VERT_MARGIN);\r
-                       titleNode.layout();\r
-               }\r
-               \r
-               // Plot-Ruler area width \r
-               double praw = w-HORIZ_MARGIN*2;\r
-               \r
-               // Plot height\r
-               double ph = h-VERT_MARGIN*2-MILESTONE_HEIGHT-HORIZ_RULER_HEIGHT;\r
-               if ( titleNode != null ) {\r
-                       ph -= titleNode.th;\r
-               }\r
-               \r
-               // Analog & Binary area height\r
-               double aah, bah;\r
-               if (!analogItems.isEmpty()) {\r
-                       bah = binaryItems.size() * BINARY[3];\r
-                       aah = Math.max(0, ph-bah);\r
-                       if (aah+bah>ph) bah = ph-aah;\r
-               } else {\r
-                       // No analog items\r
-                       aah = 0;\r
-                       bah = ph; \r
-               }\r
-               // Vertical ruler\r
-               for (VertRuler vertRuler : vertRulers) {\r
-                       vertRuler.setHeight(aah);\r
-                       vertRuler.layout();\r
-               }\r
-               plot.analogAreaHeight = aah;\r
-               plot.binaryAreaHeight = bah;\r
-\r
-               // Vertical ruler widths\r
-               double vrws = 0;\r
-               for (VertRuler vertRuler : vertRulers) {\r
-                       vrws += vertRuler.getWidth();\r
-               }\r
-               \r
-               // Add room for Binary label\r
-               if ( !binaryItems.isEmpty() ) {\r
-                       double maxLabelWidth = BINARY_LABEL_WIDTH;\r
-                       for (ItemNode node : binaryItems) {\r
-                               if (node.item != null) {\r
-                                       GlyphVector glyphVector = RULER_FONT.createGlyphVector(GridUtil.frc, node.item.label);\r
-                                       double labelWidth = glyphVector.getVisualBounds().getWidth();\r
-                                       maxLabelWidth = Math.max( maxLabelWidth, labelWidth );\r
-                               }\r
-                       }\r
-                       vrws = Math.max(maxLabelWidth, vrws);\r
-               }\r
-               \r
-               // Plot Width\r
-               double pw = praw - vrws;\r
-               plot.setTranslate(HORIZ_MARGIN, (titleNode!=null?titleNode.th:0)+VERT_MARGIN+MILESTONE_HEIGHT);\r
-               plot.setSize(pw, ph);\r
-               \r
-               horizRuler.layout();\r
-               horizRuler.setTranslate(HORIZ_MARGIN, plot.getY()+plot.getHeight()+3);\r
-               boolean l = horizRuler.setWidth(pw);\r
-               l |= horizRuler.setFromEnd(horizRuler.from, horizRuler.end);\r
-               if (l) horizRuler.layout();\r
-               \r
-               // Move vertical rulers\r
-               double vrx = plot.getX() + plot.getWidth() + 3; \r
-               for (VertRuler vertRuler : vertRulers) {\r
-                       vertRuler.setTranslate(vrx, plot.getY());\r
-                       vrx += vertRuler.getWidth() + 3;\r
-               }\r
-               \r
-       }\r
-\r
-       public void setSize(double width, double height) {\r
-               bounds.setFrame(0, 0, width, height);\r
-       }\r
-       \r
-       @Override\r
-       public void render(Graphics2D g2d) {\r
-               // Set Quality High\r
-               QualityHints qh = QualityHints.getQuality(g2d);\r
-        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, quality.textQuality==LineQuality.Antialias?RenderingHints.VALUE_TEXT_ANTIALIAS_ON:RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);\r
-               \r
-               // Set Bounds\r
-               Rectangle bounds = g2d.getClipBounds();         \r
-               if (bounds.getWidth()!=this.bounds.getWidth() || bounds.getHeight()!=this.bounds.getHeight()) {\r
-                       setSize(bounds.getWidth(), bounds.getHeight());\r
-                       layout();\r
-               }\r
-               \r
-               // Flush history subscriptions\r
-               flushHistory();\r
-\r
-               // Render children\r
-               super.render(g2d);\r
-               \r
-               // Render plot's value tip\r
-               plot.renderValueTip(g2d);\r
-               \r
-               // Restore quality\r
-               qh.setQuality(g2d);\r
-       }\r
-       \r
-       @Override\r
-       public Rectangle2D getBoundsInLocal() {\r
-               return bounds;\r
-       }\r
-       \r
-       /**\r
-        * Return true if the viewport is not moving and shows only past values \r
-        * @return\r
-        */\r
-       public boolean allPast() {\r
-               TimeWindow timeWindow = spec.viewProfile.timeWindow;\r
-               boolean fixedWindow = !horizRuler.autoscroll || (timeWindow.timeWindowStart!=null && timeWindow.timeWindowLength!=null);\r
-               if (fixedWindow) {\r
-                       for (ItemNode item : allItems) {\r
-                               if (item.end <= horizRuler.end) return false;\r
-                       }\r
-                       return true;\r
-               } else {\r
-                       return false;\r
-               }\r
-       }\r
-\r
-       public void flushHistory() {\r
-               if (collector == null || collector instanceof CollectorImpl == false) return;\r
-               CollectorImpl fh = (CollectorImpl) collector;\r
-               fh.flush( itemIds );\r
-       }\r
-       \r
-       /**\r
-        * Read values of min,max,from,end to all items.\r
-        * Put iMin,iMax,iFrom,iEnd to all axes.\r
-        */\r
-       public void readMinMaxFromEnd() {\r
-               flushHistory();\r
-               // Read min,max,from,end from all items - Gather collective ranges\r
-               for (ItemNode item : allItems) {\r
-                       item.readMinMaxFromEnd();\r
-               }\r
-               horizRuler.setKnownFromEnd();\r
-               for (VertRuler vertRuler : vertRulers) vertRuler.setKnownMinMax();\r
-       }\r
-\r
-       \r
-       \r
-       /**\r
-        * Flushes history, resets streams,\r
-        * reads the latest time and value ranges from disc.\r
-        * Scales the axes.\r
-        * Sets dirty true if there was change in axis.\r
-        */\r
-       public boolean autoscale(boolean timeAxis, boolean valueAxis) {\r
-               if (!timeAxis && !valueAxis) return false;\r
-\r
-               readMinMaxFromEnd();\r
-               boolean changed = false;\r
-                               \r
-               // Time axis\r
-               if (timeAxis) {\r
-                       changed |= horizRuler.autoscale();\r
-                       if ( changed && !printing ) horizRuler.fireListener();\r
-               }\r
-\r
-               // Y-Axis\r
-               if (valueAxis) {\r
-                       for (VertRuler vertRuler : vertRulers) changed |= vertRuler.autoscale();\r
-               }       \r
-\r
-               return changed;\r
-       }\r
-\r
-       public boolean updateValueTipTime() {\r
-               if (valueTipTime != null && spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {\r
-                       double endTime = horizRuler.getItemEndTime();\r
-                       if (!Double.isNaN(endTime)) {\r
-                               boolean changed = Double.compare(valueTipTime, endTime) != 0;\r
-                               valueTipTime = endTime;\r
-                               return changed;\r
-                       }\r
-               }\r
-               return false;\r
-       }\r
-\r
-       public void zoomIn(double x, double y, double width, double height, boolean horiz, boolean vert) {\r
-               if (horiz) {\r
-                       horizRuler.zoomIn(x, width);\r
-               }\r
-               \r
-               if (vert) {\r
-                       for (VertRuler vertRuler : vertRulers) {\r
-                               vertRuler.zoomIn(y, height);\r
-                       }\r
-               }\r
-               shapedirty = true;\r
-       }\r
-       \r
-       public void zoomOut() {\r
-               horizRuler.zoomOut();\r
-               for (VertRuler vertRuler : vertRulers) {\r
-                       vertRuler.zoomOut();\r
-               }\r
-               shapedirty = true;\r
-       }\r
-       \r
-       public TrendSpec getTrendSpec() {\r
-               return spec;\r
-       }\r
-\r
-       public Viewport getViewport() \r
-       {\r
-               Viewport vp = new Viewport();\r
-               vp.init();\r
-               vp.from = horizRuler.from;\r
-               vp.end = horizRuler.end;\r
-               for (VertRuler vr : vertRulers) {\r
-                       AxisViewport avp = new AxisViewport();\r
-                       avp.min = vr.min;\r
-                       avp.max = vr.max;\r
-                       vp.axesports.add( avp );\r
-               }\r
-               return vp;\r
-       }\r
-       \r
-       public void setViewport( Viewport vp ) \r
-       {\r
-               horizRuler.from = vp.from;\r
-               horizRuler.end = vp.end;\r
-               int i=0; \r
-               for ( AxisViewport avp : vp.axesports ) {\r
-                       if ( i>=vertRulers.size() ) break;\r
-                       VertRuler vr = vertRulers.get(i++);\r
-                       vr.min = avp.min;\r
-                       vr.max = avp.max;\r
-               }\r
-       }\r
-       \r
-       public void setQuality(TrendQualitySpec quality)\r
-       {\r
-               this.quality = quality;\r
-       }\r
-       \r
-       public TrendQualitySpec getQuality()\r
-       {\r
-               return quality;\r
-       }\r
-       \r
-       public Double snapToValue(double time, double snapToleranceInTime) throws HistoryException, AccessorException\r
-       {\r
-               double from = horizRuler.from;\r
-               double end = horizRuler.end;\r
-               double pixelsPerSecond = (end-from) / plot.getWidth();\r
-               \r
-               TreeSet<Double> values = new TreeSet<Double>(Bindings.DOUBLE);\r
-               for (ItemNode item : allItems) {                        \r
-                       Stream s = item.openStream( pixelsPerSecond );\r
-                       if ( s==null ) continue;\r
-                       int pos = s.binarySearch(Bindings.DOUBLE, time);\r
-                       ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());\r
-                       // Exact match\r
-                       if (pos >= 0) {\r
-                               return time;\r
-                       }\r
-                               \r
-                       int prev = -pos-2;\r
-                       int next = -pos-1;\r
-                       int count = s.count();          \r
-                       Double prevTime = null, nextTime = null;\r
-                               \r
-                       if ( prev>=0 && prev<count ) {\r
-                               s.accessor.get(prev, s.sampleBinding, vb.getSample());\r
-                               if ( !vb.isNanSample() ) {\r
-                                       prevTime = vb.getTimeDouble();\r
-                                       if ( vb.hasEndTime() ) {\r
-                                               Double nTime = vb.getEndTimeDouble();\r
-                                               if (nTime!=null && nTime+snapToleranceInTime>time) nextTime = nTime;\r
-                                       }\r
-                               }\r
-                       }\r
-                               \r
-                       if ( nextTime==null && next>=0 && next<count ) {\r
-                               s.accessor.get(next, s.sampleBinding, vb.getSample());\r
-                               if ( !vb.isNanSample() ) {\r
-                                       nextTime = vb.getTimeDouble();\r
-                               }\r
-                       }\r
-                       \r
-                       if (prevTime==null && nextTime==null) continue;\r
-                               \r
-                       if (prevTime==null) {\r
-                               if ( nextTime - time < snapToleranceInTime ) \r
-                                       values.add(nextTime);\r
-                       } else if (nextTime==null) {\r
-                               if ( time - prevTime < snapToleranceInTime ) \r
-                                       values.add(prevTime);\r
-                       } else {\r
-                               values.add(nextTime);\r
-                               values.add(prevTime);\r
-                       }\r
-               }\r
-               if (values.isEmpty()) return null;\r
-               \r
-               Double lower = values.floor( time );\r
-               Double higher = values.ceiling( time );\r
-               \r
-               if ( lower == null ) return higher;\r
-               if ( higher == null ) return lower;\r
-               double result = time-lower < higher-time ? lower : higher;\r
-               \r
-               \r
-               return result;\r
-               \r
-       }\r
-       \r
-       \r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
+ * 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.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.font.GlyphVector;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.simantics.databoard.Bindings;
+import org.simantics.databoard.accessor.error.AccessorException;
+import org.simantics.databoard.binding.error.BindingException;
+import org.simantics.databoard.util.Bean;
+import org.simantics.g2d.utils.GridUtil;
+import org.simantics.history.Collector;
+import org.simantics.history.History;
+import org.simantics.history.HistoryException;
+import org.simantics.history.HistoryManager;
+import org.simantics.history.ItemManager;
+import org.simantics.history.impl.CollectorImpl;
+import org.simantics.history.util.Stream;
+import org.simantics.history.util.ValueBand;
+import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.utils.QualityHints;
+import org.simantics.trend.configuration.ItemPlacement;
+import org.simantics.trend.configuration.LineQuality;
+import org.simantics.trend.configuration.Scale;
+import org.simantics.trend.configuration.TimeFormat;
+import org.simantics.trend.configuration.TimeWindow;
+import org.simantics.trend.configuration.TrendItem;
+import org.simantics.trend.configuration.TrendItem.Renderer;
+import org.simantics.trend.configuration.TrendQualitySpec;
+import org.simantics.trend.configuration.TrendSpec;
+import org.simantics.trend.configuration.Viewport;
+import org.simantics.trend.configuration.Viewport.AxisViewport;
+import org.simantics.trend.configuration.YAxisMode;
+import org.simantics.utils.format.ValueFormat;
+
+import gnu.trove.map.TObjectIntMap;
+import gnu.trove.map.hash.TObjectIntHashMap;
+import gnu.trove.procedure.TObjectProcedure;
+
+public class TrendNode extends G2DParentNode implements TrendLayout {
+
+       private static final long serialVersionUID = -8339696405893626168L;
+
+       /** Title node */
+       public TextNode titleNode;
+
+       /** Plot node */
+       public Plot plot;
+       public HorizRuler horizRuler;
+       VertRuler vertRuler;   // Selected vertical ruler, null if there are no analog items
+       int vertRulerIndex;    // Index of the selected vertical ruler
+       List<VertRuler> vertRulers = new ArrayList<VertRuler>();
+       SelectionNode selection;
+       public MilestoneSpec milestones;
+       
+       /** Control bounds */
+       Rectangle2D bounds = new Rectangle2D.Double();
+       
+       /** Trend spec */
+       public TrendSpec spec;
+       public ViewRenderingProfile renderingProfile = new ViewRenderingProfile();
+       public TrendQualitySpec quality = TrendQualitySpec.DEFAULT;
+       public boolean printing = false;
+       boolean singleAxis;
+       
+       // Data nodes
+       List<ItemNode> analogItems = new ArrayList<ItemNode>();
+       List<ItemNode> binaryItems = new ArrayList<ItemNode>();
+       List<ItemNode> allItems = new ArrayList<ItemNode>();
+
+       // History
+       static HistoryManager DUMMY_HISTORY = History.createMemoryHistory();
+       public HistoryManager historian = DUMMY_HISTORY;
+       
+       // Collector - set for flushing the stream, right before drawing
+       public Collector collector = null;
+       Set<String> itemIds = new HashSet<String>();
+       
+       // Signal to indicate history has changed.
+       public boolean datadirty = false;
+       // Signal to indicate the cached shapes are dirty. This will cause reloading of data.
+       public boolean shapedirty = false;
+       
+       public boolean autoscaletime = true;
+       
+       public TimeFormat timeFormat = TimeFormat.Time;
+       public ValueFormat valueFormat = ValueFormat.Default;
+       public boolean drawSamples = false;
+       
+       /** Time at mouse, when mouse hovers over trend. This value is set by TrendParticipant. NaN is mouse is not hovering */
+       public double mouseHoverTime = Double.NaN, lastMouseHoverTime = 0.0;
+       
+       /** If set, valueTip is drawn at the time */
+       public Double valueTipTime = null, prevValueTipTime = null;
+       public boolean valueTipHover = false;
+
+       public ItemPlacement itemPlacement = ItemPlacement.Overlapping;
+       
+       @Override
+       public void init() {
+               spec = new TrendSpec();
+               spec.init();
+               
+               milestones = new MilestoneSpec();
+               milestones.init();
+               
+               //// Create title node
+               titleNode = addNode( "Title", TextNode.class );
+               titleNode.setFont( new Font("Arial", 0, 30) );
+               titleNode.setColor( Color.BLACK );
+               titleNode.setText( "<title here>");
+               titleNode.setSize(300, 40);
+               
+               plot = addNode( "Plot", Plot.class );
+               
+               horizRuler = addNode("HorizRuler", HorizRuler.class);
+               vertRuler = addNode("VertRuler", VertRuler.class);
+               vertRulers.add( vertRuler );
+               
+               /// Set some bounds
+               horizRuler.setFromEnd(0, 100);
+               vertRuler.setMinMax(0, 100);
+               setSize(480, 320);
+       }
+       
+       /**
+        * Set source of data
+        * @param historian
+        * @param collector (Optional) Used for flushing collectors 
+        */
+       public void setHistorian(HistoryManager historian, Collector collector) {
+               this.historian = historian==null?DUMMY_HISTORY:historian;
+               this.collector = collector;
+               itemIds.clear();
+               ItemManager allFiles = ItemManager.createUnchecked( getHistoryItems() );
+               for (ItemNode item : allItems) {
+                       item.setTrendItem(item.item, allFiles);
+                       for (Bean historyItem : item.historyItems ) {
+                               try {
+                                       itemIds.add( (String) historyItem.getField("id") );
+                               } catch (BindingException e) {
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * Get info of all history items.
+        * This util is created for polling strategy.  
+        * 
+        * @return
+        */
+       Bean[] getHistoryItems() {
+               Bean[] result = null;
+               HistoryException e = null;
+               for (int attempt=0; attempt<10; attempt++) {
+                       try {
+                               result = historian.getItems();
+                               break;
+                       } catch (HistoryException e2) {
+                               if (e==null) e = e2;
+                               try {
+                                       Thread.sleep(1);
+                               } catch (InterruptedException e1) {
+                               }
+                       }
+               }
+               if (result!=null) return result;
+               //throw new RuntimeException(e);
+               return new Bean[0];
+       }
+       
+       public void setMilestones(Bean milestones) {
+               if (this.milestones.equals(milestones)) return;
+               this.milestones.readFrom( milestones );         
+               boolean hasBaseline = this.milestones.baseline>=0;
+
+               // Sort by first, put baseline on top
+               final Milestone baseline = this.milestones.baseline>=0 ? this.milestones.milestones.get( this.milestones.baseline ) : null;
+               Collections.sort(this.milestones.milestones, new Comparator<Milestone>() {
+                       public int compare(Milestone o1, Milestone o2) {
+                               if (o1==baseline) return -1;
+                               if (o2==baseline) return 1;
+                               return Double.compare(o1.time, o1.time);
+                       }});
+               
+               this.milestones.baseline = hasBaseline ? 0 : -1;                
+               double newBasetime = hasBaseline ? this.milestones.milestones.get(this.milestones.baseline).time : 0.;
+               if (newBasetime != horizRuler.basetime) {                       
+                       horizRuler.basetime = newBasetime;
+                       horizRuler.layout();
+               }
+               shapedirty = true;
+       }
+       
+       public void selectVertRuler(int index) {
+               vertRulerIndex = index;
+               if (index<0 || index>=vertRulers.size()) {
+                       vertRuler = vertRulers.get(0);
+               } else {
+                       vertRuler = vertRulers.get(index);
+               }
+               shapedirty = true;
+       }
+       
+       @Override
+       public void cleanup() {
+               spec = new TrendSpec();
+               spec.init();
+               analogItems.clear();
+               binaryItems.clear();
+               allItems.clear();
+               historian = DUMMY_HISTORY;
+               super.cleanup();
+       }
+
+       private static TObjectIntMap<String> itemIndexMap(List<TrendItem> items) {
+               TObjectIntMap<String> map = new TObjectIntHashMap<>(items.size(), 0.5f, -1);
+               for (int i = 0; i < items.size(); ++i) {
+                       TrendItem it = items.get(i);
+                       map.put(it.groupItemId, i);
+               }
+               return map;
+       }
+
+       private static <T> TObjectIntMap<T> subtract(TObjectIntMap<T> a, TObjectIntMap<T> b) {
+               TObjectIntMap<T> r = new TObjectIntHashMap<>(a);
+               b.forEachKey(new TObjectProcedure<T>() {
+                       @Override
+                       public boolean execute(T key) {
+                               r.remove(key);
+                               return true;
+                       }
+               });
+               return r;
+       }
+
+       /**
+        * @param newSpec
+        *            new trending specification, cannot not be <code>null</code>.
+        *            Use {@link TrendSpec#EMPTY} instead of <code>null</code>.
+        */
+       public void setTrendSpec(TrendSpec newSpec) {
+               //System.out.println(newSpec);
+               // Check if equal & Read spec
+               if (newSpec.equals(this.spec)) return;
+
+               boolean timeWindowChange = !this.spec.viewProfile.timeWindow.equals( newSpec.viewProfile.timeWindow );
+               boolean yaxisModeChanged = this.spec.axisMode != newSpec.axisMode;
+
+               TObjectIntMap<String> newItemMap = itemIndexMap(newSpec.items);
+               TObjectIntMap<String> currentItemMap = itemIndexMap(spec.items);
+               TObjectIntMap<String> removedItemMap = subtract(currentItemMap, newItemMap);
+               Map<String, VertRuler> existingRulers = new HashMap<>();
+               if (this.spec.axisMode == YAxisMode.MultiAxis) {
+                       for (ItemNode item : analogItems)
+                               if (item.ruler != null)
+                                       existingRulers.put(item.item.groupItemId, item.ruler);
+               }
+
+               this.spec.readFrom( newSpec );
+               this.spec.sortItems();
+               this.renderingProfile.read(this.spec.viewProfile);
+
+               // Set title
+               if (titleNode != null) titleNode.setText( spec.name );
+
+               // Setup trend item nodes
+               itemIds.clear();
+               for (ItemNode item : allItems) removeNode(item);
+               analogItems.clear();
+               binaryItems.clear();
+               allItems.clear();
+               
+               ItemManager itemManager = ItemManager.createUnchecked( getHistoryItems() );
+               for (int i = 0; i<spec.items.size(); i++) {
+                       TrendItem item = spec.items.get(i);
+                       if (item.hidden)
+                               continue;
+                       
+                       ItemNode node = createItemNode(item, itemManager);
+                       for (Bean historyItem : node.historyItems) {
+                               try {
+                                       itemIds.add( (String) historyItem.getField("id") );
+                               } catch (BindingException e) {
+                               }
+                       }
+                       
+                       if (item.renderer == Renderer.Analog) {
+                               analogItems.add(node);
+                       } else {
+                               binaryItems.add(node);
+                       }
+                       allItems.add(node);
+               }
+
+               // Setup vertical ruler nodes
+               singleAxis = spec.axisMode == YAxisMode.SingleAxis;
+               if (singleAxis) {
+                       if (yaxisModeChanged || vertRulers.size() != 1 || vertRuler == null) {
+                               for (VertRuler vr : vertRulers) removeNode(vr);
+                               vertRulers.clear();
+
+                               vertRuler = addNode("VertRuler", VertRuler.class);
+                               vertRulers.add( vertRuler );
+                       }
+
+                       vertRuler.manualscale = true;
+                       for (int i=0; i<analogItems.size(); i++) {
+                               ItemNode item = analogItems.get(i);
+                               item.ruler = vertRuler;
+                               item.trendNode = this;
+                               if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false;
+                       }
+               } else {
+                       if (yaxisModeChanged) {
+                               // Recreate all rulers
+                               for (VertRuler vr : vertRulers) removeNode(vr);
+                               vertRulers.clear();
+                               for (int i=0; i<analogItems.size(); i++)
+                                       vertRulers.add( addNode(VertRuler.class) );
+                       } else {
+                               // Remove rulers of the items that were removed
+                               // and add new rulers to have enough of them for
+                               // each separate analog signal.
+                               removedItemMap.forEachKey(new TObjectProcedure<String>() {
+                                       @Override
+                                       public boolean execute(String id) {
+                                               VertRuler vr = existingRulers.get(id);
+                                               if (vr != null) {
+                                                       removeNode(vr);
+                                                       vertRulers.remove(vr);
+                                               }
+                                               return true;
+                                       }
+                               });
+                               for (int i = vertRulers.size(); i < analogItems.size(); ++i) {
+                                       VertRuler ruler = addNode(VertRuler.class);
+                                       vertRulers.add(ruler);
+                               }
+                       }
+
+                       for (int i = 0; i < analogItems.size(); i++) {
+                               ItemNode item = analogItems.get(i);
+                               VertRuler vr = vertRulers.get(i);
+                               vr.setZIndex(1000 + i);
+                               vr.color = item.color;
+                               vr.label = item.item.label;
+                               vr.manualscale = item.item.scale instanceof Scale.Manual;
+                               item.ruler = vr;
+                               item.trendNode = this;
+                       }
+                       // Select vert ruler
+                       vertRuler = vertRulers.isEmpty() ? null : vertRulers.get( vertRulerIndex <= 0 || vertRulerIndex >= vertRulers.size() ? 0 : vertRulerIndex );
+               }
+               
+               // Locked
+               TimeWindow tw = spec.viewProfile.timeWindow;
+               horizRuler.manualscale = tw.timeWindowLength!=null && tw.timeWindowStart!=null;
+               
+               if (timeWindowChange) {
+                       horizRuler.autoscale();
+               }
+               shapedirty = true;
+       }
+
+    private ItemNode createItemNode(TrendItem item, ItemManager itemManager) {
+        ItemNode node = addNode( ItemNode.class );
+        //node.trendNode = this;
+        node.setTrendItem(item, itemManager);
+        node.color = toColor(item.customColor);
+        if (node.color == null)
+            node.color = JarisPaints.getColor( item.index );
+        node.stroke = item.customStrokeWidth != null
+                ? withStrokeWidth(item.customStrokeWidth, Plot.TREND_LINE_STROKE)
+                : Plot.TREND_LINE_STROKE;
+        return node;
+    }
+
+    private static BasicStroke withStrokeWidth(float width, BasicStroke stroke) {
+        return new BasicStroke(width,
+                stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),
+                stroke.getDashArray(), stroke.getDashPhase());
+    }
+
+    private static Color toColor(float[] components) {
+        if (components == null)
+            return null;
+        switch (components.length) {
+        case 3: return new Color(components[0], components[1], components[2]);
+        case 4: return new Color(components[0], components[1], components[2], components[3]);
+        default: return null;
+        }
+    }
+
+       /**
+        * Layout graphical nodes based on bounds
+        */
+       public void layout() {
+               double w = bounds.getWidth();
+               double h = bounds.getHeight();
+               if ( titleNode != null ) {
+                       titleNode.setSize(w, h * 0.02);
+                       titleNode.setTranslate(0, VERT_MARGIN);
+                       titleNode.layout();
+               }
+               
+               // Plot-Ruler area width 
+               double praw = w-HORIZ_MARGIN*2;
+               
+               // Plot height
+               double ph = h-VERT_MARGIN*2-MILESTONE_HEIGHT-HORIZ_RULER_HEIGHT;
+               if ( titleNode != null ) {
+                       ph -= titleNode.th;
+               }
+               
+               // Analog & Binary area height
+               double aah, bah;
+               if (!analogItems.isEmpty()) {
+                       bah = binaryItems.size() * BINARY[3];
+                       aah = Math.max(0, ph-bah);
+                       if (aah+bah>ph) bah = ph-aah;
+               } else {
+                       // No analog items
+                       aah = 0;
+                       bah = ph; 
+               }
+               // Vertical ruler
+               for (VertRuler vertRuler : vertRulers) {
+                       vertRuler.setHeight(aah);
+                       vertRuler.layout();
+               }
+               plot.analogAreaHeight = aah;
+               plot.binaryAreaHeight = bah;
+
+               // Vertical ruler widths
+               double vrws = 0;
+               for (VertRuler vertRuler : vertRulers) {
+                       vrws += vertRuler.getWidth();
+               }
+               
+               // Add room for Binary label
+               if ( !binaryItems.isEmpty() ) {
+                       double maxLabelWidth = BINARY_LABEL_WIDTH;
+                       for (ItemNode node : binaryItems) {
+                               if (node.item != null) {
+                                       GlyphVector glyphVector = RULER_FONT.createGlyphVector(GridUtil.frc, node.item.label);
+                                       double labelWidth = glyphVector.getVisualBounds().getWidth();
+                                       maxLabelWidth = Math.max( maxLabelWidth, labelWidth );
+                               }
+                       }
+                       vrws = Math.max(maxLabelWidth, vrws);
+               }
+               
+               // Plot Width
+               double pw = praw - vrws;
+               plot.setTranslate(HORIZ_MARGIN, (titleNode!=null?titleNode.th:0)+VERT_MARGIN+MILESTONE_HEIGHT);
+               plot.setSize(pw, ph);
+               
+               horizRuler.layout();
+               horizRuler.setTranslate(HORIZ_MARGIN, plot.getY()+plot.getHeight()+3);
+               boolean l = horizRuler.setWidth(pw);
+               l |= horizRuler.setFromEnd(horizRuler.from, horizRuler.end);
+               if (l) horizRuler.layout();
+               
+               // Move vertical rulers
+               double vrx = plot.getX() + plot.getWidth() + 3; 
+               for (VertRuler vertRuler : vertRulers) {
+                       vertRuler.setTranslate(vrx, plot.getY());
+                       vrx += vertRuler.getWidth() + 3;
+               }
+               
+       }
+
+       public void setSize(double width, double height) {
+               bounds.setFrame(0, 0, width, height);
+       }
+       
+       @Override
+       public void render(Graphics2D g2d) {
+               // Set Quality High
+               QualityHints qh = QualityHints.getQuality(g2d);
+        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, quality.textQuality==LineQuality.Antialias?RenderingHints.VALUE_TEXT_ANTIALIAS_ON:RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
+               
+               // Set Bounds
+               Rectangle bounds = g2d.getClipBounds();         
+               if (bounds.getWidth()!=this.bounds.getWidth() || bounds.getHeight()!=this.bounds.getHeight()) {
+                       setSize(bounds.getWidth(), bounds.getHeight());
+                       layout();
+               }
+               
+               // Flush history subscriptions
+               flushHistory();
+
+               // Render children
+               super.render(g2d);
+               
+               // Render plot's value tip
+               plot.renderValueTip(g2d);
+               
+               // Restore quality
+               qh.setQuality(g2d);
+       }
+       
+       @Override
+       public Rectangle2D getBoundsInLocal() {
+               return bounds;
+       }
+       
+       /**
+        * Return true if the viewport is not moving and shows only past values 
+        * @return
+        */
+       public boolean allPast() {
+               TimeWindow timeWindow = spec.viewProfile.timeWindow;
+               boolean fixedWindow = !horizRuler.autoscroll || (timeWindow.timeWindowStart!=null && timeWindow.timeWindowLength!=null);
+               if (fixedWindow) {
+                       for (ItemNode item : allItems) {
+                               if (item.end <= horizRuler.end) return false;
+                       }
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       public void flushHistory() {
+               if (collector == null || collector instanceof CollectorImpl == false) return;
+               CollectorImpl fh = (CollectorImpl) collector;
+               fh.flush( itemIds );
+       }
+       
+       /**
+        * Read values of min,max,from,end to all items.
+        * Put iMin,iMax,iFrom,iEnd to all axes.
+        */
+       public void readMinMaxFromEnd() {
+               flushHistory();
+               // Read min,max,from,end from all items - Gather collective ranges
+               for (ItemNode item : allItems) {
+                       item.readMinMaxFromEnd();
+               }
+               horizRuler.setKnownFromEnd();
+               for (VertRuler vertRuler : vertRulers) vertRuler.setKnownMinMax();
+       }
+
+       
+       
+       /**
+        * Flushes history, resets streams,
+        * reads the latest time and value ranges from disc.
+        * Scales the axes.
+        * Sets dirty true if there was change in axis.
+        */
+       public boolean autoscale(boolean timeAxis, boolean valueAxis) {
+               if (!timeAxis && !valueAxis) return false;
+
+               readMinMaxFromEnd();
+               boolean changed = false;
+                               
+               // Time axis
+               if (timeAxis) {
+                       changed |= horizRuler.autoscale();
+                       if ( changed && !printing ) horizRuler.fireListener();
+               }
+
+               // Y-Axis
+               if (valueAxis) {
+                       for (VertRuler vertRuler : vertRulers) changed |= vertRuler.autoscale();
+               }       
+
+               return changed;
+       }
+
+       public boolean updateValueTipTime() {
+               if (valueTipTime != null && spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
+                       double endTime = horizRuler.getItemEndTime();
+                       if (!Double.isNaN(endTime)) {
+                               boolean changed = Double.compare(valueTipTime, endTime) != 0;
+                               valueTipTime = endTime;
+                               return changed;
+                       }
+               }
+               return false;
+       }
+
+       public void zoomIn(double x, double y, double width, double height, boolean horiz, boolean vert) {
+               if (horiz) {
+                       horizRuler.zoomIn(x, width);
+               }
+               
+               if (vert) {
+                       for (VertRuler vertRuler : vertRulers) {
+                               vertRuler.zoomIn(y, height);
+                       }
+               }
+               shapedirty = true;
+       }
+       
+       public void zoomOut() {
+               horizRuler.zoomOut();
+               for (VertRuler vertRuler : vertRulers) {
+                       vertRuler.zoomOut();
+               }
+               shapedirty = true;
+       }
+       
+       public TrendSpec getTrendSpec() {
+               return spec;
+       }
+
+       public Viewport getViewport() 
+       {
+               Viewport vp = new Viewport();
+               vp.init();
+               vp.from = horizRuler.from;
+               vp.end = horizRuler.end;
+               for (VertRuler vr : vertRulers) {
+                       AxisViewport avp = new AxisViewport();
+                       avp.min = vr.min;
+                       avp.max = vr.max;
+                       vp.axesports.add( avp );
+               }
+               return vp;
+       }
+       
+       public void setViewport( Viewport vp ) 
+       {
+               horizRuler.from = vp.from;
+               horizRuler.end = vp.end;
+               int i=0; 
+               for ( AxisViewport avp : vp.axesports ) {
+                       if ( i>=vertRulers.size() ) break;
+                       VertRuler vr = vertRulers.get(i++);
+                       vr.min = avp.min;
+                       vr.max = avp.max;
+               }
+       }
+       
+       public void setQuality(TrendQualitySpec quality)
+       {
+               this.quality = quality;
+       }
+       
+       public TrendQualitySpec getQuality()
+       {
+               return quality;
+       }
+       
+       public Double snapToValue(double time, double snapToleranceInTime) throws HistoryException, AccessorException
+       {
+               double from = horizRuler.from;
+               double end = horizRuler.end;
+               double pixelsPerSecond = (end-from) / plot.getWidth();
+               
+               TreeSet<Double> values = new TreeSet<Double>(Bindings.DOUBLE);
+               for (ItemNode item : allItems) {                        
+                       Stream s = item.openStream( pixelsPerSecond );
+                       if ( s==null ) continue;
+                       int pos = s.binarySearch(Bindings.DOUBLE, time);
+                       ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());
+                       // Exact match
+                       if (pos >= 0) {
+                               return time;
+                       }
+                               
+                       int prev = -pos-2;
+                       int next = -pos-1;
+                       int count = s.count();          
+                       Double prevTime = null, nextTime = null;
+                               
+                       if ( prev>=0 && prev<count ) {
+                               s.accessor.get(prev, s.sampleBinding, vb.getSample());
+                               if ( !vb.isNanSample() ) {
+                                       prevTime = vb.getTimeDouble();
+                                       if ( vb.hasEndTime() ) {
+                                               Double nTime = vb.getEndTimeDouble();
+                                               if (nTime!=null && nTime+snapToleranceInTime>time) nextTime = nTime;
+                                       }
+                               }
+                       }
+                               
+                       if ( nextTime==null && next>=0 && next<count ) {
+                               s.accessor.get(next, s.sampleBinding, vb.getSample());
+                               if ( !vb.isNanSample() ) {
+                                       nextTime = vb.getTimeDouble();
+                               }
+                       }
+                       
+                       if (prevTime==null && nextTime==null) continue;
+                               
+                       if (prevTime==null) {
+                               if ( nextTime - time < snapToleranceInTime ) 
+                                       values.add(nextTime);
+                       } else if (nextTime==null) {
+                               if ( time - prevTime < snapToleranceInTime ) 
+                                       values.add(prevTime);
+                       } else {
+                               values.add(nextTime);
+                               values.add(prevTime);
+                       }
+               }
+               if (values.isEmpty()) return null;
+               
+               Double lower = values.floor( time );
+               Double higher = values.ceiling( time );
+               
+               if ( lower == null ) return higher;
+               if ( higher == null ) return lower;
+               double result = time-lower < higher-time ? lower : higher;
+               
+               
+               return result;
+               
+       }
+       
+       
+}