X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=bundles%2Forg.simantics.trend%2Fsrc%2Forg%2Fsimantics%2Ftrend%2Fimpl%2FTrendNode.java;h=ddbd4934e0763dd6dd82468a0031563b2ea56f5d;hb=e2e0b33c6a7b55c4b0f7a268dd37e545feefc5a2;hp=dba76ccad2afdc13565cdc1412a62ab20a358883;hpb=04007e0f8ce828baeb99272e2069930215117670;p=simantics%2Fplatform.git diff --git a/bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendNode.java b/bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendNode.java index dba76ccad..ddbd4934e 100644 --- a/bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendNode.java +++ b/bundles/org.simantics.trend/src/org/simantics/trend/impl/TrendNode.java @@ -1,743 +1,744 @@ -/******************************************************************************* - * 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; - - } - - -} +/******************************************************************************* + * 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<VertRuler> vertRulers = new ArrayList<VertRuler>(); + 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<ItemNode> analogItems = new ArrayList<ItemNode>(); + List<ItemNode> binaryItems = new ArrayList<ItemNode>(); + List<ItemNode> allItems = new ArrayList<ItemNode>(); + + // 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<String> itemIds = new HashSet<String>(); + + // 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( "<title here>"); + 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); + if (!it.hidden) + 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; + + } + + +}