4c58595a102775634f59722821f76c6f579667ad
[simantics/platform.git] / bundles / org.simantics.charts / src / org / simantics / charts / editor / e4 / TimeSeriesEditor.java
1 /*******************************************************************************
2  * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
3  * Industry THTH ry.
4  * All rights reserved. This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License v1.0
6  * which accompanies this distribution, and is available at
7  * http://www.eclipse.org/legal/epl-v10.html
8  *
9  * Contributors:
10  *     VTT Technical Research Centre of Finland - initial API and implementation
11  *******************************************************************************/
12 package org.simantics.charts.editor.e4;
13
14 import java.awt.geom.Point2D;
15 import java.util.Collections;
16 import java.util.List;
17 import java.util.Set;
18 import java.util.UUID;
19 import java.util.logging.Level;
20 import java.util.logging.Logger;
21
22 import javax.inject.Inject;
23
24 import org.eclipse.core.runtime.IAdaptable;
25 import org.eclipse.core.runtime.IStatus;
26 import org.eclipse.core.runtime.Status;
27 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
28 import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
29 import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
30 import org.eclipse.core.runtime.preferences.InstanceScope;
31 import org.eclipse.e4.ui.model.application.MApplication;
32 import org.eclipse.e4.ui.model.application.MApplicationElement;
33 import org.eclipse.e4.ui.model.application.ui.MUIElement;
34 import org.eclipse.e4.ui.model.application.ui.basic.MPart;
35 import org.eclipse.e4.ui.model.application.ui.menu.MToolBar;
36 import org.eclipse.e4.ui.services.EContextService;
37 import org.eclipse.e4.ui.workbench.modeling.EModelService;
38 import org.eclipse.e4.ui.workbench.modeling.ISelectionListener;
39 import org.eclipse.jface.action.IMenuListener;
40 import org.eclipse.jface.action.IMenuManager;
41 import org.eclipse.jface.action.MenuManager;
42 import org.eclipse.jface.action.Separator;
43 import org.eclipse.jface.resource.ImageDescriptor;
44 import org.eclipse.jface.viewers.StructuredSelection;
45 import org.eclipse.swt.SWT;
46 import org.eclipse.swt.graphics.Point;
47 import org.eclipse.swt.widgets.Composite;
48 import org.eclipse.swt.widgets.Display;
49 import org.eclipse.swt.widgets.Menu;
50 import org.eclipse.swt.widgets.Text;
51 import org.eclipse.ui.IEditorInput;
52 import org.eclipse.ui.IEditorSite;
53 import org.simantics.Simantics;
54 import org.simantics.browsing.ui.model.browsecontexts.BrowseContext;
55 import org.simantics.charts.Activator;
56 import org.simantics.charts.ITrendSupport;
57 import org.simantics.charts.editor.ActiveRunQuery;
58 import org.simantics.charts.editor.ChartData;
59 import org.simantics.charts.editor.ChartKeyBindings;
60 import org.simantics.charts.editor.ChartKeys;
61 import org.simantics.charts.editor.ChartPasteHandler2;
62 import org.simantics.charts.editor.HideItemsAction;
63 import org.simantics.charts.editor.MoveHairlineAction;
64 import org.simantics.charts.editor.PropertiesAction;
65 import org.simantics.charts.editor.SendCommandAction;
66 import org.simantics.charts.editor.SubscriptionDropParticipant;
67 import org.simantics.charts.editor.TrackExperimentTimeAction;
68 import org.simantics.charts.ontology.ChartResource;
69 import org.simantics.charts.preference.ChartPreferences;
70 import org.simantics.charts.query.FindChartItemForTrendItem;
71 import org.simantics.charts.query.MilestoneSpecQuery;
72 import org.simantics.charts.query.SetProperty;
73 import org.simantics.charts.query.TrendSpecQuery;
74 import org.simantics.charts.ui.ChartLinkData;
75 import org.simantics.databoard.Bindings;
76 import org.simantics.databoard.util.ObjectUtils;
77 import org.simantics.db.AsyncReadGraph;
78 import org.simantics.db.ReadGraph;
79 import org.simantics.db.Resource;
80 import org.simantics.db.Session;
81 import org.simantics.db.common.request.ParametrizedRead;
82 import org.simantics.db.common.request.UniqueRead;
83 import org.simantics.db.exception.DatabaseException;
84 import org.simantics.db.layer0.request.combinations.Combinators;
85 import org.simantics.db.layer0.variable.RVI;
86 import org.simantics.db.layer0.variable.RVIBuilder;
87 import org.simantics.db.layer0.variable.Variable;
88 import org.simantics.db.layer0.variable.Variables;
89 import org.simantics.db.layer0.variable.Variables.Role;
90 import org.simantics.db.procedure.AsyncListener;
91 import org.simantics.db.procedure.SyncListener;
92 import org.simantics.diagram.participant.SGFocusParticipant;
93 import org.simantics.diagram.participant.e4.ContextUtil;
94 import org.simantics.g2d.canvas.ICanvasContext;
95 import org.simantics.g2d.canvas.impl.CanvasContext;
96 import org.simantics.g2d.chassis.AWTChassis;
97 import org.simantics.g2d.chassis.ICanvasChassis;
98 import org.simantics.g2d.chassis.IChassisListener;
99 import org.simantics.g2d.chassis.SWTChassis;
100 import org.simantics.g2d.participant.KeyToCommand;
101 import org.simantics.g2d.participant.TimeParticipant;
102 import org.simantics.g2d.utils.CanvasUtils;
103 import org.simantics.history.Collector;
104 import org.simantics.history.HistoryManager;
105 import org.simantics.history.impl.FileHistory;
106 import org.simantics.project.IProject;
107 import org.simantics.scenegraph.INode;
108 import org.simantics.scenegraph.g2d.events.Event;
109 import org.simantics.scenegraph.g2d.events.EventTypes;
110 import org.simantics.scenegraph.g2d.events.IEventHandler;
111 import org.simantics.scenegraph.g2d.events.MouseEvent;
112 import org.simantics.scenegraph.g2d.events.MouseEvent.MouseButtonReleasedEvent;
113 import org.simantics.scenegraph.g2d.events.command.Commands;
114 import org.simantics.selectionview.StandardPropertyPage;
115 import org.simantics.simulation.data.Datasource;
116 import org.simantics.simulation.experiment.ExperimentState;
117 import org.simantics.simulation.experiment.IExperiment;
118 import org.simantics.simulation.experiment.IExperimentListener;
119 import org.simantics.simulation.ontology.SimulationResource;
120 import org.simantics.trend.TrendInitializer;
121 import org.simantics.trend.TrendInitializer.StepListener;
122 import org.simantics.trend.configuration.ItemPlacement;
123 import org.simantics.trend.configuration.LineQuality;
124 import org.simantics.trend.configuration.TimeFormat;
125 import org.simantics.trend.configuration.TrendSpec;
126 import org.simantics.trend.impl.HorizRuler;
127 import org.simantics.trend.impl.ItemNode;
128 import org.simantics.trend.impl.MilestoneSpec;
129 import org.simantics.trend.impl.TrendNode;
130 import org.simantics.trend.impl.TrendParticipant;
131 import org.simantics.ui.workbench.IPropertyPage;
132 import org.simantics.ui.workbench.IResourceEditorInput;
133 import org.simantics.ui.workbench.ResourceEditorInput;
134 import org.simantics.ui.workbench.action.PerformDefaultAction;
135 import org.simantics.ui.workbench.e4.E4ResourceEditorBase;
136 import org.simantics.ui.workbench.editor.input.InputValidationCombinators;
137 import org.simantics.utils.datastructures.hints.HintListenerAdapter;
138 import org.simantics.utils.datastructures.hints.IHintContext;
139 import org.simantics.utils.datastructures.hints.IHintContext.Key;
140 import org.simantics.utils.datastructures.hints.IHintObservable;
141 import org.simantics.utils.format.ValueFormat;
142 import org.simantics.utils.threads.AWTThread;
143 import org.simantics.utils.threads.IThreadWorkQueue;
144 import org.simantics.utils.threads.SWTThread;
145 import org.simantics.utils.threads.ThreadUtils;
146 import org.simantics.utils.ui.BundleUtils;
147 import org.simantics.utils.ui.ErrorLogger;
148 import org.simantics.utils.ui.ExceptionUtils;
149 import org.simantics.utils.ui.SWTUtils;
150 import org.simantics.utils.ui.dialogs.ShowMessage;
151 import org.simantics.utils.ui.jface.ActiveSelectionProvider;
152
153 /**
154  * TimeSeriesEditor is an interactive part that draws a time series chart.
155  * 
156  * The configuration model is {@link TrendSpec} which is read through
157  * {@link TrendSpecQuery}. In Simantics Environment the
158  * editor input is {@link ResourceEditorInput}.
159  * 
160  * @author Toni Kalajainen <toni.kalajainen@vtt.fi>
161  * @author Tuukka Lehtonen
162  */
163 public class TimeSeriesEditor extends E4ResourceEditorBase implements IAdaptable {
164
165     ParametrizedRead<IResourceEditorInput, Boolean> INPUT_VALIDATOR =
166         Combinators.compose(
167                 InputValidationCombinators.hasURI(),
168                 InputValidationCombinators.extractInputResource()
169         );
170
171     protected ParametrizedRead<IResourceEditorInput, Boolean> getInputValidator() {
172         return INPUT_VALIDATOR;
173     }
174
175     /**
176      * The root property browse context of the time series editor. A transitive
177      * closure is calculated for this context.
178      */
179     private static String       ROOT_PROPERTY_BROWSE_CONTEXT = ChartResource.URIs.ChartBrowseContext;
180
181     /**
182      * ID of the this editor part extension.
183      */
184     public static final String  ID                  = "org.simantics.charts.editor.timeseries";
185
186     public static final String CONTRIBUTION_URI     = "bundleclass://org.simantics.charts/org.simantics.charts.editor.e4.TimeSeriesEditor";
187
188     private static final String CONTEXT_MENU_ID     = "#timeSeriesChart";
189
190     private IEclipsePreferences chartPreferenceNode;
191
192     private final ImageDescriptor IMG_ZOOM_TO_FIT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horizAndVert16.png");
193     private final ImageDescriptor IMG_ZOOM_TO_FIT_HORIZ = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/horiz16.png");
194     private final ImageDescriptor IMG_ZOOM_TO_FIT_VERT = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/vert16.png");
195     private final ImageDescriptor IMG_AUTOSCALE = BundleUtils.getImageDescriptorFromPlugin(Activator.PLUGIN_ID, "icons/autoscale16.png");
196
197     IPreferenceChangeListener preferenceListener = new IPreferenceChangeListener() {
198
199         @Override
200         public void preferenceChange(PreferenceChangeEvent event) {
201             if (isDisposed()) {
202                 System.err.println("Warning: pref change to disposed TimeSeriesEditor");
203                 return;
204             }
205
206             if ( event.getKey().equals(ChartPreferences.P_REDRAW_INTERVAL ) || 
207                     event.getKey().equals(ChartPreferences.P_AUTOSCALE_INTERVAL )) {
208                 long redraw_interval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
209                 long autoscale_interval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
210                 setInterval( redraw_interval, autoscale_interval );
211             }
212             if ( event.getKey().equals(ChartPreferences.P_DRAW_SAMPLES )) {
213                 boolean draw_samples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
214                 setDrawSamples( draw_samples );
215             }
216             if ( event.getKey().equals(ChartPreferences.P_TIMEFORMAT ) ) {
217                 String s = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT);
218                 TimeFormat tf = TimeFormat.valueOf( s );
219                 if (tf!=null) setTimeFormat( tf );
220             }
221             if ( event.getKey().equals(ChartPreferences.P_VALUEFORMAT ) ) {
222                 String s = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
223                 ValueFormat vf = ValueFormat.valueOf( s );
224                 if (vf!=null) setValueFormat( vf );
225             }
226             if ( event.getKey().equals(ChartPreferences.P_ITEMPLACEMENT)) {
227                 String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
228                 ItemPlacement ip = ItemPlacement.valueOf(s);
229                 if (trendNode!=null) trendNode.itemPlacement = ip;
230             }
231             if ( event.getKey().equals(ChartPreferences.P_TEXTQUALITY) || event.getKey().equals(ChartPreferences.P_LINEQUALITY) ) {
232                 String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
233                 String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
234                 LineQuality q1 = LineQuality.valueOf(s1);
235                 LineQuality q2 = LineQuality.valueOf(s2);
236                 if (trendNode!=null) trendNode.quality.textQuality = q1;
237                 if (trendNode!=null) trendNode.quality.lineQuality = q2;
238             }
239             
240         }
241     };
242
243     /**
244      * The project which this editor is listening to for changes to
245      * {@link ChartKeys.ChartSourceKey keys}.
246      */
247     IProject                    project;
248
249     /**
250      * The text widget shown only if there is no IProject available at the time
251      * of editor part creation.
252      */
253     Text                        errorText;
254
255     /**
256      * A unique key for making DB requests chart editor specific without binding
257      * the requests to the editor object itself.
258      */
259     UUID                        uniqueChartEditorId = UUID.randomUUID();
260     Logger                      log;
261     Display                     display;
262     SWTChassis                  canvas;
263     CanvasContext               cvsCtx;
264     TrendParticipant            tp;
265     TrendNode                   trendNode;
266     StepListener                stepListener;
267     MilestoneSpecListener       milestoneListener;
268     MilestoneSpecQuery          milestoneQuery;
269
270     /**
271      * The ChartData instance used by this editor for sourcing data at any given
272      * moment. Project hint instances are copied into this instance.
273      */
274     final ChartData             chartData = new ChartData(null, null, null, null, null, null);
275
276     /**
277      * The ChartSourceKey to match the model this editor was opened for.
278      * @see #model
279      * @see #init(IEditorSite, IEditorInput)
280      */
281     ChartKeys.ChartSourceKey    chartDataKey;
282     
283     
284     /**
285      * Context management utils
286      */
287     protected IThreadWorkQueue           swt;
288     protected ContextUtil                contextUtil;
289
290     class ExperimentStateListener implements IExperimentListener {
291         @Override
292         public void stateChanged(ExperimentState state) {
293             TrendSpec spec = trendNode.getTrendSpec();
294             spec.experimentIsRunning = state == ExperimentState.RUNNING;
295             if (spec.experimentIsRunning && spec.viewProfile.trackExperimentTime) {
296                 TrendParticipant t = tp;
297                 if (t != null)
298                     t.setDirty();
299             }
300         }
301     }
302
303     ExperimentStateListener experimentStateListener = new ExperimentStateListener();
304
305     class ChartDataListener extends HintListenerAdapter implements Runnable {
306         @Override
307         public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
308             if (key.equals(chartDataKey)) {
309                 // @Thread any
310                 if (!cvsCtx.isDisposed() && cvsCtx.isAlive()) {
311                     cvsCtx.getThreadAccess().asyncExec(this);
312                 }
313             }
314         }
315         @Override
316         public void run() {
317             // @Thread AWT
318             if (cvsCtx.isDisposed() || !cvsCtx.isAlive()) return;
319             ChartData data = Simantics.getProject().getHint(chartDataKey);
320             setInput( data, trendNode.getTrendSpec() );
321         }
322     }
323
324     ChartDataListener chartDataListener = new ChartDataListener();
325
326     class ValueTipBoxPositionListener extends HintListenerAdapter {
327         @Override
328         public void hintChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
329             if (key.equals(TrendParticipant.KEY_VALUE_TIP_BOX_RELATIVE_POS) && newValue != null) {
330                 Session s = Simantics.getSession();
331                 ChartResource CHART = s.getService(ChartResource.class);
332                 Point2D p = (Point2D) newValue;
333                 double[] value = { p.getX(), p.getY() };
334                 s.asyncRequest(new SetProperty(getInputResource(), CHART.Chart_valueViewPosition, value, Bindings.DOUBLE_ARRAY));
335             }
336         }
337     }
338
339     ValueTipBoxPositionListener valueTipBoxPositionListener = new ValueTipBoxPositionListener();
340
341     
342     private void handleLinkDataChange() {
343         final ChartLinkData newData = linkData;
344         trendNode.autoscaletime = newData == null || newData.sender == TimeSeriesEditor.this;
345         
346         if ( newData == null || newData.sender==TimeSeriesEditor.this ) return;
347         TrendNode tn = trendNode;
348         HorizRuler hr = tn!=null ? tn.horizRuler : null;
349
350         ChartLinkData oldData = new ChartLinkData();
351         getFromEnd(oldData);
352
353         if ( hr != null && !ObjectUtils.objectEquals(tn.valueTipTime, newData.valueTipTime)) {
354             tn.valueTipTime = newData.valueTipTime;
355             tp.setDirty();
356         }
357         
358         if ( hr != null && (oldData.from!=newData.from || oldData.sx!=newData.sx)) {
359             
360             cvsCtx.getThreadAccess().asyncExec( new Runnable() {
361                 @Override
362                 public void run() {
363                     boolean b = trendNode.horizRuler.setFromScale(newData.from, newData.sx);
364                     trendNode.horizRuler.autoscroll = false;
365                     if (b) {
366                         trendNode.layout();
367                         tp.setDirty();
368                     }
369                 }});
370         }
371     }
372     
373     // Link-Time
374 //    State linkTimeState;
375 //    IStateListener linkTimeStateListener = new IStateListener() {
376 //              @Override
377 //              public void handleStateChange(State state, Object oldValue) {
378 //                      
379 //                      
380 //              }
381 //    };
382     
383     HorizRuler.TimeWindowListener horizRulerListener = new HorizRuler.TimeWindowListener() {
384                 @Override
385                 public void onNewWindow(double from, double end, double scalex) {
386                         final ChartLinkData oldData = linkData;
387                         if (oldData != null) {
388                                 ChartLinkData data = new ChartLinkData(TimeSeriesEditor.this, from, end, scalex);
389                                 data.valueTipTime = trendNode.valueTipTime;
390                                 linkData = data;
391                                 handleLinkDataChange();
392                         }
393                 }
394         };
395
396     class ChassisListener implements IChassisListener {
397         @Override
398         public void chassisClosed(ICanvasChassis sender) {
399             // Prevent deadlock while disposing which using syncExec would result in. 
400             final ICanvasContext ctx = cvsCtx;
401             ThreadUtils.asyncExec(ctx.getThreadAccess(), new Runnable() {
402                 @Override
403                 public void run() {
404                     if (ctx != null) {
405                         AWTChassis awt = canvas.getAWTComponent();
406                         if (awt != null)
407                             awt.setCanvasContext(null);
408                         ctx.dispose();
409                     }
410                 }
411             });
412             canvas.removeChassisListener(ChassisListener.this);
413         }
414     }
415
416     ActiveSelectionProvider     selectionProvider   = new ActiveSelectionProvider();
417     MenuManager                 menuManager;
418
419     @Inject
420     EContextService contextService;
421
422     private ChartLinkData linkData;
423
424     public TimeSeriesEditor() {
425         log = Logger.getLogger( this.getClass().getName() );
426     }
427
428     @Override
429     public void initImpl(MPart part) {
430         EModelService modelService = getContext().get(EModelService.class);
431         MApplication app = getContext().get(MApplication.class);
432         MUIElement element = modelService.cloneSnippet(app, "org.simantics.charts.toolbar.default.snippet", null);
433         if (element != null) {
434             element.setVisible(true);
435             element.setToBeRendered(true);
436             part.setToolbar((MToolBar)element);
437         }
438         
439 //        MBindingContext dialogAndWindowContext = findMAppElementById(app.getRootContext(), "org.eclipse.ui.contexts.dialogAndWindow");
440 //        MBindingContext windowContext = findMAppElementById(dialogAndWindowContext.getChildren(), "org.eclipse.ui.contexts.window");
441 //        MBindingContext chartContext = findMAppElementById(windowContext.getChildren(), "org.simantics.charts.editor.context");
442 //        if (chartContext == null) {
443 //            chartContext = MCommandsFactory.INSTANCE.createBindingContext();
444 //            chartContext.setContributorURI("bundle://test.uri");
445 //            chartContext.setElementId("org.simantics.charts.editor.context");
446 //            chartContext.setName("In Chart Editor");
447 //            chartContext.setDescription("Chart Editor Binding Context");
448 //            windowContext.getChildren().add(chartContext);
449 //        }
450 //        MBindingTable chartBindingTable = findMAppElementById(app.getBindingTables(), "org.simantics.charts.editor.context");
451 //        if (chartBindingTable == null) {
452 //            chartBindingTable = MCommandsFactory.INSTANCE.createBindingTable();
453 //            chartBindingTable.setContributorURI("bundle://test.test.uri");
454 //            chartBindingTable.setElementId("org.simantics.charts.editor.context");
455 //            chartBindingTable.setBindingContext(chartContext);
456 //            
457 //            for (MCommand command : app.getCommands()) {
458 //                if (command.getElementId().equals("org.simantics.chart.canvasCommandDelegateCommand")) {
459 //                    
460 //                    MParameter param = MCommandsFactory.INSTANCE.createParameter();
461 //                    param.setElementId("id");
462 //                    param.setName("org.simantics.charts.commandparameter.canvasCommandDelegateParameter");
463 //                    param.setValue("zoomToFit");
464 //                    MKeyBinding keyBinding = MCommandsFactory.INSTANCE.createKeyBinding();
465 //                    keyBinding.setKeySequence("1");
466 //                    keyBinding.setCommand(command);
467 //                    keyBinding.getParameters().add(param);
468 //                    chartBindingTable.getBindings().add(keyBinding);
469 //                    
470 //                    break;
471 //                }
472 //            }
473 //            
474 //            app.getBindingTables().add(chartBindingTable);
475 //        }
476 //        if (!part.getBindingContexts().contains(chartContext))
477 //            part.getBindingContexts().add(chartContext);
478     }
479
480     private <T extends MApplicationElement> T findMAppElementById(List<T> appElements, String elementId) {
481         for (T element : appElements) {
482             if (element.getElementId() != null && element.getElementId().equals(elementId)) {
483                 return element;
484             }
485         }
486         return null;
487     }
488
489
490     /**
491      * Invoke this only from the AWT thread.
492      * @param context
493      */
494     protected void setCanvasContext(final SWTChassis chassis, final ICanvasContext context) {
495         // Cannot directly invoke SWTChassis.setCanvasContext only because it
496         // needs to be invoked in the SWT thread and AWTChassis.setCanvasContext in the
497         // AWT thread, but directly invoking SWTChassis.setCanvasContext would call both
498         // in the SWT thread which would cause synchronous scheduling of AWT
499         // runnables which is always a potential source of deadlocks.
500         chassis.getAWTComponent().setCanvasContext(context);
501         SWTUtils.asyncExec(chassis, new Runnable() {
502             @Override
503             public void run() {
504                 if (!chassis.isDisposed())
505                     // For AWT, this is a no-operation.
506                     chassis.setCanvasContext(context);
507             }
508         });
509     }
510
511     @Override
512     public void createPartControl(Composite parent) {
513         this.chartDataKey = ChartKeys.chartSourceKey(assertInputModelResource());
514         display = parent.getDisplay();
515         swt = SWTThread.getThreadAccess(display);
516
517         // Must have a project to attach to, otherwise the editor is useless.
518         project = Simantics.peekProject();
519         if (project == null) {
520             errorText = new Text(parent, SWT.NONE);
521             errorText.setText("No project is open.");
522             errorText.setEditable(false);
523             return;
524         }
525
526         // Create the canvas context here before finishing createPartControl
527         // to give anybody requiring access to this editor's ICanvasContext
528         // a chance to do their work.
529         // The context can be created in SWT thread without scheduling
530         // to the context thread and having potential deadlocks.
531         // The context is locked here and unlocked after it has been
532         // initialized in the AWT thread.
533         IThreadWorkQueue thread = AWTThread.getThreadAccess();
534         cvsCtx = new CanvasContext(thread);
535         cvsCtx.setLocked(true);
536
537         canvas = new SWTChassis(parent, SWT.NONE);
538         canvas.populate(parameter -> {
539             if (!isDisposed()) {
540                 canvas.addChassisListener(new ChassisListener());
541                 initializeCanvas(canvas, cvsCtx);
542             }
543         });
544
545         // Link time
546 //        ICommandService service = (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class);
547 //        Command command = service.getCommand( LinkTimeHandler.COMMAND_ID );
548 //        linkTimeState = command.getState( LinkTimeHandler.STATE_ID );
549 //        if ( linkTimeState != null ) linkTimeState.addListener( linkTimeStateListener );
550         
551         // TODO: Link time E4 way!
552
553         
554         linkData = new ChartLinkData();
555         
556         addPopupMenu();
557         
558         // Start tracking editor input validity. 
559         activateValidation();
560
561         // Provide input as selection for property page.
562 //        selectionProvider.setSelection( new StructuredSelection(getInputResource()) );
563         
564         getSelectionService().setSelection(new StructuredSelection(getInputResource()));
565         getSelectionService().addSelectionListener(getPart().getElementId(), new ISelectionListener() {
566             
567             @Override
568             public void selectionChanged(MPart part, Object selection) {
569                 System.out.println("selection changed!");
570             }
571         });
572         
573 //        getSite().setSelectionProvider( selectionProvider );
574     }
575
576     protected void initializeCanvas(final SWTChassis chassis, CanvasContext cvsCtx) {
577         // Initialize canvas context
578         TrendSpec nodata = new TrendSpec();
579         nodata.init();
580         cvsCtx = TrendInitializer.defaultInitializeCanvas(cvsCtx, null, null, null, nodata);
581
582         tp = cvsCtx.getAtMostOneItemOfClass(TrendParticipant.class);
583
584         
585 //        IContextService contextService = (IContextService) getSite().getService(IContextService.class);
586         contextUtil = new ContextUtil(contextService, swt);
587         
588         
589         cvsCtx.add( new SubscriptionDropParticipant( getInputResource() ) );
590         cvsCtx.add( new KeyToCommand( ChartKeyBindings.DEFAULT_BINDINGS ) );
591         cvsCtx.add( new ChartPasteHandler2(getInputResource()) );
592         cvsCtx.add(contextUtil);
593         
594         // Context management
595         cvsCtx.add(new SGFocusParticipant(canvas, "org.simantics.charts.editor.context"));
596
597         stepListener = new StepListener( tp );
598         trendNode = tp.getTrend();
599         trendNode.titleNode.remove();
600         trendNode.titleNode = null;
601
602         // Link time
603         trendNode.horizRuler.listener = horizRulerListener;
604         
605         final ChartLinkData linkTime = linkData;
606         if (linkTime!=null) trendNode.horizRuler.setFromEnd(linkTime.from, linkTime.sx);
607         
608         // Handle mouse moved event after TrendParticipant.
609         // This handler forwards trend.mouseHoverTime to linkTimeState
610         cvsCtx.getEventHandlerStack().add( new IEventHandler() {
611
612                         @Override
613                         public int getEventMask() {
614                                 return EventTypes.MouseMovedMask | EventTypes.MouseClickMask | EventTypes.CommandMask | EventTypes.KeyPressed;
615                         }
616
617                         @Override
618                         public boolean handleEvent(Event e) {
619
620 //                              System.out.println("LinkEventHandler: "+e);
621                                 ChartLinkData oldData = linkData;
622                                 if (oldData!=null) {
623                                         ChartLinkData newData = new ChartLinkData();
624                                         getFromEnd(newData);
625                                         if (!newData.equals(oldData)) {
626 //                                              System.out.println("Sending new link-data");
627                                                 linkData = newData;
628                                                 handleLinkDataChange();
629                                         }
630                                 }
631                                 return false;
632                         }}, -1);
633
634         // Canvas context is initialized, unlock it now to allow rendering.
635         cvsCtx.setLocked(false);
636
637         setCanvasContext(chassis, cvsCtx);
638
639         cvsCtx.getEventHandlerStack().add(new IEventHandler() {
640             @Override
641             public boolean handleEvent(Event e) {
642                 MouseButtonReleasedEvent event = (MouseButtonReleasedEvent) e;
643                 if (event.button != MouseEvent.RIGHT_BUTTON)
644                     return false;
645
646                 final Point p = new Point((int) event.screenPosition.getX(), (int) event.screenPosition.getY());
647                 SWTUtils.asyncExec(chassis, new Runnable() {
648                     @Override
649                     public void run() {
650                         if (!canvas.isDisposed())
651                             showPopup(p);
652                     }
653                 });
654                 return true;
655             }
656             @Override
657             public int getEventMask() {
658                 return EventTypes.MouseButtonReleasedMask;
659             }
660         }, 1000000);
661
662         // Track data source and preinitialize chartData
663         project.addHintListener(chartDataListener);
664         chartData.readFrom( (ChartData) project.getHint( chartDataKey ) );
665
666         if (chartData.run != null) {
667             milestoneListener = new MilestoneSpecListener();
668             milestoneQuery = new MilestoneSpecQuery( chartData.run );
669             Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
670         }
671
672         // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration
673         trackChartConfiguration();
674         trackPreferences();
675
676         // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]
677         // back to the graph database.
678         cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);
679     }
680
681     private void addPopupMenu() {
682
683 //          #TODO: FINISH THIS WHEN E4 WORKBENCH
684 //          MPopupMenu popupMenu = MMenuFactory.INSTANCE.createPopupMenu();
685 //          popupMenu.setLabel("Time Series Editor");
686 //          popupMenu.setElementId(CONTEXT_MENU_ID);
687 //          
688 //        for (MCommand command : getPart().getContext().get(MApplication.class).getCommands()) {
689 //            if (command.getElementId().equals("org.simantics.chart.canvasCommandDelegateCommand")) {
690 //                MParameter param = MCommandsFactory.INSTANCE.createParameter();
691 //                param.setElementId("id");
692 //                param.setName("org.simantics.charts.commandparameter.canvasCommandDelegateParameter");
693 //                param.setValue("zoomToFit");
694 //                MHandledMenuItem mmenuItem = MMenuFactory.INSTANCE.createHandledMenuItem();
695 //                mmenuItem.setLabel("Zoom to Fit");
696 //                mmenuItem.setCommand(command);
697 //                mmenuItem.getParameters().add(param);
698 //            }
699 //        }
700 //          
701 //          getPart().getContext().get(EMenuService.class).registerContextMenu(canvas, CONTEXT_MENU_ID);
702             
703             
704             
705         menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID);
706         menuManager.setRemoveAllWhenShown(true);
707         Menu menu = menuManager.createContextMenu(canvas);
708         canvas.setMenu(menu);
709         // TODO: link to modeled menu in E4 way
710 //        getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider);
711
712         // Add support for some built-in actions in the context menu.
713         menuManager.addMenuListener(new IMenuListener() {
714             @Override
715             public void menuAboutToShow(IMenuManager manager) {
716                 // Not initialized yet, prevent NPE.
717                 TrendNode trendNode = TimeSeriesEditor.this.trendNode;
718                 TrendParticipant tp = TimeSeriesEditor.this.tp;
719                 if (trendNode == null || tp == null)
720                     return;
721
722                 TrendSpec trendSpec = trendNode.getTrendSpec();
723                 ItemNode hoverItem = tp.hoveringItem;
724                 if (hoverItem != null && hoverItem.item != null) {
725                     Resource component = resolveReferencedComponent(hoverItem.item.variableId);
726                     if (component != null) {
727                         manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component));
728                     }
729
730                     Resource chart = TimeSeriesEditor.this.getInputResource();
731                     if ( chart != null ) {
732                         try {
733                             Resource chartItem = Simantics.getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) );
734                             if (chartItem != null) {
735                                 manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem)));
736                                 manager.add(new Separator());
737                                 manager.add(new PropertiesAction("Item Properties", canvas, chartItem));
738                                 manager.add(new PropertiesAction("Chart Properties", canvas, chart));
739                             }
740                         } catch (DatabaseException e) {
741                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));
742                         }
743                     }
744                 } else {
745                     boolean hairlineMovementAllowed =
746                             !(trendSpec.experimentIsRunning &&
747                               trendSpec.viewProfile.trackExperimentTime);
748
749                     Resource chart = TimeSeriesEditor.this.getInputResource();
750                     manager.add(new Separator());
751                     manager.add(new MoveHairlineAction(
752                             "Move Hairline Here",
753                             chart,
754                             hairlineMovementAllowed,
755                             trendNode,
756                             trendNode.mouseHoverTime
757                             ));
758                     manager.add(new MoveHairlineAction(
759                             "Move Hairline To Current Time",
760                             chart,
761                             hairlineMovementAllowed,
762                             trendNode,
763                             trendNode.horizRuler.getItemEndTime(),
764                             Boolean.FALSE
765                             ));
766                     manager.add(new TrackExperimentTimeAction(
767                             "Hairline Tracks Current Time",
768                             chart,
769                             trendSpec.viewProfile.trackExperimentTime));
770                     manager.add(new Separator());
771                     manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT));
772                     manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ));
773                     manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT));
774                     manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE));
775                     manager.add(new Separator());
776                     manager.add(new PropertiesAction("Chart Properties", canvas, chart));
777                 }
778             }
779         });
780     }
781
782     protected Resource resolveReferencedComponent(final String variableId) {
783         try {
784             return Simantics.getSession().sync(new UniqueRead<Resource>() {
785                 @Override
786                 public Resource perform(ReadGraph graph) throws DatabaseException {
787                     Variable configuration = Variables.getConfigurationContext(graph, getInputResource());
788                     RVI rvi = RVI.fromResourceFormat(graph, variableId);
789                     rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI();
790                     if (rvi.isEmpty())
791                         return null;
792                     Variable var = rvi.resolve(graph, configuration);
793                     return var.getPossibleRepresents(graph);
794                 }
795             });
796         } catch (DatabaseException e) {
797             ErrorLogger.defaultLogError(e);
798         }
799         return null;
800     }
801
802     private void showPopup(Point p) {
803         menuManager.getMenu().setLocation(p);
804         menuManager.getMenu().setVisible(true);
805     }
806
807     private void trackChartConfiguration() {
808         Simantics.getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());
809         Simantics.getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());
810     }
811
812     @Override
813     public void setFocus() {
814         if (errorText != null)
815             errorText.setFocus();
816         else
817             canvas.setFocus();
818     }
819
820     @Override
821     public void dispose() {
822         if (isDisposed() == true) return;
823
824         if (trendNode!=null && trendNode.horizRuler!=null) {
825             trendNode.horizRuler.listener = null;
826         }
827
828 //        if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );
829
830         canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );
831         canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );
832         canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );
833
834         if ( chartPreferenceNode!= null ) {
835             chartPreferenceNode.removePreferenceChangeListener( preferenceListener );
836         }
837
838         MilestoneSpecListener ml = milestoneListener;
839         if (ml!=null) ml.dispose();
840
841         if (project != null) {
842             project.removeHintListener(chartDataListener);
843         }
844
845         if (chartData != null) {
846             if (chartData.datasource!=null)
847                 chartData.datasource.removeListener( stepListener );
848             if (chartData.experiment!=null)
849                 chartData.experiment.removeListener( experimentStateListener );
850             chartData.readFrom( null );
851         }
852     }
853
854     /**
855      * @param data new data or null
856      * @param newSpec new spec or null
857      * @thread AWT
858      */
859     @SuppressWarnings("unused")
860     public void setInput(ChartData data, TrendSpec newSpec) {
861         boolean doLayout = false;
862
863         // Disregard input if it is not for this chart's containing model.
864         if (data != null && data.model != null && !data.model.equals(assertInputModelResource()))
865             data = null;
866
867         // Accommodate Datasource changes
868         Datasource: {
869                 Datasource oldDatasource = chartData==null?null:chartData.datasource;
870                 Datasource newDatasource = data==null?null:data.datasource;
871                 //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) ) 
872                 {
873                         if (oldDatasource!=null) oldDatasource.removeListener( stepListener );
874                         if (newDatasource!=null) newDatasource.addListener( stepListener );
875                 }
876         }
877
878         Experiment: {
879             IExperiment oldExperiment = chartData==null?null:chartData.experiment;
880             IExperiment newExperiment = data==null?null:data.experiment;
881             //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) ) 
882             {
883                 if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );
884                 if (newExperiment!=null) newExperiment.addListener( experimentStateListener );
885             }
886         }
887
888         // Accommodate Historian changes
889         Historian: {
890                 HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian;
891                 HistoryManager newHistorian = data==null?null:data.history;
892                 Collector newCollector = data==null?null:data.collector;
893         //      if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) ) 
894                 {
895                         if (newHistorian instanceof FileHistory) {
896                                 FileHistory fh = (FileHistory) newHistorian;
897                                 System.out.println("History = "+fh.getWorkarea());
898                         }
899                         trendNode.setHistorian( newHistorian, newCollector );
900                         doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);
901                 }
902
903                 // Accommodate TrendSpec changes
904                 TrendSpec oldSpec = trendNode.getTrendSpec();
905                 if ( !newSpec.equals(oldSpec) ) {
906                     trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );
907                     doLayout = true;
908                 }
909                 
910         }
911
912         Resource newExperimentResource = data==null ? null : data.run;
913         Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;
914         
915         // Track milestones
916         Milestones: {
917                 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
918
919                         // Dispose old listener & Query
920                         if (milestoneListener!=null) {
921                                 milestoneListener.dispose();
922                                 milestoneListener = null;
923                         }
924                         if (milestoneQuery!=null) {
925                                 milestoneQuery = null;
926                         }
927
928                         trendNode.setMilestones( MilestoneSpec.EMPTY );
929                         
930                         if (newExperimentResource != null) {
931                                 milestoneListener = new MilestoneSpecListener();
932                                 milestoneQuery = new MilestoneSpecQuery( newExperimentResource );
933                                 Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
934                         }
935                 }
936                 
937         }
938
939         if (doLayout) trendNode.layout();
940         this.chartData.readFrom( data );
941         tp.setDirty();
942         
943         if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
944                 resetViewAfterDataChange();
945         }
946         
947     }
948
949     class ActiveRunListener implements SyncListener<Resource> {
950         @Override
951         public void exception(ReadGraph graph, Throwable throwable) {
952             ErrorLogger.defaultLogError(throwable);
953             ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
954         }
955         @Override
956         public void execute(ReadGraph graph, final Resource run) throws DatabaseException {
957                 if(run != null) {
958                         SimulationResource SIMU = SimulationResource.getInstance(graph);
959                         Variable var = Variables.getVariable(graph, run);
960                         IExperiment exp = var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment);
961                         ITrendSupport ts = exp.getService(ITrendSupport.class);
962                         ts.setChartData(graph);
963                 }
964         }
965         @Override
966         public boolean isDisposed() {
967             return TimeSeriesEditor.this.isDisposed();
968         }
969     }
970     
971     class TrendSpecListener implements AsyncListener<TrendSpec> {
972         @Override
973         public void exception(AsyncReadGraph graph, Throwable throwable) {
974                 
975             ErrorLogger.defaultLogError(throwable);
976             ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
977         }
978         @Override
979         public void execute(AsyncReadGraph graph, final TrendSpec result) {
980             if (result == null) {
981                 log.log(Level.INFO, "Chart configuration removed");
982             } else {
983                 log.log(Level.INFO, "Chart configuration updated: " + result);
984             }
985
986             // Reload chart in AWT Thread
987             AWTThread.getThreadAccess().asyncExec(new Runnable() {
988                 @Override
989                 public void run() {
990                     if (!isDisposed())
991                         setInput( chartData, result );
992                 }
993             });
994         }
995         @Override
996         public boolean isDisposed() {
997             return TimeSeriesEditor.this.isDisposed();
998         }
999     }
1000     
1001     class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {
1002         boolean disposed = false;
1003                 @Override
1004                 public void execute(AsyncReadGraph graph, final MilestoneSpec result) {
1005                         AWTThread.INSTANCE.asyncExec(new Runnable() {
1006                                 public void run() {
1007                                         trendNode.setMilestones(result);
1008                                 }});
1009                 }
1010
1011                 @Override
1012                 public void exception(AsyncReadGraph graph, Throwable throwable) {
1013                         
1014                 }
1015
1016                 @Override
1017                 public boolean isDisposed() {
1018                         return disposed;
1019                 }
1020                 
1021                 public void dispose() {
1022                         disposed = true;
1023                 }
1024         
1025     }
1026
1027     private void trackPreferences() {
1028         chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" );
1029         chartPreferenceNode.addPreferenceChangeListener( preferenceListener );
1030         long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
1031         long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
1032         setInterval(redrawInterval, autoscaleInterval);
1033         
1034         String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); 
1035         TimeFormat tf = TimeFormat.valueOf( timeFormat );
1036         if (tf!=null) setTimeFormat( tf );
1037         
1038         Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
1039         setDrawSamples(drawSamples);
1040
1041         String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
1042         ValueFormat vf = ValueFormat.valueOf( valueFormat );
1043         if (vf!=null) setValueFormat( vf );
1044         
1045         String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
1046         ItemPlacement ip = ItemPlacement.valueOf(s);
1047         if (trendNode!=null) trendNode.itemPlacement = ip;
1048         
1049         String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
1050         String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
1051         LineQuality q1 = LineQuality.valueOf(s1);
1052         LineQuality q2 = LineQuality.valueOf(s2);
1053         if (trendNode!=null) trendNode.quality.textQuality = q1;
1054         if (trendNode!=null) trendNode.quality.lineQuality = q2;
1055         
1056     }
1057
1058     private void setInterval(long redrawInterval, long autoscaleInterval) {
1059         redrawInterval = Math.max(1, redrawInterval);
1060         long pulse = Math.min(50, redrawInterval);
1061         pulse = Math.min(pulse, autoscaleInterval);
1062         IHintContext h = canvas.getCanvasContext().getDefaultHintContext();
1063         h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse);
1064         h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval);
1065         h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval);        
1066     }
1067
1068     private void setDrawSamples(boolean value) {
1069         trendNode.drawSamples = value;
1070         trendNode.layout();
1071         tp.setDirty();
1072     }
1073
1074     private void setTimeFormat( TimeFormat tf ) {
1075         if (trendNode.timeFormat == tf) return;
1076         trendNode.timeFormat = tf;
1077         trendNode.layout();
1078         tp.setDirty();
1079     }
1080
1081     private void setValueFormat( ValueFormat vf ) {
1082         if (trendNode.valueFormat == vf) return;
1083         trendNode.valueFormat = vf;
1084         trendNode.layout();
1085         tp.setDirty();
1086     }
1087
1088     @SuppressWarnings("unchecked")
1089     @Override
1090     public <T> T getAdapter(Class<T> adapter) {
1091         if (adapter == INode.class) {
1092             ICanvasContext ctx = cvsCtx;
1093             if (ctx != null)
1094                 return (T) ctx.getSceneGraph();
1095         }
1096         if (adapter == IPropertyPage.class)
1097             // TODO: pure e4 property page without sites
1098             return (T) new StandardPropertyPage(null, getPropertyPageContexts());
1099         if (adapter == ICanvasContext.class)
1100             return (T) cvsCtx;
1101         return null;
1102     }
1103
1104     protected Set<String> getPropertyPageContexts() {
1105         try {
1106             return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT));
1107         } catch (DatabaseException e) {
1108             ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e);
1109             return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT);
1110         }
1111     }
1112
1113     /**
1114      * Add from, end, (scale x) to argument array
1115      * @param fromEnd array of 2 or 3
1116      */
1117     public void getFromEnd(ChartLinkData data) {
1118         data.sender = this;
1119         TrendNode tn = trendNode;
1120         data.valueTipTime = tn.valueTipTime;
1121         HorizRuler hr = tn!=null ? tn.horizRuler : null;
1122         if ( hr != null ) {
1123                 data.from = hr.from;
1124                 data.end = hr.end;
1125                         double len = hr.end-hr.from; 
1126                         double wid = tn.plot.getWidth();
1127                         if ( wid==0.0 ) wid = 0.1;
1128                         data.sx = len/wid;
1129         }
1130     }
1131
1132     @SuppressWarnings("unused")
1133     private static boolean doubleEquals(double a, double b) {
1134         if (Double.isNaN(a) && Double.isNaN(b)) return true;
1135         return a==b;
1136     }
1137
1138     protected void resetViewAfterDataChange() {
1139         
1140         CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);
1141         CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);
1142         
1143     }
1144
1145 //    private Resource getInputResource() {
1146 //        return resource;
1147 //    }
1148 }