/*
* This file is part of the OpenSCADA project
* Copyright (C) 2006-2011 TH4 SYSTEMS GmbH (http://th4-systems.com)
*
* OpenSCADA is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3
* only, as published by the Free Software Foundation.
*
* OpenSCADA is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License version 3 for more details
* (a copy is included in the LICENSE file that accompanied this code).
*
* You should have received a copy of the GNU Lesser General Public License
* version 3 along with OpenSCADA. If not, see
* <http://opensource.org/licenses/lgpl-3.0.html> for a copy of the LGPLv3 License.
*/
package org.openscada.hd.ui.views;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DragDetectEvent;
import org.eclipse.swt.events.DragDetectListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.ColorDialog;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Spinner;
import org.openscada.hd.QueryParameters;
import org.openscada.hd.QueryState;
import org.openscada.hd.Value;
import org.openscada.hd.ValueInformation;
import org.openscada.hd.chart.DataAtPoint;
import org.openscada.hd.chart.TrendChart;
import org.openscada.hd.ui.data.AbstractQueryBuffer;
import org.openscada.utils.lang.Pair;
import org.swtchart.IAxis;
import org.swtchart.ILineSeries;
import org.swtchart.ILineSeries.PlotSymbolType;
import org.swtchart.ISeries;
import org.swtchart.ISeries.SeriesType;
import org.swtchart.LineStyle;
import org.swtchart.Range;
public abstract class AbstractTrendView extends QueryViewPart
{
/**
* @author Jürgen Rose
*
* holds a range of two dates (from - two), is used as a return value for zooming functionality
*/
public static class DateRange
{
public final Date start;
public final Date end;
public DateRange ( final Date start, final Date end )
{
this.start = start;
this.end = end;
}
}
public static class SeriesParameters
{
public final String name;
public final int width;
public SeriesParameters ( final String name, final int width )
{
this.name = name;
this.width = width;
}
}
private static final String KEY_QUALITY = "quality"; //$NON-NLS-1$
private static final String KEY_MANUAL = "manual"; //$NON-NLS-1$
private static final String KEY_WHITE = "__white"; //$NON-NLS-1$
private static final String KEY_BLACK = "__black"; //$NON-NLS-1$
private static final long GUI_JOB_DELAY = 150;
private static final long GUI_RESIZE_JOB_DELAY = 1500;
private static final String SMALL_LABEL_FONT = "small-label-font"; //$NON-NLS-1$
private final AtomicReference<Job> parameterUpdateJob = new AtomicReference<Job> ();
private final AtomicReference<Job> rangeUpdateJob = new AtomicReference<Job> ();
private final AtomicReference<Job> dataUpdateJob = new AtomicReference<Job> ();
private final AtomicReference<Job> scalingUpdateJob = new AtomicReference<Job> ();
private final ConcurrentMap<String, double[]> data = new ConcurrentHashMap<String, double[]> ();
private final AtomicReference<double[]> dataQuality = new AtomicReference<double[]> ();
private final AtomicReference<double[]> dataManual = new AtomicReference<double[]> ();
private final AtomicReference<long[]> dataSourceValues = new AtomicReference<long[]> ();
private final AtomicReference<Date[]> dataTimestamp = new AtomicReference<Date[]> ();
private final AtomicReference<ChartParameters> chartParameters = new AtomicReference<ChartParameters> ();
private final Object updateLock = new Object ();
private Composite parent;
private Composite panel;
private RowLayout groupLayout;
private Group scaleGroup;
private Button scaleAutomaticallyCheckbox;
private Spinner scaleMinSpinner;
private Spinner scaleMaxSpinner;
private Group qualityGroup;
private Spinner qualitySpinner;
private Button qualityColorButton;
private Group manualGroup;
private Spinner manualSpinner;
private Button manualColorButton;
private TrendChart chart;
private final ConcurrentMap<String, Group> seriesGroups = new ConcurrentHashMap<String, Group> ();
private Cursor dragCursor;
private Cursor zoomInCursor;
private Cursor zoomOutCursor;
private volatile boolean dragStarted = false;
private volatile int dragStartedX = -1;
private FontRegistry fontRegistry;
private ColorRegistry colorRegistry;
private volatile double scaleYMin = 0.0;
private volatile double scaleYMax = 1.0;
private volatile Double currentYMin = null;
private volatile Double currentYMax = null;
private volatile boolean scaleYAutomatically = true;
public AbstractTrendView ()
{
super ();
}
@Override
public void createPartControl ( final Composite parent )
{
// create predefined cursors
this.dragCursor = new Cursor ( parent.getDisplay (), SWT.CURSOR_HAND );
final ImageData zoomInImage = new ImageData ( getClass ().getClassLoader ().getResourceAsStream ( "org/openscada/hd/ui/zoomin.gif" ) ); //$NON-NLS-1$
this.zoomInCursor = new Cursor ( parent.getDisplay (), zoomInImage, 15, 15 );
final ImageData zoomOutImage = new ImageData ( getClass ().getClassLoader ().getResourceAsStream ( "org/openscada/hd/ui/zoomout.gif" ) ); //$NON-NLS-1$
this.zoomOutCursor = new Cursor ( parent.getDisplay (), zoomOutImage, 15, 15 );
// create predefined colors
this.colorRegistry = new ColorRegistry ( parent.getDisplay () );
this.colorRegistry.put ( KEY_WHITE, new RGB ( 255, 255, 255 ) );
this.colorRegistry.put ( KEY_BLACK, new RGB ( 0, 0, 0 ) );
this.colorRegistry.put ( KEY_QUALITY, new RGB ( 255, 192, 192 ) );
this.colorRegistry.put ( KEY_MANUAL, new RGB ( 192, 192, 255 ) );
this.colorRegistry.put ( "MIN", new RGB ( 255, 0, 0 ) ); //$NON-NLS-1$
this.colorRegistry.put ( "MAX", new RGB ( 0, 255, 0 ) ); //$NON-NLS-1$
this.colorRegistry.put ( "AVG", new RGB ( 0, 0, 255 ) ); //$NON-NLS-1$
// chart has some predefined parameters, quality of 0.75, from yesterday to today
this.chartParameters.set ( ChartParameters.create ().construct () );
this.parent = parent;
// layout for composite
final GridLayout layout = new GridLayout ();
parent.setLayout ( layout );
// create panel to contain items for chart control
this.panel = new Composite ( parent, SWT.NONE );
this.panel.setLayoutData ( new GridData ( SWT.CENTER, SWT.BEGINNING, true, false ) );
final RowLayout panelLayout = new RowLayout ( SWT.HORIZONTAL );
panelLayout.center = true;
this.panel.setLayout ( panelLayout );
this.groupLayout = new RowLayout ( SWT.HORIZONTAL );
this.groupLayout.center = true;
// add group for scaling
this.scaleGroup = new Group ( this.panel, SWT.SHADOW_ETCHED_IN );
this.scaleGroup.setLayout ( this.groupLayout );
this.scaleGroup.setText ( Messages.TrendView_Scaling );
this.scaleAutomaticallyCheckbox = new Button ( this.scaleGroup, SWT.CHECK );
this.scaleAutomaticallyCheckbox.setText ( Messages.TrendView_Automatically );
this.scaleAutomaticallyCheckbox.setSelection ( this.scaleYAutomatically );
this.scaleAutomaticallyCheckbox.addSelectionListener ( new SelectionListener () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
if ( AbstractTrendView.this.scaleAutomaticallyCheckbox.getSelection () )
{
AbstractTrendView.this.scaleYAutomatically = true;
AbstractTrendView.this.scaleMinSpinner.setEnabled ( false );
AbstractTrendView.this.scaleMaxSpinner.setEnabled ( false );
}
else
{
AbstractTrendView.this.scaleYAutomatically = false;
AbstractTrendView.this.scaleMinSpinner.setEnabled ( true );
AbstractTrendView.this.scaleMaxSpinner.setEnabled ( true );
}
updateSpinner ();
adjustRange ();
AbstractTrendView.this.chart.redraw ();
}
@Override
public void widgetDefaultSelected ( final SelectionEvent e )
{
}
} );
this.scaleMinSpinner = new Spinner ( this.scaleGroup, SWT.BORDER );
this.scaleMinSpinner.setEnabled ( !this.scaleYAutomatically );
this.scaleMinSpinner.setDigits ( 3 );
this.scaleMinSpinner.setMaximum ( Integer.MAX_VALUE );
this.scaleMinSpinner.setMinimum ( Integer.MIN_VALUE );
this.scaleMinSpinner.setSelection ( (int)Math.round ( this.scaleYMin * 1000 ) );
this.scaleMinSpinner.addSelectionListener ( new SelectionListener () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
AbstractTrendView.this.scalingUpdateJob.get ().schedule ( GUI_RESIZE_JOB_DELAY );
}
@Override
public void widgetDefaultSelected ( final SelectionEvent e )
{
}
} );
this.scaleMaxSpinner = new Spinner ( this.scaleGroup, SWT.BORDER );
this.scaleMaxSpinner.setEnabled ( !this.scaleYAutomatically );
this.scaleMaxSpinner.setDigits ( 3 );
this.scaleMaxSpinner.setMaximum ( Integer.MAX_VALUE );
this.scaleMaxSpinner.setMinimum ( Integer.MIN_VALUE );
this.scaleMaxSpinner.setSelection ( (int)Math.round ( this.scaleYMax * 1000 ) );
this.scaleMaxSpinner.addSelectionListener ( new SelectionListener () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
AbstractTrendView.this.scalingUpdateJob.get ().schedule ( GUI_RESIZE_JOB_DELAY );
}
@Override
public void widgetDefaultSelected ( final SelectionEvent e )
{
}
} );
// quality spinner
this.qualityGroup = new Group ( this.panel, SWT.SHADOW_ETCHED_IN );
this.qualityColorButton = new Button ( this.qualityGroup, SWT.PUSH );
this.qualitySpinner = new Spinner ( this.qualityGroup, SWT.BORDER );
this.qualityGroup.setLayout ( this.groupLayout );
this.qualityGroup.setText ( Messages.TrendView_Quality );
this.qualityColorButton.setText ( Messages.TrendView_Color );
this.qualitySpinner.setBackground ( this.colorRegistry.get ( KEY_QUALITY ) );
this.qualityColorButton.addSelectionListener ( new SelectionListener () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
final ColorDialog cd = new ColorDialog ( parent.getShell () );
cd.setText ( Messages.TrendView_SelectColor );
final RGB resultColor = cd.open ();
if ( resultColor != null )
{
AbstractTrendView.this.colorRegistry.put ( KEY_QUALITY, resultColor );
AbstractTrendView.this.qualitySpinner.setBackground ( AbstractTrendView.this.colorRegistry.get ( KEY_QUALITY ) );
AbstractTrendView.this.qualitySpinner.setForeground ( contrastForeground ( AbstractTrendView.this.colorRegistry.get ( KEY_QUALITY ) ) );
AbstractTrendView.this.parameterUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
}
@Override
public void widgetDefaultSelected ( final SelectionEvent e )
{
}
} );
this.qualitySpinner.setDigits ( 2 );
this.qualitySpinner.setMaximum ( 100 );
this.qualitySpinner.setMinimum ( 0 );
this.qualitySpinner.setSelection ( this.chartParameters.get ().getQuality () );
this.qualitySpinner.addModifyListener ( new ModifyListener () {
@Override
public void modifyText ( final ModifyEvent e )
{
final ChartParameters newParameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).quality ( AbstractTrendView.this.qualitySpinner.getSelection () ).construct ();
AbstractTrendView.this.chartParameters.set ( newParameters );
AbstractTrendView.this.parameterUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
} );
// manual spinner
this.manualGroup = new Group ( this.panel, SWT.SHADOW_ETCHED_IN );
this.manualColorButton = new Button ( this.manualGroup, SWT.PUSH );
this.manualSpinner = new Spinner ( this.manualGroup, SWT.BORDER );
this.manualGroup.setLayout ( this.groupLayout );
this.manualGroup.setText ( Messages.TrendView_Manual );
this.manualColorButton.setText ( Messages.TrendView_Color );
this.manualSpinner.setBackground ( this.colorRegistry.get ( KEY_MANUAL ) );
this.manualColorButton.addSelectionListener ( new SelectionListener () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
final ColorDialog cd = new ColorDialog ( parent.getShell () );
cd.setText ( Messages.TrendView_SelectColor );
final RGB resultColor = cd.open ();
if ( resultColor != null )
{
AbstractTrendView.this.colorRegistry.put ( KEY_MANUAL, resultColor );
AbstractTrendView.this.manualSpinner.setBackground ( AbstractTrendView.this.colorRegistry.get ( KEY_MANUAL ) );
AbstractTrendView.this.manualSpinner.setForeground ( contrastForeground ( AbstractTrendView.this.colorRegistry.get ( KEY_MANUAL ) ) );
AbstractTrendView.this.parameterUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
}
@Override
public void widgetDefaultSelected ( final SelectionEvent e )
{
}
} );
this.manualSpinner.setDigits ( 2 );
this.manualSpinner.setMaximum ( 100 );
this.manualSpinner.setMinimum ( 0 );
this.manualSpinner.setSelection ( this.chartParameters.get ().getManual () );
this.manualSpinner.addModifyListener ( new ModifyListener () {
@Override
public void modifyText ( final ModifyEvent e )
{
final ChartParameters newParameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).manual ( AbstractTrendView.this.manualSpinner.getSelection () ).construct ();
AbstractTrendView.this.chartParameters.set ( newParameters );
AbstractTrendView.this.parameterUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
} );
// font for chart labels
final FontData[] smallFont = JFaceResources.getDefaultFontDescriptor ().getFontData ();
//// smallFont[0].setHeight ( smallFont[0].getHeight () - 2 );
smallFont[0].setHeight ( 7 );
this.fontRegistry = new FontRegistry ( parent.getDisplay () );
this.fontRegistry.put ( SMALL_LABEL_FONT, smallFont );
// add chart
this.chart = new TrendChart ( parent, SWT.NONE );
this.chart.setLayoutData ( new GridData ( SWT.FILL, SWT.FILL, true, true ) );
this.chart.getTitle ().setText ( Messages.TrendView_NoItemSelected );
this.chart.getTitle ().setForeground ( parent.getDisplay ().getSystemColor ( SWT.COLOR_WIDGET_FOREGROUND ) );
this.chart.getTitle ().setFont ( JFaceResources.getHeaderFont () );
this.chart.getLegend ().setPosition ( SWT.BOTTOM );
this.chart.getAxisSet ().getXAxis ( 0 ).getTitle ().setVisible ( false );
this.chart.getAxisSet ().getXAxis ( 0 ).getTick ().setForeground ( parent.getDisplay ().getSystemColor ( SWT.COLOR_WIDGET_FOREGROUND ) );
this.chart.getAxisSet ().getXAxis ( 0 ).getTick ().setFont ( this.fontRegistry.get ( SMALL_LABEL_FONT ) );
this.chart.getAxisSet ().getXAxis ( 0 ).getGrid ().setStyle ( LineStyle.NONE );
this.chart.getAxisSet ().getYAxis ( 0 ).getTitle ().setVisible ( false );
this.chart.getAxisSet ().getYAxis ( 0 ).getTick ().setForeground ( parent.getDisplay ().getSystemColor ( SWT.COLOR_WIDGET_FOREGROUND ) );
this.chart.getAxisSet ().getYAxis ( 0 ).getTick ().setFont ( this.fontRegistry.get ( SMALL_LABEL_FONT ) );
this.chart.getAxisSet ().getYAxis ( 0 ).getGrid ().setStyle ( LineStyle.NONE );
// if size of plot has changed, a new request should be made to account
// for changed numbers of displayed entries
this.chart.getPlotArea ().addControlListener ( new ControlListener () {
@Override
public void controlResized ( final ControlEvent e )
{
final ChartParameters newParameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).numOfEntries ( AbstractTrendView.this.chart.getPlotArea ().getBounds ().width ).construct ();
AbstractTrendView.this.chartParameters.set ( newParameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_RESIZE_JOB_DELAY );
}
@Override
public void controlMoved ( final ControlEvent e )
{
}
} );
this.chart.getPlotArea ().addKeyListener ( new KeyListener () {
@Override
public void keyReleased ( final KeyEvent e )
{
if ( e.keyCode == SWT.SHIFT )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( null );
}
else if ( e.keyCode == SWT.ALT )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( null );
}
}
@Override
public void keyPressed ( final KeyEvent e )
{
if ( e.keyCode == SWT.SHIFT )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( AbstractTrendView.this.zoomInCursor );
}
else if ( e.keyCode == SWT.ALT )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( AbstractTrendView.this.zoomOutCursor );
}
}
} );
this.chart.getPlotArea ().addMouseTrackListener ( new MouseTrackListener () {
@Override
public void mouseHover ( final MouseEvent e )
{
if ( ( e.stateMask & SWT.SHIFT ) == SWT.SHIFT )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( AbstractTrendView.this.zoomInCursor );
}
else if ( ( e.stateMask & SWT.ALT ) == SWT.ALT )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( AbstractTrendView.this.zoomOutCursor );
}
}
@Override
public void mouseExit ( final MouseEvent e )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( null );
}
@Override
public void mouseEnter ( final MouseEvent e )
{
AbstractTrendView.this.chart.getPlotArea ().setFocus ();
}
} );
this.chart.getPlotArea ().addDragDetectListener ( new DragDetectListener () {
@Override
public void dragDetected ( final DragDetectEvent e )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( AbstractTrendView.this.dragCursor );
AbstractTrendView.this.dragStarted = true;
AbstractTrendView.this.dragStartedX = e.x;
}
} );
this.chart.getPlotArea ().addMouseListener ( new MouseListener () {
@Override
public void mouseUp ( final MouseEvent e )
{
if ( AbstractTrendView.this.dragStarted )
{
AbstractTrendView.this.dragStarted = false;
AbstractTrendView.this.chart.getPlotArea ().setCursor ( null );
// zoom in range
final DateRange zoomResult = moveRange ( AbstractTrendView.this.dragStartedX, e.x, 0, AbstractTrendView.this.chart.getPlotArea ().getSize ().x, AbstractTrendView.this.chartParameters.get ().getStartTime (), AbstractTrendView.this.chartParameters.get ().getEndTime () );
final ChartParameters parameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).startTime ( zoomResult.start ).endTime ( zoomResult.end ).construct ();
AbstractTrendView.this.chartParameters.set ( parameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
else
{
if ( e.button == 1 && ( e.stateMask & SWT.SHIFT ) == SWT.SHIFT )
{
// zoom in
final DateRange zoomResult = zoomIn ( e.x, 0, AbstractTrendView.this.chart.getPlotArea ().getSize ().x, AbstractTrendView.this.chartParameters.get ().getStartTime (), AbstractTrendView.this.chartParameters.get ().getEndTime () );
final ChartParameters parameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).startTime ( zoomResult.start ).endTime ( zoomResult.end ).construct ();
AbstractTrendView.this.chartParameters.set ( parameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
else if ( e.button == 1 && ( e.stateMask & SWT.ALT ) == SWT.ALT )
{
// zoom out
final DateRange zoomResult = zoomOut ( e.x, 0, AbstractTrendView.this.chart.getPlotArea ().getSize ().x, AbstractTrendView.this.chartParameters.get ().getStartTime (), AbstractTrendView.this.chartParameters.get ().getEndTime () );
final ChartParameters parameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).startTime ( zoomResult.start ).endTime ( zoomResult.end ).construct ();
AbstractTrendView.this.chartParameters.set ( parameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
else if ( e.button == 1 )
{
AbstractTrendView.this.chart.getPlotArea ().setCursor ( null );
// zoom in range
final DateRange zoomResult = moveRange ( e.x, AbstractTrendView.this.chart.getPlotArea ().getSize ().x / 2, 0, AbstractTrendView.this.chart.getPlotArea ().getSize ().x, AbstractTrendView.this.chartParameters.get ().getStartTime (), AbstractTrendView.this.chartParameters.get ().getEndTime () );
final ChartParameters parameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).startTime ( zoomResult.start ).endTime ( zoomResult.end ).construct ();
AbstractTrendView.this.chartParameters.set ( parameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
else if ( e.button == 3 )
{
AbstractTrendView.this.chart.getMenu ().setVisible ( true );
}
}
}
@Override
public void mouseDown ( final MouseEvent e )
{
}
@Override
public void mouseDoubleClick ( final MouseEvent e )
{
}
} );
this.chart.getPlotArea ().addMouseWheelListener ( new MouseWheelListener () {
@Override
public void mouseScrolled ( final MouseEvent e )
{
if ( e.count > 0 )
{
// zoom in
final DateRange zoomResult = zoomIn ( e.x, 0, AbstractTrendView.this.chart.getPlotArea ().getSize ().x, AbstractTrendView.this.chartParameters.get ().getStartTime (), AbstractTrendView.this.chartParameters.get ().getEndTime () );
final ChartParameters parameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).startTime ( zoomResult.start ).endTime ( zoomResult.end ).construct ();
AbstractTrendView.this.chartParameters.set ( parameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
else
{
// zoom out
final DateRange zoomResult = zoomOut ( e.x, 0, AbstractTrendView.this.chart.getPlotArea ().getSize ().x, AbstractTrendView.this.chartParameters.get ().getStartTime (), AbstractTrendView.this.chartParameters.get ().getEndTime () );
final ChartParameters parameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).startTime ( zoomResult.start ).endTime ( zoomResult.end ).construct ();
AbstractTrendView.this.chartParameters.set ( parameters );
AbstractTrendView.this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
}
} );
this.chart.setDataAtPoint ( new DataAtPoint () {
private int coordinateToIndex ( final int x )
{
final int margin = 10;
try
{
final int numOfEntries = AbstractTrendView.this.chartParameters.get ().getNumOfEntries ();
final int pixels = AbstractTrendView.this.chart.getPlotArea ().getBounds ().width - 2 * margin;
final double factor = (double)numOfEntries / (double)pixels;
final int i = (int)Math.round ( ( x - margin ) * factor );
return i;
}
catch ( final Exception e )
{
// pass
}
return 0;
}
@Override
public Date getTimestamp ( final int x )
{
return AbstractTrendView.this.dataTimestamp.get ()[coordinateToIndex ( x )];
}
@Override
public double getQuality ( final int x )
{
return AbstractTrendView.this.dataQuality.get ()[coordinateToIndex ( x )];
}
@Override
public double getManual ( final int x )
{
return AbstractTrendView.this.dataManual.get ()[coordinateToIndex ( x )];
};
@Override
public long getSourceValues ( final int x )
{
return AbstractTrendView.this.dataSourceValues.get ()[coordinateToIndex ( x )];
}
@Override
public Map<String, Double> getData ( final int x )
{
final Map<String, Double> result = new HashMap<String, Double> ();
for ( final SeriesParameters seriesParameters : AbstractTrendView.this.chartParameters.get ().getAvailableSeries () )
{
result.put ( seriesParameters.name, AbstractTrendView.this.data.get ( seriesParameters.name )[coordinateToIndex ( x )] );
}
return result;
}
} );
final Menu m = new Menu ( this.chart );
final MenuItem miSaveAsImage = new MenuItem ( m, SWT.NONE );
miSaveAsImage.setText ( Messages.TrendView_SaveAsImage );
miSaveAsImage.addSelectionListener ( new SelectionListener () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
final FileDialog dlg = new FileDialog ( parent.getShell (), SWT.SAVE );
dlg.setText ( Messages.TrendView_SaveTrendAsImage );
dlg.setFilterExtensions ( new String[] { Messages.AbstractTrendView_FilterExtensions } );
dlg.setFilterNames ( new String[] { Messages.AbstractTrendView_FilterNames } );
// dlg.setOverwrite ( true );
final String filename = dlg.open ();
if ( filename != null )
{
final File file = new File ( filename );
try
{
if ( file.canWrite () || file.createNewFile () )
{
AbstractTrendView.this.chart.update ();
AbstractTrendView.this.chart.save ( filename, SWT.IMAGE_PNG );
}
else
{
MessageDialog.openError ( parent.getShell (), Messages.TrendView_SaveTrendAsImage, Messages.TrendView_FileCouldNotBeSaved );
}
}
catch ( final IOException ex )
{
MessageDialog.openError ( parent.getShell (), Messages.TrendView_SaveTrendAsImage, Messages.TrendView_FileCouldNotBeSaved );
}
}
}
@Override
public void widgetDefaultSelected ( final SelectionEvent e )
{
}
} );
this.chart.setMenu ( m );
// set up job for updating chart in case of parameter change
this.parameterUpdateJob.set ( new Job ( "updateChartParameters" ) { //$NON-NLS-1$
@Override
protected IStatus run ( final IProgressMonitor monitor )
{
doUpdateChartParameters ();
return Status.OK_STATUS;
}
} );
// set up job for updating chart in case of range change
this.rangeUpdateJob.set ( new Job ( "updateRangeParameters" ) { //$NON-NLS-1$
@Override
protected IStatus run ( final IProgressMonitor monitor )
{
doUpdateRangeParameters ();
return Status.OK_STATUS;
}
} );
// set up job for updating chart on data change
this.dataUpdateJob.set ( new Job ( "updateChartData" ) { //$NON-NLS-1$
@Override
protected IStatus run ( final IProgressMonitor monitor )
{
doUpdateChartData ();
return Status.OK_STATUS;
}
} );
// set up job for updating chart scaling
this.scalingUpdateJob.set ( new Job ( "updateScaling" ) { //$NON-NLS-1$
@Override
protected IStatus run ( final IProgressMonitor monitor )
{
doUpdateScaling ();
try
{
Thread.sleep ( 100 );
}
catch ( final InterruptedException e )
{
// pass
}
return Status.OK_STATUS;
}
} );
}
protected void updateSpinner ()
{
this.scaleMinSpinner.setSelection ( (int)Math.round ( AbstractTrendView.this.scaleYMin * 1000 ) );
this.scaleMaxSpinner.setSelection ( (int)Math.round ( AbstractTrendView.this.scaleYMax * 1000 ) );
}
/**
* FIXME: implement zoom out correctly, now its just a very primitive version of it
*
* @param x position where clicked
* @param xStart should be 0 in most cases (left edge of chart)
* @param xEnd
* @param startTime
* @param endTime
* @return
*/
private DateRange zoomOut ( final int x, final int xStart, final int xEnd, Date startTime, Date endTime )
{
if ( endTime.getTime () - startTime.getTime () == 0 )
{
startTime = new Date ( startTime.getTime () - 2 );
endTime = new Date ( endTime.getTime () + 2 );
}
final long factor = ( endTime.getTime () - startTime.getTime () ) / ( xEnd - xStart );
final long dTimeQ = ( endTime.getTime () - startTime.getTime () ) / 4;
final long timeshift = factor * ( x - ( xEnd - xStart ) / 2 );
startTime = new Date ( startTime.getTime () + timeshift );
endTime = new Date ( endTime.getTime () + timeshift );
return new DateRange ( new Date ( startTime.getTime () - dTimeQ ), new Date ( endTime.getTime () + dTimeQ ) );
}
/**
* @param x
* @param xStart
* @param xEnd
* @param startTime
* @param endTime
* @return
*/
private DateRange zoomIn ( final int x, final int xStart, final int xEnd, final Date startTime, final Date endTime )
{
final long factor = ( endTime.getTime () - startTime.getTime () ) / ( xEnd - xStart );
final long dTimeQ = ( endTime.getTime () - startTime.getTime () ) / 4;
return new DateRange ( new Date ( startTime.getTime () + x * factor - dTimeQ ), new Date ( startTime.getTime () + x * factor + dTimeQ ) );
}
/**
* @param drag1
* @param drag2
* @param xStart
* @param xEnd
* @param startTime
* @param endTime
* @return
*/
private DateRange moveRange ( final int drag1, final int drag2, final int xStart, final int xEnd, final Date startTime, final Date endTime )
{
final long factor = ( endTime.getTime () - startTime.getTime () ) / ( xEnd - xStart );
final long timediff = Math.abs ( drag1 - drag2 ) * factor;
if ( drag2 > drag1 )
{
return new DateRange ( new Date ( startTime.getTime () - timediff ), new Date ( endTime.getTime () - timediff ) );
}
else
{
return new DateRange ( new Date ( startTime.getTime () + timediff ), new Date ( endTime.getTime () + timediff ) );
}
}
@Override
public void setFocus ()
{
this.chart.setFocus ();
}
@Override
public void updateParameters ( final QueryParameters parameters, final Set<String> valueTypes )
{
boolean updateRequired = false;
synchronized ( this.updateLock )
{
// update model
this.data.clear ();
this.dataTimestamp.set ( new Date[parameters.getEntries ()] );
this.dataQuality.set ( new double[parameters.getEntries ()] );
this.dataManual.set ( new double[parameters.getEntries ()] );
this.dataSourceValues.set ( new long[parameters.getEntries ()] );
for ( final String seriesId : valueTypes )
{
this.data.put ( seriesId, new double[parameters.getEntries ()] );
}
final ChartParameters newChartParameters = ChartParameters.create ().from ( this.chartParameters.get () ).startTime ( parameters.getStartTimestamp ().getTime () ).endTime ( parameters.getEndTimestamp ().getTime () ).numOfEntries ( parameters.getEntries () ).availableSeries ( valueTypes ).construct ();
if ( !newChartParameters.equals ( this.chartParameters.get () ) )
{
this.chartParameters.set ( newChartParameters );
updateRequired = true;
}
this.currentYMin = null;
this.currentYMax = null;
}
if ( updateRequired )
{
this.parameterUpdateJob.get ().schedule ( GUI_JOB_DELAY );
this.rangeUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
}
@Override
public void updateData ( final int index, final Map<String, Value[]> values, final ValueInformation[] valueInformation )
{
synchronized ( this.updateLock )
{
for ( final SeriesParameters series : this.chartParameters.get ().getAvailableSeries () )
{
// use local ref for faster access
final Value[] valueArray = values.get ( series.name );
final double[] chartValues = this.data.get ( series.name );
// now copy values from data source to our data array
for ( int i = 0; i < valueInformation.length; i++ )
{
final double d = valueArray[i].toDouble ();
chartValues[i + index] = d;
if ( !Double.isInfinite ( d ) && !Double.isNaN ( d ) && d != 0.0 )
{
if ( this.currentYMin == null )
{
this.currentYMin = d;
}
if ( this.currentYMax == null )
{
this.currentYMax = d;
}
final double diff = this.currentYMax - this.currentYMin;
if ( d > this.currentYMax )
{
this.currentYMax = d + diff * 0.2;
}
if ( d < this.currentYMin )
{
this.currentYMin = d - diff * 0.2;
}
}
}
}
// now copy values for date axis and quality
for ( int i = 0; i < valueInformation.length; i++ )
{
this.dataTimestamp.get ()[i + index] = valueInformation[i].getStartTimestamp ().getTime ();
this.dataQuality.get ()[i + index] = valueInformation[i].getQuality ();
this.dataManual.get ()[i + index] = valueInformation[i].getManualPercentage ();
this.dataSourceValues.get ()[i + index] = valueInformation[i].getSourceValues ();
}
}
this.dataUpdateJob.get ().schedule ( GUI_JOB_DELAY );
}
@Override
public void updateState ( final QueryState state )
{
}
/**
* must be run in GUI thread, does the actual modification of chart
* parameters
* @param parameters
*/
private void doUpdateChartParameters ()
{
if ( this.parent.isDisposed () )
{
return;
}
final Display display = this.parent.getDisplay ();
if ( display.isDisposed () )
{
return;
}
display.asyncExec ( new Runnable () {
@Override
public void run ()
{
if ( AbstractTrendView.this.parent.isDisposed () )
{
return;
}
if ( AbstractTrendView.this.panel.isDisposed () )
{
return;
}
if ( AbstractTrendView.this.chart.isDisposed () )
{
return;
}
// update GUI with new parameters
// remove old Series
AbstractTrendView.this.chart.setQualityColor ( AbstractTrendView.this.colorRegistry.get ( KEY_QUALITY ) );
AbstractTrendView.this.chart.setQualityThreshold ( AbstractTrendView.this.chartParameters.get ().getQuality () / 100.0 );
AbstractTrendView.this.chart.setManualColor ( AbstractTrendView.this.colorRegistry.get ( KEY_MANUAL ) );
AbstractTrendView.this.chart.setManualThreshold ( AbstractTrendView.this.chartParameters.get ().getManual () / 100.0 );
final List<String> seriesIds = new ArrayList<String> ();
for ( final ISeries series : AbstractTrendView.this.chart.getSeriesSet ().getSeries () )
{
seriesIds.add ( series.getId () );
}
for ( final String seriesId : seriesIds )
{
AbstractTrendView.this.chart.getSeriesSet ().deleteSeries ( seriesId );
}
for ( final Group group : AbstractTrendView.this.seriesGroups.values () )
{
group.dispose ();
}
// add new series
for ( final SeriesParameters seriesParameters : AbstractTrendView.this.chartParameters.get ().getAvailableSeries () )
{
final ILineSeries series = (ILineSeries)AbstractTrendView.this.chart.getSeriesSet ().createSeries ( SeriesType.LINE, seriesParameters.name );
final Group group = new Group ( AbstractTrendView.this.panel, SWT.SHADOW_ETCHED_IN );
final Button colorButton = new Button ( group, SWT.PUSH );
final Spinner widthSpinner = new Spinner ( group, SWT.BORDER );
series.setYAxisId ( 0 );
series.setXAxisId ( 0 );
series.setVisible ( seriesParameters.width > 0 );
series.enableStep ( true );
series.setAntialias ( SWT.ON );
series.setSymbolType ( PlotSymbolType.NONE );
series.setLineColor ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) );
series.setLineWidth ( seriesParameters.width );
AbstractTrendView.this.seriesGroups.put ( seriesParameters.name, group );
group.setText ( seriesParameters.name );
group.setLayout ( AbstractTrendView.this.groupLayout );
colorButton.setText ( Messages.TrendView_Color );
colorButton.setVisible ( true );
widthSpinner.setBackground ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) );
widthSpinner.setForeground ( contrastForeground ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) ) );
colorButton.setForeground ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) );
colorButton.addSelectionListener ( new SelectionAdapter () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
final ColorDialog cd = new ColorDialog ( AbstractTrendView.this.parent.getShell () );
cd.setText ( Messages.TrendView_SelectColor );
final RGB resultColor = cd.open ();
if ( resultColor != null )
{
AbstractTrendView.this.colorRegistry.put ( seriesParameters.name, resultColor );
widthSpinner.setBackground ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) );
widthSpinner.setForeground ( contrastForeground ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) ) );
series.setLineColor ( AbstractTrendView.this.colorRegistry.get ( seriesParameters.name ) );
AbstractTrendView.this.chart.redraw ();
}
}
} );
widthSpinner.setDigits ( 0 );
widthSpinner.setMinimum ( 0 );
widthSpinner.setMaximum ( 25 );
widthSpinner.setSelection ( seriesParameters.width );
widthSpinner.addSelectionListener ( new SelectionAdapter () {
@Override
public void widgetSelected ( final SelectionEvent e )
{
final ChartParameters newChartParameters = ChartParameters.create ().from ( AbstractTrendView.this.chartParameters.get () ).seriesWidth ( seriesParameters.name, widthSpinner.getSelection () ).construct ();
AbstractTrendView.this.chartParameters.set ( newChartParameters );
series.setLineWidth ( widthSpinner.getSelection () );
series.setVisible ( widthSpinner.getSelection () > 0 );
AbstractTrendView.this.chart.redraw ();
}
} );
}
if ( AbstractTrendView.this.query != null )
{
AbstractTrendView.this.chart.getTitle ().setText ( AbstractTrendView.this.query.getItemId () );
}
AbstractTrendView.this.chart.getAxisSet ().getYAxis ( 0 ).getTick ().setTickMarkStepHint ( 33 );
AbstractTrendView.this.chart.getAxisSet ().getXAxis ( 0 ).getTick ().setTickMarkStepHint ( 33 );
AbstractTrendView.this.parent.layout ( true, true );
AbstractTrendView.this.chart.redraw ();
}
} );
}
private void doUpdateRangeParameters ()
{
final AbstractQueryBuffer query = this.query;
if ( query != null )
{
final Calendar startTime = new GregorianCalendar ();
startTime.setTime ( this.chartParameters.get ().getStartTime () );
final Calendar endTime = new GregorianCalendar ();
endTime.setTime ( this.chartParameters.get ().getEndTime () );
synchronized ( this.updateLock )
{
query.changeProperties ( new QueryParameters ( startTime, endTime, this.chartParameters.get ().getNumOfEntries () ) );
}
}
}
private void doUpdateChartData ()
{
if ( this.chart.isDisposed () )
{
return;
}
final Display display = this.chart.getDisplay ();
if ( display.isDisposed () )
{
return;
}
display.asyncExec ( new Runnable () {
@Override
public void run ()
{
for ( final SeriesParameters seriesParameter : AbstractTrendView.this.chartParameters.get ().getAvailableSeries () )
{
final ISeries series = AbstractTrendView.this.chart.getSeriesSet ().getSeries ( seriesParameter.name );
// I'm not sure in which cases the series can even be null, but just try to continue as usual
if ( series == null )
{
continue;
}
series.setXDateSeries ( AbstractTrendView.this.dataTimestamp.get () );
series.setYSeries ( convertInvalidData ( AbstractTrendView.this.data.get ( seriesParameter.name ) ) );
}
AbstractTrendView.this.chart.getAxisSet ().getXAxis ( 0 ).getTick ().setFormat ( new SimpleDateFormat ( formatByRange () ) );
AbstractTrendView.this.chart.setQuality ( AbstractTrendView.this.dataQuality.get () );
AbstractTrendView.this.chart.setManual ( AbstractTrendView.this.dataManual.get () );
adjustRange ();
AbstractTrendView.this.chart.redraw ();
}
} );
}
private void doUpdateScaling ()
{
if ( this.chart.isDisposed () )
{
return;
}
final Display display = this.chart.getDisplay ();
if ( display.isDisposed () )
{
return;
}
display.asyncExec ( new Runnable () {
@Override
public void run ()
{
double v = AbstractTrendView.this.scaleMinSpinner.getSelection () / 1000.0;
if ( v >= AbstractTrendView.this.scaleYMax )
{
AbstractTrendView.this.scaleYMin = AbstractTrendView.this.scaleYMax - 0.001;
}
else
{
AbstractTrendView.this.scaleYMin = v;
}
AbstractTrendView.this.scaleMinSpinner.setSelection ( (int) ( AbstractTrendView.this.scaleYMin * 1000 ) );
v = AbstractTrendView.this.scaleMaxSpinner.getSelection () / 1000.0;
if ( v <= AbstractTrendView.this.scaleYMin )
{
AbstractTrendView.this.scaleYMax = AbstractTrendView.this.scaleYMin + 0.001;
}
else
{
AbstractTrendView.this.scaleYMax = v;
}
AbstractTrendView.this.scaleMaxSpinner.setSelection ( (int) ( AbstractTrendView.this.scaleYMax * 1000 ) );
adjustRange ();
AbstractTrendView.this.chart.redraw ();
}
} );
}
private void adjustRange ()
{
if ( this.scaleYAutomatically )
{
final Pair<Double, Double> scale = calcScale ();
this.scaleYMin = scale.first;
this.scaleYMax = scale.second;
updateSpinner ();
}
for ( final IAxis axis : this.chart.getAxisSet ().getXAxes () )
{
axis.adjustRange ();
}
for ( final IAxis axis : this.chart.getAxisSet ().getYAxes () )
{
axis.setRange ( new Range ( this.scaleYMin, this.scaleYMax ) );
}
}
private Pair<Double, Double> calcScale ()
{
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
final double feather = 0.05;
final double[] quality = this.dataQuality.get ();
final double minQuality = this.chartParameters.get ().getQuality () / 100.0;
for ( final Map.Entry<String, double[]> entry : this.data.entrySet () )
{
for ( int i = 0; i < entry.getValue ().length; i++ )
{
final double d = entry.getValue ()[i];
if ( Double.isInfinite ( d ) || Double.isNaN ( d ) )
{
continue;
}
if ( quality[i] >= minQuality )
{
min = Math.min ( min, d );
max = Math.max ( max, d );
}
}
}
final Pair<Double, Double> result = new Pair<Double, Double> ( Double.isInfinite ( min ) ? 0.0 : min, Double.isInfinite ( max ) ? 1.0 : max );
final double diff = result.second - result.first;
result.first -= diff * feather;
result.second += diff * feather;
return result;
}
/**
* tries to adjust labels for x axis according to range
* @return
*/
private String formatByRange ()
{
final long range = this.chartParameters.get ().getEndTime ().getTime () - this.chartParameters.get ().getStartTime ().getTime ();
if ( range < 1000 * 60 )
{
return "HH:mm:ss.SSS"; //$NON-NLS-1$
}
else if ( range < 1000 * 60 * 60 )
{
return "EEE HH:mm:ss"; //$NON-NLS-1$
}
else if ( range < 1000 * 60 * 60 * 12 )
{
return "dd. MMM HH:mm"; //$NON-NLS-1$
}
else
{
return "yyyy-MM-dd HH"; //$NON-NLS-1$
}
}
/**
* FIXME: this is just a temporary fix until the chart is able to handle infinity or NaN
* @param data
* @return
*/
private double[] convertInvalidData ( final double[] data )
{
if ( data == null )
{
return null;
}
final double[] result = new double[data.length];
for ( int i = 0; i < data.length; i++ )
{
result[i] = Double.isNaN ( data[i] ) || Double.isInfinite ( data[i] ) ? 0.0 : data[i];
}
return result;
}
/**
* returns white for dark background, black for light background
* @param c
* @return
*/
private Color contrastForeground ( final Color c )
{
final int grey = (int) ( c.getRed () * 0.299 + c.getGreen () * 0.587 + c.getBlue () * 0.114 );
if ( grey > 186 )
{
return this.colorRegistry.get ( KEY_BLACK );
}
else
{
return this.colorRegistry.get ( KEY_WHITE );
}
}
@Override
public void dispose ()
{
this.parameterUpdateJob.get ().cancel ();
this.rangeUpdateJob.get ().cancel ();
this.dataUpdateJob.get ().cancel ();
this.dragCursor.dispose ();
this.zoomInCursor.dispose ();
this.zoomOutCursor.dispose ();
super.dispose ();
}
}