-/*******************************************************************************\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.TIntProcedure;\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> addedItemMap = subtract(newItemMap, currentItemMap);\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
- 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
- addedItemMap.forEachValue(new TIntProcedure() {\r
- @Override\r
- public boolean execute(int index) {\r
- vertRulers.add( index, addNode(VertRuler.class) );\r
- return true;\r
- }\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;
+ boolean singleAxisShowLegends;
+
+ // 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);
+ if (!it.hidden)
+ 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;
+ singleAxisShowLegends = spec.axisMode == YAxisMode.SingleAxisShowLegends;
+ if(singleAxisShowLegends) {
+ 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;
+ vertRuler.singleAxisShowLegendsMaxLegends = spec.singleAxisShowLegendsMaxLegends;
+ for (int i=0; i<analogItems.size(); i++) {
+ ItemNode item = analogItems.get(i);
+ vertRuler.addExtraLabel(item.item.label, item.color);
+ item.ruler = vertRuler;
+ item.trendNode = this;
+ if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false;
+ }
+ }
+ else 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.label = item.item.label;
+ vr.color = item.color;
+ 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;
+
+ }
+
+
+}