]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.charts / src / org / simantics / charts / editor / TimeSeriesEditor.java
diff --git a/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java b/bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java
new file mode 100644 (file)
index 0000000..4377edd
--- /dev/null
@@ -0,0 +1,1048 @@
+/*******************************************************************************\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