-/*******************************************************************************\r
- * Copyright (c) 2007, 2011 Association for Decentralized Information Management in\r
- * Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- * VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.trend.impl;\r
-\r
-import java.awt.AlphaComposite;\r
-import java.awt.BasicStroke;\r
-import java.awt.Color;\r
-import java.awt.Composite;\r
-import java.awt.Graphics2D;\r
-import java.awt.RenderingHints;\r
-import java.awt.geom.AffineTransform;\r
-import java.awt.geom.Path2D;\r
-import java.awt.geom.Point2D;\r
-import java.awt.geom.Rectangle2D;\r
-import java.util.Arrays;\r
-import java.util.List;\r
-import java.util.TreeSet;\r
-import java.util.logging.Level;\r
-import java.util.logging.Logger;\r
-\r
-import org.simantics.databoard.Bindings;\r
-import org.simantics.databoard.accessor.StreamAccessor;\r
-import org.simantics.databoard.accessor.error.AccessorException;\r
-import org.simantics.databoard.binding.Binding;\r
-import org.simantics.databoard.binding.BooleanBinding;\r
-import org.simantics.databoard.binding.ByteBinding;\r
-import org.simantics.databoard.binding.error.BindingException;\r
-import org.simantics.databoard.binding.error.RuntimeBindingException;\r
-import org.simantics.databoard.type.BooleanType;\r
-import org.simantics.databoard.type.Datatype;\r
-import org.simantics.databoard.type.NumberType;\r
-import org.simantics.databoard.type.RecordType;\r
-import org.simantics.databoard.util.Bean;\r
-import org.simantics.history.HistoryException;\r
-import org.simantics.history.HistoryItem;\r
-import org.simantics.history.HistoryManager;\r
-import org.simantics.history.ItemManager;\r
-import org.simantics.history.util.Stream;\r
-import org.simantics.history.util.ValueBand;\r
-import org.simantics.history.util.subscription.SamplingFormat;\r
-import org.simantics.scenegraph.g2d.G2DNode;\r
-import org.simantics.trend.configuration.LineQuality;\r
-import org.simantics.trend.configuration.Scale;\r
-import org.simantics.trend.configuration.TrendItem;\r
-import org.simantics.trend.configuration.TrendItem.DrawMode;\r
-import org.simantics.trend.configuration.TrendItem.Renderer;\r
-import org.simantics.trend.util.KvikDeviationBuilder;\r
-\r
-/**\r
- * Data node for a TrendItem\r
- * \r
- * @author toni.kalajainen\r
- */\r
-public class ItemNode extends G2DNode implements TrendLayout {\r
-\r
- private static final long serialVersionUID = -4741446944761752871L;\r
-\r
- public static final AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .61f);\r
-\r
- public TrendItem item;\r
- VertRuler ruler;\r
- TrendNode trendNode;\r
-\r
- /** History Items */\r
- Bean historyItems[];\r
- \r
- /** A list of items that is ordered in by interval value, starting from the lowest interval */\r
- Bean[] renderableItems = new Bean[0];\r
- \r
- /** Format for the minmax stream, or null */ \r
- Bean minmaxItem;\r
- \r
- /** Known item min->max value range, and from->end time range */\r
- double from=Double.NaN, end=Double.NaN, min=0, max=1;\r
-\r
- BasicStroke stroke;\r
- Color color;\r
- \r
- Stream openStream;\r
- StreamAccessor openStreamAccessor;\r
- Bean openStreamItem;\r
- \r
- Logger log = Logger.getLogger( this.getClass().getName() ); \r
- \r
- boolean disabled = false;\r
- Point2D.Double pt = new Point2D.Double();\r
- Point2D.Double pt2 = new Point2D.Double();\r
- \r
- // Cached shapes\r
- KvikDeviationBuilder dev = new KvikDeviationBuilder();\r
- Path2D.Double line = new Path2D.Double();\r
- \r
- /**\r
- * Set trend item and initialize history\r
- * @param ti\r
- * @param items all items in history\r
- */\r
- public void setTrendItem(TrendItem ti, ItemManager items) {\r
- if (openStream!=null) {\r
- openStream.close();\r
- openStream = null;\r
- openStreamAccessor = null;\r
- openStreamItem = null;\r
- }\r
- this.item = ti;\r
- this.minmaxItem = null;\r
- this.renderableItems = new HistoryItem[0];\r
- disabled = true;\r
- if (ti==null) return;\r
- \r
- try {\r
- List<Bean> trendItems = items.search("groupId", ti.groupId, "groupItemId", ti.groupItemId, "variableId", ti.variableId);\r
- this.historyItems = trendItems.toArray( new Bean[trendItems.size()] );\r
- Arrays.sort( this.historyItems, SamplingFormat.INTERVAL_COMPARATOR );\r
- \r
- // Read renderable formats, and minmax format\r
- TreeSet<Bean> streamFormats = new TreeSet<Bean>( SamplingFormat.INTERVAL_COMPARATOR );\r
- for (Bean item : trendItems) {\r
- SamplingFormat format = new SamplingFormat();\r
- format.readAvailableFields(item);\r
- RecordType rt = (RecordType) format.format;\r
- Boolean enabled = (Boolean) item.getField("enabled");\r
- if (!enabled) continue;\r
- \r
- boolean isMinMaxFormat = format.interval==Double.MAX_VALUE &&\r
- format.deadband==Double.MAX_VALUE &&\r
- rt.getComponentIndex2("min")>=0 &&\r
- rt.getComponentIndex2("max")>=0;\r
- \r
- if (isMinMaxFormat) {\r
- this.minmaxItem = item;\r
- } else {\r
- streamFormats.add(item);\r
- } \r
- }\r
- if (streamFormats.isEmpty()) return;\r
- \r
- renderableItems = streamFormats.toArray( new Bean[streamFormats.size()] );\r
- disabled = false;\r
- } catch (BindingException e) {\r
- throw new RuntimeException( e );\r
- }\r
- }\r
-\r
- /**\r
- * Draw to graphics context as time,value pairs are.\r
- * \r
- * Phases, 0-Init data, 1-Draw deviation, 2-Draw line, 3-Cleanup \r
- * \r
- * @param g\r
- * @param phase \r
- */\r
- public void draw(Graphics2D g, int phase, boolean bold) {\r
- boolean devAndLine = \r
- item.drawMode==DrawMode.DeviationAndAverage || \r
- item.drawMode==DrawMode.DeviationAndLine || \r
- item.drawMode==DrawMode.DeviationAndSample;\r
- \r
- // Draw deviation\r
- Object newQuality = getTrendNode().printing||getTrendNode().quality.lineQuality==LineQuality.Antialias?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF;\r
- if (phase == 1) { \r
- g.setColor(color);\r
- if (!dev.isEmpty()) {\r
- Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);\r
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, newQuality);\r
-\r
- if (item.renderer == Renderer.Binary) {\r
- dev.drawRectangles(g);\r
- } \r
- \r
- if (item.renderer == Renderer.Analog) {\r
- if (devAndLine) {\r
- // Draw opaque\r
- Composite c = g.getComposite();\r
- g.setComposite( composite );\r
- dev.drawRectangles(g);\r
- g.setComposite( c );\r
- } else if (item.drawMode == DrawMode.Deviation) {\r
- // Draw solid\r
- dev.drawRectangles(g);\r
- }\r
- }\r
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);\r
- } \r
- }\r
- \r
- // Draw line\r
- if (phase == 2) {\r
- g.setColor(color);\r
- Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);\r
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, newQuality);\r
- g.draw(line); \r
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);\r
- }\r
- \r
- }\r
- \r
- private boolean validSample(ValueBand band) throws HistoryException {\r
- if(band.getTimeDouble() > 1e50) return false;\r
- if(band.getTimeDouble() > band.getEndTimeDouble()) return false;\r
-// if(band.getTimeDouble() < 0) return false;\r
-// if(band.getEndTimeDouble() < 0) return false;\r
- //if(band.getEndTimeDouble()-band.getTimeDouble() < 1e-9) return false;\r
- return true;\r
- }\r
- \r
- // Draw state needed for clipping\r
- double currentX;\r
- double currentY;\r
- double minX;\r
- double maxX;\r
- \r
- void moveTo(double x, double y) {\r
- line.moveTo(x, y);\r
- currentX = x;\r
- currentY = y;\r
- }\r
-\r
- void lineToOrdered(double x, double y) {\r
- \r
- if(x < minX) return;\r
- if(currentX > maxX) return;\r
- \r
- // We have something to draw\r
- \r
- // Clip left\r
- if(currentX < minX) {\r
- double ny = currentY + (y-currentY) * (minX-currentX) / (x-currentX);\r
- line.moveTo(minX, ny);\r
- currentX = minX;\r
- currentY = ny;\r
- }\r
-\r
- // Right clip\r
- if(x > maxX) {\r
- double ny = currentY + (y-currentY) * (maxX-currentX) / (x-currentX);\r
- line.lineTo(maxX, ny);\r
- line.moveTo(x, y); \r
- } else {\r
- line.lineTo(x, y); \r
- }\r
- \r
- }\r
- \r
- void lineTo(double x, double y) {\r
-\r
- // First assert ordering - if samples are in wrong order draw nothing\r
- if(currentX <= x) {\r
- lineToOrdered(x, y);\r
- } else {\r
- line.moveTo(x, y);\r
- }\r
- \r
- currentX = x;\r
- currentY = y;\r
- \r
- }\r
- \r
-\r
- /**\r
- * This method prepares line and deviation shapes.\r
- * @param from\r
- * @param end\r
- * @param secondsPerPixel\r
- * @param at \r
- * @throws HistoryException \r
- * @throws AccessorException \r
- */\r
- public void prepareLine(double from, double end, double pixelsPerSecond, AffineTransform at) throws HistoryException, AccessorException\r
- {\r
-// boolean devAndLine = \r
-// item.drawMode==DrawMode.DeviationAndAverage || \r
-// item.drawMode==DrawMode.DeviationAndLine || \r
-// item.drawMode==DrawMode.DeviationAndSample;\r
-// boolean deviationEnabled = devAndLine || \r
-// item.drawMode == DrawMode.Deviation;\r
-\r
- boolean devAndLine = false;\r
- boolean deviationEnabled = false;\r
- \r
- // Collect data\r
- dev.reset();\r
- line.reset();\r
- Stream s = openStream(pixelsPerSecond);\r
- if (s==null) return;\r
- ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());\r
- boolean hasDeviation = vb.hasMin() && vb.hasMax();\r
- boolean drawDeviation = hasDeviation & deviationEnabled;\r
- \r
- Datatype valueType = vb.getValueBinding().type();\r
- s.reset();\r
- if ( valueType instanceof BooleanType == false && valueType instanceof NumberType == false ) return;\r
- \r
- int start = s.binarySearch(Bindings.DOUBLE, from);\r
- int count = s.count();\r
- if (start<0) start = -2-start;\r
- if (start<0) start = 0;\r
- \r
- // moveTo, on next draw, if true\r
- boolean lineNotAttached = true; // =next draw should be move to\r
- boolean devNotAttached = true;\r
- \r
- currentX = Double.NaN;\r
- currentY = Double.NaN;\r
-\r
- pt.setLocation(from, 0);\r
- at.transform(pt, pt);\r
- minX = pt.x;\r
- \r
- pt.setLocation(end, 0);\r
- at.transform(pt, pt);\r
- maxX = pt.x;\r
- \r
- s.reset();\r
- \r
- boolean wentOver = false;\r
- \r
- for (int i=start; i<count; i++) {\r
- // Read sample\r
- s.accessor.get(i, s.sampleBinding, vb.getSample());\r
- \r
- if(!validSample(vb)) {\r
- System.err.println("-invalid value band: " + i + "/" + count + ":" + vb);\r
- continue;\r
- }\r
- // Non-continuation point\r
- if (vb.hasQuality()) {\r
- Byte b = (Byte) vb.getQuality(Bindings.BYTE);\r
- boolean noncontinuation = b.equals( ValueBand.QUALITY_NOVALUE ); \r
- if ( noncontinuation ) {\r
- lineNotAttached = true;\r
- devNotAttached = true;\r
- continue;\r
- }\r
- }\r
- \r
- // Line\r
- double t1 = vb.getTimeDouble();\r
- double t2 = vb.hasEndTime() ? vb.getEndTimeDouble() : t1;\r
- \r
- // Analog signal\r
- if (item.renderer == Renderer.Analog) {\r
- \r
- // Add points to line\r
- if (item.drawMode == DrawMode.Deviation && hasDeviation) {\r
- // ...\r
- } else {\r
- \r
- double yy = Double.NaN;\r
-// boolean showBand = true;\r
- boolean flat = false;\r
-\r
- if( trendNode != null && trendNode.drawSamples ) {\r
- yy = vb.getValueDouble();\r
- flat = true;\r
- } else {\r
- yy = vb.getValueDouble();\r
- // Only render the last band\r
-// if(i < count-1) showBand = false; \r
- }\r
- \r
-// if (item.drawMode == DrawMode.DeviationAndAverage) {\r
-// yy = vb.hasMedian() ? vb.getMedianDouble() : ( vb.hasAvg() ? vb.getAvgDouble() : vb.getValueDouble() );\r
-// } else if (item.drawMode == DrawMode.Average || item.drawMode == DrawMode.DeviationAndAverage) {\r
-// yy = vb.hasAvg() ? vb.getAvgDouble() : vb.getValueDouble();\r
-// } else if (item.drawMode == DrawMode.Line || item.drawMode == DrawMode.DeviationAndLine) {\r
-// yy = vb.getValueDouble();\r
-// // Only render the last band\r
-// if(i < count-1) showBand = false; \r
-// } else /* if (item.drawMode == DrawMode.Sample || item.drawMode == DrawMode.DeviationAndSample) */ {\r
-// yy = vb.getValueDouble();\r
-// flat = true;\r
-// } \r
- \r
- if ( Double.isNaN(yy) ) {\r
- lineNotAttached = true;\r
- } else {\r
-// pt.setLocation(t1, yy);\r
-// at.transform(pt, pt);\r
-// if ( t1==t2 ) {\r
-// if (lineNotAttached) {\r
-// moveTo(pt.getX(), pt.getY());\r
-// } else {\r
-// if(flat) {\r
-// \r
-// } else {\r
-// lineTo(pt.getX(), pt.getY());\r
-// }\r
-// }\r
-// } else {\r
- // Static variables may have months data that consists of single value\r
- // When zoomed in, the single line may be drawn from -1e10, ... 1e10\r
- // This is too much for Graphics2D, it refuses to draw.\r
-// if (t1<from) {\r
-// t1 = from;\r
-// }\r
-// if (t2>end) {\r
-// t2 = end;\r
-// }\r
- \r
- pt.setLocation(t1, yy);\r
- at.transform(pt, pt);\r
- pt2.setLocation(t2, yy);\r
- at.transform(pt2, pt2);\r
-\r
- double x1 = pt.x, y1 = pt.y, x2 = pt2.x, y2 = pt2.y;\r
-\r
- if (lineNotAttached) {\r
- moveTo(x1, y1);\r
- } else {\r
- if(flat) {\r
- lineTo(x1, currentY);\r
- lineTo(x1, y1);\r
- } else {\r
- lineTo(x1, y1);\r
- }\r
- }\r
-\r
- lineTo(x2, y2);\r
- \r
-// if(showBand) {\r
-// lineTo(x2, y2);\r
-// }\r
- \r
-// }\r
- lineNotAttached = false; \r
- }\r
- }\r
- \r
- if (drawDeviation) {\r
- double min = vb.getMinDouble();\r
- double max = vb.getMaxDouble();\r
-\r
- pt.setLocation(t1, min);\r
- at.transform(pt, pt);\r
- pt2.setLocation(t2, max);\r
- at.transform(pt2, pt2);\r
- double x1 = pt.x;\r
- double x2 = pt2.x;\r
- double y1 = pt.y;\r
- double y2 = pt2.y;\r
- \r
- double width = x2-x1;\r
- boolean tooWide = devAndLine && (width>2.0);\r
- if (Double.isNaN(min) || Double.isNaN(max) || tooWide || width<=0.0 || y1==y2) {\r
- devNotAttached = true;\r
- } else {\r
- if (devNotAttached) {\r
- dev.addRectangle(x1, x2, y1, y2);\r
- } else {\r
- dev.extendRectangle(x1);\r
- dev.addRectangle(x1, x2, y1, y2);\r
- }\r
- devNotAttached = false;\r
- }\r
- }\r
- }\r
- \r
- // Binary signal\r
- if (item.renderer == Renderer.Binary) {\r
- byte value = 0;\r
- if (vb.getValueBinding() instanceof BooleanBinding) {\r
- value = ((Boolean) vb.getValue(Bindings.BOOLEAN)) ? (byte)0 : (byte)1;\r
- } else if (vb.getValueBinding() instanceof ByteBinding) {\r
- value = ((Byte) vb.getValue(Bindings.BYTE));\r
- } else if (vb.hasMax()) {\r
- value = (Byte) vb.getMax(Bindings.BYTE);\r
- } else {\r
- value = (Byte) vb.getValue(Bindings.BYTE);\r
- }\r
- pt.setLocation(t1, value==1 ? BINARY[1] : BINARY[0]);\r
- at.transform(pt, pt);\r
- pt2.setLocation(t2, BINARY[2]);\r
- at.transform(pt2, pt2);\r
- double x1 = pt.x;\r
- double x2 = pt2.x;\r
- double y1 = pt.y;\r
- double y2 = pt2.y;\r
- dev.extendRectangle(x1);\r
- dev.addRectangle(x1, x2, y1, y2);\r
- devNotAttached = false; \r
- }\r
- \r
- // Already over => break\r
- if(wentOver) break;\r
- \r
- // Out of range\r
- if (t2>=end) {\r
- wentOver = true;\r
- }\r
- } \r
- \r
- }\r
- \r
- public boolean readMinMaxFromEnd() {\r
- if (disabled) return false;\r
- HistoryManager history = getTrendNode().historian;\r
-\r
- boolean hasVariable = !item.variableId.isEmpty() && !item.groupItemId.isEmpty() && !item.groupId.isEmpty();\r
- boolean canReadMinMax = minmaxItem != null;\r
- boolean manualScale = item.scale instanceof Scale.Manual;\r
- \r
- if ( !hasVariable ) {\r
- min = 0;\r
- max = 1;\r
- from = Double.NaN;\r
- end = Double.NaN;\r
- return false;\r
- }\r
- \r
- try {\r
- if (canReadMinMax && !manualScale) { \r
- String id = (String) minmaxItem.getFieldUnchecked("id");\r
- StreamAccessor sa = history.openStream(id, "r");\r
- if ( sa==null ) {\r
- min = 0;\r
- max = 1;\r
- from = Double.NaN;\r
- end = Double.NaN;\r
- return false;\r
- } else \r
- try {\r
- if (sa.size()==0) return false;\r
- min = Double.MAX_VALUE;\r
- max = -Double.MAX_VALUE;\r
- from = Double.MAX_VALUE;\r
- end = -Double.MAX_VALUE;\r
- for (int i=0; i<sa.size(); i++) {\r
- Binding binding = Bindings.getBinding( sa.type().componentType() );\r
- Object sample = sa.get(i, binding);\r
- ValueBand vb = new ValueBand(binding, sample);\r
- if (!vb.isNullValue() && !vb.isNanSample()) {\r
- min = Math.min(min, vb.getMinDouble());\r
- max = Math.max(max, vb.getMaxDouble());\r
- } \r
- if (!vb.isNullValue()) {\r
- from = Math.min(from, vb.getTimeDouble());\r
- end = Math.max(end, vb.getEndTimeDouble());\r
- }\r
- }\r
- if ( min==Double.MAX_VALUE || max==-Double.MAX_VALUE) {\r
- min = 0; max = 1.0;\r
- }\r
- if ( from==Double.MAX_VALUE || end==-Double.MAX_VALUE) {\r
- from = 0; end = 100.0;\r
- }\r
- return true;\r
- } finally {\r
- try { sa.close(); } catch (AccessorException e) {}\r
- } \r
- } else {\r
- \r
- if (manualScale) {\r
- Scale.Manual ms = (Scale.Manual) item.scale;\r
- min = ms.min;\r
- max = ms.max;\r
- } \r
- \r
- // Read from, end from any stream\r
- if (openStreamAccessor==null) {\r
- openStream(1);\r
- } \r
- if (openStreamAccessor!=null){\r
- // Open some stream\r
- StreamAccessor sa = openStreamAccessor;\r
- sa.reset();\r
- int count = sa.size();\r
- if (count>0) {\r
- Binding binding = Bindings.getBinding( sa.type().componentType() );\r
- Object sample = sa.get(0, binding);\r
- ValueBand vb = new ValueBand(binding, sample);\r
- from = vb.getTimeDouble();\r
- sa.get(count-1, binding, sample);\r
- end = vb.hasEndTime() ? vb.getEndTimeDouble() : vb.getTimeDouble();\r
- }\r
- return true;\r
- } else {\r
- return false;\r
- }\r
- }\r
- } catch (AccessorException e) {\r
- log.log(Level.FINE, e.toString(), e);\r
- return false;\r
- } catch (HistoryException e) {\r
- log.log(Level.FINE, e.toString(), e);\r
- return false;\r
- }\r
- }\r
- \r
- @Override\r
- public void cleanup() {\r
- trendNode = null;\r
- if (openStreamAccessor != null) {\r
- try {\r
- openStreamAccessor.close();\r
- } catch (AccessorException e) {\r
- }\r
- openStreamAccessor = null;\r
- openStreamItem = null;\r
- }\r
- super.cleanup();\r
- }\r
- \r
- Bean getFormat(double pixelsPerSecond) {\r
- Bean result = null;\r
- if (renderableItems == null)\r
- return null;\r
- \r
- for (Bean format : renderableItems)\r
- {\r
- double interval = 0.0;\r
- try {\r
- interval = format.hasField("interval") ? (Double) format.getFieldUnchecked("interval") : 0.0;\r
- } catch (RuntimeBindingException e) {\r
- } catch (BindingException e) {\r
- }\r
- if (Double.isNaN( interval ) || interval<=pixelsPerSecond) {\r
- result = format;\r
- } else {\r
- break;\r
- }\r
- } \r
- if (result==null) {\r
- if ( renderableItems.length == 0 ) {\r
- result = null;\r
- } else {\r
- result = renderableItems[0];\r
- }\r
- }\r
- \r
- return result;\r
- }\r
- \r
- public Stream openStream(double pixelsPerSecond) {\r
- Bean f = getFormat(pixelsPerSecond);\r
- if (f==openStreamItem) return openStream;\r
- \r
- if (openStream != null) {\r
- openStream.close();\r
- openStreamAccessor = null;\r
- openStreamItem = null;\r
- openStream = null;\r
- }\r
- \r
- if (disabled) return null;\r
- \r
- if (f!=null) {\r
- HistoryManager historian = getTrendNode().historian;\r
- try {\r
- String id = (String) f.getFieldUnchecked("id");\r
- openStreamAccessor = historian.openStream(id, "r");\r
- if ( openStreamAccessor!=null ) {\r
- openStream = new Stream(openStreamAccessor);\r
- openStreamItem = f; \r
- } else {\r
- openStream = null;\r
- }\r
- } catch (HistoryException e) {\r
- log.log(Level.FINE, e.toString(), e);\r
- }\r
- }\r
- \r
- return openStream;\r
- }\r
- \r
- @Override\r
- public void render(Graphics2D g2d) {\r
- }\r
-\r
- @Override\r
- public Rectangle2D getBoundsInLocal() {\r
- return null;\r
- }\r
- \r
- TrendNode getTrendNode() {\r
- return (TrendNode) getParent();\r
- }\r
- \r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.trend.impl;
+
+import java.awt.AlphaComposite;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Composite;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.Arrays;
+import java.util.List;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.simantics.databoard.Bindings;
+import org.simantics.databoard.accessor.StreamAccessor;
+import org.simantics.databoard.accessor.error.AccessorException;
+import org.simantics.databoard.binding.Binding;
+import org.simantics.databoard.binding.BooleanBinding;
+import org.simantics.databoard.binding.ByteBinding;
+import org.simantics.databoard.binding.error.BindingException;
+import org.simantics.databoard.binding.error.RuntimeBindingException;
+import org.simantics.databoard.type.BooleanType;
+import org.simantics.databoard.type.Datatype;
+import org.simantics.databoard.type.NumberType;
+import org.simantics.databoard.type.RecordType;
+import org.simantics.databoard.util.Bean;
+import org.simantics.history.HistoryException;
+import org.simantics.history.HistoryItem;
+import org.simantics.history.HistoryManager;
+import org.simantics.history.ItemManager;
+import org.simantics.history.util.Stream;
+import org.simantics.history.util.ValueBand;
+import org.simantics.history.util.subscription.SamplingFormat;
+import org.simantics.scenegraph.g2d.G2DNode;
+import org.simantics.trend.configuration.LineQuality;
+import org.simantics.trend.configuration.Scale;
+import org.simantics.trend.configuration.TrendItem;
+import org.simantics.trend.configuration.TrendItem.DrawMode;
+import org.simantics.trend.configuration.TrendItem.Renderer;
+import org.simantics.trend.util.KvikDeviationBuilder;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Data node for a TrendItem
+ *
+ * @author toni.kalajainen
+ */
+public class ItemNode extends G2DNode implements TrendLayout {
+
+ private static final long serialVersionUID = -4741446944761752871L;
+ private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ItemNode.class);
+
+ public static final AlphaComposite composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, .61f);
+
+ public TrendItem item;
+ VertRuler ruler;
+ TrendNode trendNode;
+
+ /** History Items */
+ Bean historyItems[];
+
+ /** A list of items that is ordered in by interval value, starting from the lowest interval */
+ Bean[] renderableItems = new Bean[0];
+
+ /** Format for the minmax stream, or null */
+ Bean minmaxItem;
+
+ /** Known item min->max value range, and from->end time range */
+ double from=Double.NaN, end=Double.NaN, min=0, max=1;
+
+ BasicStroke stroke;
+ Color color;
+
+ Stream openStream;
+ StreamAccessor openStreamAccessor;
+ Bean openStreamItem;
+
+ Logger log = Logger.getLogger( this.getClass().getName() );
+
+ boolean disabled = false;
+ Point2D.Double pt = new Point2D.Double();
+ Point2D.Double pt2 = new Point2D.Double();
+
+ // Cached shapes
+ KvikDeviationBuilder dev = new KvikDeviationBuilder();
+ Path2D.Double line = new Path2D.Double();
+
+ /**
+ * Set trend item and initialize history
+ * @param ti
+ * @param items all items in history
+ */
+ public void setTrendItem(TrendItem ti, ItemManager items) {
+ if (openStream!=null) {
+ openStream.close();
+ openStream = null;
+ openStreamAccessor = null;
+ openStreamItem = null;
+ }
+ this.item = ti;
+ this.minmaxItem = null;
+ this.renderableItems = new HistoryItem[0];
+ disabled = true;
+ if (ti==null) return;
+
+ try {
+ List<Bean> trendItems = items.search("groupId", ti.groupId, "groupItemId", ti.groupItemId, "variableId", ti.variableId);
+ this.historyItems = trendItems.toArray( new Bean[trendItems.size()] );
+ Arrays.sort( this.historyItems, SamplingFormat.INTERVAL_COMPARATOR );
+
+ // Read renderable formats, and minmax format
+ TreeSet<Bean> streamFormats = new TreeSet<Bean>( SamplingFormat.INTERVAL_COMPARATOR );
+ for (Bean item : trendItems) {
+ SamplingFormat format = new SamplingFormat();
+ format.readAvailableFields(item);
+ RecordType rt = (RecordType) format.format;
+ Boolean enabled = (Boolean) item.getField("enabled");
+ if (!enabled) continue;
+
+ boolean isMinMaxFormat = format.interval==Double.MAX_VALUE &&
+ format.deadband==Double.MAX_VALUE &&
+ rt.getComponentIndex2("min")>=0 &&
+ rt.getComponentIndex2("max")>=0;
+
+ if (isMinMaxFormat) {
+ this.minmaxItem = item;
+ } else {
+ streamFormats.add(item);
+ }
+ }
+ if (streamFormats.isEmpty()) return;
+
+ renderableItems = streamFormats.toArray( new Bean[streamFormats.size()] );
+ disabled = false;
+ } catch (BindingException e) {
+ throw new RuntimeException( e );
+ }
+ }
+
+ /**
+ * Draw to graphics context as time,value pairs are.
+ *
+ * Phases, 0-Init data, 1-Draw deviation, 2-Draw line, 3-Cleanup
+ *
+ * @param g
+ * @param phase
+ */
+ public void draw(Graphics2D g, int phase, boolean bold) {
+ boolean devAndLine =
+ item.drawMode==DrawMode.DeviationAndAverage ||
+ item.drawMode==DrawMode.DeviationAndLine ||
+ item.drawMode==DrawMode.DeviationAndSample;
+
+ // Draw deviation
+ Object newQuality = getTrendNode().printing||getTrendNode().quality.lineQuality==LineQuality.Antialias?RenderingHints.VALUE_ANTIALIAS_ON:RenderingHints.VALUE_ANTIALIAS_OFF;
+ if (phase == 1) {
+ g.setColor(color);
+ if (!dev.isEmpty()) {
+ Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, newQuality);
+
+ if (item.renderer == Renderer.Binary) {
+ dev.drawRectangles(g);
+ }
+
+ if (item.renderer == Renderer.Analog) {
+ if (devAndLine) {
+ // Draw opaque
+ Composite c = g.getComposite();
+ g.setComposite( composite );
+ dev.drawRectangles(g);
+ g.setComposite( c );
+ } else if (item.drawMode == DrawMode.Deviation) {
+ // Draw solid
+ dev.drawRectangles(g);
+ }
+ }
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
+ }
+ }
+
+ // Draw line
+ if (phase == 2) {
+ g.setColor(color);
+ Object old = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, newQuality);
+ g.draw(line);
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, old);
+ }
+
+ }
+
+ private boolean validSample(ValueBand band) throws HistoryException {
+ if(band.getTimeDouble() > 1e50) return false;
+ if(band.getTimeDouble() > band.getEndTimeDouble()) return false;
+// if(band.getTimeDouble() < 0) return false;
+// if(band.getEndTimeDouble() < 0) return false;
+ //if(band.getEndTimeDouble()-band.getTimeDouble() < 1e-9) return false;
+ return true;
+ }
+
+ // Draw state needed for clipping
+ double currentX;
+ double currentY;
+ double minX;
+ double maxX;
+
+ void moveTo(double x, double y) {
+ line.moveTo(x, y);
+ currentX = x;
+ currentY = y;
+ }
+
+ void lineToOrdered(double x, double y) {
+
+ if(x < minX) return;
+ if(currentX > maxX) return;
+
+ // We have something to draw
+
+ // Clip left
+ if(currentX < minX) {
+ double ny = currentY + (y-currentY) * (minX-currentX) / (x-currentX);
+ line.moveTo(minX, ny);
+ currentX = minX;
+ currentY = ny;
+ }
+
+ // Right clip
+ if(x > maxX) {
+ double ny = currentY + (y-currentY) * (maxX-currentX) / (x-currentX);
+ line.lineTo(maxX, ny);
+ line.moveTo(x, y);
+ } else {
+ if (x != currentX || y != currentY) {
+ line.lineTo(x, y);
+ }
+ }
+
+ }
+
+ void lineTo(double x, double y) {
+
+ // First assert ordering - if samples are in wrong order draw nothing
+ if(currentX <= x) {
+ lineToOrdered(x, y);
+ } else {
+ line.moveTo(x, y);
+ }
+
+ currentX = x;
+ currentY = y;
+
+ }
+
+
+ /**
+ * This method prepares line and deviation shapes.
+ * @param from
+ * @param end
+ * @param secondsPerPixel
+ * @param at
+ * @throws HistoryException
+ * @throws AccessorException
+ */
+ public void prepareLine(double from, double end, double pixelsPerSecond, AffineTransform at) throws HistoryException, AccessorException
+ {
+// boolean devAndLine =
+// item.drawMode==DrawMode.DeviationAndAverage ||
+// item.drawMode==DrawMode.DeviationAndLine ||
+// item.drawMode==DrawMode.DeviationAndSample;
+// boolean deviationEnabled = devAndLine ||
+// item.drawMode == DrawMode.Deviation;
+
+ boolean devAndLine = false;
+ boolean deviationEnabled = false;
+
+ // Collect data
+ dev.reset();
+ line.reset();
+ Stream s = openStream(pixelsPerSecond);
+ if (s==null) return;
+ ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked());
+ boolean hasDeviation = vb.hasMin() && vb.hasMax();
+ boolean drawDeviation = hasDeviation & deviationEnabled;
+
+ Datatype valueType = vb.getValueBinding().type();
+ s.reset();
+ if ( valueType instanceof BooleanType == false && valueType instanceof NumberType == false ) return;
+
+ int start = s.binarySearch(Bindings.DOUBLE, from);
+ int count = s.count();
+ if (start<0) start = -2-start;
+ if (start<0) start = 0;
+
+ // moveTo, on next draw, if true
+ boolean lineNotAttached = true; // =next draw should be move to
+ boolean devNotAttached = true;
+
+ currentX = Double.NaN;
+ currentY = Double.NaN;
+
+ pt.setLocation(from, 0);
+ at.transform(pt, pt);
+ minX = pt.x;
+
+ pt.setLocation(end, 0);
+ at.transform(pt, pt);
+ maxX = pt.x;
+
+ s.reset();
+
+ boolean wentOver = false;
+
+ for (int i=start; i<count; i++) {
+ // Read sample
+ s.accessor.get(i, s.sampleBinding, vb.getSample());
+
+ if(!validSample(vb)) {
+ LOGGER.warn("-invalid value band: " + i + "/" + count + ":" + vb);
+ continue;
+ }
+ // Non-continuation point
+ if (vb.hasQuality()) {
+ Byte b = (Byte) vb.getQuality(Bindings.BYTE);
+ boolean noncontinuation = b.equals( ValueBand.QUALITY_NOVALUE );
+ if ( noncontinuation ) {
+ lineNotAttached = true;
+ devNotAttached = true;
+ continue;
+ }
+ }
+
+ // Line
+ double t1 = vb.getTimeDouble();
+ double t2 = vb.hasEndTime() ? vb.getEndTimeDouble() : t1;
+
+ // Analog signal
+ if (item.renderer == Renderer.Analog) {
+
+ // Add points to line
+ if (item.drawMode == DrawMode.Deviation && hasDeviation) {
+ // ...
+ } else {
+
+ double yy = Double.NaN;
+// boolean showBand = true;
+ boolean flat = false;
+
+ if( trendNode != null && trendNode.drawSamples ) {
+ yy = vb.getValueDouble();
+ flat = true;
+ } else {
+ yy = vb.getValueDouble();
+ // Only render the last band
+// if(i < count-1) showBand = false;
+ }
+
+// if (item.drawMode == DrawMode.DeviationAndAverage) {
+// yy = vb.hasMedian() ? vb.getMedianDouble() : ( vb.hasAvg() ? vb.getAvgDouble() : vb.getValueDouble() );
+// } else if (item.drawMode == DrawMode.Average || item.drawMode == DrawMode.DeviationAndAverage) {
+// yy = vb.hasAvg() ? vb.getAvgDouble() : vb.getValueDouble();
+// } else if (item.drawMode == DrawMode.Line || item.drawMode == DrawMode.DeviationAndLine) {
+// yy = vb.getValueDouble();
+// // Only render the last band
+// if(i < count-1) showBand = false;
+// } else /* if (item.drawMode == DrawMode.Sample || item.drawMode == DrawMode.DeviationAndSample) */ {
+// yy = vb.getValueDouble();
+// flat = true;
+// }
+
+ if ( Double.isNaN(yy) ) {
+ lineNotAttached = true;
+ } else {
+// pt.setLocation(t1, yy);
+// at.transform(pt, pt);
+// if ( t1==t2 ) {
+// if (lineNotAttached) {
+// moveTo(pt.getX(), pt.getY());
+// } else {
+// if(flat) {
+//
+// } else {
+// lineTo(pt.getX(), pt.getY());
+// }
+// }
+// } else {
+ // Static variables may have months data that consists of single value
+ // When zoomed in, the single line may be drawn from -1e10, ... 1e10
+ // This is too much for Graphics2D, it refuses to draw.
+// if (t1<from) {
+// t1 = from;
+// }
+// if (t2>end) {
+// t2 = end;
+// }
+
+ pt.setLocation(t1, yy);
+ at.transform(pt, pt);
+ pt2.setLocation(t2, yy);
+ at.transform(pt2, pt2);
+
+ double x1 = pt.x, y1 = pt.y, x2 = pt2.x, y2 = pt2.y;
+
+ if (lineNotAttached) {
+ moveTo(x1, y1);
+ } else {
+ if(flat) {
+ lineTo(x1, currentY);
+ lineTo(x1, y1);
+ } else {
+ lineTo(x1, y1);
+ }
+ }
+
+ // gitlab #35: t2 > t1 ensure that the value band is
+ // rendered as a horizontal line until the the time when
+ // the band's value went out of dead-band.
+ if (flat || t2 > t1)
+ lineTo(x2, y2);
+
+// if(showBand) {
+// lineTo(x2, y2);
+// }
+
+// }
+ lineNotAttached = false;
+ }
+ }
+
+ if (drawDeviation) {
+ double min = vb.getMinDouble();
+ double max = vb.getMaxDouble();
+
+ pt.setLocation(t1, min);
+ at.transform(pt, pt);
+ pt2.setLocation(t2, max);
+ at.transform(pt2, pt2);
+ double x1 = pt.x;
+ double x2 = pt2.x;
+ double y1 = pt.y;
+ double y2 = pt2.y;
+
+ double width = x2-x1;
+ boolean tooWide = devAndLine && (width>2.0);
+ if (Double.isNaN(min) || Double.isNaN(max) || tooWide || width<=0.0 || y1==y2) {
+ devNotAttached = true;
+ } else {
+ if (devNotAttached) {
+ dev.addRectangle(x1, x2, y1, y2);
+ } else {
+ dev.extendRectangle(x1);
+ dev.addRectangle(x1, x2, y1, y2);
+ }
+ devNotAttached = false;
+ }
+ }
+ }
+
+ // Binary signal
+ else if (item.renderer == Renderer.Binary) {
+ byte value = 0;
+ if (vb.getValueBinding() instanceof BooleanBinding) {
+ value = ((Boolean) vb.getValue(Bindings.BOOLEAN)) ? (byte)0 : (byte)1;
+ } else if (vb.getValueBinding() instanceof ByteBinding) {
+ value = ((Byte) vb.getValue(Bindings.BYTE));
+ } else if (vb.hasMax()) {
+ value = (Byte) vb.getMax(Bindings.BYTE);
+ } else {
+ value = (Byte) vb.getValue(Bindings.BYTE);
+ }
+ pt.setLocation(t1, value==1 ? BINARY[1] : BINARY[0]);
+ at.transform(pt, pt);
+ pt2.setLocation(t2, BINARY[2]);
+ at.transform(pt2, pt2);
+ double x1 = pt.x;
+ double x2 = pt2.x;
+ double y1 = pt.y;
+ double y2 = pt2.y;
+ dev.extendRectangle(x1);
+ dev.addRectangle(x1, x2, y1, y2);
+ devNotAttached = false;
+ }
+
+ // Already over => break
+ if(wentOver) break;
+
+ // Out of range
+ if (t2>=end) {
+ wentOver = true;
+ }
+ }
+
+ }
+
+ public boolean readMinMaxFromEnd() {
+ if (disabled) return false;
+ HistoryManager history = getTrendNode().historian;
+
+ boolean hasVariable = !item.variableId.isEmpty() && !item.groupItemId.isEmpty() && !item.groupId.isEmpty();
+ boolean canReadMinMax = minmaxItem != null;
+ boolean manualScale = item.scale instanceof Scale.Manual;
+
+ if ( !hasVariable ) {
+ min = 0;
+ max = 1;
+ from = Double.NaN;
+ end = Double.NaN;
+ return false;
+ }
+
+ try {
+ if (canReadMinMax && !manualScale) {
+ String id = (String) minmaxItem.getFieldUnchecked("id");
+ StreamAccessor sa = history.openStream(id, "r");
+ if ( sa==null ) {
+ min = 0;
+ max = 1;
+ from = Double.NaN;
+ end = Double.NaN;
+ return false;
+ } else
+ try {
+ if (sa.size()==0) return false;
+ min = Double.MAX_VALUE;
+ max = -Double.MAX_VALUE;
+ from = Double.MAX_VALUE;
+ end = -Double.MAX_VALUE;
+ for (int i=0; i<sa.size(); i++) {
+ Binding binding = Bindings.getBinding( sa.type().componentType() );
+ Object sample = sa.get(i, binding);
+ ValueBand vb = new ValueBand(binding, sample);
+ if (!vb.isNullValue() && !vb.isNanSample()) {
+ min = Math.min(min, vb.getMinDouble());
+ max = Math.max(max, vb.getMaxDouble());
+ }
+ if (!vb.isNullValue()) {
+ from = Math.min(from, vb.getTimeDouble());
+ end = Math.max(end, vb.getEndTimeDouble());
+ }
+ }
+ if ( min==Double.MAX_VALUE || max==-Double.MAX_VALUE) {
+ min = 0; max = 1.0;
+ }
+ if ( from==Double.MAX_VALUE || end==-Double.MAX_VALUE) {
+ from = 0; end = 100.0;
+ }
+ return true;
+ } finally {
+ try { sa.close(); } catch (AccessorException e) {}
+ }
+ } else {
+
+ if (manualScale) {
+ Scale.Manual ms = (Scale.Manual) item.scale;
+ min = ms.min;
+ max = ms.max;
+ }
+
+ // Read from, end from any stream
+ if (openStreamAccessor==null) {
+ openStream(1);
+ }
+ if (openStreamAccessor!=null){
+ // Open some stream
+ StreamAccessor sa = openStreamAccessor;
+ sa.reset();
+ int count = sa.size();
+ if (count>0) {
+ Binding binding = Bindings.getBinding( sa.type().componentType() );
+ Object sample = sa.get(0, binding);
+ ValueBand vb = new ValueBand(binding, sample);
+ from = vb.getTimeDouble();
+ sa.get(count-1, binding, sample);
+ end = vb.hasEndTime() ? vb.getEndTimeDouble() : vb.getTimeDouble();
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+ } catch (AccessorException e) {
+ log.log(Level.FINE, e.toString(), e);
+ return false;
+ } catch (HistoryException e) {
+ log.log(Level.FINE, e.toString(), e);
+ return false;
+ }
+ }
+
+ @Override
+ public void cleanup() {
+ trendNode = null;
+ if (openStreamAccessor != null) {
+ try {
+ openStreamAccessor.close();
+ } catch (AccessorException e) {
+ }
+ openStreamAccessor = null;
+ openStreamItem = null;
+ }
+ super.cleanup();
+ }
+
+ Bean getFormat(double pixelsPerSecond) {
+ Bean result = null;
+ if (renderableItems == null)
+ return null;
+
+ for (Bean format : renderableItems)
+ {
+ double interval = 0.0;
+ try {
+ interval = format.hasField("interval") ? (Double) format.getFieldUnchecked("interval") : 0.0;
+ } catch (RuntimeBindingException e) {
+ } catch (BindingException e) {
+ }
+ if (Double.isNaN( interval ) || interval<=pixelsPerSecond) {
+ result = format;
+ } else {
+ break;
+ }
+ }
+ if (result==null) {
+ if ( renderableItems.length == 0 ) {
+ result = null;
+ } else {
+ result = renderableItems[0];
+ }
+ }
+
+ return result;
+ }
+
+ public Stream openStream(double pixelsPerSecond) {
+ Bean f = getFormat(pixelsPerSecond);
+ if (f==openStreamItem) return openStream;
+
+ if (openStream != null) {
+ openStream.close();
+ openStreamAccessor = null;
+ openStreamItem = null;
+ openStream = null;
+ }
+
+ if (disabled) return null;
+
+ if (f!=null) {
+ HistoryManager historian = getTrendNode().historian;
+ try {
+ String id = (String) f.getFieldUnchecked("id");
+ openStreamAccessor = historian.openStream(id, "r");
+ if ( openStreamAccessor!=null ) {
+ openStream = new Stream(openStreamAccessor);
+ openStreamItem = f;
+ } else {
+ openStream = null;
+ }
+ } catch (HistoryException e) {
+ log.log(Level.FINE, e.toString(), e);
+ }
+ }
+
+ return openStream;
+ }
+
+ @Override
+ public void render(Graphics2D g2d) {
+ }
+
+ @Override
+ public Rectangle2D getBoundsInLocal() {
+ return null;
+ }
+
+ TrendNode getTrendNode() {
+ return (TrendNode) getParent();
+ }
+
+}