/******************************************************************************* * 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.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.font.GlyphVector; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.simantics.databoard.Bindings; import org.simantics.databoard.accessor.error.AccessorException; import org.simantics.databoard.binding.error.BindingException; import org.simantics.databoard.util.Bean; import org.simantics.g2d.utils.GridUtil; import org.simantics.history.Collector; import org.simantics.history.History; import org.simantics.history.HistoryException; import org.simantics.history.HistoryManager; import org.simantics.history.ItemManager; import org.simantics.history.impl.CollectorImpl; import org.simantics.history.util.Stream; import org.simantics.history.util.ValueBand; import org.simantics.scenegraph.g2d.G2DParentNode; import org.simantics.scenegraph.utils.QualityHints; import org.simantics.trend.configuration.ItemPlacement; import org.simantics.trend.configuration.LineQuality; import org.simantics.trend.configuration.Scale; import org.simantics.trend.configuration.TimeFormat; import org.simantics.trend.configuration.TimeWindow; import org.simantics.trend.configuration.TrendItem; import org.simantics.trend.configuration.TrendItem.Renderer; import org.simantics.trend.configuration.TrendQualitySpec; import org.simantics.trend.configuration.TrendSpec; import org.simantics.trend.configuration.Viewport; import org.simantics.trend.configuration.Viewport.AxisViewport; import org.simantics.trend.configuration.YAxisMode; import org.simantics.utils.format.ValueFormat; import gnu.trove.map.TObjectIntMap; import gnu.trove.map.hash.TObjectIntHashMap; import gnu.trove.procedure.TObjectProcedure; public class TrendNode extends G2DParentNode implements TrendLayout { private static final long serialVersionUID = -8339696405893626168L; /** Title node */ public TextNode titleNode; /** Plot node */ public Plot plot; public HorizRuler horizRuler; VertRuler vertRuler; // Selected vertical ruler, null if there are no analog items int vertRulerIndex; // Index of the selected vertical ruler List vertRulers = new ArrayList(); SelectionNode selection; public MilestoneSpec milestones; /** Control bounds */ Rectangle2D bounds = new Rectangle2D.Double(); /** Trend spec */ public TrendSpec spec; public ViewRenderingProfile renderingProfile = new ViewRenderingProfile(); public TrendQualitySpec quality = TrendQualitySpec.DEFAULT; public boolean printing = false; boolean singleAxis; // Data nodes List analogItems = new ArrayList(); List binaryItems = new ArrayList(); List allItems = new ArrayList(); // History static HistoryManager DUMMY_HISTORY = History.createMemoryHistory(); public HistoryManager historian = DUMMY_HISTORY; // Collector - set for flushing the stream, right before drawing public Collector collector = null; Set itemIds = new HashSet(); // Signal to indicate history has changed. public boolean datadirty = false; // Signal to indicate the cached shapes are dirty. This will cause reloading of data. public boolean shapedirty = false; public boolean autoscaletime = true; public TimeFormat timeFormat = TimeFormat.Time; public ValueFormat valueFormat = ValueFormat.Default; public boolean drawSamples = false; /** Time at mouse, when mouse hovers over trend. This value is set by TrendParticipant. NaN is mouse is not hovering */ public double mouseHoverTime = Double.NaN, lastMouseHoverTime = 0.0; /** If set, valueTip is drawn at the time */ public Double valueTipTime = null, prevValueTipTime = null; public boolean valueTipHover = false; public ItemPlacement itemPlacement = ItemPlacement.Overlapping; @Override public void init() { spec = new TrendSpec(); spec.init(); milestones = new MilestoneSpec(); milestones.init(); //// Create title node titleNode = addNode( "Title", TextNode.class ); titleNode.setFont( new Font("Arial", 0, 30) ); titleNode.setColor( Color.BLACK ); titleNode.setText( ""); titleNode.setSize(300, 40); plot = addNode( "Plot", Plot.class ); horizRuler = addNode("HorizRuler", HorizRuler.class); vertRuler = addNode("VertRuler", VertRuler.class); vertRulers.add( vertRuler ); /// Set some bounds horizRuler.setFromEnd(0, 100); vertRuler.setMinMax(0, 100); setSize(480, 320); } /** * Set source of data * @param historian * @param collector (Optional) Used for flushing collectors */ public void setHistorian(HistoryManager historian, Collector collector) { this.historian = historian==null?DUMMY_HISTORY:historian; this.collector = collector; itemIds.clear(); ItemManager allFiles = ItemManager.createUnchecked( getHistoryItems() ); for (ItemNode item : allItems) { item.setTrendItem(item.item, allFiles); for (Bean historyItem : item.historyItems ) { try { itemIds.add( (String) historyItem.getField("id") ); } catch (BindingException e) { } } } } /** * Get info of all history items. * This util is created for polling strategy. * * @return */ Bean[] getHistoryItems() { Bean[] result = null; HistoryException e = null; for (int attempt=0; attempt<10; attempt++) { try { result = historian.getItems(); break; } catch (HistoryException e2) { if (e==null) e = e2; try { Thread.sleep(1); } catch (InterruptedException e1) { } } } if (result!=null) return result; //throw new RuntimeException(e); return new Bean[0]; } public void setMilestones(Bean milestones) { if (this.milestones.equals(milestones)) return; this.milestones.readFrom( milestones ); boolean hasBaseline = this.milestones.baseline>=0; // Sort by first, put baseline on top final Milestone baseline = this.milestones.baseline>=0 ? this.milestones.milestones.get( this.milestones.baseline ) : null; Collections.sort(this.milestones.milestones, new Comparator<Milestone>() { public int compare(Milestone o1, Milestone o2) { if (o1==baseline) return -1; if (o2==baseline) return 1; return Double.compare(o1.time, o1.time); }}); this.milestones.baseline = hasBaseline ? 0 : -1; double newBasetime = hasBaseline ? this.milestones.milestones.get(this.milestones.baseline).time : 0.; if (newBasetime != horizRuler.basetime) { horizRuler.basetime = newBasetime; horizRuler.layout(); } shapedirty = true; } public void selectVertRuler(int index) { vertRulerIndex = index; if (index<0 || index>=vertRulers.size()) { vertRuler = vertRulers.get(0); } else { vertRuler = vertRulers.get(index); } shapedirty = true; } @Override public void cleanup() { spec = new TrendSpec(); spec.init(); analogItems.clear(); binaryItems.clear(); allItems.clear(); historian = DUMMY_HISTORY; super.cleanup(); } private static TObjectIntMap<String> itemIndexMap(List<TrendItem> items) { TObjectIntMap<String> map = new TObjectIntHashMap<>(items.size(), 0.5f, -1); for (int i = 0; i < items.size(); ++i) { TrendItem it = items.get(i); map.put(it.groupItemId, i); } return map; } private static <T> TObjectIntMap<T> subtract(TObjectIntMap<T> a, TObjectIntMap<T> b) { TObjectIntMap<T> r = new TObjectIntHashMap<>(a); b.forEachKey(new TObjectProcedure<T>() { @Override public boolean execute(T key) { r.remove(key); return true; } }); return r; } /** * @param newSpec * new trending specification, cannot not be <code>null</code>. * Use {@link TrendSpec#EMPTY} instead of <code>null</code>. */ public void setTrendSpec(TrendSpec newSpec) { //System.out.println(newSpec); // Check if equal & Read spec if (newSpec.equals(this.spec)) return; boolean timeWindowChange = !this.spec.viewProfile.timeWindow.equals( newSpec.viewProfile.timeWindow ); boolean yaxisModeChanged = this.spec.axisMode != newSpec.axisMode; TObjectIntMap<String> newItemMap = itemIndexMap(newSpec.items); TObjectIntMap<String> currentItemMap = itemIndexMap(spec.items); TObjectIntMap<String> removedItemMap = subtract(currentItemMap, newItemMap); Map<String, VertRuler> existingRulers = new HashMap<>(); if (this.spec.axisMode == YAxisMode.MultiAxis) { for (ItemNode item : analogItems) if (item.ruler != null) existingRulers.put(item.item.groupItemId, item.ruler); } this.spec.readFrom( newSpec ); this.spec.sortItems(); this.renderingProfile.read(this.spec.viewProfile); // Set title if (titleNode != null) titleNode.setText( spec.name ); // Setup trend item nodes itemIds.clear(); for (ItemNode item : allItems) removeNode(item); analogItems.clear(); binaryItems.clear(); allItems.clear(); ItemManager itemManager = ItemManager.createUnchecked( getHistoryItems() ); for (int i = 0; i<spec.items.size(); i++) { TrendItem item = spec.items.get(i); if (item.hidden) continue; ItemNode node = createItemNode(item, itemManager); for (Bean historyItem : node.historyItems) { try { itemIds.add( (String) historyItem.getField("id") ); } catch (BindingException e) { } } if (item.renderer == Renderer.Analog) { analogItems.add(node); } else { binaryItems.add(node); } allItems.add(node); } // Setup vertical ruler nodes singleAxis = spec.axisMode == YAxisMode.SingleAxis; if (singleAxis) { if (yaxisModeChanged || vertRulers.size() != 1 || vertRuler == null) { for (VertRuler vr : vertRulers) removeNode(vr); vertRulers.clear(); vertRuler = addNode("VertRuler", VertRuler.class); vertRulers.add( vertRuler ); } vertRuler.manualscale = true; for (int i=0; i<analogItems.size(); i++) { ItemNode item = analogItems.get(i); item.ruler = vertRuler; item.trendNode = this; if (item.item.scale instanceof Scale.Manual == false) vertRuler.manualscale = false; } } else { if (yaxisModeChanged) { // Recreate all rulers for (VertRuler vr : vertRulers) removeNode(vr); vertRulers.clear(); for (int i=0; i<analogItems.size(); i++) vertRulers.add( addNode(VertRuler.class) ); } else { // Remove rulers of the items that were removed // and add new rulers to have enough of them for // each separate analog signal. removedItemMap.forEachKey(new TObjectProcedure<String>() { @Override public boolean execute(String id) { VertRuler vr = existingRulers.get(id); if (vr != null) { removeNode(vr); vertRulers.remove(vr); } return true; } }); for (int i = vertRulers.size(); i < analogItems.size(); ++i) { VertRuler ruler = addNode(VertRuler.class); vertRulers.add(ruler); } } for (int i = 0; i < analogItems.size(); i++) { ItemNode item = analogItems.get(i); VertRuler vr = vertRulers.get(i); vr.setZIndex(1000 + i); vr.color = item.color; vr.label = item.item.label; vr.manualscale = item.item.scale instanceof Scale.Manual; item.ruler = vr; item.trendNode = this; } // Select vert ruler vertRuler = vertRulers.isEmpty() ? null : vertRulers.get( vertRulerIndex <= 0 || vertRulerIndex >= vertRulers.size() ? 0 : vertRulerIndex ); } // Locked TimeWindow tw = spec.viewProfile.timeWindow; horizRuler.manualscale = tw.timeWindowLength!=null && tw.timeWindowStart!=null; if (timeWindowChange) { horizRuler.autoscale(); } shapedirty = true; } private ItemNode createItemNode(TrendItem item, ItemManager itemManager) { ItemNode node = addNode( ItemNode.class ); //node.trendNode = this; node.setTrendItem(item, itemManager); node.color = toColor(item.customColor); if (node.color == null) node.color = JarisPaints.getColor( item.index ); node.stroke = item.customStrokeWidth != null ? withStrokeWidth(item.customStrokeWidth, Plot.TREND_LINE_STROKE) : Plot.TREND_LINE_STROKE; return node; } private static BasicStroke withStrokeWidth(float width, BasicStroke stroke) { return new BasicStroke(width, stroke.getEndCap(), stroke.getLineJoin(), stroke.getMiterLimit(), stroke.getDashArray(), stroke.getDashPhase()); } private static Color toColor(float[] components) { if (components == null) return null; switch (components.length) { case 3: return new Color(components[0], components[1], components[2]); case 4: return new Color(components[0], components[1], components[2], components[3]); default: return null; } } /** * Layout graphical nodes based on bounds */ public void layout() { double w = bounds.getWidth(); double h = bounds.getHeight(); if ( titleNode != null ) { titleNode.setSize(w, h * 0.02); titleNode.setTranslate(0, VERT_MARGIN); titleNode.layout(); } // Plot-Ruler area width double praw = w-HORIZ_MARGIN*2; // Plot height double ph = h-VERT_MARGIN*2-MILESTONE_HEIGHT-HORIZ_RULER_HEIGHT; if ( titleNode != null ) { ph -= titleNode.th; } // Analog & Binary area height double aah, bah; if (!analogItems.isEmpty()) { bah = binaryItems.size() * BINARY[3]; aah = Math.max(0, ph-bah); if (aah+bah>ph) bah = ph-aah; } else { // No analog items aah = 0; bah = ph; } // Vertical ruler for (VertRuler vertRuler : vertRulers) { vertRuler.setHeight(aah); vertRuler.layout(); } plot.analogAreaHeight = aah; plot.binaryAreaHeight = bah; // Vertical ruler widths double vrws = 0; for (VertRuler vertRuler : vertRulers) { vrws += vertRuler.getWidth(); } // Add room for Binary label if ( !binaryItems.isEmpty() ) { double maxLabelWidth = BINARY_LABEL_WIDTH; for (ItemNode node : binaryItems) { if (node.item != null) { GlyphVector glyphVector = RULER_FONT.createGlyphVector(GridUtil.frc, node.item.label); double labelWidth = glyphVector.getVisualBounds().getWidth(); maxLabelWidth = Math.max( maxLabelWidth, labelWidth ); } } vrws = Math.max(maxLabelWidth, vrws); } // Plot Width double pw = praw - vrws; plot.setTranslate(HORIZ_MARGIN, (titleNode!=null?titleNode.th:0)+VERT_MARGIN+MILESTONE_HEIGHT); plot.setSize(pw, ph); horizRuler.layout(); horizRuler.setTranslate(HORIZ_MARGIN, plot.getY()+plot.getHeight()+3); boolean l = horizRuler.setWidth(pw); l |= horizRuler.setFromEnd(horizRuler.from, horizRuler.end); if (l) horizRuler.layout(); // Move vertical rulers double vrx = plot.getX() + plot.getWidth() + 3; for (VertRuler vertRuler : vertRulers) { vertRuler.setTranslate(vrx, plot.getY()); vrx += vertRuler.getWidth() + 3; } } public void setSize(double width, double height) { bounds.setFrame(0, 0, width, height); } @Override public void render(Graphics2D g2d) { // Set Quality High QualityHints qh = QualityHints.getQuality(g2d); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, quality.textQuality==LineQuality.Antialias?RenderingHints.VALUE_TEXT_ANTIALIAS_ON:RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); // Set Bounds Rectangle bounds = g2d.getClipBounds(); if (bounds.getWidth()!=this.bounds.getWidth() || bounds.getHeight()!=this.bounds.getHeight()) { setSize(bounds.getWidth(), bounds.getHeight()); layout(); } // Flush history subscriptions flushHistory(); // Render children super.render(g2d); // Render plot's value tip plot.renderValueTip(g2d); // Restore quality qh.setQuality(g2d); } @Override public Rectangle2D getBoundsInLocal() { return bounds; } /** * Return true if the viewport is not moving and shows only past values * @return */ public boolean allPast() { TimeWindow timeWindow = spec.viewProfile.timeWindow; boolean fixedWindow = !horizRuler.autoscroll || (timeWindow.timeWindowStart!=null && timeWindow.timeWindowLength!=null); if (fixedWindow) { for (ItemNode item : allItems) { if (item.end <= horizRuler.end) return false; } return true; } else { return false; } } public void flushHistory() { if (collector == null || collector instanceof CollectorImpl == false) return; CollectorImpl fh = (CollectorImpl) collector; fh.flush( itemIds ); } /** * Read values of min,max,from,end to all items. * Put iMin,iMax,iFrom,iEnd to all axes. */ public void readMinMaxFromEnd() { flushHistory(); // Read min,max,from,end from all items - Gather collective ranges for (ItemNode item : allItems) { item.readMinMaxFromEnd(); } horizRuler.setKnownFromEnd(); for (VertRuler vertRuler : vertRulers) vertRuler.setKnownMinMax(); } /** * Flushes history, resets streams, * reads the latest time and value ranges from disc. * Scales the axes. * Sets dirty true if there was change in axis. */ public boolean autoscale(boolean timeAxis, boolean valueAxis) { if (!timeAxis && !valueAxis) return false; readMinMaxFromEnd(); boolean changed = false; // Time axis if (timeAxis) { changed |= horizRuler.autoscale(); if ( changed && !printing ) horizRuler.fireListener(); } // Y-Axis if (valueAxis) { for (VertRuler vertRuler : vertRulers) changed |= vertRuler.autoscale(); } return changed; } public boolean updateValueTipTime() { if (valueTipTime != null && spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) { double endTime = horizRuler.getItemEndTime(); if (!Double.isNaN(endTime)) { boolean changed = Double.compare(valueTipTime, endTime) != 0; valueTipTime = endTime; return changed; } } return false; } public void zoomIn(double x, double y, double width, double height, boolean horiz, boolean vert) { if (horiz) { horizRuler.zoomIn(x, width); } if (vert) { for (VertRuler vertRuler : vertRulers) { vertRuler.zoomIn(y, height); } } shapedirty = true; } public void zoomOut() { horizRuler.zoomOut(); for (VertRuler vertRuler : vertRulers) { vertRuler.zoomOut(); } shapedirty = true; } public TrendSpec getTrendSpec() { return spec; } public Viewport getViewport() { Viewport vp = new Viewport(); vp.init(); vp.from = horizRuler.from; vp.end = horizRuler.end; for (VertRuler vr : vertRulers) { AxisViewport avp = new AxisViewport(); avp.min = vr.min; avp.max = vr.max; vp.axesports.add( avp ); } return vp; } public void setViewport( Viewport vp ) { horizRuler.from = vp.from; horizRuler.end = vp.end; int i=0; for ( AxisViewport avp : vp.axesports ) { if ( i>=vertRulers.size() ) break; VertRuler vr = vertRulers.get(i++); vr.min = avp.min; vr.max = avp.max; } } public void setQuality(TrendQualitySpec quality) { this.quality = quality; } public TrendQualitySpec getQuality() { return quality; } public Double snapToValue(double time, double snapToleranceInTime) throws HistoryException, AccessorException { double from = horizRuler.from; double end = horizRuler.end; double pixelsPerSecond = (end-from) / plot.getWidth(); TreeSet<Double> values = new TreeSet<Double>(Bindings.DOUBLE); for (ItemNode item : allItems) { Stream s = item.openStream( pixelsPerSecond ); if ( s==null ) continue; int pos = s.binarySearch(Bindings.DOUBLE, time); ValueBand vb = new ValueBand(s.sampleBinding, s.sampleBinding.createDefaultUnchecked()); // Exact match if (pos >= 0) { return time; } int prev = -pos-2; int next = -pos-1; int count = s.count(); Double prevTime = null, nextTime = null; if ( prev>=0 && prev<count ) { s.accessor.get(prev, s.sampleBinding, vb.getSample()); if ( !vb.isNanSample() ) { prevTime = vb.getTimeDouble(); if ( vb.hasEndTime() ) { Double nTime = vb.getEndTimeDouble(); if (nTime!=null && nTime+snapToleranceInTime>time) nextTime = nTime; } } } if ( nextTime==null && next>=0 && next<count ) { s.accessor.get(next, s.sampleBinding, vb.getSample()); if ( !vb.isNanSample() ) { nextTime = vb.getTimeDouble(); } } if (prevTime==null && nextTime==null) continue; if (prevTime==null) { if ( nextTime - time < snapToleranceInTime ) values.add(nextTime); } else if (nextTime==null) { if ( time - prevTime < snapToleranceInTime ) values.add(prevTime); } else { values.add(nextTime); values.add(prevTime); } } if (values.isEmpty()) return null; Double lower = values.floor( time ); Double higher = values.ceiling( time ); if ( lower == null ) return higher; if ( higher == null ) return lower; double result = time-lower < higher-time ? lower : higher; return result; } }