/******************************************************************************* * 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; /** * Data node for a TrendItem * * @author toni.kalajainen */ public class ItemNode extends G2DNode implements TrendLayout { private static final long serialVersionUID = -4741446944761752871L; 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 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 streamFormats = new TreeSet( 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 { 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; iend) { // 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); } } 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 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; i0) { 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(); } }