]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java
Fixed all line endings of the repository
[simantics/platform.git] / bundles / org.simantics.charts / src / org / simantics / charts / editor / TimeSeriesEditor.java
index 4377eddc6076782c1997ea766618506e1f23c1e9..d83a37ad4f665241aed27d1b3618b1a549216714 100644 (file)
-/*******************************************************************************\r
- * Copyright (c) 2007, 2011 Association for Decentralized Information Management in\r
- * Industry THTH ry.\r
- * All rights reserved. This program and the accompanying materials\r
- * are made available under the terms of the Eclipse Public License v1.0\r
- * which accompanies this distribution, and is available at\r
- * http://www.eclipse.org/legal/epl-v10.html\r
- *\r
- * Contributors:\r
- *     VTT Technical Research Centre of Finland - initial API and implementation\r
- *******************************************************************************/\r
-package org.simantics.charts.editor;\r
-\r
-import java.awt.geom.Point2D;\r
-import java.util.Collections;\r
-import java.util.Set;\r
-import java.util.UUID;\r
-import java.util.logging.Level;\r
-import java.util.logging.Logger;\r
-\r
-import org.eclipse.core.commands.Command;\r
-import org.eclipse.core.commands.IStateListener;\r
-import org.eclipse.core.commands.State;\r
-import org.eclipse.core.runtime.IStatus;\r
-import org.eclipse.core.runtime.Status;\r
-import org.eclipse.core.runtime.preferences.IEclipsePreferences;\r
-import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;\r
-import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;\r
-import org.eclipse.core.runtime.preferences.InstanceScope;\r
-import org.eclipse.jface.action.IMenuListener;\r
-import org.eclipse.jface.action.IMenuManager;\r
-import org.eclipse.jface.action.MenuManager;\r
-import org.eclipse.jface.action.Separator;\r
-import org.eclipse.jface.resource.ImageDescriptor;\r
-import org.eclipse.jface.viewers.StructuredSelection;\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.graphics.Point;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Display;\r
-import org.eclipse.swt.widgets.Menu;\r
-import org.eclipse.swt.widgets.Text;\r
-import org.eclipse.ui.IEditorInput;\r
-import org.eclipse.ui.IEditorSite;\r
-import org.eclipse.ui.IWorkbenchPage;\r
-import org.eclipse.ui.IWorkbenchWindow;\r
-import org.eclipse.ui.PartInitException;\r
-import org.eclipse.ui.PlatformUI;\r
-import org.eclipse.ui.commands.ICommandService;\r
-import org.eclipse.ui.contexts.IContextService;\r
-import org.simantics.Simantics;\r
-import org.simantics.browsing.ui.model.browsecontexts.BrowseContext;\r
-import org.simantics.charts.Activator;\r
-import org.simantics.charts.ITrendSupport;\r
-import org.simantics.charts.ontology.ChartResource;\r
-import org.simantics.charts.preference.ChartPreferences;\r
-import org.simantics.charts.query.FindChartItemForTrendItem;\r
-import org.simantics.charts.query.MilestoneSpecQuery;\r
-import org.simantics.charts.query.SetProperty;\r
-import org.simantics.charts.query.TrendSpecQuery;\r
-import org.simantics.charts.ui.ChartLinkData;\r
-import org.simantics.charts.ui.LinkTimeHandler;\r
-import org.simantics.databoard.Bindings;\r
-import org.simantics.databoard.util.ObjectUtils;\r
-import org.simantics.db.AsyncReadGraph;\r
-import org.simantics.db.ReadGraph;\r
-import org.simantics.db.Resource;\r
-import org.simantics.db.Session;\r
-import org.simantics.db.common.request.ParametrizedRead;\r
-import org.simantics.db.common.request.UniqueRead;\r
-import org.simantics.db.exception.DatabaseException;\r
-import org.simantics.db.layer0.request.Model;\r
-import org.simantics.db.layer0.request.combinations.Combinators;\r
-import org.simantics.db.layer0.variable.RVI;\r
-import org.simantics.db.layer0.variable.RVIBuilder;\r
-import org.simantics.db.layer0.variable.Variable;\r
-import org.simantics.db.layer0.variable.Variables;\r
-import org.simantics.db.layer0.variable.Variables.Role;\r
-import org.simantics.db.procedure.AsyncListener;\r
-import org.simantics.db.procedure.SyncListener;\r
-import org.simantics.diagram.participant.ContextUtil;\r
-import org.simantics.diagram.participant.SGFocusParticipant;\r
-import org.simantics.g2d.canvas.ICanvasContext;\r
-import org.simantics.g2d.canvas.impl.CanvasContext;\r
-import org.simantics.g2d.chassis.AWTChassis;\r
-import org.simantics.g2d.chassis.ICanvasChassis;\r
-import org.simantics.g2d.chassis.IChassisListener;\r
-import org.simantics.g2d.chassis.SWTChassis;\r
-import org.simantics.g2d.participant.KeyToCommand;\r
-import org.simantics.g2d.participant.TimeParticipant;\r
-import org.simantics.g2d.utils.CanvasUtils;\r
-import org.simantics.history.Collector;\r
-import org.simantics.history.HistoryManager;\r
-import org.simantics.history.impl.FileHistory;\r
-import org.simantics.project.IProject;\r
-import org.simantics.scenegraph.INode;\r
-import org.simantics.scenegraph.g2d.events.Event;\r
-import org.simantics.scenegraph.g2d.events.EventTypes;\r
-import org.simantics.scenegraph.g2d.events.IEventHandler;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent;\r
-import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;\r
-import org.simantics.scenegraph.g2d.events.command.Commands;\r
-import org.simantics.selectionview.StandardPropertyPage;\r
-import org.simantics.simulation.data.Datasource;\r
-import org.simantics.simulation.experiment.ExperimentState;\r
-import org.simantics.simulation.experiment.IExperiment;\r
-import org.simantics.simulation.experiment.IExperimentListener;\r
-import org.simantics.simulation.ontology.SimulationResource;\r
-import org.simantics.trend.TrendInitializer;\r
-import org.simantics.trend.TrendInitializer.StepListener;\r
-import org.simantics.trend.configuration.ItemPlacement;\r
-import org.simantics.trend.configuration.LineQuality;\r
-import org.simantics.trend.configuration.TimeFormat;\r
-import org.simantics.trend.configuration.TrendSpec;\r
-import org.simantics.trend.impl.HorizRuler;\r
-import org.simantics.trend.impl.ItemNode;\r
-import org.simantics.trend.impl.MilestoneSpec;\r
-import org.simantics.trend.impl.TrendNode;\r
-import org.simantics.trend.impl.TrendParticipant;\r
-import org.simantics.ui.workbench.IPropertyPage;\r
-import org.simantics.ui.workbench.IResourceEditorInput;\r
-import org.simantics.ui.workbench.ResourceEditorInput;\r
-import org.simantics.ui.workbench.ResourceEditorPart;\r
-import org.simantics.ui.workbench.action.PerformDefaultAction;\r
-import org.simantics.ui.workbench.editor.input.InputValidationCombinators;\r
-import org.simantics.utils.datastructures.hints.HintListenerAdapter;\r
-import org.simantics.utils.datastructures.hints.IHintContext;\r
-import org.simantics.utils.datastructures.hints.IHintContext.Key;\r
-import org.simantics.utils.datastructures.hints.IHintObservable;\r
-import org.simantics.utils.format.ValueFormat;\r
-import org.simantics.utils.threads.AWTThread;\r
-import org.simantics.utils.threads.IThreadWorkQueue;\r
-import org.simantics.utils.threads.SWTThread;\r
-import org.simantics.utils.threads.ThreadUtils;\r
-import org.simantics.utils.ui.BundleUtils;\r
-import org.simantics.utils.ui.ErrorLogger;\r
-import org.simantics.utils.ui.ExceptionUtils;\r
-import org.simantics.utils.ui.SWTUtils;\r
-import org.simantics.utils.ui.dialogs.ShowMessage;\r
-import org.simantics.utils.ui.jface.ActiveSelectionProvider;\r
-\r
-/**\r
- * TimeSeriesEditor is an interactive part that draws a time series chart.\r
- * \r
- * The configuration model is {@link TrendSpec} which is read through\r
- * {@link TrendSpecQuery}. In Simantics Environment the\r
- * editor input is {@link ResourceEditorInput}.\r
- * \r
- * @author Toni Kalajainen <toni.kalajainen@vtt.fi>\r
- * @author Tuukka Lehtonen\r
- */\r
-public class TimeSeriesEditor extends ResourceEditorPart {     \r
-\r
-    ParametrizedRead<IResourceEditorInput, Boolean> INPUT_VALIDATOR =\r
-        Combinators.compose(\r
-                InputValidationCombinators.hasURI(),\r
-                InputValidationCombinators.extractInputResource()\r
-        );\r
-\r
-    @Override\r
-    protected ParametrizedRead<IResourceEditorInput, Boolean> getInputValidator() {\r
-        return INPUT_VALIDATOR;\r
-    }\r
-\r
-    /**\r
-     * The root property browse context of the time series editor. A transitive\r
-     * closure is calculated for this context.\r
-     */\r
-    private static String       ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext;\r
-\r
-    /**\r
-     * ID of the this editor part extension.\r
-     */\r
-    public static final String  ID                  = "org.simantics.charts.editor.timeseries";\r
-\r
-    private static final String CONTEXT_MENU_ID     = "#timeSeriesChart";\r
-\r
-    private IEclipsePreferences chartPreferenceNode;\r
-\r
-    private final ImageDescriptor IMG_ZOOM_TO_FIT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horizAndVert16.png");\r
-    private final ImageDescriptor IMG_ZOOM_TO_FIT_HORIZ = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horiz16.png");\r
-    private final ImageDescriptor IMG_ZOOM_TO_FIT_VERT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/vert16.png");\r
-    private final ImageDescriptor IMG_AUTOSCALE = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/autoscale16.png");\r
-\r
-    IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() {\r
-        @Override\r
-        public void preferenceChange(PreferenceChangeEvent event) {\r
-            if (disposed) {\r
-                System.err.println("Warning: pref change to disposed TimeSeriesEditor");\r
-                return;\r
-            }\r
-\r
-            if ( event.getKey().equals(ChartPreferences.P_REDRAW_INTERVAL ) || \r
-                    event.getKey().equals(ChartPreferences.P_AUTOSCALE_INTERVAL )) {\r
-                long redraw_interval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);\r
-                long autoscale_interval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);\r
-                setInterval( redraw_interval, autoscale_interval );\r
-            }\r
-            if ( event.getKey().equals(ChartPreferences.P_DRAW_SAMPLES )) {\r
-                boolean draw_samples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);\r
-                setDrawSamples( draw_samples );\r
-            }\r
-            if ( event.getKey().equals(ChartPreferences.P_TIMEFORMAT ) ) {\r
-                String s = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);\r
-                TimeFormat tf = TimeFormat.valueOf( s );\r
-                if (tf!=null) setTimeFormat( tf );\r
-            }\r
-            if ( event.getKey().equals(ChartPreferences.P_VALUEFORMAT ) ) {\r
-                String s = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);\r
-                ValueFormat vf = ValueFormat.valueOf( s );\r
-                if (vf!=null) setValueFormat( vf );\r
-            }\r
-            if ( event.getKey().equals(ChartPreferences.P_ITEMPLACEMENT)) {\r
-                String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);\r
-                ItemPlacement ip = ItemPlacement.valueOf(s);\r
-                if (trendNode!=null) trendNode.itemPlacement = ip;\r
-            }\r
-            if ( event.getKey().equals(ChartPreferences.P_TEXTQUALITY) || event.getKey().equals(ChartPreferences.P_LINEQUALITY) ) {\r
-                String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);\r
-                String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);\r
-                LineQuality q1 = LineQuality.valueOf(s1);\r
-                LineQuality q2 = LineQuality.valueOf(s2);\r
-                if (trendNode!=null) trendNode.quality.textQuality = q1;\r
-                if (trendNode!=null) trendNode.quality.lineQuality = q2;\r
-            }\r
-            \r
-        }\r
-    };\r
-\r
-    /**\r
-     * The project which this editor is listening to for changes to\r
-     * {@link ChartKeys.ChartSourceKey keys}.\r
-     */\r
-    IProject                    project;\r
-\r
-    /**\r
-     * The model resource containing the input chart resource.\r
-     */\r
-    Resource                    model;\r
-\r
-    /**\r
-     * The text widget shown only if there is no IProject available at the time\r
-     * of editor part creation.\r
-     */\r
-    Text                        errorText;\r
-\r
-    /**\r
-     * A unique key for making DB requests chart editor specific without binding\r
-     * the requests to the editor object itself.\r
-     */\r
-    UUID                        uniqueChartEditorId = UUID.randomUUID();\r
-    Logger                      log;\r
-    Display                     display;\r
-    SWTChassis                  canvas;\r
-    CanvasContext               cvsCtx;\r
-    TrendParticipant            tp;\r
-    TrendNode                   trendNode;\r
-    StepListener                stepListener;\r
-    MilestoneSpecListener       milestoneListener;\r
-    MilestoneSpecQuery          milestoneQuery;\r
-\r
-    /**\r
-     * The ChartData instance used by this editor for sourcing data at any given\r
-     * moment. Project hint instances are copied into this instance.\r
-     */\r
-    final ChartData             chartData = new ChartData(null, null, null, null, null, null);\r
-\r
-    /**\r
-     * The ChartSourceKey to match the model this editor was opened for.\r
-     * @see #model\r
-     * @see #init(IEditorSite, IEditorInput)\r
-     */\r
-    ChartKeys.ChartSourceKey    chartDataKey;\r
-    \r
-    \r
-    /**\r
-     * Context management utils\r
-     */\r
-    protected IThreadWorkQueue           swt;\r
-    protected ContextUtil                contextUtil;\r
-\r
-    class ExperimentStateListener implements IExperimentListener {\r
-        @Override\r
-        public void stateChanged(ExperimentState state) {\r
-            TrendSpec spec = trendNode.getTrendSpec();\r
-            spec.experimentIsRunning = state == ExperimentState.RUNNING;\r
-            if (spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {\r
-                TrendParticipant t = tp;\r
-                if (t != null)\r
-                    t.setDirty();\r
-            }\r
-        }\r
-    }\r
-\r
-    ExperimentStateListener experimentStateListener = new ExperimentStateListener();\r
-\r
-    class ChartDataListener extends HintListenerAdapter implements Runnable {\r
-        @Override\r
-        public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {\r
-            if (key.equals(chartDataKey)) {\r
-                // @Thread any\r
-                if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) {\r
-                    cvsCtx.getThreadAccess().asyncExec(this);\r
-                }\r
-            }\r
-        }\r
-        @Override\r
-        public void run() {\r
-            // @Thread AWT\r
-            if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return;\r
-            ChartData data = Simantics.getProject().getHint(chartDataKey);\r
-            setInput( data, trendNode.getTrendSpec() );\r
-        }\r
-    }\r
-\r
-    ChartDataListener chartDataListener = new ChartDataListener();\r
-\r
-    class ValueTipBoxPositionListener extends HintListenerAdapter {\r
-        @Override\r
-        public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {\r
-            if (key.equals(TrendParticipant.KEY_VALUE_TIP_BOX_RELATIVE_POS) && newValue != null) {\r
-                Session s = Simantics.getSession();\r
-                ChartResource CHART = s.getService(ChartResource.class);\r
-                Point2D p = (Point2D) newValue;\r
-                double[] value = { p.getX(), p.getY() };\r
-                s.asyncRequest(new SetProperty(getInputResource(), CHART.Chart_valueViewPosition, value, Bindings.DOUBLE_ARRAY));\r
-            }\r
-        }\r
-    }\r
-\r
-    ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener();\r
-\r
-    // Link-Time\r
-    State linkTimeState;\r
-    IStateListener linkTimeStateListener = new IStateListener() {\r
-               @Override\r
-               public void handleStateChange(State state, Object oldValue) {\r
-                       final ChartLinkData newData = (ChartLinkData) linkTimeState.getValue();\r
-                       trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this;\r
-                       \r
-                       if ( newData == null || newData.sender==TimeSeriesEditor.this ) return;\r
-                       TrendNode tn = trendNode;\r
-                       HorizRuler hr = tn!=null ? tn.horizRuler : null;\r
-\r
-                       ChartLinkData oldData = new ChartLinkData();\r
-                       getFromEnd(oldData);\r
-\r
-                       if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) {\r
-                               tn.valueTipTime = newData.valueTipTime;\r
-                               tp.setDirty();\r
-                       }\r
-                       \r
-                       if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) {\r
-                               \r
-                       cvsCtx.getThreadAccess().asyncExec( new Runnable() {\r
-                                       @Override\r
-                                       public void run() {\r
-                                               boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx);\r
-                                               trendNode.horizRuler.autoscroll = false;\r
-                                               if (b) {\r
-                                                       trendNode.layout();\r
-                                                       tp.setDirty();\r
-                                               }\r
-                                       }});\r
-                       }\r
-                       \r
-               }\r
-    };\r
-    HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() {\r
-               @Override\r
-               public void onNewWindow(double from, double end, double scalex) {\r
-                       final ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();\r
-                       if (oldData != null) {\r
-                               ChartLinkData data = new ChartLinkData(TimeSeriesEditor.this, from, end, scalex);\r
-                               data.valueTipTime = trendNode.valueTipTime;\r
-                               linkTimeState.setValue( data );\r
-                       }\r
-               }\r
-       };\r
-\r
-    class ChassisListener implements IChassisListener {\r
-        @Override\r
-        public void chassisClosed(ICanvasChassis sender) {\r
-            // Prevent deadlock while disposing which using syncExec would result in. \r
-            final ICanvasContext ctx = cvsCtx;\r
-            ThreadUtils.asyncExec(ctx.getThreadAccess(), new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                    if (ctx != null) {\r
-                        AWTChassis awt = canvas.getAWTComponent();\r
-                        if (awt != null)\r
-                            awt.setCanvasContext(null);\r
-                        ctx.dispose();\r
-                    }\r
-                }\r
-            });\r
-            canvas.removeChassisListener(ChassisListener.this);\r
-        }\r
-    }\r
-\r
-    ActiveSelectionProvider     selectionProvider   = new ActiveSelectionProvider();\r
-    MenuManager                 menuManager;\r
-\r
-    public TimeSeriesEditor() {\r
-        log = Logger.getLogger( this.getClass().getName() );\r
-    }\r
-\r
-    boolean isTimeLinked() {\r
-        if (linkTimeState==null) return false;\r
-        Boolean isLinked = (Boolean) linkTimeState.getValue();\r
-        return isLinked != null && isLinked;\r
-    }\r
-\r
-    @Override\r
-    public void init(IEditorSite site, IEditorInput input) throws PartInitException {\r
-        super.init(site, input);\r
-        try {\r
-            this.model = Simantics.getSession().syncRequest( new Model( getInputResource() ) );\r
-            this.chartDataKey = ChartKeys.chartSourceKey(model);\r
-        } catch (DatabaseException e) {\r
-            throw new PartInitException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Input " + getInputResource() + " is not part of a model.", e));\r
-        }\r
-    }\r
-    \r
-    /**\r
-     * Invoke this only from the AWT thread.\r
-     * @param context\r
-     */\r
-    protected void setCanvasContext(final SWTChassis chassis, final ICanvasContext context) {\r
-        // Cannot directly invoke SWTChassis.setCanvasContext only because it\r
-        // needs to be invoked in the SWT thread and AWTChassis.setCanvasContext in the\r
-        // AWT thread, but directly invoking SWTChassis.setCanvasContext would call both\r
-        // in the SWT thread which would cause synchronous scheduling of AWT\r
-        // runnables which is always a potential source of deadlocks.\r
-        chassis.getAWTComponent().setCanvasContext(context);\r
-        SWTUtils.asyncExec(chassis, new Runnable() {\r
-            @Override\r
-            public void run() {\r
-                if (!chassis.isDisposed())\r
-                    // For AWT, this is a no-operation.\r
-                    chassis.setCanvasContext(context);\r
-            }\r
-        });\r
-    }\r
-\r
-    @Override\r
-    public void createPartControl(Composite parent) {\r
-        display = parent.getDisplay();\r
-        swt = SWTThread.getThreadAccess(display);\r
-\r
-        // Must have a project to attach to, otherwise the editor is useless.\r
-        project = Simantics.peekProject();\r
-        if (project == null) {\r
-            errorText = new Text(parent, SWT.NONE);\r
-            errorText.setText("No project is open.");\r
-            errorText.setEditable(false);\r
-            return;\r
-        }\r
-\r
-        // Create the canvas context here before finishing createPartControl\r
-        // to give anybody requiring access to this editor's ICanvasContext\r
-        // a chance to do their work.\r
-        // The context can be created in SWT thread without scheduling\r
-        // to the context thread and having potential deadlocks.\r
-        // The context is locked here and unlocked after it has been\r
-        // initialized in the AWT thread.\r
-        IThreadWorkQueue thread = AWTThread.getThreadAccess();\r
-        cvsCtx = new CanvasContext(thread);\r
-        cvsCtx.setLocked(true);\r
-\r
-        final IWorkbenchWindow win = getEditorSite().getWorkbenchWindow();\r
-        final IWorkbenchPage page = getEditorSite().getPage();\r
-\r
-        canvas = new SWTChassis(parent, SWT.NONE);\r
-        canvas.populate(parameter -> {\r
-            if (!disposed) {\r
-                canvas.addChassisListener(new ChassisListener());\r
-                initializeCanvas(canvas, cvsCtx, win, page);\r
-            }\r
-        });\r
-\r
-        // Link time\r
-        ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);\r
-        Command command = service.getCommand( LinkTimeHandler.COMMAND_ID );\r
-        linkTimeState = command.getState( LinkTimeHandler.STATE_ID );\r
-        if ( linkTimeState != null ) linkTimeState.addListener( linkTimeStateListener );\r
-\r
-        addPopupMenu();\r
-\r
-        // Start tracking editor input validity. \r
-        activateValidation();\r
-\r
-        // Provide input as selection for property page.\r
-        selectionProvider.setSelection( new StructuredSelection(getInputResource()) );\r
-        getSite().setSelectionProvider( selectionProvider );\r
-    }\r
-\r
-    protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx, IWorkbenchWindow window, IWorkbenchPage page) {\r
-        // Initialize canvas context\r
-        TrendSpec nodata = new TrendSpec();\r
-        nodata.init();\r
-        cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata);\r
-\r
-        tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class);\r
-\r
-        \r
-        IContextService contextService = (IContextService) getSite().getService(IContextService.class);\r
-        contextUtil = new ContextUtil(contextService, swt);\r
-\r
-        \r
-        cvsCtx.add( new SubscriptionDropParticipant( getInputResource() ) );\r
-        cvsCtx.add( new KeyToCommand( ChartKeyBindings.DEFAULT_BINDINGS ) );\r
-        cvsCtx.add( new ChartPasteHandler2(getInputResource().get()) );\r
-        cvsCtx.add(contextUtil);\r
-        \r
-        // Context management\r
-        cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context"));\r
-\r
-        stepListener = new StepListener( tp );\r
-        trendNode = tp.getTrend();\r
-        trendNode.titleNode.remove();\r
-        trendNode.titleNode = null;\r
-\r
-        // Link time\r
-        trendNode.horizRuler.listener = horizRulerListener;\r
-        \r
-        final ChartLinkData linkTime = (ChartLinkData) linkTimeState.getValue();\r
-        if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx);\r
-        \r
-        // Handle mouse moved event after TrendParticipant.\r
-        // This handler forwards trend.mouseHoverTime to linkTimeState\r
-        cvsCtx.getEventHandlerStack().add( new IEventHandler() {\r
-\r
-                       @Override\r
-                       public int getEventMask() {\r
-                               return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed;\r
-                       }\r
-\r
-                       @Override\r
-                       public boolean handleEvent(Event e) {\r
-\r
-//                             System.out.println("LinkEventHandler: "+e);\r
-                               ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();\r
-                               if (oldData!=null) {\r
-                                       ChartLinkData newData = new ChartLinkData();\r
-                                       getFromEnd(newData);\r
-                                       if (!newData.equals(oldData)) {\r
-//                                             System.out.println("Sending new link-data");\r
-                                               linkTimeState.setValue( newData );\r
-                                       }\r
-                               }\r
-                               return false;\r
-                       }}, -1);\r
-        \r
-        canvas.getHintContext().setHint( SWTChassis.KEY_EDITORPART, this);\r
-        canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHPAGE, page);\r
-        canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHWINDOW, window);\r
-\r
-        // Canvas context is initialized, unlock it now to allow rendering.\r
-        cvsCtx.setLocked(false);\r
-\r
-        setCanvasContext(chassis, cvsCtx);\r
-\r
-        cvsCtx.getEventHandlerStack().add(new IEventHandler() {\r
-            @Override\r
-            public boolean handleEvent(Event e) {\r
-                MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e;\r
-                if (event.button != MouseEvent.RIGHT_BUTTON)\r
-                    return false;\r
-\r
-                final Point p = new Point((int) event.screenPosition.getX(), (int) event.screenPosition.getY());\r
-                SWTUtils.asyncExec(chassis, new Runnable() {\r
-                    @Override\r
-                    public void run() {\r
-                        if (!canvas.isDisposed())\r
-                            showPopup(p);\r
-                    }\r
-                });\r
-                return true;\r
-            }\r
-            @Override\r
-            public int getEventMask() {\r
-                return EventTypes.MouseButtonReleasedMask;\r
-            }\r
-        }, 1000000);\r
-\r
-        // Track data source and preinitialize chartData\r
-        project.addHintListener(chartDataListener);\r
-        chartData.readFrom( (ChartData) project.getHint( chartDataKey ) );\r
-\r
-        if (chartData.run != null) {\r
-            milestoneListener = new MilestoneSpecListener();\r
-            milestoneQuery = new MilestoneSpecQuery( chartData.run );\r
-            getSession().asyncRequest( milestoneQuery, milestoneListener );\r
-        }\r
-\r
-        // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration\r
-        trackChartConfiguration();\r
-        trackPreferences();\r
-\r
-        // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]\r
-        // back to the graph database.\r
-        cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);\r
-       }\r
-\r
-       private void addPopupMenu() {\r
-        menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID);\r
-        menuManager.setRemoveAllWhenShown(true);\r
-        Menu menu = menuManager.createContextMenu(canvas);\r
-        canvas.setMenu(menu);\r
-        getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider);\r
-\r
-        // Add support for some built-in actions in the context menu.\r
-        menuManager.addMenuListener(new IMenuListener() {\r
-            @Override\r
-            public void menuAboutToShow(IMenuManager manager) {\r
-                // Not initialized yet, prevent NPE.\r
-                TrendNode trendNode = TimeSeriesEditor.this.trendNode;\r
-                TrendParticipant tp = TimeSeriesEditor.this.tp;\r
-                if (trendNode == null || tp == null)\r
-                    return;\r
-\r
-                TrendSpec trendSpec = trendNode.getTrendSpec();\r
-                ItemNode hoverItem = tp.hoveringItem;\r
-                if (hoverItem != null && hoverItem.item != null) {\r
-                    Resource component = resolveReferencedComponent(getResourceInput(), hoverItem.item.variableId);\r
-                    if (component != null) {\r
-                        manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component));\r
-                    }\r
-\r
-                    Resource chart = TimeSeriesEditor.this.getInputResource();\r
-                    if ( chart != null ) {\r
-                        try {\r
-                            Resource chartItem = getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) );\r
-                            if (chartItem != null) {\r
-                                manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem)));\r
-                                manager.add(new Separator());\r
-                                manager.add(new PropertiesAction("Item Properties", canvas, chartItem));\r
-                                manager.add(new PropertiesAction("Chart Properties", canvas, chart));\r
-                            }\r
-                        } catch (DatabaseException e) {\r
-                            Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));\r
-                        }\r
-                    }\r
-                } else {\r
-                    boolean hairlineMovementAllowed =\r
-                            !(trendSpec.experimentIsRunning &&\r
-                              trendSpec.viewProfile.trackExperimentTime);\r
-\r
-                    Resource chart = TimeSeriesEditor.this.getInputResource();\r
-                    manager.add(new Separator());\r
-                    manager.add(new MoveHairlineAction(\r
-                            "Move Hairline Here",\r
-                            chart,\r
-                            hairlineMovementAllowed,\r
-                            trendNode,\r
-                            trendNode.mouseHoverTime\r
-                            ));\r
-                    manager.add(new MoveHairlineAction(\r
-                            "Move Hairline To Current Time",\r
-                            chart,\r
-                            hairlineMovementAllowed,\r
-                            trendNode,\r
-                            trendNode.horizRuler.getItemEndTime(),\r
-                            Boolean.FALSE\r
-                            ));\r
-                    manager.add(new TrackExperimentTimeAction(\r
-                            "Hairline Tracks Current Time",\r
-                            chart,\r
-                            trendSpec.viewProfile.trackExperimentTime));\r
-                    manager.add(new Separator());\r
-                    manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT));\r
-                    manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ));\r
-                    manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT));\r
-                    manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE));\r
-                    manager.add(new Separator());\r
-                    manager.add(new PropertiesAction("Chart Properties", canvas, chart));\r
-                }\r
-            }\r
-        });\r
-    }\r
-\r
-    protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) {\r
-        try {\r
-            return getSession().sync(new UniqueRead<Resource>() {\r
-                @Override\r
-                public Resource perform(ReadGraph graph) throws DatabaseException {\r
-                    Variable configuration = Variables.getConfigurationContext(graph, getInputResource());\r
-                    RVI rvi = RVI.fromResourceFormat(graph, variableId);\r
-                    rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI();\r
-                    if (rvi.isEmpty())\r
-                        return null;\r
-                    Variable var = rvi.resolve(graph, configuration);\r
-                    return var.getPossibleRepresents(graph);\r
-                }\r
-            });\r
-        } catch (DatabaseException e) {\r
-            ErrorLogger.defaultLogError(e);\r
-        }\r
-        return null;\r
-    }\r
-\r
-    private void showPopup(Point p) {\r
-        menuManager.getMenu().setLocation(p);\r
-        menuManager.getMenu().setVisible(true);\r
-    }\r
-\r
-    private void trackChartConfiguration() {\r
-        getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());\r
-        getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());\r
-    }\r
-\r
-    @Override\r
-    public void setFocus() {\r
-        if (errorText != null)\r
-            errorText.setFocus();\r
-        else\r
-            canvas.setFocus();\r
-    }\r
-\r
-    @Override\r
-    public void dispose() {\r
-        if (disposed == true) return;\r
-        disposed = true;\r
-\r
-        if (trendNode!=null && trendNode.horizRuler!=null) {\r
-            trendNode.horizRuler.listener = null;\r
-        }\r
-\r
-        if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );\r
-\r
-        canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );\r
-        canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );\r
-        canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );\r
-\r
-        if ( chartPreferenceNode!= null ) {\r
-            chartPreferenceNode.removePreferenceChangeListener( preferenceListener );\r
-        }\r
-\r
-        MilestoneSpecListener ml = milestoneListener;\r
-        if (ml!=null) ml.dispose();\r
-\r
-        if (project != null) {\r
-            project.removeHintListener(chartDataListener);\r
-        }\r
-\r
-        if (chartData != null) {\r
-            if (chartData.datasource!=null)\r
-                chartData.datasource.removeListener( stepListener );\r
-            if (chartData.experiment!=null)\r
-                chartData.experiment.removeListener( experimentStateListener );\r
-            chartData.readFrom( null );\r
-        }\r
-\r
-        super.dispose();\r
-    }\r
-\r
-    /**\r
-     * @param data new data or null\r
-     * @param newSpec new spec or null\r
-     * @thread AWT\r
-     */\r
-    @SuppressWarnings("unused")\r
-    public void setInput(ChartData data, TrendSpec newSpec) {\r
-       boolean doLayout = false;\r
-\r
-       // Disregard input if it is not for this chart's containing model.\r
-       if (data != null && data.model != null && !data.model.equals(model))\r
-           data = null;\r
-\r
-       // Accommodate Datasource changes\r
-       Datasource: {\r
-               Datasource oldDatasource = chartData==null?null:chartData.datasource;\r
-               Datasource newDatasource = data==null?null:data.datasource;\r
-               //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) ) \r
-               {\r
-                       if (oldDatasource!=null) oldDatasource.removeListener( stepListener );\r
-                       if (newDatasource!=null) newDatasource.addListener( stepListener );\r
-               }\r
-       }\r
-\r
-        Experiment: {\r
-            IExperiment oldExperiment = chartData==null?null:chartData.experiment;\r
-            IExperiment newExperiment = data==null?null:data.experiment;\r
-            //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) ) \r
-            {\r
-                if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );\r
-                if (newExperiment!=null) newExperiment.addListener( experimentStateListener );\r
-            }\r
-        }\r
-\r
-       // Accommodate Historian changes\r
-       Historian: {\r
-               HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian;\r
-               HistoryManager newHistorian = data==null?null:data.history;\r
-               Collector newCollector = data==null?null:data.collector;\r
-       //      if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) ) \r
-               {\r
-                       if (newHistorian instanceof FileHistory) {\r
-                               FileHistory fh = (FileHistory) newHistorian;\r
-                               System.out.println("History = "+fh.getWorkarea());\r
-                       }\r
-                       trendNode.setHistorian( newHistorian, newCollector );\r
-                       doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);\r
-               }\r
-\r
-               // Accommodate TrendSpec changes\r
-               TrendSpec oldSpec = trendNode.getTrendSpec();\r
-               if ( !newSpec.equals(oldSpec) ) {\r
-                   trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );\r
-                   doLayout = true;\r
-               }\r
-               \r
-       }\r
-\r
-        Resource newExperimentResource = data==null ? null : data.run;\r
-        Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;\r
-       \r
-        // Track milestones\r
-        Milestones: {\r
-               if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {\r
-\r
-                       // Dispose old listener & Query\r
-                       if (milestoneListener!=null) {\r
-                               milestoneListener.dispose();\r
-                               milestoneListener = null;\r
-                       }\r
-                       if (milestoneQuery!=null) {\r
-                               milestoneQuery = null;\r
-                       }\r
-\r
-                       trendNode.setMilestones( MilestoneSpec.EMPTY );\r
-                       \r
-                       if (newExperimentResource != null) {\r
-                               milestoneListener = new MilestoneSpecListener();\r
-                               milestoneQuery = new MilestoneSpecQuery( newExperimentResource );\r
-                               Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );\r
-                       }\r
-               }\r
-               \r
-        }\r
-\r
-        if (doLayout) trendNode.layout();\r
-        this.chartData.readFrom( data );\r
-        tp.setDirty();\r
-        \r
-        if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {\r
-               resetViewAfterDataChange();\r
-        }\r
-        \r
-    }\r
-\r
-    class ActiveRunListener implements SyncListener<Resource> {\r
-        @Override\r
-        public void exception(ReadGraph graph, Throwable throwable) {\r
-            ErrorLogger.defaultLogError(throwable);\r
-            ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());\r
-        }\r
-        @Override\r
-        public void execute(ReadGraph graph, final Resource run) throws DatabaseException {\r
-               if(run != null) {\r
-                       SimulationResource SIMU = SimulationResource.getInstance(graph);\r
-                       Variable var = Variables.getVariable(graph, run);\r
-                       IExperiment exp = var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment);\r
-                       ITrendSupport ts = exp.getService(ITrendSupport.class);\r
-                       if (ts != null)\r
-                           ts.setChartData(graph);\r
-               }\r
-        }\r
-        @Override\r
-        public boolean isDisposed() {\r
-            return TimeSeriesEditor.this.disposed;\r
-        }\r
-    }\r
-    \r
-    class TrendSpecListener implements AsyncListener<TrendSpec> {\r
-        @Override\r
-        public void exception(AsyncReadGraph graph, Throwable throwable) {\r
-               \r
-            ErrorLogger.defaultLogError(throwable);\r
-            ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());\r
-        }\r
-        @Override\r
-        public void execute(AsyncReadGraph graph, final TrendSpec result) {\r
-            if (result == null) {\r
-                log.log(Level.INFO, "Chart configuration removed");\r
-            } else {\r
-                log.log(Level.INFO, "Chart configuration updated: " + result);\r
-            }\r
-\r
-            // Reload chart in AWT Thread\r
-            AWTThread.getThreadAccess().asyncExec(new Runnable() {\r
-                @Override\r
-                public void run() {\r
-                    if (!disposed)\r
-                        setInput( chartData, result );\r
-                }\r
-            });\r
-        }\r
-        @Override\r
-        public boolean isDisposed() {\r
-            return TimeSeriesEditor.this.disposed;\r
-        }\r
-    }\r
-    \r
-    class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {\r
-       boolean disposed = false;\r
-               @Override\r
-               public void execute(AsyncReadGraph graph, final MilestoneSpec result) {\r
-                       AWTThread.INSTANCE.asyncExec(new Runnable() {\r
-                               public void run() {\r
-                                       trendNode.setMilestones(result);\r
-                               }});\r
-               }\r
-\r
-               @Override\r
-               public void exception(AsyncReadGraph graph, Throwable throwable) {\r
-                       \r
-               }\r
-\r
-               @Override\r
-               public boolean isDisposed() {\r
-                       return disposed;\r
-               }\r
-               \r
-               public void dispose() {\r
-                       disposed = true;\r
-               }\r
-       \r
-    }\r
-\r
-    private void trackPreferences() {\r
-        chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" );\r
-        chartPreferenceNode.addPreferenceChangeListener( preferenceListener );\r
-        long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);\r
-        long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);\r
-        setInterval(redrawInterval, autoscaleInterval);\r
-        \r
-        String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); \r
-        TimeFormat tf = TimeFormat.valueOf( timeFormat );\r
-        if (tf!=null) setTimeFormat( tf );\r
-        \r
-        Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);\r
-        setDrawSamples(drawSamples);\r
-\r
-        String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);\r
-        ValueFormat vf = ValueFormat.valueOf( valueFormat );\r
-        if (vf!=null) setValueFormat( vf );\r
-        \r
-       String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);\r
-       ItemPlacement ip = ItemPlacement.valueOf(s);\r
-       if (trendNode!=null) trendNode.itemPlacement = ip;\r
-       \r
-        String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);\r
-        String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);\r
-        LineQuality q1 = LineQuality.valueOf(s1);\r
-        LineQuality q2 = LineQuality.valueOf(s2);\r
-        if (trendNode!=null) trendNode.quality.textQuality = q1;\r
-        if (trendNode!=null) trendNode.quality.lineQuality = q2;\r
-       \r
-    }\r
-\r
-    private void setInterval(long redrawInterval, long autoscaleInterval) {\r
-        redrawInterval = Math.max(1, redrawInterval);\r
-        long pulse = Math.min(50, redrawInterval);\r
-        pulse = Math.min(pulse, autoscaleInterval);\r
-        IHintContext h = canvas.getCanvasContext().getDefaultHintContext();\r
-        h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse);\r
-        h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval);\r
-        h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval);        \r
-    }\r
-\r
-    private void setDrawSamples(boolean value) {\r
-       trendNode.drawSamples = value;\r
-       trendNode.layout();\r
-       tp.setDirty();\r
-    }\r
-\r
-    private void setTimeFormat( TimeFormat tf ) {\r
-       if (trendNode.timeFormat == tf) return;\r
-       trendNode.timeFormat = tf;\r
-       trendNode.layout();\r
-        tp.setDirty();\r
-    }\r
-\r
-    private void setValueFormat( ValueFormat vf ) {\r
-       if (trendNode.valueFormat == vf) return;\r
-       trendNode.valueFormat = vf;\r
-       trendNode.layout();\r
-        tp.setDirty();\r
-    }\r
-\r
-    @SuppressWarnings("rawtypes")\r
-    @Override\r
-    public Object getAdapter(Class adapter) {\r
-        if (adapter == INode.class) {\r
-            ICanvasContext ctx = cvsCtx;\r
-            if (ctx != null)\r
-                return ctx.getSceneGraph();\r
-        }\r
-        if (adapter == IPropertyPage.class)\r
-            return new StandardPropertyPage(getSite(), getPropertyPageContexts());\r
-        if (adapter == ICanvasContext.class)\r
-            return cvsCtx;\r
-        return super.getAdapter(adapter);\r
-    }\r
-\r
-    protected Set<String> getPropertyPageContexts() {\r
-        try {\r
-            return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT));\r
-        } catch (DatabaseException e) {\r
-            ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e);\r
-            return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT);\r
-        }\r
-    }\r
-\r
-    /**\r
-     * Add from, end, (scale x) to argument array\r
-     * @param fromEnd array of 2 or 3\r
-     */\r
-    public void getFromEnd(ChartLinkData data) {\r
-       data.sender = this;\r
-       TrendNode tn = trendNode;\r
-       data.valueTipTime = tn.valueTipTime;\r
-       HorizRuler hr = tn!=null ? tn.horizRuler : null;\r
-       if ( hr != null ) {\r
-               data.from = hr.from;\r
-               data.end = hr.end;\r
-                       double len = hr.end-hr.from; \r
-                       double wid = tn.plot.getWidth();\r
-                       if ( wid==0.0 ) wid = 0.1;\r
-                       data.sx = len/wid;\r
-       }\r
-    }\r
-\r
-    @SuppressWarnings("unused")\r
-    private static boolean doubleEquals(double a, double b) {\r
-       if (Double.isNaN(a) && Double.isNaN(b)) return true;\r
-       return a==b;\r
-    }\r
-\r
-    protected void resetViewAfterDataChange() {\r
-       \r
-       CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);\r
-       CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);\r
-       \r
-    }\r
-\r
-}\r
+/*******************************************************************************
+ * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
+ * Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     VTT Technical Research Centre of Finland - initial API and implementation
+ *******************************************************************************/
+package org.simantics.charts.editor;
+
+import java.awt.geom.Point2D;
+import java.util.Collections;
+import java.util.Set;
+import java.util.UUID;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.eclipse.core.commands.Command;
+import org.eclipse.core.commands.IStateListener;
+import org.eclipse.core.commands.State;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.commands.ICommandService;
+import org.eclipse.ui.contexts.IContextService;
+import org.simantics.Simantics;
+import org.simantics.browsing.ui.model.browsecontexts.BrowseContext;
+import org.simantics.charts.Activator;
+import org.simantics.charts.ITrendSupport;
+import org.simantics.charts.ontology.ChartResource;
+import org.simantics.charts.preference.ChartPreferences;
+import org.simantics.charts.query.FindChartItemForTrendItem;
+import org.simantics.charts.query.MilestoneSpecQuery;
+import org.simantics.charts.query.SetProperty;
+import org.simantics.charts.query.TrendSpecQuery;
+import org.simantics.charts.ui.ChartLinkData;
+import org.simantics.charts.ui.LinkTimeHandler;
+import org.simantics.databoard.Bindings;
+import org.simantics.databoard.util.ObjectUtils;
+import org.simantics.db.AsyncReadGraph;
+import org.simantics.db.ReadGraph;
+import org.simantics.db.Resource;
+import org.simantics.db.Session;
+import org.simantics.db.common.request.ParametrizedRead;
+import org.simantics.db.common.request.UniqueRead;
+import org.simantics.db.exception.DatabaseException;
+import org.simantics.db.layer0.request.Model;
+import org.simantics.db.layer0.request.combinations.Combinators;
+import org.simantics.db.layer0.variable.RVI;
+import org.simantics.db.layer0.variable.RVIBuilder;
+import org.simantics.db.layer0.variable.Variable;
+import org.simantics.db.layer0.variable.Variables;
+import org.simantics.db.layer0.variable.Variables.Role;
+import org.simantics.db.procedure.AsyncListener;
+import org.simantics.db.procedure.SyncListener;
+import org.simantics.diagram.participant.ContextUtil;
+import org.simantics.diagram.participant.SGFocusParticipant;
+import org.simantics.g2d.canvas.ICanvasContext;
+import org.simantics.g2d.canvas.impl.CanvasContext;
+import org.simantics.g2d.chassis.AWTChassis;
+import org.simantics.g2d.chassis.ICanvasChassis;
+import org.simantics.g2d.chassis.IChassisListener;
+import org.simantics.g2d.chassis.SWTChassis;
+import org.simantics.g2d.participant.KeyToCommand;
+import org.simantics.g2d.participant.TimeParticipant;
+import org.simantics.g2d.utils.CanvasUtils;
+import org.simantics.history.Collector;
+import org.simantics.history.HistoryManager;
+import org.simantics.history.impl.FileHistory;
+import org.simantics.project.IProject;
+import org.simantics.scenegraph.INode;
+import org.simantics.scenegraph.g2d.events.Event;
+import org.simantics.scenegraph.g2d.events.EventTypes;
+import org.simantics.scenegraph.g2d.events.IEventHandler;
+import org.simantics.scenegraph.g2d.events.MouseEvent;
+import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
+import org.simantics.scenegraph.g2d.events.command.Commands;
+import org.simantics.selectionview.StandardPropertyPage;
+import org.simantics.simulation.data.Datasource;
+import org.simantics.simulation.experiment.ExperimentState;
+import org.simantics.simulation.experiment.IExperiment;
+import org.simantics.simulation.experiment.IExperimentListener;
+import org.simantics.simulation.ontology.SimulationResource;
+import org.simantics.trend.TrendInitializer;
+import org.simantics.trend.TrendInitializer.StepListener;
+import org.simantics.trend.configuration.ItemPlacement;
+import org.simantics.trend.configuration.LineQuality;
+import org.simantics.trend.configuration.TimeFormat;
+import org.simantics.trend.configuration.TrendSpec;
+import org.simantics.trend.impl.HorizRuler;
+import org.simantics.trend.impl.ItemNode;
+import org.simantics.trend.impl.MilestoneSpec;
+import org.simantics.trend.impl.TrendNode;
+import org.simantics.trend.impl.TrendParticipant;
+import org.simantics.ui.workbench.IPropertyPage;
+import org.simantics.ui.workbench.IResourceEditorInput;
+import org.simantics.ui.workbench.ResourceEditorInput;
+import org.simantics.ui.workbench.ResourceEditorPart;
+import org.simantics.ui.workbench.action.PerformDefaultAction;
+import org.simantics.ui.workbench.editor.input.InputValidationCombinators;
+import org.simantics.utils.datastructures.hints.HintListenerAdapter;
+import org.simantics.utils.datastructures.hints.IHintContext;
+import org.simantics.utils.datastructures.hints.IHintContext.Key;
+import org.simantics.utils.datastructures.hints.IHintObservable;
+import org.simantics.utils.format.ValueFormat;
+import org.simantics.utils.threads.AWTThread;
+import org.simantics.utils.threads.IThreadWorkQueue;
+import org.simantics.utils.threads.SWTThread;
+import org.simantics.utils.threads.ThreadUtils;
+import org.simantics.utils.ui.BundleUtils;
+import org.simantics.utils.ui.ErrorLogger;
+import org.simantics.utils.ui.ExceptionUtils;
+import org.simantics.utils.ui.SWTUtils;
+import org.simantics.utils.ui.dialogs.ShowMessage;
+import org.simantics.utils.ui.jface.ActiveSelectionProvider;
+
+/**
+ * TimeSeriesEditor is an interactive part that draws a time series chart.
+ * 
+ * The configuration model is {@link TrendSpec} which is read through
+ * {@link TrendSpecQuery}. In Simantics Environment the
+ * editor input is {@link ResourceEditorInput}.
+ * 
+ * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
+ * @author Tuukka Lehtonen
+ */
+public class TimeSeriesEditor extends ResourceEditorPart {     
+
+    ParametrizedRead<IResourceEditorInput, Boolean> INPUT_VALIDATOR =
+        Combinators.compose(
+                InputValidationCombinators.hasURI(),
+                InputValidationCombinators.extractInputResource()
+        );
+
+    @Override
+    protected ParametrizedRead<IResourceEditorInput, Boolean> getInputValidator() {
+        return INPUT_VALIDATOR;
+    }
+
+    /**
+     * The root property browse context of the time series editor. A transitive
+     * closure is calculated for this context.
+     */
+    private static String       ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext;
+
+    /**
+     * ID of the this editor part extension.
+     */
+    public static final String  ID                  = "org.simantics.charts.editor.timeseries";
+
+    private static final String CONTEXT_MENU_ID     = "#timeSeriesChart";
+
+    private IEclipsePreferences chartPreferenceNode;
+
+    private final ImageDescriptor IMG_ZOOM_TO_FIT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horizAndVert16.png");
+    private final ImageDescriptor IMG_ZOOM_TO_FIT_HORIZ = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horiz16.png");
+    private final ImageDescriptor IMG_ZOOM_TO_FIT_VERT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/vert16.png");
+    private final ImageDescriptor IMG_AUTOSCALE = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/autoscale16.png");
+
+    IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() {
+        @Override
+        public void preferenceChange(PreferenceChangeEvent event) {
+            if (disposed) {
+                System.err.println("Warning: pref change to disposed TimeSeriesEditor");
+                return;
+            }
+
+            if ( event.getKey().equals(ChartPreferences.P_REDRAW_INTERVAL ) || 
+                    event.getKey().equals(ChartPreferences.P_AUTOSCALE_INTERVAL )) {
+                long redraw_interval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
+                long autoscale_interval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
+                setInterval( redraw_interval, autoscale_interval );
+            }
+            if ( event.getKey().equals(ChartPreferences.P_DRAW_SAMPLES )) {
+                boolean draw_samples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
+                setDrawSamples( draw_samples );
+            }
+            if ( event.getKey().equals(ChartPreferences.P_TIMEFORMAT ) ) {
+                String s = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);
+                TimeFormat tf = TimeFormat.valueOf( s );
+                if (tf!=null) setTimeFormat( tf );
+            }
+            if ( event.getKey().equals(ChartPreferences.P_VALUEFORMAT ) ) {
+                String s = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
+                ValueFormat vf = ValueFormat.valueOf( s );
+                if (vf!=null) setValueFormat( vf );
+            }
+            if ( event.getKey().equals(ChartPreferences.P_ITEMPLACEMENT)) {
+                String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
+                ItemPlacement ip = ItemPlacement.valueOf(s);
+                if (trendNode!=null) trendNode.itemPlacement = ip;
+            }
+            if ( event.getKey().equals(ChartPreferences.P_TEXTQUALITY) || event.getKey().equals(ChartPreferences.P_LINEQUALITY) ) {
+                String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
+                String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
+                LineQuality q1 = LineQuality.valueOf(s1);
+                LineQuality q2 = LineQuality.valueOf(s2);
+                if (trendNode!=null) trendNode.quality.textQuality = q1;
+                if (trendNode!=null) trendNode.quality.lineQuality = q2;
+            }
+            
+        }
+    };
+
+    /**
+     * The project which this editor is listening to for changes to
+     * {@link ChartKeys.ChartSourceKey keys}.
+     */
+    IProject                    project;
+
+    /**
+     * The model resource containing the input chart resource.
+     */
+    Resource                    model;
+
+    /**
+     * The text widget shown only if there is no IProject available at the time
+     * of editor part creation.
+     */
+    Text                        errorText;
+
+    /**
+     * A unique key for making DB requests chart editor specific without binding
+     * the requests to the editor object itself.
+     */
+    UUID                        uniqueChartEditorId = UUID.randomUUID();
+    Logger                      log;
+    Display                     display;
+    SWTChassis                  canvas;
+    CanvasContext               cvsCtx;
+    TrendParticipant            tp;
+    TrendNode                   trendNode;
+    StepListener                stepListener;
+    MilestoneSpecListener       milestoneListener;
+    MilestoneSpecQuery          milestoneQuery;
+
+    /**
+     * The ChartData instance used by this editor for sourcing data at any given
+     * moment. Project hint instances are copied into this instance.
+     */
+    final ChartData             chartData = new ChartData(null, null, null, null, null, null);
+
+    /**
+     * The ChartSourceKey to match the model this editor was opened for.
+     * @see #model
+     * @see #init(IEditorSite, IEditorInput)
+     */
+    ChartKeys.ChartSourceKey    chartDataKey;
+    
+    
+    /**
+     * Context management utils
+     */
+    protected IThreadWorkQueue           swt;
+    protected ContextUtil                contextUtil;
+
+    class ExperimentStateListener implements IExperimentListener {
+        @Override
+        public void stateChanged(ExperimentState state) {
+            TrendSpec spec = trendNode.getTrendSpec();
+            spec.experimentIsRunning = state == ExperimentState.RUNNING;
+            if (spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
+                TrendParticipant t = tp;
+                if (t != null)
+                    t.setDirty();
+            }
+        }
+    }
+
+    ExperimentStateListener experimentStateListener = new ExperimentStateListener();
+
+    class ChartDataListener extends HintListenerAdapter implements Runnable {
+        @Override
+        public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+            if (key.equals(chartDataKey)) {
+                // @Thread any
+                if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) {
+                    cvsCtx.getThreadAccess().asyncExec(this);
+                }
+            }
+        }
+        @Override
+        public void run() {
+            // @Thread AWT
+            if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return;
+            ChartData data = Simantics.getProject().getHint(chartDataKey);
+            setInput( data, trendNode.getTrendSpec() );
+        }
+    }
+
+    ChartDataListener chartDataListener = new ChartDataListener();
+
+    class ValueTipBoxPositionListener extends HintListenerAdapter {
+        @Override
+        public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+            if (key.equals(TrendParticipant.KEY_VALUE_TIP_BOX_RELATIVE_POS) && newValue != null) {
+                Session s = Simantics.getSession();
+                ChartResource CHART = s.getService(ChartResource.class);
+                Point2D p = (Point2D) newValue;
+                double[] value = { p.getX(), p.getY() };
+                s.asyncRequest(new SetProperty(getInputResource(), CHART.Chart_valueViewPosition, value, Bindings.DOUBLE_ARRAY));
+            }
+        }
+    }
+
+    ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener();
+
+    // Link-Time
+    State linkTimeState;
+    IStateListener linkTimeStateListener = new IStateListener() {
+               @Override
+               public void handleStateChange(State state, Object oldValue) {
+                       final ChartLinkData newData = (ChartLinkData) linkTimeState.getValue();
+                       trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this;
+                       
+                       if ( newData == null || newData.sender==TimeSeriesEditor.this ) return;
+                       TrendNode tn = trendNode;
+                       HorizRuler hr = tn!=null ? tn.horizRuler : null;
+
+                       ChartLinkData oldData = new ChartLinkData();
+                       getFromEnd(oldData);
+
+                       if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) {
+                               tn.valueTipTime = newData.valueTipTime;
+                               tp.setDirty();
+                       }
+                       
+                       if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) {
+                               
+                       cvsCtx.getThreadAccess().asyncExec( new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx);
+                                               trendNode.horizRuler.autoscroll = false;
+                                               if (b) {
+                                                       trendNode.layout();
+                                                       tp.setDirty();
+                                               }
+                                       }});
+                       }
+                       
+               }
+    };
+    HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() {
+               @Override
+               public void onNewWindow(double from, double end, double scalex) {
+                       final ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
+                       if (oldData != null) {
+                               ChartLinkData data = new ChartLinkData(TimeSeriesEditor.this, from, end, scalex);
+                               data.valueTipTime = trendNode.valueTipTime;
+                               linkTimeState.setValue( data );
+                       }
+               }
+       };
+
+    class ChassisListener implements IChassisListener {
+        @Override
+        public void chassisClosed(ICanvasChassis sender) {
+            // Prevent deadlock while disposing which using syncExec would result in. 
+            final ICanvasContext ctx = cvsCtx;
+            ThreadUtils.asyncExec(ctx.getThreadAccess(), new Runnable() {
+                @Override
+                public void run() {
+                    if (ctx != null) {
+                        AWTChassis awt = canvas.getAWTComponent();
+                        if (awt != null)
+                            awt.setCanvasContext(null);
+                        ctx.dispose();
+                    }
+                }
+            });
+            canvas.removeChassisListener(ChassisListener.this);
+        }
+    }
+
+    ActiveSelectionProvider     selectionProvider   = new ActiveSelectionProvider();
+    MenuManager                 menuManager;
+
+    public TimeSeriesEditor() {
+        log = Logger.getLogger( this.getClass().getName() );
+    }
+
+    boolean isTimeLinked() {
+        if (linkTimeState==null) return false;
+        Boolean isLinked = (Boolean) linkTimeState.getValue();
+        return isLinked != null && isLinked;
+    }
+
+    @Override
+    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+        super.init(site, input);
+        try {
+            this.model = Simantics.getSession().syncRequest( new Model( getInputResource() ) );
+            this.chartDataKey = ChartKeys.chartSourceKey(model);
+        } catch (DatabaseException e) {
+            throw new PartInitException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Input " + getInputResource() + " is not part of a model.", e));
+        }
+    }
+    
+    /**
+     * Invoke this only from the AWT thread.
+     * @param context
+     */
+    protected void setCanvasContext(final SWTChassis chassis, final ICanvasContext context) {
+        // Cannot directly invoke SWTChassis.setCanvasContext only because it
+        // needs to be invoked in the SWT thread and AWTChassis.setCanvasContext in the
+        // AWT thread, but directly invoking SWTChassis.setCanvasContext would call both
+        // in the SWT thread which would cause synchronous scheduling of AWT
+        // runnables which is always a potential source of deadlocks.
+        chassis.getAWTComponent().setCanvasContext(context);
+        SWTUtils.asyncExec(chassis, new Runnable() {
+            @Override
+            public void run() {
+                if (!chassis.isDisposed())
+                    // For AWT, this is a no-operation.
+                    chassis.setCanvasContext(context);
+            }
+        });
+    }
+
+    @Override
+    public void createPartControl(Composite parent) {
+        display = parent.getDisplay();
+        swt = SWTThread.getThreadAccess(display);
+
+        // Must have a project to attach to, otherwise the editor is useless.
+        project = Simantics.peekProject();
+        if (project == null) {
+            errorText = new Text(parent, SWT.NONE);
+            errorText.setText("No project is open.");
+            errorText.setEditable(false);
+            return;
+        }
+
+        // Create the canvas context here before finishing createPartControl
+        // to give anybody requiring access to this editor's ICanvasContext
+        // a chance to do their work.
+        // The context can be created in SWT thread without scheduling
+        // to the context thread and having potential deadlocks.
+        // The context is locked here and unlocked after it has been
+        // initialized in the AWT thread.
+        IThreadWorkQueue thread = AWTThread.getThreadAccess();
+        cvsCtx = new CanvasContext(thread);
+        cvsCtx.setLocked(true);
+
+        final IWorkbenchWindow win = getEditorSite().getWorkbenchWindow();
+        final IWorkbenchPage page = getEditorSite().getPage();
+
+        canvas = new SWTChassis(parent, SWT.NONE);
+        canvas.populate(parameter -> {
+            if (!disposed) {
+                canvas.addChassisListener(new ChassisListener());
+                initializeCanvas(canvas, cvsCtx, win, page);
+            }
+        });
+
+        // Link time
+        ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
+        Command command = service.getCommand( LinkTimeHandler.COMMAND_ID );
+        linkTimeState = command.getState( LinkTimeHandler.STATE_ID );
+        if ( linkTimeState != null ) linkTimeState.addListener( linkTimeStateListener );
+
+        addPopupMenu();
+
+        // Start tracking editor input validity. 
+        activateValidation();
+
+        // Provide input as selection for property page.
+        selectionProvider.setSelection( new StructuredSelection(getInputResource()) );
+        getSite().setSelectionProvider( selectionProvider );
+    }
+
+    protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx, IWorkbenchWindow window, IWorkbenchPage page) {
+        // Initialize canvas context
+        TrendSpec nodata = new TrendSpec();
+        nodata.init();
+        cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata);
+
+        tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class);
+
+        
+        IContextService contextService = (IContextService) getSite().getService(IContextService.class);
+        contextUtil = new ContextUtil(contextService, swt);
+
+        
+        cvsCtx.add( new SubscriptionDropParticipant( getInputResource() ) );
+        cvsCtx.add( new KeyToCommand( ChartKeyBindings.DEFAULT_BINDINGS ) );
+        cvsCtx.add( new ChartPasteHandler2(getInputResource().get()) );
+        cvsCtx.add(contextUtil);
+        
+        // Context management
+        cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context"));
+
+        stepListener = new StepListener( tp );
+        trendNode = tp.getTrend();
+        trendNode.titleNode.remove();
+        trendNode.titleNode = null;
+
+        // Link time
+        trendNode.horizRuler.listener = horizRulerListener;
+        
+        final ChartLinkData linkTime = (ChartLinkData) linkTimeState.getValue();
+        if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx);
+        
+        // Handle mouse moved event after TrendParticipant.
+        // This handler forwards trend.mouseHoverTime to linkTimeState
+        cvsCtx.getEventHandlerStack().add( new IEventHandler() {
+
+                       @Override
+                       public int getEventMask() {
+                               return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed;
+                       }
+
+                       @Override
+                       public boolean handleEvent(Event e) {
+
+//                             System.out.println("LinkEventHandler: "+e);
+                               ChartLinkData oldData = (ChartLinkData) linkTimeState.getValue();
+                               if (oldData!=null) {
+                                       ChartLinkData newData = new ChartLinkData();
+                                       getFromEnd(newData);
+                                       if (!newData.equals(oldData)) {
+//                                             System.out.println("Sending new link-data");
+                                               linkTimeState.setValue( newData );
+                                       }
+                               }
+                               return false;
+                       }}, -1);
+        
+        canvas.getHintContext().setHint( SWTChassis.KEY_EDITORPART, this);
+        canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHPAGE, page);
+        canvas.getHintContext().setHint( SWTChassis.KEY_WORKBENCHWINDOW, window);
+
+        // Canvas context is initialized, unlock it now to allow rendering.
+        cvsCtx.setLocked(false);
+
+        setCanvasContext(chassis, cvsCtx);
+
+        cvsCtx.getEventHandlerStack().add(new IEventHandler() {
+            @Override
+            public boolean handleEvent(Event e) {
+                MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e;
+                if (event.button != MouseEvent.RIGHT_BUTTON)
+                    return false;
+
+                final Point p = new Point((int) event.screenPosition.getX(), (int) event.screenPosition.getY());
+                SWTUtils.asyncExec(chassis, new Runnable() {
+                    @Override
+                    public void run() {
+                        if (!canvas.isDisposed())
+                            showPopup(p);
+                    }
+                });
+                return true;
+            }
+            @Override
+            public int getEventMask() {
+                return EventTypes.MouseButtonReleasedMask;
+            }
+        }, 1000000);
+
+        // Track data source and preinitialize chartData
+        project.addHintListener(chartDataListener);
+        chartData.readFrom( (ChartData) project.getHint( chartDataKey ) );
+
+        if (chartData.run != null) {
+            milestoneListener = new MilestoneSpecListener();
+            milestoneQuery = new MilestoneSpecQuery( chartData.run );
+            getSession().asyncRequest( milestoneQuery, milestoneListener );
+        }
+
+        // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration
+        trackChartConfiguration();
+        trackPreferences();
+
+        // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]
+        // back to the graph database.
+        cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);
+       }
+
+       private void addPopupMenu() {
+        menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID);
+        menuManager.setRemoveAllWhenShown(true);
+        Menu menu = menuManager.createContextMenu(canvas);
+        canvas.setMenu(menu);
+        getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider);
+
+        // Add support for some built-in actions in the context menu.
+        menuManager.addMenuListener(new IMenuListener() {
+            @Override
+            public void menuAboutToShow(IMenuManager manager) {
+                // Not initialized yet, prevent NPE.
+                TrendNode trendNode = TimeSeriesEditor.this.trendNode;
+                TrendParticipant tp = TimeSeriesEditor.this.tp;
+                if (trendNode == null || tp == null)
+                    return;
+
+                TrendSpec trendSpec = trendNode.getTrendSpec();
+                ItemNode hoverItem = tp.hoveringItem;
+                if (hoverItem != null && hoverItem.item != null) {
+                    Resource component = resolveReferencedComponent(getResourceInput(), hoverItem.item.variableId);
+                    if (component != null) {
+                        manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component));
+                    }
+
+                    Resource chart = TimeSeriesEditor.this.getInputResource();
+                    if ( chart != null ) {
+                        try {
+                            Resource chartItem = getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) );
+                            if (chartItem != null) {
+                                manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem)));
+                                manager.add(new Separator());
+                                manager.add(new PropertiesAction("Item Properties", canvas, chartItem));
+                                manager.add(new PropertiesAction("Chart Properties", canvas, chart));
+                            }
+                        } catch (DatabaseException e) {
+                            Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));
+                        }
+                    }
+                } else {
+                    boolean hairlineMovementAllowed =
+                            !(trendSpec.experimentIsRunning &&
+                              trendSpec.viewProfile.trackExperimentTime);
+
+                    Resource chart = TimeSeriesEditor.this.getInputResource();
+                    manager.add(new Separator());
+                    manager.add(new MoveHairlineAction(
+                            "Move Hairline Here",
+                            chart,
+                            hairlineMovementAllowed,
+                            trendNode,
+                            trendNode.mouseHoverTime
+                            ));
+                    manager.add(new MoveHairlineAction(
+                            "Move Hairline To Current Time",
+                            chart,
+                            hairlineMovementAllowed,
+                            trendNode,
+                            trendNode.horizRuler.getItemEndTime(),
+                            Boolean.FALSE
+                            ));
+                    manager.add(new TrackExperimentTimeAction(
+                            "Hairline Tracks Current Time",
+                            chart,
+                            trendSpec.viewProfile.trackExperimentTime));
+                    manager.add(new Separator());
+                    manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT));
+                    manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ));
+                    manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT));
+                    manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE));
+                    manager.add(new Separator());
+                    manager.add(new PropertiesAction("Chart Properties", canvas, chart));
+                }
+            }
+        });
+    }
+
+    protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) {
+        try {
+            return getSession().sync(new UniqueRead<Resource>() {
+                @Override
+                public Resource perform(ReadGraph graph) throws DatabaseException {
+                    Variable configuration = Variables.getConfigurationContext(graph, getInputResource());
+                    RVI rvi = RVI.fromResourceFormat(graph, variableId);
+                    rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI();
+                    if (rvi.isEmpty())
+                        return null;
+                    Variable var = rvi.resolve(graph, configuration);
+                    return var.getPossibleRepresents(graph);
+                }
+            });
+        } catch (DatabaseException e) {
+            ErrorLogger.defaultLogError(e);
+        }
+        return null;
+    }
+
+    private void showPopup(Point p) {
+        menuManager.getMenu().setLocation(p);
+        menuManager.getMenu().setVisible(true);
+    }
+
+    private void trackChartConfiguration() {
+        getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());
+        getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());
+    }
+
+    @Override
+    public void setFocus() {
+        if (errorText != null)
+            errorText.setFocus();
+        else
+            canvas.setFocus();
+    }
+
+    @Override
+    public void dispose() {
+        if (disposed == true) return;
+        disposed = true;
+
+        if (trendNode!=null && trendNode.horizRuler!=null) {
+            trendNode.horizRuler.listener = null;
+        }
+
+        if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );
+
+        canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );
+        canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );
+        canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );
+
+        if ( chartPreferenceNode!= null ) {
+            chartPreferenceNode.removePreferenceChangeListener( preferenceListener );
+        }
+
+        MilestoneSpecListener ml = milestoneListener;
+        if (ml!=null) ml.dispose();
+
+        if (project != null) {
+            project.removeHintListener(chartDataListener);
+        }
+
+        if (chartData != null) {
+            if (chartData.datasource!=null)
+                chartData.datasource.removeListener( stepListener );
+            if (chartData.experiment!=null)
+                chartData.experiment.removeListener( experimentStateListener );
+            chartData.readFrom( null );
+        }
+
+        super.dispose();
+    }
+
+    /**
+     * @param data new data or null
+     * @param newSpec new spec or null
+     * @thread AWT
+     */
+    @SuppressWarnings("unused")
+    public void setInput(ChartData data, TrendSpec newSpec) {
+       boolean doLayout = false;
+
+       // Disregard input if it is not for this chart's containing model.
+       if (data != null && data.model != null && !data.model.equals(model))
+           data = null;
+
+       // Accommodate Datasource changes
+       Datasource: {
+               Datasource oldDatasource = chartData==null?null:chartData.datasource;
+               Datasource newDatasource = data==null?null:data.datasource;
+               //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) ) 
+               {
+                       if (oldDatasource!=null) oldDatasource.removeListener( stepListener );
+                       if (newDatasource!=null) newDatasource.addListener( stepListener );
+               }
+       }
+
+        Experiment: {
+            IExperiment oldExperiment = chartData==null?null:chartData.experiment;
+            IExperiment newExperiment = data==null?null:data.experiment;
+            //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) ) 
+            {
+                if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );
+                if (newExperiment!=null) newExperiment.addListener( experimentStateListener );
+            }
+        }
+
+       // Accommodate Historian changes
+       Historian: {
+               HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian;
+               HistoryManager newHistorian = data==null?null:data.history;
+               Collector newCollector = data==null?null:data.collector;
+       //      if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) ) 
+               {
+                       if (newHistorian instanceof FileHistory) {
+                               FileHistory fh = (FileHistory) newHistorian;
+                               System.out.println("History = "+fh.getWorkarea());
+                       }
+                       trendNode.setHistorian( newHistorian, newCollector );
+                       doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);
+               }
+
+               // Accommodate TrendSpec changes
+               TrendSpec oldSpec = trendNode.getTrendSpec();
+               if ( !newSpec.equals(oldSpec) ) {
+                   trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );
+                   doLayout = true;
+               }
+               
+       }
+
+        Resource newExperimentResource = data==null ? null : data.run;
+        Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;
+       
+        // Track milestones
+        Milestones: {
+               if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
+
+                       // Dispose old listener & Query
+                       if (milestoneListener!=null) {
+                               milestoneListener.dispose();
+                               milestoneListener = null;
+                       }
+                       if (milestoneQuery!=null) {
+                               milestoneQuery = null;
+                       }
+
+                       trendNode.setMilestones( MilestoneSpec.EMPTY );
+                       
+                       if (newExperimentResource != null) {
+                               milestoneListener = new MilestoneSpecListener();
+                               milestoneQuery = new MilestoneSpecQuery( newExperimentResource );
+                               Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
+                       }
+               }
+               
+        }
+
+        if (doLayout) trendNode.layout();
+        this.chartData.readFrom( data );
+        tp.setDirty();
+        
+        if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
+               resetViewAfterDataChange();
+        }
+        
+    }
+
+    class ActiveRunListener implements SyncListener<Resource> {
+        @Override
+        public void exception(ReadGraph graph, Throwable throwable) {
+            ErrorLogger.defaultLogError(throwable);
+            ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
+        }
+        @Override
+        public void execute(ReadGraph graph, final Resource run) throws DatabaseException {
+               if(run != null) {
+                       SimulationResource SIMU = SimulationResource.getInstance(graph);
+                       Variable var = Variables.getVariable(graph, run);
+                       IExperiment exp = var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment);
+                       ITrendSupport ts = exp.getService(ITrendSupport.class);
+                       if (ts != null)
+                           ts.setChartData(graph);
+               }
+        }
+        @Override
+        public boolean isDisposed() {
+            return TimeSeriesEditor.this.disposed;
+        }
+    }
+    
+    class TrendSpecListener implements AsyncListener<TrendSpec> {
+        @Override
+        public void exception(AsyncReadGraph graph, Throwable throwable) {
+               
+            ErrorLogger.defaultLogError(throwable);
+            ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
+        }
+        @Override
+        public void execute(AsyncReadGraph graph, final TrendSpec result) {
+            if (result == null) {
+                log.log(Level.INFO, "Chart configuration removed");
+            } else {
+                log.log(Level.INFO, "Chart configuration updated: " + result);
+            }
+
+            // Reload chart in AWT Thread
+            AWTThread.getThreadAccess().asyncExec(new Runnable() {
+                @Override
+                public void run() {
+                    if (!disposed)
+                        setInput( chartData, result );
+                }
+            });
+        }
+        @Override
+        public boolean isDisposed() {
+            return TimeSeriesEditor.this.disposed;
+        }
+    }
+    
+    class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {
+       boolean disposed = false;
+               @Override
+               public void execute(AsyncReadGraph graph, final MilestoneSpec result) {
+                       AWTThread.INSTANCE.asyncExec(new Runnable() {
+                               public void run() {
+                                       trendNode.setMilestones(result);
+                               }});
+               }
+
+               @Override
+               public void exception(AsyncReadGraph graph, Throwable throwable) {
+                       
+               }
+
+               @Override
+               public boolean isDisposed() {
+                       return disposed;
+               }
+               
+               public void dispose() {
+                       disposed = true;
+               }
+       
+    }
+
+    private void trackPreferences() {
+        chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" );
+        chartPreferenceNode.addPreferenceChangeListener( preferenceListener );
+        long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
+        long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
+        setInterval(redrawInterval, autoscaleInterval);
+        
+        String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); 
+        TimeFormat tf = TimeFormat.valueOf( timeFormat );
+        if (tf!=null) setTimeFormat( tf );
+        
+        Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
+        setDrawSamples(drawSamples);
+
+        String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
+        ValueFormat vf = ValueFormat.valueOf( valueFormat );
+        if (vf!=null) setValueFormat( vf );
+        
+       String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
+       ItemPlacement ip = ItemPlacement.valueOf(s);
+       if (trendNode!=null) trendNode.itemPlacement = ip;
+       
+        String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
+        String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
+        LineQuality q1 = LineQuality.valueOf(s1);
+        LineQuality q2 = LineQuality.valueOf(s2);
+        if (trendNode!=null) trendNode.quality.textQuality = q1;
+        if (trendNode!=null) trendNode.quality.lineQuality = q2;
+       
+    }
+
+    private void setInterval(long redrawInterval, long autoscaleInterval) {
+        redrawInterval = Math.max(1, redrawInterval);
+        long pulse = Math.min(50, redrawInterval);
+        pulse = Math.min(pulse, autoscaleInterval);
+        IHintContext h = canvas.getCanvasContext().getDefaultHintContext();
+        h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse);
+        h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval);
+        h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval);        
+    }
+
+    private void setDrawSamples(boolean value) {
+       trendNode.drawSamples = value;
+       trendNode.layout();
+       tp.setDirty();
+    }
+
+    private void setTimeFormat( TimeFormat tf ) {
+       if (trendNode.timeFormat == tf) return;
+       trendNode.timeFormat = tf;
+       trendNode.layout();
+        tp.setDirty();
+    }
+
+    private void setValueFormat( ValueFormat vf ) {
+       if (trendNode.valueFormat == vf) return;
+       trendNode.valueFormat = vf;
+       trendNode.layout();
+        tp.setDirty();
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Object getAdapter(Class adapter) {
+        if (adapter == INode.class) {
+            ICanvasContext ctx = cvsCtx;
+            if (ctx != null)
+                return ctx.getSceneGraph();
+        }
+        if (adapter == IPropertyPage.class)
+            return new StandardPropertyPage(getSite(), getPropertyPageContexts());
+        if (adapter == ICanvasContext.class)
+            return cvsCtx;
+        return super.getAdapter(adapter);
+    }
+
+    protected Set<String> getPropertyPageContexts() {
+        try {
+            return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT));
+        } catch (DatabaseException e) {
+            ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e);
+            return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT);
+        }
+    }
+
+    /**
+     * Add from, end, (scale x) to argument array
+     * @param fromEnd array of 2 or 3
+     */
+    public void getFromEnd(ChartLinkData data) {
+       data.sender = this;
+       TrendNode tn = trendNode;
+       data.valueTipTime = tn.valueTipTime;
+       HorizRuler hr = tn!=null ? tn.horizRuler : null;
+       if ( hr != null ) {
+               data.from = hr.from;
+               data.end = hr.end;
+                       double len = hr.end-hr.from; 
+                       double wid = tn.plot.getWidth();
+                       if ( wid==0.0 ) wid = 0.1;
+                       data.sx = len/wid;
+       }
+    }
+
+    @SuppressWarnings("unused")
+    private static boolean doubleEquals(double a, double b) {
+       if (Double.isNaN(a) && Double.isNaN(b)) return true;
+       return a==b;
+    }
+
+    protected void resetViewAfterDataChange() {
+       
+       CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);
+       CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);
+       
+    }
+
+}