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