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