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.AlphaComposite;
\r
15 import java.awt.BasicStroke;
\r
16 import java.awt.Color;
\r
17 import java.awt.Composite;
\r
18 import java.awt.Graphics2D;
\r
19 import java.awt.RenderingHints;
\r
20 import java.awt.geom.AffineTransform;
\r
21 import java.awt.geom.Path2D;
\r
22 import java.awt.geom.Point2D;
\r
23 import java.awt.geom.Rectangle2D;
\r
24 import java.util.Arrays;
\r
25 import java.util.List;
\r
26 import java.util.TreeSet;
\r
27 import java.util.logging.Level;
\r
28 import java.util.logging.Logger;
\r
30 import org.simantics.databoard.Bindings;
\r
31 import org.simantics.databoard.accessor.StreamAccessor;
\r
32 import org.simantics.databoard.accessor.error.AccessorException;
\r
33 import org.simantics.databoard.binding.Binding;
\r
34 import org.simantics.databoard.binding.BooleanBinding;
\r
35 import org.simantics.databoard.binding.ByteBinding;
\r
36 import org.simantics.databoard.binding.error.BindingException;
\r
37 import org.simantics.databoard.binding.error.RuntimeBindingException;
\r
38 import org.simantics.databoard.type.BooleanType;
\r
39 import org.simantics.databoard.type.Datatype;
\r
40 import org.simantics.databoard.type.NumberType;
\r
41 import org.simantics.databoard.type.RecordType;
\r
42 import org.simantics.databoard.util.Bean;
\r
43 import org.simantics.history.HistoryException;
\r
44 import org.simantics.history.HistoryItem;
\r
45 import org.simantics.history.HistoryManager;
\r
46 import org.simantics.history.ItemManager;
\r
47 import org.simantics.history.util.Stream;
\r
48 import org.simantics.history.util.ValueBand;
\r
49 import org.simantics.history.util.subscription.SamplingFormat;
\r
50 import org.simantics.scenegraph.g2d.G2DNode;
\r
51 import org.simantics.trend.configuration.LineQuality;
\r
52 import org.simantics.trend.configuration.Scale;
\r
53 import org.simantics.trend.configuration.TrendItem;
\r
54 import org.simantics.trend.configuration.TrendItem.DrawMode;
\r
55 import org.simantics.trend.configuration.TrendItem.Renderer;
\r
56 import org.simantics.trend.util.KvikDeviationBuilder;
\r
59 * Data node for a TrendItem
\r
61 * @author toni.kalajainen
\r
63 public class ItemNode extends G2DNode implements TrendLayout {
\r
65 private static final long serialVersionUID = -4741446944761752871L;
\r
67 public static final AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .61f);
\r
69 public TrendItem item;
\r
71 TrendNode trendNode;
\r
73 /** History Items */
\r
74 Bean historyItems[];
\r
76 /** A list of items that is ordered in by interval value, starting from the lowest interval */
\r
77 Bean[] renderableItems = new Bean[0];
\r
79 /** Format for the minmax stream, or null */
\r
82 /** Known item min->max value range, and from->end time range */
\r
83 double from=Double.NaN, end=Double.NaN, min=0, max=1;
\r
89 StreamAccessor openStreamAccessor;
\r
90 Bean openStreamItem;
\r
92 Logger log = Logger.getLogger( this.getClass().getName() );
\r
94 boolean disabled = false;
\r
95 Point2D.Double pt = new Point2D.Double();
\r
96 Point2D.Double pt2 = new Point2D.Double();
\r
99 KvikDeviationBuilder dev = new KvikDeviationBuilder();
\r
100 Path2D.Double line = new Path2D.Double();
\r
103 * Set trend item and initialize history
\r
105 * @param items all items in history
\r
107 public void setTrendItem(TrendItem ti, ItemManager items) {
\r
108 if (openStream!=null) {
\r
109 openStream.close();
\r
111 openStreamAccessor = null;
\r
112 openStreamItem = null;
\r
115 this.minmaxItem = null;
\r
116 this.renderableItems = new HistoryItem[0];
\r
118 if (ti==null) return;
\r
121 List<Bean> trendItems = items.search("groupId", ti.groupId, "groupItemId", ti.groupItemId, "variableId", ti.variableId);
\r
122 this.historyItems = trendItems.toArray( new Bean[trendItems.size()] );
\r
123 Arrays.sort( this.historyItems, SamplingFormat.INTERVAL_COMPARATOR );
\r
125 // Read renderable formats, and minmax format
\r
126 TreeSet<Bean> streamFormats = new TreeSet<Bean>( SamplingFormat.INTERVAL_COMPARATOR );
\r
127 for (Bean item : trendItems) {
\r
128 SamplingFormat format = new SamplingFormat();
\r
129 format.readAvailableFields(item);
\r
130 RecordType rt = (RecordType) format.format;
\r
131 Boolean enabled = (Boolean) item.getField("enabled");
\r
132 if (!enabled) continue;
\r
134 boolean isMinMaxFormat = format.interval==Double.MAX_VALUE &&
\r
135 format.deadband==Double.MAX_VALUE &&
\r
136 rt.getComponentIndex2("min")>=0 &&
\r
137 rt.getComponentIndex2("max")>=0;
\r
139 if (isMinMaxFormat) {
\r
140 this.minmaxItem = item;
\r
142 streamFormats.add(item);
\r
145 if (streamFormats.isEmpty()) return;
\r
147 renderableItems = streamFormats.toArray( new Bean[streamFormats.size()] );
\r
149 } catch (BindingException e) {
\r
150 throw new RuntimeException( e );
\r
155 * Draw to graphics context as time,value pairs are.
\r
157 * Phases, 0-Init data, 1-Draw deviation, 2-Draw line, 3-Cleanup
\r
162 public void draw(Graphics2D g, int phase, boolean bold) {
\r
163 boolean devAndLine =
\r
164 item.drawMode==DrawMode.DeviationAndAverage ||
\r
165 item.drawMode==DrawMode.DeviationAndLine ||
\r
166 item.drawMode==DrawMode.DeviationAndSample;
\r
169 Object newQuality = getTrendNode().printing||getTrendNode().quality.lineQuality==LineQuality.Antialias?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF;
\r
172 if (!dev.isEmpty()) {
\r
173 Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
\r
174 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, newQuality);
\r
176 if (item.renderer == Renderer.Binary) {
\r
177 dev.drawRectangles(g);
\r
180 if (item.renderer == Renderer.Analog) {
\r
183 Composite c = g.getComposite();
\r
184 g.setComposite( composite );
\r
185 dev.drawRectangles(g);
\r
186 g.setComposite( c );
\r
187 } else if (item.drawMode == DrawMode.Deviation) {
\r
189 dev.drawRectangles(g);
\r
192 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
\r
199 Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
\r
200 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, newQuality);
\r
202 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
\r
207 private boolean validSample(ValueBand band) throws HistoryException {
\r
208 if(band.getTimeDouble() > 1e50) return false;
\r
209 if(band.getTimeDouble() > band.getEndTimeDouble()) return false;
\r
210 // if(band.getTimeDouble() < 0) return false;
\r
211 // if(band.getEndTimeDouble() < 0) return false;
\r
212 //if(band.getEndTimeDouble()-band.getTimeDouble() < 1e-9) return false;
\r
216 // Draw state needed for clipping
\r
222 void moveTo(double x, double y) {
\r
228 void lineToOrdered(double x, double y) {
\r
230 if(x < minX) return;
\r
231 if(currentX > maxX) return;
\r
233 // We have something to draw
\r
236 if(currentX < minX) {
\r
237 double ny = currentY + (y-currentY) * (minX-currentX) / (x-currentX);
\r
238 line.moveTo(minX, ny);
\r
245 double ny = currentY + (y-currentY) * (maxX-currentX) / (x-currentX);
\r
246 line.lineTo(maxX, ny);
\r
247 line.moveTo(x, y);
\r
249 line.lineTo(x, y);
\r
254 void lineTo(double x, double y) {
\r
256 // First assert ordering - if samples are in wrong order draw nothing
\r
257 if(currentX <= x) {
\r
258 lineToOrdered(x, y);
\r
270 * This method prepares line and deviation shapes.
\r
273 * @param secondsPerPixel
\r
275 * @throws HistoryException
\r
276 * @throws AccessorException
\r
278 public void prepareLine(double from, double end, double pixelsPerSecond, AffineTransform at) throws HistoryException, AccessorException
\r
280 // boolean devAndLine =
\r
281 // item.drawMode==DrawMode.DeviationAndAverage ||
\r
282 // item.drawMode==DrawMode.DeviationAndLine ||
\r
283 // item.drawMode==DrawMode.DeviationAndSample;
\r
284 // boolean deviationEnabled = devAndLine ||
\r
285 // item.drawMode == DrawMode.Deviation;
\r
287 boolean devAndLine = false;
\r
288 boolean deviationEnabled = false;
\r
293 Stream s = openStream(pixelsPerSecond);
\r
294 if (s==null) return;
\r
295 ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());
\r
296 boolean hasDeviation = vb.hasMin() && vb.hasMax();
\r
297 boolean drawDeviation = hasDeviation & deviationEnabled;
\r
299 Datatype valueType = vb.getValueBinding().type();
\r
301 if ( valueType instanceof BooleanType == false && valueType instanceof NumberType == false ) return;
\r
303 int start = s.binarySearch(Bindings.DOUBLE, from);
\r
304 int count = s.count();
\r
305 if (start<0) start = -2-start;
\r
306 if (start<0) start = 0;
\r
308 // moveTo, on next draw, if true
\r
309 boolean lineNotAttached = true; // =next draw should be move to
\r
310 boolean devNotAttached = true;
\r
312 currentX = Double.NaN;
\r
313 currentY = Double.NaN;
\r
315 pt.setLocation(from, 0);
\r
316 at.transform(pt, pt);
\r
319 pt.setLocation(end, 0);
\r
320 at.transform(pt, pt);
\r
325 boolean wentOver = false;
\r
327 for (int i=start; i<count; i++) {
\r
329 s.accessor.get(i, s.sampleBinding, vb.getSample());
\r
331 if(!validSample(vb)) {
\r
332 System.err.println("-invalid value band: " + i + "/" + count + ":" + vb);
\r
335 // Non-continuation point
\r
336 if (vb.hasQuality()) {
\r
337 Byte b = (Byte) vb.getQuality(Bindings.BYTE);
\r
338 boolean noncontinuation = b.equals( ValueBand.QUALITY_NOVALUE );
\r
339 if ( noncontinuation ) {
\r
340 lineNotAttached = true;
\r
341 devNotAttached = true;
\r
347 double t1 = vb.getTimeDouble();
\r
348 double t2 = vb.hasEndTime() ? vb.getEndTimeDouble() : t1;
\r
351 if (item.renderer == Renderer.Analog) {
\r
353 // Add points to line
\r
354 if (item.drawMode == DrawMode.Deviation && hasDeviation) {
\r
358 double yy = Double.NaN;
\r
359 // boolean showBand = true;
\r
360 boolean flat = false;
\r
362 if( trendNode != null && trendNode.drawSamples ) {
\r
363 yy = vb.getValueDouble();
\r
366 yy = vb.getValueDouble();
\r
367 // Only render the last band
\r
368 // if(i < count-1) showBand = false;
\r
371 // if (item.drawMode == DrawMode.DeviationAndAverage) {
\r
372 // yy = vb.hasMedian() ? vb.getMedianDouble() : ( vb.hasAvg() ? vb.getAvgDouble() : vb.getValueDouble() );
\r
373 // } else if (item.drawMode == DrawMode.Average || item.drawMode == DrawMode.DeviationAndAverage) {
\r
374 // yy = vb.hasAvg() ? vb.getAvgDouble() : vb.getValueDouble();
\r
375 // } else if (item.drawMode == DrawMode.Line || item.drawMode == DrawMode.DeviationAndLine) {
\r
376 // yy = vb.getValueDouble();
\r
377 // // Only render the last band
\r
378 // if(i < count-1) showBand = false;
\r
379 // } else /* if (item.drawMode == DrawMode.Sample || item.drawMode == DrawMode.DeviationAndSample) */ {
\r
380 // yy = vb.getValueDouble();
\r
384 if ( Double.isNaN(yy) ) {
\r
385 lineNotAttached = true;
\r
387 // pt.setLocation(t1, yy);
\r
388 // at.transform(pt, pt);
\r
390 // if (lineNotAttached) {
\r
391 // moveTo(pt.getX(), pt.getY());
\r
396 // lineTo(pt.getX(), pt.getY());
\r
400 // Static variables may have months data that consists of single value
\r
401 // When zoomed in, the single line may be drawn from -1e10, ... 1e10
\r
402 // This is too much for Graphics2D, it refuses to draw.
\r
410 pt.setLocation(t1, yy);
\r
411 at.transform(pt, pt);
\r
412 pt2.setLocation(t2, yy);
\r
413 at.transform(pt2, pt2);
\r
415 double x1 = pt.x, y1 = pt.y, x2 = pt2.x, y2 = pt2.y;
\r
417 if (lineNotAttached) {
\r
421 lineTo(x1, currentY);
\r
435 lineNotAttached = false;
\r
439 if (drawDeviation) {
\r
440 double min = vb.getMinDouble();
\r
441 double max = vb.getMaxDouble();
\r
443 pt.setLocation(t1, min);
\r
444 at.transform(pt, pt);
\r
445 pt2.setLocation(t2, max);
\r
446 at.transform(pt2, pt2);
\r
452 double width = x2-x1;
\r
453 boolean tooWide = devAndLine && (width>2.0);
\r
454 if (Double.isNaN(min) || Double.isNaN(max) || tooWide || width<=0.0 || y1==y2) {
\r
455 devNotAttached = true;
\r
457 if (devNotAttached) {
\r
458 dev.addRectangle(x1, x2, y1, y2);
\r
460 dev.extendRectangle(x1);
\r
461 dev.addRectangle(x1, x2, y1, y2);
\r
463 devNotAttached = false;
\r
469 if (item.renderer == Renderer.Binary) {
\r
471 if (vb.getValueBinding() instanceof BooleanBinding) {
\r
472 value = ((Boolean) vb.getValue(Bindings.BOOLEAN)) ? (byte)0 : (byte)1;
\r
473 } else if (vb.getValueBinding() instanceof ByteBinding) {
\r
474 value = ((Byte) vb.getValue(Bindings.BYTE));
\r
475 } else if (vb.hasMax()) {
\r
476 value = (Byte) vb.getMax(Bindings.BYTE);
\r
478 value = (Byte) vb.getValue(Bindings.BYTE);
\r
480 pt.setLocation(t1, value==1 ? BINARY[1] : BINARY[0]);
\r
481 at.transform(pt, pt);
\r
482 pt2.setLocation(t2, BINARY[2]);
\r
483 at.transform(pt2, pt2);
\r
488 dev.extendRectangle(x1);
\r
489 dev.addRectangle(x1, x2, y1, y2);
\r
490 devNotAttached = false;
\r
493 // Already over => break
\r
494 if(wentOver) break;
\r
504 public boolean readMinMaxFromEnd() {
\r
505 if (disabled) return false;
\r
506 HistoryManager history = getTrendNode().historian;
\r
508 boolean hasVariable = !item.variableId.isEmpty() && !item.groupItemId.isEmpty() && !item.groupId.isEmpty();
\r
509 boolean canReadMinMax = minmaxItem != null;
\r
510 boolean manualScale = item.scale instanceof Scale.Manual;
\r
512 if ( !hasVariable ) {
\r
521 if (canReadMinMax && !manualScale) {
\r
522 String id = (String) minmaxItem.getFieldUnchecked("id");
\r
523 StreamAccessor sa = history.openStream(id, "r");
\r
532 if (sa.size()==0) return false;
\r
533 min = Double.MAX_VALUE;
\r
534 max = -Double.MAX_VALUE;
\r
535 from = Double.MAX_VALUE;
\r
536 end = -Double.MAX_VALUE;
\r
537 for (int i=0; i<sa.size(); i++) {
\r
538 Binding binding = Bindings.getBinding( sa.type().componentType() );
\r
539 Object sample = sa.get(i, binding);
\r
540 ValueBand vb = new ValueBand(binding, sample);
\r
541 if (!vb.isNullValue() && !vb.isNanSample()) {
\r
542 min = Math.min(min, vb.getMinDouble());
\r
543 max = Math.max(max, vb.getMaxDouble());
\r
545 if (!vb.isNullValue()) {
\r
546 from = Math.min(from, vb.getTimeDouble());
\r
547 end = Math.max(end, vb.getEndTimeDouble());
\r
550 if ( min==Double.MAX_VALUE || max==-Double.MAX_VALUE) {
\r
551 min = 0; max = 1.0;
\r
553 if ( from==Double.MAX_VALUE || end==-Double.MAX_VALUE) {
\r
554 from = 0; end = 100.0;
\r
558 try { sa.close(); } catch (AccessorException e) {}
\r
563 Scale.Manual ms = (Scale.Manual) item.scale;
\r
568 // Read from, end from any stream
\r
569 if (openStreamAccessor==null) {
\r
572 if (openStreamAccessor!=null){
\r
573 // Open some stream
\r
574 StreamAccessor sa = openStreamAccessor;
\r
576 int count = sa.size();
\r
578 Binding binding = Bindings.getBinding( sa.type().componentType() );
\r
579 Object sample = sa.get(0, binding);
\r
580 ValueBand vb = new ValueBand(binding, sample);
\r
581 from = vb.getTimeDouble();
\r
582 sa.get(count-1, binding, sample);
\r
583 end = vb.hasEndTime() ? vb.getEndTimeDouble() : vb.getTimeDouble();
\r
590 } catch (AccessorException e) {
\r
591 log.log(Level.FINE, e.toString(), e);
\r
593 } catch (HistoryException e) {
\r
594 log.log(Level.FINE, e.toString(), e);
\r
600 public void cleanup() {
\r
602 if (openStreamAccessor != null) {
\r
604 openStreamAccessor.close();
\r
605 } catch (AccessorException e) {
\r
607 openStreamAccessor = null;
\r
608 openStreamItem = null;
\r
613 Bean getFormat(double pixelsPerSecond) {
\r
614 Bean result = null;
\r
615 if (renderableItems == null)
\r
618 for (Bean format : renderableItems)
\r
620 double interval = 0.0;
\r
622 interval = format.hasField("interval") ? (Double) format.getFieldUnchecked("interval") : 0.0;
\r
623 } catch (RuntimeBindingException e) {
\r
624 } catch (BindingException e) {
\r
626 if (Double.isNaN( interval ) || interval<=pixelsPerSecond) {
\r
632 if (result==null) {
\r
633 if ( renderableItems.length == 0 ) {
\r
636 result = renderableItems[0];
\r
643 public Stream openStream(double pixelsPerSecond) {
\r
644 Bean f = getFormat(pixelsPerSecond);
\r
645 if (f==openStreamItem) return openStream;
\r
647 if (openStream != null) {
\r
648 openStream.close();
\r
649 openStreamAccessor = null;
\r
650 openStreamItem = null;
\r
654 if (disabled) return null;
\r
657 HistoryManager historian = getTrendNode().historian;
\r
659 String id = (String) f.getFieldUnchecked("id");
\r
660 openStreamAccessor = historian.openStream(id, "r");
\r
661 if ( openStreamAccessor!=null ) {
\r
662 openStream = new Stream(openStreamAccessor);
\r
663 openStreamItem = f;
\r
667 } catch (HistoryException e) {
\r
668 log.log(Level.FINE, e.toString(), e);
\r
676 public void render(Graphics2D g2d) {
\r
680 public Rectangle2D getBoundsInLocal() {
\r
684 TrendNode getTrendNode() {
\r
685 return (TrendNode) getParent();
\r