]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendNode.java
Fixed multiple issues causing dangling references to discarded queries
[simantics/platform.git] / bundles / org.simantics.trend / src / org / simantics / trend / impl / TrendNode.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
3  * Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.trend.impl;
13
14 import java.awt.BasicStroke;
15 import java.awt.Color;
16 import java.awt.Font;
17 import java.awt.Graphics2D;
18 import java.awt.Rectangle;
19 import java.awt.RenderingHints;
20 import java.awt.font.GlyphVector;
21 import java.awt.geom.Rectangle2D;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Comparator;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.TreeSet;
31
32 import org.simantics.databoard.Bindings;
33 import org.simantics.databoard.accessor.error.AccessorException;
34 import org.simantics.databoard.binding.error.BindingException;
35 import org.simantics.databoard.util.Bean;
36 import org.simantics.g2d.utils.GridUtil;
37 import org.simantics.history.Collector;
38 import org.simantics.history.History;
39 import org.simantics.history.HistoryException;
40 import org.simantics.history.HistoryManager;
41 import org.simantics.history.ItemManager;
42 import org.simantics.history.impl.CollectorImpl;
43 import org.simantics.history.util.Stream;
44 import org.simantics.history.util.ValueBand;
45 import org.simantics.scenegraph.g2d.G2DParentNode;
46 import org.simantics.scenegraph.utils.QualityHints;
47 import org.simantics.trend.configuration.ItemPlacement;
48 import org.simantics.trend.configuration.LineQuality;
49 import org.simantics.trend.configuration.Scale;
50 import org.simantics.trend.configuration.TimeFormat;
51 import org.simantics.trend.configuration.TimeWindow;
52 import org.simantics.trend.configuration.TrendItem;
53 import org.simantics.trend.configuration.TrendItem.Renderer;
54 import org.simantics.trend.configuration.TrendQualitySpec;
55 import org.simantics.trend.configuration.TrendSpec;
56 import org.simantics.trend.configuration.Viewport;
57 import org.simantics.trend.configuration.Viewport.AxisViewport;
58 import org.simantics.trend.configuration.YAxisMode;
59 import org.simantics.utils.format.ValueFormat;
60
61 import gnu.trove.map.TObjectIntMap;
62 import gnu.trove.map.hash.TObjectIntHashMap;
63 import gnu.trove.procedure.TObjectProcedure;
64
65 public class TrendNode extends G2DParentNode implements TrendLayout {
66
67         private static final long serialVersionUID = -8339696405893626168L;
68
69         /** Title node */
70         public TextNode titleNode;
71
72         /** Plot node */
73         public Plot plot;
74         public HorizRuler horizRuler;
75         VertRuler vertRuler;   // Selected vertical ruler, null if there are no analog items
76         int vertRulerIndex;    // Index of the selected vertical ruler
77         List<VertRuler> vertRulers = new ArrayList<VertRuler>();
78         SelectionNode selection;
79         public MilestoneSpec milestones;
80         
81         /** Control bounds */
82         Rectangle2D bounds = new Rectangle2D.Double();
83         
84         /** Trend spec */
85         public TrendSpec spec;
86         public ViewRenderingProfile renderingProfile = new ViewRenderingProfile();
87         public TrendQualitySpec quality = TrendQualitySpec.DEFAULT;
88         public boolean printing = false;
89         boolean singleAxis;
90         
91         // Data nodes
92         List<ItemNode> analogItems = new ArrayList<ItemNode>();
93         List<ItemNode> binaryItems = new ArrayList<ItemNode>();
94         List<ItemNode> allItems = new ArrayList<ItemNode>();
95
96         // History
97         static HistoryManager DUMMY_HISTORY = History.createMemoryHistory();
98         public HistoryManager historian = DUMMY_HISTORY;
99         
100         // Collector - set for flushing the stream, right before drawing
101         public Collector collector = null;
102         Set<String> itemIds = new HashSet<String>();
103         
104         // Signal to indicate history has changed.
105         public boolean datadirty = false;
106         // Signal to indicate the cached shapes are dirty. This will cause reloading of data.
107         public boolean shapedirty = false;
108         
109         public boolean autoscaletime = true;
110         
111         public TimeFormat timeFormat = TimeFormat.Time;
112         public ValueFormat valueFormat = ValueFormat.Default;
113         public boolean drawSamples = false;
114         
115         /** Time at mouse, when mouse hovers over trend. This value is set by TrendParticipant. NaN is mouse is not hovering */
116         public double mouseHoverTime = Double.NaN, lastMouseHoverTime = 0.0;
117         
118         /** If set, valueTip is drawn at the time */
119         public Double valueTipTime = null, prevValueTipTime = null;
120         public boolean valueTipHover = false;
121
122         public ItemPlacement itemPlacement = ItemPlacement.Overlapping;
123         
124         @Override
125         public void init() {
126                 spec = new TrendSpec();
127                 spec.init();
128                 
129                 milestones = new MilestoneSpec();
130                 milestones.init();
131                 
132                 //// Create title node
133                 titleNode = addNode( "Title", TextNode.class );
134                 titleNode.setFont( new Font("Arial", 0, 30) );
135                 titleNode.setColor( Color.BLACK );
136                 titleNode.setText( "<title here>");
137                 titleNode.setSize(300, 40);
138                 
139                 plot = addNode( "Plot", Plot.class );
140                 
141                 horizRuler = addNode("HorizRuler", HorizRuler.class);
142                 vertRuler = addNode("VertRuler", VertRuler.class);
143                 vertRulers.add( vertRuler );
144                 
145                 /// Set some bounds
146                 horizRuler.setFromEnd(0, 100);
147                 vertRuler.setMinMax(0, 100);
148                 setSize(480, 320);
149         }
150         
151         /**
152          * Set source of data
153          * @param historian
154          * @param collector (Optional) Used for flushing collectors 
155          */
156         public void setHistorian(HistoryManager historian, Collector collector) {
157                 this.historian = historian==null?DUMMY_HISTORY:historian;
158                 this.collector = collector;
159                 itemIds.clear();
160                 ItemManager allFiles = ItemManager.createUnchecked( getHistoryItems() );
161                 for (ItemNode item : allItems) {
162                         item.setTrendItem(item.item, allFiles);
163                         for (Bean historyItem : item.historyItems ) {
164                                 try {
165                                         itemIds.add( (String) historyItem.getField("id") );
166                                 } catch (BindingException e) {
167                                 }
168                         }
169                 }
170         }
171         
172         /**
173          * Get info of all history items.
174          * This util is created for polling strategy.  
175          * 
176          * @return
177          */
178         Bean[] getHistoryItems() {
179                 Bean[] result = null;
180                 HistoryException e = null;
181                 for (int attempt=0; attempt<10; attempt++) {
182                         try {
183                                 result = historian.getItems();
184                                 break;
185                         } catch (HistoryException e2) {
186                                 if (e==null) e = e2;
187                                 try {
188                                         Thread.sleep(1);
189                                 } catch (InterruptedException e1) {
190                                 }
191                         }
192                 }
193                 if (result!=null) return result;
194                 //throw new RuntimeException(e);
195                 return new Bean[0];
196         }
197         
198         public void setMilestones(Bean milestones) {
199                 if (this.milestones.equals(milestones)) return;
200                 this.milestones.readFrom( milestones );         
201                 boolean hasBaseline = this.milestones.baseline>=0;
202
203                 // Sort by first, put baseline on top
204                 final Milestone baseline = this.milestones.baseline>=0 ? this.milestones.milestones.get( this.milestones.baseline ) : null;
205                 Collections.sort(this.milestones.milestones, new Comparator<Milestone>() {
206                         public int compare(Milestone o1, Milestone o2) {
207                                 if (o1==baseline) return -1;
208                                 if (o2==baseline) return 1;
209                                 return Double.compare(o1.time, o1.time);
210                         }});
211                 
212                 this.milestones.baseline = hasBaseline ? 0 : -1;                
213                 double newBasetime = hasBaseline ? this.milestones.milestones.get(this.milestones.baseline).time : 0.;
214                 if (newBasetime != horizRuler.basetime) {                       
215                         horizRuler.basetime = newBasetime;
216                         horizRuler.layout();
217                 }
218                 shapedirty = true;
219         }
220         
221         public void selectVertRuler(int index) {
222                 vertRulerIndex = index;
223                 if (index<0 || index>=vertRulers.size()) {
224                         vertRuler = vertRulers.get(0);
225                 } else {
226                         vertRuler = vertRulers.get(index);
227                 }
228                 shapedirty = true;
229         }
230         
231         @Override
232         public void cleanup() {
233                 spec = new TrendSpec();
234                 spec.init();
235                 analogItems.clear();
236                 binaryItems.clear();
237                 allItems.clear();
238                 historian = DUMMY_HISTORY;
239                 super.cleanup();
240         }
241
242         private static TObjectIntMap<String> itemIndexMap(List<TrendItem> items) {
243                 TObjectIntMap<String> map = new TObjectIntHashMap<>(items.size(), 0.5f, -1);
244                 for (int i = 0; i < items.size(); ++i) {
245                         TrendItem it = items.get(i);
246                         if (!it.hidden)
247                                 map.put(it.groupItemId, i);
248                 }
249                 return map;
250         }
251
252         private static <T> TObjectIntMap<T> subtract(TObjectIntMap<T> a, TObjectIntMap<T> b) {
253                 TObjectIntMap<T> r = new TObjectIntHashMap<>(a);
254                 b.forEachKey(new TObjectProcedure<T>() {
255                         @Override
256                         public boolean execute(T key) {
257                                 r.remove(key);
258                                 return true;
259                         }
260                 });
261                 return r;
262         }
263
264         /**
265          * @param newSpec
266          *            new trending specification, cannot not be <code>null</code>.
267          *            Use {@link TrendSpec#EMPTY} instead of <code>null</code>.
268          */
269         public void setTrendSpec(TrendSpec newSpec) {
270                 //System.out.println(newSpec);
271                 // Check if equal & Read spec
272                 if (newSpec.equals(this.spec)) return;
273
274                 boolean timeWindowChange = !this.spec.viewProfile.timeWindow.equals( newSpec.viewProfile.timeWindow );
275                 boolean yaxisModeChanged = this.spec.axisMode != newSpec.axisMode;
276
277                 TObjectIntMap<String> newItemMap = itemIndexMap(newSpec.items);
278                 TObjectIntMap<String> currentItemMap = itemIndexMap(spec.items);
279                 TObjectIntMap<String> removedItemMap = subtract(currentItemMap, newItemMap);
280                 Map<String, VertRuler> existingRulers = new HashMap<>();
281                 if (this.spec.axisMode == YAxisMode.MultiAxis) {
282                         for (ItemNode item : analogItems)
283                                 if (item.ruler != null)
284                                         existingRulers.put(item.item.groupItemId, item.ruler);
285                 }
286
287                 this.spec.readFrom( newSpec );
288                 this.spec.sortItems();
289                 this.renderingProfile.read(this.spec.viewProfile);
290
291                 // Set title
292                 if (titleNode != null) titleNode.setText( spec.name );
293
294                 // Setup trend item nodes
295                 itemIds.clear();
296                 for (ItemNode item : allItems) removeNode(item);
297                 analogItems.clear();
298                 binaryItems.clear();
299                 allItems.clear();
300                 
301                 ItemManager itemManager = ItemManager.createUnchecked( getHistoryItems() );
302                 for (int i = 0; i<spec.items.size(); i++) {
303                         TrendItem item = spec.items.get(i);
304                         if (item.hidden)
305                                 continue;
306                         
307                         ItemNode node = createItemNode(item, itemManager);
308                         for (Bean historyItem : node.historyItems) {
309                                 try {
310                                         itemIds.add( (String) historyItem.getField("id") );
311                                 } catch (BindingException e) {
312                                 }
313                         }
314                         
315                         if (item.renderer == Renderer.Analog) {
316                                 analogItems.add(node);
317                         } else {
318                                 binaryItems.add(node);
319                         }
320                         allItems.add(node);
321                 }
322
323                 // Setup vertical ruler nodes
324                 singleAxis = spec.axisMode == YAxisMode.SingleAxis;
325                 if (singleAxis) {
326                         if (yaxisModeChanged || vertRulers.size() != 1 || vertRuler == null) {
327                                 for (VertRuler vr : vertRulers) removeNode(vr);
328                                 vertRulers.clear();
329
330                                 vertRuler = addNode("VertRuler", VertRuler.class);
331                                 vertRulers.add( vertRuler );
332                         }
333
334                         vertRuler.manualscale = true;
335                         for (int i=0; i<analogItems.size(); i++) {
336                                 ItemNode item = analogItems.get(i);
337                                 item.ruler = vertRuler;
338                                 item.trendNode = this;
339                                 if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false;
340                         }
341                 } else {
342                         if (yaxisModeChanged) {
343                                 // Recreate all rulers
344                                 for (VertRuler vr : vertRulers) removeNode(vr);
345                                 vertRulers.clear();
346                                 for (int i=0; i<analogItems.size(); i++)
347                                         vertRulers.add( addNode(VertRuler.class) );
348                         } else {
349                                 // Remove rulers of the items that were removed
350                                 // and add new rulers to have enough of them for
351                                 // each separate analog signal.
352                                 removedItemMap.forEachKey(new TObjectProcedure<String>() {
353                                         @Override
354                                         public boolean execute(String id) {
355                                                 VertRuler vr = existingRulers.get(id);
356                                                 if (vr != null) {
357                                                         removeNode(vr);
358                                                         vertRulers.remove(vr);
359                                                 }
360                                                 return true;
361                                         }
362                                 });
363                                 for (int i = vertRulers.size(); i < analogItems.size(); ++i) {
364                                         VertRuler ruler = addNode(VertRuler.class);
365                                         vertRulers.add(ruler);
366                                 }
367                         }
368
369                         for (int i = 0; i < analogItems.size(); i++) {
370                                 ItemNode item = analogItems.get(i);
371                                 VertRuler vr = vertRulers.get(i);
372                                 vr.setZIndex(1000 + i);
373                                 vr.color = item.color;
374                                 vr.label = item.item.label;
375                                 vr.manualscale = item.item.scale instanceof Scale.Manual;
376                                 item.ruler = vr;
377                                 item.trendNode = this;
378                         }
379                         // Select vert ruler
380                         vertRuler = vertRulers.isEmpty() ? null : vertRulers.get( vertRulerIndex <= 0 || vertRulerIndex >= vertRulers.size() ? 0 : vertRulerIndex );
381                 }
382                 
383                 // Locked
384                 TimeWindow tw = spec.viewProfile.timeWindow;
385                 horizRuler.manualscale = tw.timeWindowLength!=null && tw.timeWindowStart!=null;
386                 
387                 if (timeWindowChange) {
388                         horizRuler.autoscale();
389                 }
390                 shapedirty = true;
391         }
392
393     private ItemNode createItemNode(TrendItem item, ItemManager itemManager) {
394         ItemNode node = addNode( ItemNode.class );
395         //node.trendNode = this;
396         node.setTrendItem(item, itemManager);
397         node.color = toColor(item.customColor);
398         if (node.color == null)
399             node.color = JarisPaints.getColor( item.index );
400         node.stroke = item.customStrokeWidth != null
401                 ? withStrokeWidth(item.customStrokeWidth, Plot.TREND_LINE_STROKE)
402                 : Plot.TREND_LINE_STROKE;
403         return node;
404     }
405
406     private static BasicStroke withStrokeWidth(float width, BasicStroke stroke) {
407         return new BasicStroke(width,
408                 stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),
409                 stroke.getDashArray(), stroke.getDashPhase());
410     }
411
412     private static Color toColor(float[] components) {
413         if (components == null)
414             return null;
415         switch (components.length) {
416         case 3: return new Color(components[0], components[1], components[2]);
417         case 4: return new Color(components[0], components[1], components[2], components[3]);
418         default: return null;
419         }
420     }
421
422         /**
423          * Layout graphical nodes based on bounds
424          */
425         public void layout() {
426                 double w = bounds.getWidth();
427                 double h = bounds.getHeight();
428                 if ( titleNode != null ) {
429                         titleNode.setSize(w, h * 0.02);
430                         titleNode.setTranslate(0, VERT_MARGIN);
431                         titleNode.layout();
432                 }
433                 
434                 // Plot-Ruler area width 
435                 double praw = w-HORIZ_MARGIN*2;
436                 
437                 // Plot height
438                 double ph = h-VERT_MARGIN*2-MILESTONE_HEIGHT-HORIZ_RULER_HEIGHT;
439                 if ( titleNode != null ) {
440                         ph -= titleNode.th;
441                 }
442                 
443                 // Analog & Binary area height
444                 double aah, bah;
445                 if (!analogItems.isEmpty()) {
446                         bah = binaryItems.size() * BINARY[3];
447                         aah = Math.max(0, ph-bah);
448                         if (aah+bah>ph) bah = ph-aah;
449                 } else {
450                         // No analog items
451                         aah = 0;
452                         bah = ph; 
453                 }
454                 // Vertical ruler
455                 for (VertRuler vertRuler : vertRulers) {
456                         vertRuler.setHeight(aah);
457                         vertRuler.layout();
458                 }
459                 plot.analogAreaHeight = aah;
460                 plot.binaryAreaHeight = bah;
461
462                 // Vertical ruler widths
463                 double vrws = 0;
464                 for (VertRuler vertRuler : vertRulers) {
465                         vrws += vertRuler.getWidth();
466                 }
467                 
468                 // Add room for Binary label
469                 if ( !binaryItems.isEmpty() ) {
470                         double maxLabelWidth = BINARY_LABEL_WIDTH;
471                         for (ItemNode node : binaryItems) {
472                                 if (node.item != null) {
473                                         GlyphVector glyphVector = RULER_FONT.createGlyphVector(GridUtil.frc, node.item.label);
474                                         double labelWidth = glyphVector.getVisualBounds().getWidth();
475                                         maxLabelWidth = Math.max( maxLabelWidth, labelWidth );
476                                 }
477                         }
478                         vrws = Math.max(maxLabelWidth, vrws);
479                 }
480                 
481                 // Plot Width
482                 double pw = praw - vrws;
483                 plot.setTranslate(HORIZ_MARGIN, (titleNode!=null?titleNode.th:0)+VERT_MARGIN+MILESTONE_HEIGHT);
484                 plot.setSize(pw, ph);
485                 
486                 horizRuler.layout();
487                 horizRuler.setTranslate(HORIZ_MARGIN, plot.getY()+plot.getHeight()+3);
488                 boolean l = horizRuler.setWidth(pw);
489                 l |= horizRuler.setFromEnd(horizRuler.from, horizRuler.end);
490                 if (l) horizRuler.layout();
491                 
492                 // Move vertical rulers
493                 double vrx = plot.getX() + plot.getWidth() + 3; 
494                 for (VertRuler vertRuler : vertRulers) {
495                         vertRuler.setTranslate(vrx, plot.getY());
496                         vrx += vertRuler.getWidth() + 3;
497                 }
498                 
499         }
500
501         public void setSize(double width, double height) {
502                 bounds.setFrame(0, 0, width, height);
503         }
504         
505         @Override
506         public void render(Graphics2D g2d) {
507                 // Set Quality High
508                 QualityHints qh = QualityHints.getQuality(g2d);
509         g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, quality.textQuality==LineQuality.Antialias?RenderingHints.VALUE_TEXT_ANTIALIAS_ON:RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
510                 
511                 // Set Bounds
512                 Rectangle bounds = g2d.getClipBounds();         
513                 if (bounds.getWidth()!=this.bounds.getWidth() || bounds.getHeight()!=this.bounds.getHeight()) {
514                         setSize(bounds.getWidth(), bounds.getHeight());
515                         layout();
516                 }
517                 
518                 // Flush history subscriptions
519                 flushHistory();
520
521                 // Render children
522                 super.render(g2d);
523                 
524                 // Render plot's value tip
525                 plot.renderValueTip(g2d);
526                 
527                 // Restore quality
528                 qh.setQuality(g2d);
529         }
530         
531         @Override
532         public Rectangle2D getBoundsInLocal() {
533                 return bounds;
534         }
535         
536         /**
537          * Return true if the viewport is not moving and shows only past values 
538          * @return
539          */
540         public boolean allPast() {
541                 TimeWindow timeWindow = spec.viewProfile.timeWindow;
542                 boolean fixedWindow = !horizRuler.autoscroll || (timeWindow.timeWindowStart!=null && timeWindow.timeWindowLength!=null);
543                 if (fixedWindow) {
544                         for (ItemNode item : allItems) {
545                                 if (item.end <= horizRuler.end) return false;
546                         }
547                         return true;
548                 } else {
549                         return false;
550                 }
551         }
552
553         public void flushHistory() {
554                 if (collector == null || collector instanceof CollectorImpl == false) return;
555                 CollectorImpl fh = (CollectorImpl) collector;
556                 fh.flush( itemIds );
557         }
558         
559         /**
560          * Read values of min,max,from,end to all items.
561          * Put iMin,iMax,iFrom,iEnd to all axes.
562          */
563         public void readMinMaxFromEnd() {
564                 flushHistory();
565                 // Read min,max,from,end from all items - Gather collective ranges
566                 for (ItemNode item : allItems) {
567                         item.readMinMaxFromEnd();
568                 }
569                 horizRuler.setKnownFromEnd();
570                 for (VertRuler vertRuler : vertRulers) vertRuler.setKnownMinMax();
571         }
572
573         
574         
575         /**
576          * Flushes history, resets streams,
577          * reads the latest time and value ranges from disc.
578          * Scales the axes.
579          * Sets dirty true if there was change in axis.
580          */
581         public boolean autoscale(boolean timeAxis, boolean valueAxis) {
582                 if (!timeAxis && !valueAxis) return false;
583
584                 readMinMaxFromEnd();
585                 boolean changed = false;
586                                 
587                 // Time axis
588                 if (timeAxis) {
589                         changed |= horizRuler.autoscale();
590                         if ( changed && !printing ) horizRuler.fireListener();
591                 }
592
593                 // Y-Axis
594                 if (valueAxis) {
595                         for (VertRuler vertRuler : vertRulers) changed |= vertRuler.autoscale();
596                 }       
597
598                 return changed;
599         }
600
601         public boolean updateValueTipTime() {
602                 if (valueTipTime != null && spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
603                         double endTime = horizRuler.getItemEndTime();
604                         if (!Double.isNaN(endTime)) {
605                                 boolean changed = Double.compare(valueTipTime, endTime) != 0;
606                                 valueTipTime = endTime;
607                                 return changed;
608                         }
609                 }
610                 return false;
611         }
612
613         public void zoomIn(double x, double y, double width, double height, boolean horiz, boolean vert) {
614                 if (horiz) {
615                         horizRuler.zoomIn(x, width);
616                 }
617                 
618                 if (vert) {
619                         for (VertRuler vertRuler : vertRulers) {
620                                 vertRuler.zoomIn(y, height);
621                         }
622                 }
623                 shapedirty = true;
624         }
625         
626         public void zoomOut() {
627                 horizRuler.zoomOut();
628                 for (VertRuler vertRuler : vertRulers) {
629                         vertRuler.zoomOut();
630                 }
631                 shapedirty = true;
632         }
633         
634         public TrendSpec getTrendSpec() {
635                 return spec;
636         }
637
638         public Viewport getViewport() 
639         {
640                 Viewport vp = new Viewport();
641                 vp.init();
642                 vp.from = horizRuler.from;
643                 vp.end = horizRuler.end;
644                 for (VertRuler vr : vertRulers) {
645                         AxisViewport avp = new AxisViewport();
646                         avp.min = vr.min;
647                         avp.max = vr.max;
648                         vp.axesports.add( avp );
649                 }
650                 return vp;
651         }
652         
653         public void setViewport( Viewport vp ) 
654         {
655                 horizRuler.from = vp.from;
656                 horizRuler.end = vp.end;
657                 int i=0; 
658                 for ( AxisViewport avp : vp.axesports ) {
659                         if ( i>=vertRulers.size() ) break;
660                         VertRuler vr = vertRulers.get(i++);
661                         vr.min = avp.min;
662                         vr.max = avp.max;
663                 }
664         }
665         
666         public void setQuality(TrendQualitySpec quality)
667         {
668                 this.quality = quality;
669         }
670         
671         public TrendQualitySpec getQuality()
672         {
673                 return quality;
674         }
675         
676         public Double snapToValue(double time, double snapToleranceInTime) throws HistoryException, AccessorException
677         {
678                 double from = horizRuler.from;
679                 double end = horizRuler.end;
680                 double pixelsPerSecond = (end-from) / plot.getWidth();
681                 
682                 TreeSet<Double> values = new TreeSet<Double>(Bindings.DOUBLE);
683                 for (ItemNode item : allItems) {                        
684                         Stream s = item.openStream( pixelsPerSecond );
685                         if ( s==null ) continue;
686                         int pos = s.binarySearch(Bindings.DOUBLE, time);
687                         ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());
688                         // Exact match
689                         if (pos >= 0) {
690                                 return time;
691                         }
692                                 
693                         int prev = -pos-2;
694                         int next = -pos-1;
695                         int count = s.count();          
696                         Double prevTime = null, nextTime = null;
697                                 
698                         if ( prev>=0 && prev<count ) {
699                                 s.accessor.get(prev, s.sampleBinding, vb.getSample());
700                                 if ( !vb.isNanSample() ) {
701                                         prevTime = vb.getTimeDouble();
702                                         if ( vb.hasEndTime() ) {
703                                                 Double nTime = vb.getEndTimeDouble();
704                                                 if (nTime!=null && nTime+snapToleranceInTime>time) nextTime = nTime;
705                                         }
706                                 }
707                         }
708                                 
709                         if ( nextTime==null && next>=0 && next<count ) {
710                                 s.accessor.get(next, s.sampleBinding, vb.getSample());
711                                 if ( !vb.isNanSample() ) {
712                                         nextTime = vb.getTimeDouble();
713                                 }
714                         }
715                         
716                         if (prevTime==null && nextTime==null) continue;
717                                 
718                         if (prevTime==null) {
719                                 if ( nextTime - time < snapToleranceInTime ) 
720                                         values.add(nextTime);
721                         } else if (nextTime==null) {
722                                 if ( time - prevTime < snapToleranceInTime ) 
723                                         values.add(prevTime);
724                         } else {
725                                 values.add(nextTime);
726                                 values.add(prevTime);
727                         }
728                 }
729                 if (values.isEmpty()) return null;
730                 
731                 Double lower = values.floor( time );
732                 Double higher = values.ceiling( time );
733                 
734                 if ( lower == null ) return higher;
735                 if ( higher == null ) return lower;
736                 double result = time-lower < higher-time ? lower : higher;
737                 
738                 
739                 return result;
740                 
741         }
742         
743         
744 }