1 /*******************************************************************************
2 * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
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
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.trend.impl;
14 import java.awt.BasicStroke;
15 import java.awt.Color;
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;
30 import java.util.TreeSet;
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;
61 import gnu.trove.map.TObjectIntMap;
62 import gnu.trove.map.hash.TObjectIntHashMap;
63 import gnu.trove.procedure.TObjectProcedure;
65 public class TrendNode extends G2DParentNode implements TrendLayout {
67 private static final long serialVersionUID = -8339696405893626168L;
70 public TextNode titleNode;
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;
82 Rectangle2D bounds = new Rectangle2D.Double();
85 public TrendSpec spec;
86 public ViewRenderingProfile renderingProfile = new ViewRenderingProfile();
87 public TrendQualitySpec quality = TrendQualitySpec.DEFAULT;
88 public boolean printing = false;
92 List<ItemNode> analogItems = new ArrayList<ItemNode>();
93 List<ItemNode> binaryItems = new ArrayList<ItemNode>();
94 List<ItemNode> allItems = new ArrayList<ItemNode>();
97 static HistoryManager DUMMY_HISTORY = History.createMemoryHistory();
98 public HistoryManager historian = DUMMY_HISTORY;
100 // Collector - set for flushing the stream, right before drawing
101 public Collector collector = null;
102 Set<String> itemIds = new HashSet<String>();
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;
109 public boolean autoscaletime = true;
111 public TimeFormat timeFormat = TimeFormat.Time;
112 public ValueFormat valueFormat = ValueFormat.Default;
113 public boolean drawSamples = false;
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;
118 /** If set, valueTip is drawn at the time */
119 public Double valueTipTime = null, prevValueTipTime = null;
120 public boolean valueTipHover = false;
122 public ItemPlacement itemPlacement = ItemPlacement.Overlapping;
126 spec = new TrendSpec();
129 milestones = new MilestoneSpec();
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);
139 plot = addNode( "Plot", Plot.class );
141 horizRuler = addNode("HorizRuler", HorizRuler.class);
142 vertRuler = addNode("VertRuler", VertRuler.class);
143 vertRulers.add( vertRuler );
146 horizRuler.setFromEnd(0, 100);
147 vertRuler.setMinMax(0, 100);
154 * @param collector (Optional) Used for flushing collectors
156 public void setHistorian(HistoryManager historian, Collector collector) {
157 this.historian = historian==null?DUMMY_HISTORY:historian;
158 this.collector = collector;
160 ItemManager allFiles = ItemManager.createUnchecked( getHistoryItems() );
161 for (ItemNode item : allItems) {
162 item.setTrendItem(item.item, allFiles);
163 for (Bean historyItem : item.historyItems ) {
165 itemIds.add( (String) historyItem.getField("id") );
166 } catch (BindingException e) {
173 * Get info of all history items.
174 * This util is created for polling strategy.
178 Bean[] getHistoryItems() {
179 Bean[] result = null;
180 HistoryException e = null;
181 for (int attempt=0; attempt<10; attempt++) {
183 result = historian.getItems();
185 } catch (HistoryException e2) {
189 } catch (InterruptedException e1) {
193 if (result!=null) return result;
194 //throw new RuntimeException(e);
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;
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);
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;
221 public void selectVertRuler(int index) {
222 vertRulerIndex = index;
223 if (index<0 || index>=vertRulers.size()) {
224 vertRuler = vertRulers.get(0);
226 vertRuler = vertRulers.get(index);
232 public void cleanup() {
233 spec = new TrendSpec();
238 historian = DUMMY_HISTORY;
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 map.put(it.groupItemId, i);
251 private static <T> TObjectIntMap<T> subtract(TObjectIntMap<T> a, TObjectIntMap<T> b) {
252 TObjectIntMap<T> r = new TObjectIntHashMap<>(a);
253 b.forEachKey(new TObjectProcedure<T>() {
255 public boolean execute(T key) {
265 * new trending specification, cannot not be <code>null</code>.
266 * Use {@link TrendSpec#EMPTY} instead of <code>null</code>.
268 public void setTrendSpec(TrendSpec newSpec) {
269 //System.out.println(newSpec);
270 // Check if equal & Read spec
271 if (newSpec.equals(this.spec)) return;
273 boolean timeWindowChange = !this.spec.viewProfile.timeWindow.equals( newSpec.viewProfile.timeWindow );
274 boolean yaxisModeChanged = this.spec.axisMode != newSpec.axisMode;
276 TObjectIntMap<String> newItemMap = itemIndexMap(newSpec.items);
277 TObjectIntMap<String> currentItemMap = itemIndexMap(spec.items);
278 TObjectIntMap<String> removedItemMap = subtract(currentItemMap, newItemMap);
279 Map<String, VertRuler> existingRulers = new HashMap<>();
280 if (this.spec.axisMode == YAxisMode.MultiAxis) {
281 for (ItemNode item : analogItems)
282 if (item.ruler != null)
283 existingRulers.put(item.item.groupItemId, item.ruler);
286 this.spec.readFrom( newSpec );
287 this.spec.sortItems();
288 this.renderingProfile.read(this.spec.viewProfile);
291 if (titleNode != null) titleNode.setText( spec.name );
293 // Setup trend item nodes
295 for (ItemNode item : allItems) removeNode(item);
300 ItemManager itemManager = ItemManager.createUnchecked( getHistoryItems() );
301 for (int i = 0; i<spec.items.size(); i++) {
302 TrendItem item = spec.items.get(i);
306 ItemNode node = createItemNode(item, itemManager);
307 for (Bean historyItem : node.historyItems) {
309 itemIds.add( (String) historyItem.getField("id") );
310 } catch (BindingException e) {
314 if (item.renderer == Renderer.Analog) {
315 analogItems.add(node);
317 binaryItems.add(node);
322 // Setup vertical ruler nodes
323 singleAxis = spec.axisMode == YAxisMode.SingleAxis;
325 if (yaxisModeChanged || vertRulers.size() != 1 || vertRuler == null) {
326 for (VertRuler vr : vertRulers) removeNode(vr);
329 vertRuler = addNode("VertRuler", VertRuler.class);
330 vertRulers.add( vertRuler );
333 vertRuler.manualscale = true;
334 for (int i=0; i<analogItems.size(); i++) {
335 ItemNode item = analogItems.get(i);
336 item.ruler = vertRuler;
337 item.trendNode = this;
338 if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false;
341 if (yaxisModeChanged) {
342 // Recreate all rulers
343 for (VertRuler vr : vertRulers) removeNode(vr);
345 for (int i=0; i<analogItems.size(); i++)
346 vertRulers.add( addNode(VertRuler.class) );
348 // Remove rulers of the items that were removed
349 // and add new rulers to have enough of them for
350 // each separate analog signal.
351 removedItemMap.forEachKey(new TObjectProcedure<String>() {
353 public boolean execute(String id) {
354 VertRuler vr = existingRulers.get(id);
357 vertRulers.remove(vr);
362 for (int i = vertRulers.size(); i < analogItems.size(); ++i) {
363 VertRuler ruler = addNode(VertRuler.class);
364 vertRulers.add(ruler);
368 for (int i = 0; i < analogItems.size(); i++) {
369 ItemNode item = analogItems.get(i);
370 VertRuler vr = vertRulers.get(i);
371 vr.setZIndex(1000 + i);
372 vr.color = item.color;
373 vr.label = item.item.label;
374 vr.manualscale = item.item.scale instanceof Scale.Manual;
376 item.trendNode = this;
379 vertRuler = vertRulers.isEmpty() ? null : vertRulers.get( vertRulerIndex <= 0 || vertRulerIndex >= vertRulers.size() ? 0 : vertRulerIndex );
383 TimeWindow tw = spec.viewProfile.timeWindow;
384 horizRuler.manualscale = tw.timeWindowLength!=null && tw.timeWindowStart!=null;
386 if (timeWindowChange) {
387 horizRuler.autoscale();
392 private ItemNode createItemNode(TrendItem item, ItemManager itemManager) {
393 ItemNode node = addNode( ItemNode.class );
394 //node.trendNode = this;
395 node.setTrendItem(item, itemManager);
396 node.color = toColor(item.customColor);
397 if (node.color == null)
398 node.color = JarisPaints.getColor( item.index );
399 node.stroke = item.customStrokeWidth != null
400 ? withStrokeWidth(item.customStrokeWidth, Plot.TREND_LINE_STROKE)
401 : Plot.TREND_LINE_STROKE;
405 private static BasicStroke withStrokeWidth(float width, BasicStroke stroke) {
406 return new BasicStroke(width,
407 stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(),
408 stroke.getDashArray(), stroke.getDashPhase());
411 private static Color toColor(float[] components) {
412 if (components == null)
414 switch (components.length) {
415 case 3: return new Color(components[0], components[1], components[2]);
416 case 4: return new Color(components[0], components[1], components[2], components[3]);
417 default: return null;
422 * Layout graphical nodes based on bounds
424 public void layout() {
425 double w = bounds.getWidth();
426 double h = bounds.getHeight();
427 if ( titleNode != null ) {
428 titleNode.setSize(w, h * 0.02);
429 titleNode.setTranslate(0, VERT_MARGIN);
433 // Plot-Ruler area width
434 double praw = w-HORIZ_MARGIN*2;
437 double ph = h-VERT_MARGIN*2-MILESTONE_HEIGHT-HORIZ_RULER_HEIGHT;
438 if ( titleNode != null ) {
442 // Analog & Binary area height
444 if (!analogItems.isEmpty()) {
445 bah = binaryItems.size() * BINARY[3];
446 aah = Math.max(0, ph-bah);
447 if (aah+bah>ph) bah = ph-aah;
454 for (VertRuler vertRuler : vertRulers) {
455 vertRuler.setHeight(aah);
458 plot.analogAreaHeight = aah;
459 plot.binaryAreaHeight = bah;
461 // Vertical ruler widths
463 for (VertRuler vertRuler : vertRulers) {
464 vrws += vertRuler.getWidth();
467 // Add room for Binary label
468 if ( !binaryItems.isEmpty() ) {
469 double maxLabelWidth = BINARY_LABEL_WIDTH;
470 for (ItemNode node : binaryItems) {
471 if (node.item != null) {
472 GlyphVector glyphVector = RULER_FONT.createGlyphVector(GridUtil.frc, node.item.label);
473 double labelWidth = glyphVector.getVisualBounds().getWidth();
474 maxLabelWidth = Math.max( maxLabelWidth, labelWidth );
477 vrws = Math.max(maxLabelWidth, vrws);
481 double pw = praw - vrws;
482 plot.setTranslate(HORIZ_MARGIN, (titleNode!=null?titleNode.th:0)+VERT_MARGIN+MILESTONE_HEIGHT);
483 plot.setSize(pw, ph);
486 horizRuler.setTranslate(HORIZ_MARGIN, plot.getY()+plot.getHeight()+3);
487 boolean l = horizRuler.setWidth(pw);
488 l |= horizRuler.setFromEnd(horizRuler.from, horizRuler.end);
489 if (l) horizRuler.layout();
491 // Move vertical rulers
492 double vrx = plot.getX() + plot.getWidth() + 3;
493 for (VertRuler vertRuler : vertRulers) {
494 vertRuler.setTranslate(vrx, plot.getY());
495 vrx += vertRuler.getWidth() + 3;
500 public void setSize(double width, double height) {
501 bounds.setFrame(0, 0, width, height);
505 public void render(Graphics2D g2d) {
507 QualityHints qh = QualityHints.getQuality(g2d);
508 g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, quality.textQuality==LineQuality.Antialias?RenderingHints.VALUE_TEXT_ANTIALIAS_ON:RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
511 Rectangle bounds = g2d.getClipBounds();
512 if (bounds.getWidth()!=this.bounds.getWidth() || bounds.getHeight()!=this.bounds.getHeight()) {
513 setSize(bounds.getWidth(), bounds.getHeight());
517 // Flush history subscriptions
523 // Render plot's value tip
524 plot.renderValueTip(g2d);
531 public Rectangle2D getBoundsInLocal() {
536 * Return true if the viewport is not moving and shows only past values
539 public boolean allPast() {
540 TimeWindow timeWindow = spec.viewProfile.timeWindow;
541 boolean fixedWindow = !horizRuler.autoscroll || (timeWindow.timeWindowStart!=null && timeWindow.timeWindowLength!=null);
543 for (ItemNode item : allItems) {
544 if (item.end <= horizRuler.end) return false;
552 public void flushHistory() {
553 if (collector == null || collector instanceof CollectorImpl == false) return;
554 CollectorImpl fh = (CollectorImpl) collector;
559 * Read values of min,max,from,end to all items.
560 * Put iMin,iMax,iFrom,iEnd to all axes.
562 public void readMinMaxFromEnd() {
564 // Read min,max,from,end from all items - Gather collective ranges
565 for (ItemNode item : allItems) {
566 item.readMinMaxFromEnd();
568 horizRuler.setKnownFromEnd();
569 for (VertRuler vertRuler : vertRulers) vertRuler.setKnownMinMax();
575 * Flushes history, resets streams,
576 * reads the latest time and value ranges from disc.
578 * Sets dirty true if there was change in axis.
580 public boolean autoscale(boolean timeAxis, boolean valueAxis) {
581 if (!timeAxis && !valueAxis) return false;
584 boolean changed = false;
588 changed |= horizRuler.autoscale();
589 if ( changed && !printing ) horizRuler.fireListener();
594 for (VertRuler vertRuler : vertRulers) changed |= vertRuler.autoscale();
600 public boolean updateValueTipTime() {
601 if (valueTipTime != null && spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
602 double endTime = horizRuler.getItemEndTime();
603 if (!Double.isNaN(endTime)) {
604 boolean changed = Double.compare(valueTipTime, endTime) != 0;
605 valueTipTime = endTime;
612 public void zoomIn(double x, double y, double width, double height, boolean horiz, boolean vert) {
614 horizRuler.zoomIn(x, width);
618 for (VertRuler vertRuler : vertRulers) {
619 vertRuler.zoomIn(y, height);
625 public void zoomOut() {
626 horizRuler.zoomOut();
627 for (VertRuler vertRuler : vertRulers) {
633 public TrendSpec getTrendSpec() {
637 public Viewport getViewport()
639 Viewport vp = new Viewport();
641 vp.from = horizRuler.from;
642 vp.end = horizRuler.end;
643 for (VertRuler vr : vertRulers) {
644 AxisViewport avp = new AxisViewport();
647 vp.axesports.add( avp );
652 public void setViewport( Viewport vp )
654 horizRuler.from = vp.from;
655 horizRuler.end = vp.end;
657 for ( AxisViewport avp : vp.axesports ) {
658 if ( i>=vertRulers.size() ) break;
659 VertRuler vr = vertRulers.get(i++);
665 public void setQuality(TrendQualitySpec quality)
667 this.quality = quality;
670 public TrendQualitySpec getQuality()
675 public Double snapToValue(double time, double snapToleranceInTime) throws HistoryException, AccessorException
677 double from = horizRuler.from;
678 double end = horizRuler.end;
679 double pixelsPerSecond = (end-from) / plot.getWidth();
681 TreeSet<Double> values = new TreeSet<Double>(Bindings.DOUBLE);
682 for (ItemNode item : allItems) {
683 Stream s = item.openStream( pixelsPerSecond );
684 if ( s==null ) continue;
685 int pos = s.binarySearch(Bindings.DOUBLE, time);
686 ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());
694 int count = s.count();
695 Double prevTime = null, nextTime = null;
697 if ( prev>=0 && prev<count ) {
698 s.accessor.get(prev, s.sampleBinding, vb.getSample());
699 if ( !vb.isNanSample() ) {
700 prevTime = vb.getTimeDouble();
701 if ( vb.hasEndTime() ) {
702 Double nTime = vb.getEndTimeDouble();
703 if (nTime!=null && nTime+snapToleranceInTime>time) nextTime = nTime;
708 if ( nextTime==null && next>=0 && next<count ) {
709 s.accessor.get(next, s.sampleBinding, vb.getSample());
710 if ( !vb.isNanSample() ) {
711 nextTime = vb.getTimeDouble();
715 if (prevTime==null && nextTime==null) continue;
717 if (prevTime==null) {
718 if ( nextTime - time < snapToleranceInTime )
719 values.add(nextTime);
720 } else if (nextTime==null) {
721 if ( time - prevTime < snapToleranceInTime )
722 values.add(prevTime);
724 values.add(nextTime);
725 values.add(prevTime);
728 if (values.isEmpty()) return null;
730 Double lower = values.floor( time );
731 Double higher = values.ceiling( time );
733 if ( lower == null ) return higher;
734 if ( higher == null ) return lower;
735 double result = time-lower < higher-time ? lower : higher;