1 /*******************************************************************************
\r
2 * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
\r
4 * All rights reserved. This program and the accompanying materials
\r
5 * are made available under the terms of the Eclipse Public License v1.0
\r
6 * which accompanies this distribution, and is available at
\r
7 * http://www.eclipse.org/legal/epl-v10.html
\r
10 * VTT Technical Research Centre of Finland - initial API and implementation
\r
11 *******************************************************************************/
\r
12 package org.simantics.trend.impl;
\r
14 import java.awt.BasicStroke;
\r
15 import java.awt.Color;
\r
16 import java.awt.Font;
\r
17 import java.awt.Graphics2D;
\r
18 import java.awt.Rectangle;
\r
19 import java.awt.RenderingHints;
\r
20 import java.awt.font.GlyphVector;
\r
21 import java.awt.geom.Rectangle2D;
\r
22 import java.util.ArrayList;
\r
23 import java.util.Collections;
\r
24 import java.util.Comparator;
\r
25 import java.util.HashMap;
\r
26 import java.util.HashSet;
\r
27 import java.util.List;
\r
28 import java.util.Map;
\r
29 import java.util.Set;
\r
30 import java.util.TreeSet;
\r
32 import org.simantics.databoard.Bindings;
\r
33 import org.simantics.databoard.accessor.error.AccessorException;
\r
34 import org.simantics.databoard.binding.error.BindingException;
\r
35 import org.simantics.databoard.util.Bean;
\r
36 import org.simantics.g2d.utils.GridUtil;
\r
37 import org.simantics.history.Collector;
\r
38 import org.simantics.history.History;
\r
39 import org.simantics.history.HistoryException;
\r
40 import org.simantics.history.HistoryManager;
\r
41 import org.simantics.history.ItemManager;
\r
42 import org.simantics.history.impl.CollectorImpl;
\r
43 import org.simantics.history.util.Stream;
\r
44 import org.simantics.history.util.ValueBand;
\r
45 import org.simantics.scenegraph.g2d.G2DParentNode;
\r
46 import org.simantics.scenegraph.utils.QualityHints;
\r
47 import org.simantics.trend.configuration.ItemPlacement;
\r
48 import org.simantics.trend.configuration.LineQuality;
\r
49 import org.simantics.trend.configuration.Scale;
\r
50 import org.simantics.trend.configuration.TimeFormat;
\r
51 import org.simantics.trend.configuration.TimeWindow;
\r
52 import org.simantics.trend.configuration.TrendItem;
\r
53 import org.simantics.trend.configuration.TrendItem.Renderer;
\r
54 import org.simantics.trend.configuration.TrendQualitySpec;
\r
55 import org.simantics.trend.configuration.TrendSpec;
\r
56 import org.simantics.trend.configuration.Viewport;
\r
57 import org.simantics.trend.configuration.Viewport.AxisViewport;
\r
58 import org.simantics.trend.configuration.YAxisMode;
\r
59 import org.simantics.utils.format.ValueFormat;
\r
61 import gnu.trove.map.TObjectIntMap;
\r
62 import gnu.trove.map.hash.TObjectIntHashMap;
\r
63 import gnu.trove.procedure.TIntProcedure;
\r
64 import gnu.trove.procedure.TObjectProcedure;
\r
66 public class TrendNode extends G2DParentNode implements TrendLayout {
\r
68 private static final long serialVersionUID = -8339696405893626168L;
\r
71 public TextNode titleNode;
\r
75 public HorizRuler horizRuler;
\r
76 VertRuler vertRuler; // Selected vertical ruler, null if there are no analog items
\r
77 int vertRulerIndex; // Index of the selected vertical ruler
\r
78 List<VertRuler> vertRulers = new ArrayList<VertRuler>();
\r
79 SelectionNode selection;
\r
80 public MilestoneSpec milestones;
\r
82 /** Control bounds */
\r
83 Rectangle2D bounds = new Rectangle2D.Double();
\r
86 public TrendSpec spec;
\r
87 public ViewRenderingProfile renderingProfile = new ViewRenderingProfile();
\r
88 public TrendQualitySpec quality = TrendQualitySpec.DEFAULT;
\r
89 public boolean printing = false;
\r
93 List<ItemNode> analogItems = new ArrayList<ItemNode>();
\r
94 List<ItemNode> binaryItems = new ArrayList<ItemNode>();
\r
95 List<ItemNode> allItems = new ArrayList<ItemNode>();
\r
98 static HistoryManager DUMMY_HISTORY = History.createMemoryHistory();
\r
99 public HistoryManager historian = DUMMY_HISTORY;
\r
101 // Collector - set for flushing the stream, right before drawing
\r
102 public Collector collector = null;
\r
103 Set<String> itemIds = new HashSet<String>();
\r
105 // Signal to indicate history has changed.
\r
106 public boolean datadirty = false;
\r
107 // Signal to indicate the cached shapes are dirty. This will cause reloading of data.
\r
108 public boolean shapedirty = false;
\r
110 public boolean autoscaletime = true;
\r
112 public TimeFormat timeFormat = TimeFormat.Time;
\r
113 public ValueFormat valueFormat = ValueFormat.Default;
\r
114 public boolean drawSamples = false;
\r
116 /** Time at mouse, when mouse hovers over trend. This value is set by TrendParticipant. NaN is mouse is not hovering */
\r
117 public double mouseHoverTime = Double.NaN, lastMouseHoverTime = 0.0;
\r
119 /** If set, valueTip is drawn at the time */
\r
120 public Double valueTipTime = null, prevValueTipTime = null;
\r
121 public boolean valueTipHover = false;
\r
123 public ItemPlacement itemPlacement = ItemPlacement.Overlapping;
\r
126 public void init() {
\r
127 spec = new TrendSpec();
\r
130 milestones = new MilestoneSpec();
\r
133 //// Create title node
\r
134 titleNode = addNode( "Title", TextNode.class );
\r
135 titleNode.setFont( new Font("Arial", 0, 30) );
\r
136 titleNode.setColor( Color.BLACK );
\r
137 titleNode.setText( "<title here>");
\r
138 titleNode.setSize(300, 40);
\r
140 plot = addNode( "Plot", Plot.class );
\r
142 horizRuler = addNode("HorizRuler", HorizRuler.class);
\r
143 vertRuler = addNode("VertRuler", VertRuler.class);
\r
144 vertRulers.add( vertRuler );
\r
146 /// Set some bounds
\r
147 horizRuler.setFromEnd(0, 100);
\r
148 vertRuler.setMinMax(0, 100);
\r
153 * Set source of data
\r
155 * @param collector (Optional) Used for flushing collectors
\r
157 public void setHistorian(HistoryManager historian, Collector collector) {
\r
158 this.historian = historian==null?DUMMY_HISTORY:historian;
\r
159 this.collector = collector;
\r
161 ItemManager allFiles = ItemManager.createUnchecked( getHistoryItems() );
\r
162 for (ItemNode item : allItems) {
\r
163 item.setTrendItem(item.item, allFiles);
\r
164 for (Bean historyItem : item.historyItems ) {
\r
166 itemIds.add( (String) historyItem.getField("id") );
\r
167 } catch (BindingException e) {
\r
174 * Get info of all history items.
\r
175 * This util is created for polling strategy.
\r
179 Bean[] getHistoryItems() {
\r
180 Bean[] result = null;
\r
181 HistoryException e = null;
\r
182 for (int attempt=0; attempt<10; attempt++) {
\r
184 result = historian.getItems();
\r
186 } catch (HistoryException e2) {
\r
187 if (e==null) e = e2;
\r
190 } catch (InterruptedException e1) {
\r
194 if (result!=null) return result;
\r
195 //throw new RuntimeException(e);
\r
196 return new Bean[0];
\r
199 public void setMilestones(Bean milestones) {
\r
200 if (this.milestones.equals(milestones)) return;
\r
201 this.milestones.readFrom( milestones );
\r
202 boolean hasBaseline = this.milestones.baseline>=0;
\r
204 // Sort by first, put baseline on top
\r
205 final Milestone baseline = this.milestones.baseline>=0 ? this.milestones.milestones.get( this.milestones.baseline ) : null;
\r
206 Collections.sort(this.milestones.milestones, new Comparator<Milestone>() {
\r
207 public int compare(Milestone o1, Milestone o2) {
\r
208 if (o1==baseline) return -1;
\r
209 if (o2==baseline) return 1;
\r
210 return Double.compare(o1.time, o1.time);
\r
213 this.milestones.baseline = hasBaseline ? 0 : -1;
\r
214 double newBasetime = hasBaseline ? this.milestones.milestones.get(this.milestones.baseline).time : 0.;
\r
215 if (newBasetime != horizRuler.basetime) {
\r
216 horizRuler.basetime = newBasetime;
\r
217 horizRuler.layout();
\r
222 public void selectVertRuler(int index) {
\r
223 vertRulerIndex = index;
\r
224 if (index<0 || index>=vertRulers.size()) {
\r
225 vertRuler = vertRulers.get(0);
\r
227 vertRuler = vertRulers.get(index);
\r
233 public void cleanup() {
\r
234 spec = new TrendSpec();
\r
236 analogItems.clear();
\r
237 binaryItems.clear();
\r
239 historian = DUMMY_HISTORY;
\r
243 private static TObjectIntMap<String> itemIndexMap(List<TrendItem> items) {
\r
244 TObjectIntMap<String> map = new TObjectIntHashMap<>(items.size(), 0.5f, -1);
\r
245 for (int i = 0; i < items.size(); ++i) {
\r
246 TrendItem it = items.get(i);
\r
247 map.put(it.groupItemId, i);
\r
252 private static <T> TObjectIntMap<T> subtract(TObjectIntMap<T> a, TObjectIntMap<T> b) {
\r
253 TObjectIntMap<T> r = new TObjectIntHashMap<>(a);
\r
254 b.forEachKey(new TObjectProcedure<T>() {
\r
256 public boolean execute(T key) {
\r
266 * new trending specification, cannot not be <code>null</code>.
\r
267 * Use {@link TrendSpec#EMPTY} instead of <code>null</code>.
\r
269 public void setTrendSpec(TrendSpec newSpec) {
\r
270 //System.out.println(newSpec);
\r
271 // Check if equal & Read spec
\r
272 if (newSpec.equals(this.spec)) return;
\r
274 boolean timeWindowChange = !this.spec.viewProfile.timeWindow.equals( newSpec.viewProfile.timeWindow );
\r
275 boolean yaxisModeChanged = this.spec.axisMode != newSpec.axisMode;
\r
277 TObjectIntMap<String> newItemMap = itemIndexMap(newSpec.items);
\r
278 TObjectIntMap<String> currentItemMap = itemIndexMap(spec.items);
\r
279 TObjectIntMap<String> addedItemMap = subtract(newItemMap, currentItemMap);
\r
280 TObjectIntMap<String> removedItemMap = subtract(currentItemMap, newItemMap);
\r
281 Map<String, VertRuler> existingRulers = new HashMap<>();
\r
282 if (this.spec.axisMode == YAxisMode.MultiAxis) {
\r
283 for (ItemNode item : analogItems)
\r
284 if (item.ruler != null)
\r
285 existingRulers.put(item.item.groupItemId, item.ruler);
\r
288 this.spec.readFrom( newSpec );
\r
289 this.spec.sortItems();
\r
290 this.renderingProfile.read(this.spec.viewProfile);
\r
293 if (titleNode != null) titleNode.setText( spec.name );
\r
295 // Setup trend item nodes
\r
297 for (ItemNode item : allItems) removeNode(item);
\r
298 analogItems.clear();
\r
299 binaryItems.clear();
\r
302 ItemManager itemManager = ItemManager.createUnchecked( getHistoryItems() );
\r
303 for (int i = 0; i<spec.items.size(); i++) {
\r
304 TrendItem item = spec.items.get(i);
\r
308 ItemNode node = createItemNode(item, itemManager);
\r
309 for (Bean historyItem : node.historyItems) {
\r
311 itemIds.add( (String) historyItem.getField("id") );
\r
312 } catch (BindingException e) {
\r
316 if (item.renderer == Renderer.Analog) {
\r
317 analogItems.add(node);
\r
319 binaryItems.add(node);
\r
321 allItems.add(node);
\r
324 // Setup vertical ruler nodes
\r
325 singleAxis = spec.axisMode == YAxisMode.SingleAxis;
\r
327 if (yaxisModeChanged || vertRulers.size() != 1 || vertRuler == null) {
\r
328 for (VertRuler vr : vertRulers) removeNode(vr);
\r
329 vertRulers.clear();
\r
331 vertRuler = addNode("VertRuler", VertRuler.class);
\r
332 vertRulers.add( vertRuler );
\r
335 vertRuler.manualscale = true;
\r
336 for (int i=0; i<analogItems.size(); i++) {
\r
337 ItemNode item = analogItems.get(i);
\r
338 item.ruler = vertRuler;
\r
339 item.trendNode = this;
\r
340 if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false;
\r
343 if (yaxisModeChanged) {
\r
344 // Recreate all rulers
\r
345 for (VertRuler vr : vertRulers) removeNode(vr);
\r
346 vertRulers.clear();
\r
347 for (int i=0; i<analogItems.size(); i++)
\r
348 vertRulers.add( addNode(VertRuler.class) );
\r
350 removedItemMap.forEachKey(new TObjectProcedure<String>() {
\r
352 public boolean execute(String id) {
\r
353 VertRuler vr = existingRulers.get(id);
\r
356 vertRulers.remove(vr);
\r
361 addedItemMap.forEachValue(new TIntProcedure() {
\r
363 public boolean execute(int index) {
\r
364 vertRulers.add( index, addNode(VertRuler.class) );
\r
370 for (int i = 0; i < analogItems.size(); i++) {
\r
371 ItemNode item = analogItems.get(i);
\r
372 VertRuler vr = vertRulers.get(i);
\r
373 vr.setZIndex(1000 + i);
\r
374 vr.color = item.color;
\r
375 vr.label = item.item.label;
\r
376 vr.manualscale = item.item.scale instanceof Scale.Manual;
\r
378 item.trendNode = this;
\r
380 // Select vert ruler
\r
381 vertRuler = vertRulers.isEmpty() ? null : vertRulers.get( vertRulerIndex <= 0 || vertRulerIndex >= vertRulers.size() ? 0 : vertRulerIndex );
\r
385 TimeWindow tw = spec.viewProfile.timeWindow;
\r
386 horizRuler.manualscale = tw.timeWindowLength!=null && tw.timeWindowStart!=null;
\r
388 if (timeWindowChange) {
\r
389 horizRuler.autoscale();
\r
394 private ItemNode createItemNode(TrendItem item, ItemManager itemManager) {
\r
395 ItemNode node = addNode( ItemNode.class );
\r
396 //node.trendNode = this;
\r
397 node.setTrendItem(item, itemManager);
\r
398 node.color = toColor(item.customColor);
\r
399 if (node.color == null)
\r
400 node.color = JarisPaints.getColor( item.index );
\r
401 node.stroke = item.customStrokeWidth != null
\r
402 ? withStrokeWidth(item.customStrokeWidth, Plot.TREND_LINE_STROKE)
\r
403 : Plot.TREND_LINE_STROKE;
\r
407 private static BasicStroke withStrokeWidth(float width, BasicStroke stroke) {
\r
408 return new BasicStroke(width,
\r
409 stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),
\r
410 stroke.getDashArray(), stroke.getDashPhase());
\r
413 private static Color toColor(float[] components) {
\r
414 if (components == null)
\r
416 switch (components.length) {
\r
417 case 3: return new Color(components[0], components[1], components[2]);
\r
418 case 4: return new Color(components[0], components[1], components[2], components[3]);
\r
419 default: return null;
\r
424 * Layout graphical nodes based on bounds
\r
426 public void layout() {
\r
427 double w = bounds.getWidth();
\r
428 double h = bounds.getHeight();
\r
429 if ( titleNode != null ) {
\r
430 titleNode.setSize(w, h * 0.02);
\r
431 titleNode.setTranslate(0, VERT_MARGIN);
\r
432 titleNode.layout();
\r
435 // Plot-Ruler area width
\r
436 double praw = w-HORIZ_MARGIN*2;
\r
439 double ph = h-VERT_MARGIN*2-MILESTONE_HEIGHT-HORIZ_RULER_HEIGHT;
\r
440 if ( titleNode != null ) {
\r
441 ph -= titleNode.th;
\r
444 // Analog & Binary area height
\r
446 if (!analogItems.isEmpty()) {
\r
447 bah = binaryItems.size() * BINARY[3];
\r
448 aah = Math.max(0, ph-bah);
\r
449 if (aah+bah>ph) bah = ph-aah;
\r
456 for (VertRuler vertRuler : vertRulers) {
\r
457 vertRuler.setHeight(aah);
\r
458 vertRuler.layout();
\r
460 plot.analogAreaHeight = aah;
\r
461 plot.binaryAreaHeight = bah;
\r
463 // Vertical ruler widths
\r
465 for (VertRuler vertRuler : vertRulers) {
\r
466 vrws += vertRuler.getWidth();
\r
469 // Add room for Binary label
\r
470 if ( !binaryItems.isEmpty() ) {
\r
471 double maxLabelWidth = BINARY_LABEL_WIDTH;
\r
472 for (ItemNode node : binaryItems) {
\r
473 if (node.item != null) {
\r
474 GlyphVector glyphVector = RULER_FONT.createGlyphVector(GridUtil.frc, node.item.label);
\r
475 double labelWidth = glyphVector.getVisualBounds().getWidth();
\r
476 maxLabelWidth = Math.max( maxLabelWidth, labelWidth );
\r
479 vrws = Math.max(maxLabelWidth, vrws);
\r
483 double pw = praw - vrws;
\r
484 plot.setTranslate(HORIZ_MARGIN, (titleNode!=null?titleNode.th:0)+VERT_MARGIN+MILESTONE_HEIGHT);
\r
485 plot.setSize(pw, ph);
\r
487 horizRuler.layout();
\r
488 horizRuler.setTranslate(HORIZ_MARGIN, plot.getY()+plot.getHeight()+3);
\r
489 boolean l = horizRuler.setWidth(pw);
\r
490 l |= horizRuler.setFromEnd(horizRuler.from, horizRuler.end);
\r
491 if (l) horizRuler.layout();
\r
493 // Move vertical rulers
\r
494 double vrx = plot.getX() + plot.getWidth() + 3;
\r
495 for (VertRuler vertRuler : vertRulers) {
\r
496 vertRuler.setTranslate(vrx, plot.getY());
\r
497 vrx += vertRuler.getWidth() + 3;
\r
502 public void setSize(double width, double height) {
\r
503 bounds.setFrame(0, 0, width, height);
\r
507 public void render(Graphics2D g2d) {
\r
508 // Set Quality High
\r
509 QualityHints qh = QualityHints.getQuality(g2d);
\r
510 g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, quality.textQuality==LineQuality.Antialias?RenderingHints.VALUE_TEXT_ANTIALIAS_ON:RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
\r
513 Rectangle bounds = g2d.getClipBounds();
\r
514 if (bounds.getWidth()!=this.bounds.getWidth() || bounds.getHeight()!=this.bounds.getHeight()) {
\r
515 setSize(bounds.getWidth(), bounds.getHeight());
\r
519 // Flush history subscriptions
\r
525 // Render plot's value tip
\r
526 plot.renderValueTip(g2d);
\r
529 qh.setQuality(g2d);
\r
533 public Rectangle2D getBoundsInLocal() {
\r
538 * Return true if the viewport is not moving and shows only past values
\r
541 public boolean allPast() {
\r
542 TimeWindow timeWindow = spec.viewProfile.timeWindow;
\r
543 boolean fixedWindow = !horizRuler.autoscroll || (timeWindow.timeWindowStart!=null && timeWindow.timeWindowLength!=null);
\r
545 for (ItemNode item : allItems) {
\r
546 if (item.end <= horizRuler.end) return false;
\r
554 public void flushHistory() {
\r
555 if (collector == null || collector instanceof CollectorImpl == false) return;
\r
556 CollectorImpl fh = (CollectorImpl) collector;
\r
557 fh.flush( itemIds );
\r
561 * Read values of min,max,from,end to all items.
\r
562 * Put iMin,iMax,iFrom,iEnd to all axes.
\r
564 public void readMinMaxFromEnd() {
\r
566 // Read min,max,from,end from all items - Gather collective ranges
\r
567 for (ItemNode item : allItems) {
\r
568 item.readMinMaxFromEnd();
\r
570 horizRuler.setKnownFromEnd();
\r
571 for (VertRuler vertRuler : vertRulers) vertRuler.setKnownMinMax();
\r
577 * Flushes history, resets streams,
\r
578 * reads the latest time and value ranges from disc.
\r
580 * Sets dirty true if there was change in axis.
\r
582 public boolean autoscale(boolean timeAxis, boolean valueAxis) {
\r
583 if (!timeAxis && !valueAxis) return false;
\r
585 readMinMaxFromEnd();
\r
586 boolean changed = false;
\r
590 changed |= horizRuler.autoscale();
\r
591 if ( changed && !printing ) horizRuler.fireListener();
\r
596 for (VertRuler vertRuler : vertRulers) changed |= vertRuler.autoscale();
\r
602 public boolean updateValueTipTime() {
\r
603 if (valueTipTime != null && spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
\r
604 double endTime = horizRuler.getItemEndTime();
\r
605 if (!Double.isNaN(endTime)) {
\r
606 boolean changed = Double.compare(valueTipTime, endTime) != 0;
\r
607 valueTipTime = endTime;
\r
614 public void zoomIn(double x, double y, double width, double height, boolean horiz, boolean vert) {
\r
616 horizRuler.zoomIn(x, width);
\r
620 for (VertRuler vertRuler : vertRulers) {
\r
621 vertRuler.zoomIn(y, height);
\r
627 public void zoomOut() {
\r
628 horizRuler.zoomOut();
\r
629 for (VertRuler vertRuler : vertRulers) {
\r
630 vertRuler.zoomOut();
\r
635 public TrendSpec getTrendSpec() {
\r
639 public Viewport getViewport()
\r
641 Viewport vp = new Viewport();
\r
643 vp.from = horizRuler.from;
\r
644 vp.end = horizRuler.end;
\r
645 for (VertRuler vr : vertRulers) {
\r
646 AxisViewport avp = new AxisViewport();
\r
649 vp.axesports.add( avp );
\r
654 public void setViewport( Viewport vp )
\r
656 horizRuler.from = vp.from;
\r
657 horizRuler.end = vp.end;
\r
659 for ( AxisViewport avp : vp.axesports ) {
\r
660 if ( i>=vertRulers.size() ) break;
\r
661 VertRuler vr = vertRulers.get(i++);
\r
667 public void setQuality(TrendQualitySpec quality)
\r
669 this.quality = quality;
\r
672 public TrendQualitySpec getQuality()
\r
677 public Double snapToValue(double time, double snapToleranceInTime) throws HistoryException, AccessorException
\r
679 double from = horizRuler.from;
\r
680 double end = horizRuler.end;
\r
681 double pixelsPerSecond = (end-from) / plot.getWidth();
\r
683 TreeSet<Double> values = new TreeSet<Double>(Bindings.DOUBLE);
\r
684 for (ItemNode item : allItems) {
\r
685 Stream s = item.openStream( pixelsPerSecond );
\r
686 if ( s==null ) continue;
\r
687 int pos = s.binarySearch(Bindings.DOUBLE, time);
\r
688 ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());
\r
696 int count = s.count();
\r
697 Double prevTime = null, nextTime = null;
\r
699 if ( prev>=0 && prev<count ) {
\r
700 s.accessor.get(prev, s.sampleBinding, vb.getSample());
\r
701 if ( !vb.isNanSample() ) {
\r
702 prevTime = vb.getTimeDouble();
\r
703 if ( vb.hasEndTime() ) {
\r
704 Double nTime = vb.getEndTimeDouble();
\r
705 if (nTime!=null && nTime+snapToleranceInTime>time) nextTime = nTime;
\r
710 if ( nextTime==null && next>=0 && next<count ) {
\r
711 s.accessor.get(next, s.sampleBinding, vb.getSample());
\r
712 if ( !vb.isNanSample() ) {
\r
713 nextTime = vb.getTimeDouble();
\r
717 if (prevTime==null && nextTime==null) continue;
\r
719 if (prevTime==null) {
\r
720 if ( nextTime - time < snapToleranceInTime )
\r
721 values.add(nextTime);
\r
722 } else if (nextTime==null) {
\r
723 if ( time - prevTime < snapToleranceInTime )
\r
724 values.add(prevTime);
\r
726 values.add(nextTime);
\r
727 values.add(prevTime);
\r
730 if (values.isEmpty()) return null;
\r
732 Double lower = values.floor( time );
\r
733 Double higher = values.ceiling( time );
\r
735 if ( lower == null ) return higher;
\r
736 if ( higher == null ) return lower;
\r
737 double result = time-lower < higher-time ? lower : higher;
\r