]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.charts/src/org/simantics/charts/editor/TimeSeriesEditor.java
Fixed all line endings of the repository
[simantics/platform.git] / bundles / org.simantics.charts / src / org / simantics / charts / editor / TimeSeriesEditor.java
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
590         if (chartData.run != null) {
591             milestoneListener = new MilestoneSpecListener();
592             milestoneQuery = new MilestoneSpecQuery( chartData.run );
593             getSession().asyncRequest( milestoneQuery, milestoneListener );
594         }
595
596         // IMPORTANT: Only after preinitializing chartData, start tracking chart configuration
597         trackChartConfiguration();
598         trackPreferences();
599
600         // Write changes to TrendSpec.viewProfile.valueViewPosition[XY]
601         // back to the graph database.
602         cvsCtx.getHintStack().addHintListener(valueTipBoxPositionListener);
603         }
604
605         private void addPopupMenu() {
606         menuManager = new MenuManager("Time Series Editor", CONTEXT_MENU_ID);
607         menuManager.setRemoveAllWhenShown(true);
608         Menu menu = menuManager.createContextMenu(canvas);
609         canvas.setMenu(menu);
610         getEditorSite().registerContextMenu(menuManager.getId(), menuManager, selectionProvider);
611
612         // Add support for some built-in actions in the context menu.
613         menuManager.addMenuListener(new IMenuListener() {
614             @Override
615             public void menuAboutToShow(IMenuManager manager) {
616                 // Not initialized yet, prevent NPE.
617                 TrendNode trendNode = TimeSeriesEditor.this.trendNode;
618                 TrendParticipant tp = TimeSeriesEditor.this.tp;
619                 if (trendNode == null || tp == null)
620                     return;
621
622                 TrendSpec trendSpec = trendNode.getTrendSpec();
623                 ItemNode hoverItem = tp.hoveringItem;
624                 if (hoverItem != null && hoverItem.item != null) {
625                     Resource component = resolveReferencedComponent(getResourceInput(), hoverItem.item.variableId);
626                     if (component != null) {
627                         manager.add(new PerformDefaultAction("Show Referenced Component", canvas, component));
628                     }
629
630                     Resource chart = TimeSeriesEditor.this.getInputResource();
631                     if ( chart != null ) {
632                         try {
633                             Resource chartItem = getSession().sync( new FindChartItemForTrendItem(chart, hoverItem.item) );
634                             if (chartItem != null) {
635                                 manager.add(new HideItemsAction("Hide Item", true, Collections.singletonList(chartItem)));
636                                 manager.add(new Separator());
637                                 manager.add(new PropertiesAction("Item Properties", canvas, chartItem));
638                                 manager.add(new PropertiesAction("Chart Properties", canvas, chart));
639                             }
640                         } catch (DatabaseException e) {
641                             Activator.getDefault().getLog().log(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Failed to resolve context menu items.", e));
642                         }
643                     }
644                 } else {
645                     boolean hairlineMovementAllowed =
646                             !(trendSpec.experimentIsRunning &&
647                               trendSpec.viewProfile.trackExperimentTime);
648
649                     Resource chart = TimeSeriesEditor.this.getInputResource();
650                     manager.add(new Separator());
651                     manager.add(new MoveHairlineAction(
652                             "Move Hairline Here",
653                             chart,
654                             hairlineMovementAllowed,
655                             trendNode,
656                             trendNode.mouseHoverTime
657                             ));
658                     manager.add(new MoveHairlineAction(
659                             "Move Hairline To Current Time",
660                             chart,
661                             hairlineMovementAllowed,
662                             trendNode,
663                             trendNode.horizRuler.getItemEndTime(),
664                             Boolean.FALSE
665                             ));
666                     manager.add(new TrackExperimentTimeAction(
667                             "Hairline Tracks Current Time",
668                             chart,
669                             trendSpec.viewProfile.trackExperimentTime));
670                     manager.add(new Separator());
671                     manager.add(new SendCommandAction("Zoom to Fit", IMG_ZOOM_TO_FIT, cvsCtx, Commands.ZOOM_TO_FIT));
672                     manager.add(new SendCommandAction("Zoom to Fit Horizontally", IMG_ZOOM_TO_FIT_HORIZ, cvsCtx, Commands.ZOOM_TO_FIT_HORIZ));
673                     manager.add(new SendCommandAction("Zoom to Fit Vertically", IMG_ZOOM_TO_FIT_VERT, cvsCtx, Commands.ZOOM_TO_FIT_VERT));
674                     manager.add(new SendCommandAction("Autoscale Chart", IMG_AUTOSCALE, cvsCtx, Commands.AUTOSCALE));
675                     manager.add(new Separator());
676                     manager.add(new PropertiesAction("Chart Properties", canvas, chart));
677                 }
678             }
679         });
680     }
681
682     protected Resource resolveReferencedComponent(IResourceEditorInput resourceInput, final String variableId) {
683         try {
684             return getSession().sync(new UniqueRead<Resource>() {
685                 @Override
686                 public Resource perform(ReadGraph graph) throws DatabaseException {
687                     Variable configuration = Variables.getConfigurationContext(graph, getInputResource());
688                     RVI rvi = RVI.fromResourceFormat(graph, variableId);
689                     rvi = new RVIBuilder(rvi).removeFromFirstRole(Role.PROPERTY).toRVI();
690                     if (rvi.isEmpty())
691                         return null;
692                     Variable var = rvi.resolve(graph, configuration);
693                     return var.getPossibleRepresents(graph);
694                 }
695             });
696         } catch (DatabaseException e) {
697             ErrorLogger.defaultLogError(e);
698         }
699         return null;
700     }
701
702     private void showPopup(Point p) {
703         menuManager.getMenu().setLocation(p);
704         menuManager.getMenu().setVisible(true);
705     }
706
707     private void trackChartConfiguration() {
708         getSession().asyncRequest(new TrendSpecQuery( uniqueChartEditorId, getInputResource() ), new TrendSpecListener());
709         getSession().asyncRequest(new ActiveRunQuery( uniqueChartEditorId, getInputResource() ), new ActiveRunListener());
710     }
711
712     @Override
713     public void setFocus() {
714         if (errorText != null)
715             errorText.setFocus();
716         else
717             canvas.setFocus();
718     }
719
720     @Override
721     public void dispose() {
722         if (disposed == true) return;
723         disposed = true;
724
725         if (trendNode!=null && trendNode.horizRuler!=null) {
726             trendNode.horizRuler.listener = null;
727         }
728
729         if ( linkTimeState != null ) linkTimeState.removeListener( linkTimeStateListener );
730
731         canvas.getHintContext().removeHint( SWTChassis.KEY_EDITORPART );
732         canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHPAGE );
733         canvas.getHintContext().removeHint( SWTChassis.KEY_WORKBENCHWINDOW );
734
735         if ( chartPreferenceNode!= null ) {
736             chartPreferenceNode.removePreferenceChangeListener( preferenceListener );
737         }
738
739         MilestoneSpecListener ml = milestoneListener;
740         if (ml!=null) ml.dispose();
741
742         if (project != null) {
743             project.removeHintListener(chartDataListener);
744         }
745
746         if (chartData != null) {
747             if (chartData.datasource!=null)
748                 chartData.datasource.removeListener( stepListener );
749             if (chartData.experiment!=null)
750                 chartData.experiment.removeListener( experimentStateListener );
751             chartData.readFrom( null );
752         }
753
754         super.dispose();
755     }
756
757     /**
758      * @param data new data or null
759      * @param newSpec new spec or null
760      * @thread AWT
761      */
762     @SuppressWarnings("unused")
763     public void setInput(ChartData data, TrendSpec newSpec) {
764         boolean doLayout = false;
765
766         // Disregard input if it is not for this chart's containing model.
767         if (data != null && data.model != null && !data.model.equals(model))
768             data = null;
769
770         // Accommodate Datasource changes
771         Datasource: {
772                 Datasource oldDatasource = chartData==null?null:chartData.datasource;
773                 Datasource newDatasource = data==null?null:data.datasource;
774                 //if ( !ObjectUtils.objectEquals(oldDatasource, newDatasource) ) 
775                 {
776                         if (oldDatasource!=null) oldDatasource.removeListener( stepListener );
777                         if (newDatasource!=null) newDatasource.addListener( stepListener );
778                 }
779         }
780
781         Experiment: {
782             IExperiment oldExperiment = chartData==null?null:chartData.experiment;
783             IExperiment newExperiment = data==null?null:data.experiment;
784             //if ( !ObjectUtils.objectEquals(oldExperiment, newExperiment) ) 
785             {
786                 if (oldExperiment!=null) oldExperiment.removeListener( experimentStateListener );
787                 if (newExperiment!=null) newExperiment.addListener( experimentStateListener );
788             }
789         }
790
791         // Accommodate Historian changes
792         Historian: {
793                 HistoryManager oldHistorian = trendNode.historian==null?null:trendNode.historian;
794                 HistoryManager newHistorian = data==null?null:data.history;
795                 Collector newCollector = data==null?null:data.collector;
796         //      if ( !ObjectUtils.objectEquals(oldHistorian, newHistorian) ) 
797                 {
798                         if (newHistorian instanceof FileHistory) {
799                                 FileHistory fh = (FileHistory) newHistorian;
800                                 System.out.println("History = "+fh.getWorkarea());
801                         }
802                         trendNode.setHistorian( newHistorian, newCollector );
803                         doLayout |= trendNode.autoscale(true, true) | !ObjectUtils.objectEquals(oldHistorian, newHistorian);
804                 }
805
806                 // Accommodate TrendSpec changes
807                 TrendSpec oldSpec = trendNode.getTrendSpec();
808                 if ( !newSpec.equals(oldSpec) ) {
809                     trendNode.setTrendSpec( newSpec==null?TrendSpec.EMPTY:newSpec );
810                     doLayout = true;
811                 }
812                 
813         }
814
815         Resource newExperimentResource = data==null ? null : data.run;
816         Resource oldExperimentResource = this.chartData == null ? null : this.chartData.run;
817         
818         // Track milestones
819         Milestones: {
820                 if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
821
822                         // Dispose old listener & Query
823                         if (milestoneListener!=null) {
824                                 milestoneListener.dispose();
825                                 milestoneListener = null;
826                         }
827                         if (milestoneQuery!=null) {
828                                 milestoneQuery = null;
829                         }
830
831                         trendNode.setMilestones( MilestoneSpec.EMPTY );
832                         
833                         if (newExperimentResource != null) {
834                                 milestoneListener = new MilestoneSpecListener();
835                                 milestoneQuery = new MilestoneSpecQuery( newExperimentResource );
836                                 Simantics.getSession().asyncRequest( milestoneQuery, milestoneListener );
837                         }
838                 }
839                 
840         }
841
842         if (doLayout) trendNode.layout();
843         this.chartData.readFrom( data );
844         tp.setDirty();
845         
846         if (!ObjectUtils.objectEquals(oldExperimentResource, newExperimentResource)) {
847                 resetViewAfterDataChange();
848         }
849         
850     }
851
852     class ActiveRunListener implements SyncListener<Resource> {
853         @Override
854         public void exception(ReadGraph graph, Throwable throwable) {
855             ErrorLogger.defaultLogError(throwable);
856             ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
857         }
858         @Override
859         public void execute(ReadGraph graph, final Resource run) throws DatabaseException {
860                 if(run != null) {
861                         SimulationResource SIMU = SimulationResource.getInstance(graph);
862                         Variable var = Variables.getVariable(graph, run);
863                         IExperiment exp = var.getPossiblePropertyValue(graph, SIMU.Run_iExperiment);
864                         ITrendSupport ts = exp.getService(ITrendSupport.class);
865                         if (ts != null)
866                             ts.setChartData(graph);
867                 }
868         }
869         @Override
870         public boolean isDisposed() {
871             return TimeSeriesEditor.this.disposed;
872         }
873     }
874     
875     class TrendSpecListener implements AsyncListener<TrendSpec> {
876         @Override
877         public void exception(AsyncReadGraph graph, Throwable throwable) {
878                 
879             ErrorLogger.defaultLogError(throwable);
880             ShowMessage.showError(throwable.getClass().getSimpleName(), throwable.getMessage());
881         }
882         @Override
883         public void execute(AsyncReadGraph graph, final TrendSpec result) {
884             if (result == null) {
885                 log.log(Level.INFO, "Chart configuration removed");
886             } else {
887                 log.log(Level.INFO, "Chart configuration updated: " + result);
888             }
889
890             // Reload chart in AWT Thread
891             AWTThread.getThreadAccess().asyncExec(new Runnable() {
892                 @Override
893                 public void run() {
894                     if (!disposed)
895                         setInput( chartData, result );
896                 }
897             });
898         }
899         @Override
900         public boolean isDisposed() {
901             return TimeSeriesEditor.this.disposed;
902         }
903     }
904     
905     class MilestoneSpecListener implements AsyncListener<MilestoneSpec> {
906         boolean disposed = false;
907                 @Override
908                 public void execute(AsyncReadGraph graph, final MilestoneSpec result) {
909                         AWTThread.INSTANCE.asyncExec(new Runnable() {
910                                 public void run() {
911                                         trendNode.setMilestones(result);
912                                 }});
913                 }
914
915                 @Override
916                 public void exception(AsyncReadGraph graph, Throwable throwable) {
917                         
918                 }
919
920                 @Override
921                 public boolean isDisposed() {
922                         return disposed;
923                 }
924                 
925                 public void dispose() {
926                         disposed = true;
927                 }
928         
929     }
930
931     private void trackPreferences() {
932         chartPreferenceNode = InstanceScope.INSTANCE.getNode( "org.simantics.charts" );
933         chartPreferenceNode.addPreferenceChangeListener( preferenceListener );
934         long redrawInterval = chartPreferenceNode.getLong(ChartPreferences.P_REDRAW_INTERVAL, ChartPreferences.DEFAULT_REDRAW_INTERVAL);
935         long autoscaleInterval = chartPreferenceNode.getLong(ChartPreferences.P_AUTOSCALE_INTERVAL, ChartPreferences.DEFAULT_AUTOSCALE_INTERVAL);
936         setInterval(redrawInterval, autoscaleInterval);
937         
938         String timeFormat = chartPreferenceNode.get(ChartPreferences.P_TIMEFORMAT, ChartPreferences.DEFAULT_TIMEFORMAT); 
939         TimeFormat tf = TimeFormat.valueOf( timeFormat );
940         if (tf!=null) setTimeFormat( tf );
941         
942         Boolean drawSamples = chartPreferenceNode.getBoolean(ChartPreferences.P_DRAW_SAMPLES, ChartPreferences.DEFAULT_DRAW_SAMPLES);
943         setDrawSamples(drawSamples);
944
945         String valueFormat = chartPreferenceNode.get(ChartPreferences.P_VALUEFORMAT, ChartPreferences.DEFAULT_VALUEFORMAT);
946         ValueFormat vf = ValueFormat.valueOf( valueFormat );
947         if (vf!=null) setValueFormat( vf );
948         
949         String s = chartPreferenceNode.get(ChartPreferences.P_ITEMPLACEMENT, ChartPreferences.DEFAULT_ITEMPLACEMENT);
950         ItemPlacement ip = ItemPlacement.valueOf(s);
951         if (trendNode!=null) trendNode.itemPlacement = ip;
952         
953         String s1 = chartPreferenceNode.get(ChartPreferences.P_TEXTQUALITY, ChartPreferences.DEFAULT_TEXTQUALITY);
954         String s2 = chartPreferenceNode.get(ChartPreferences.P_LINEQUALITY, ChartPreferences.DEFAULT_LINEQUALITY);
955         LineQuality q1 = LineQuality.valueOf(s1);
956         LineQuality q2 = LineQuality.valueOf(s2);
957         if (trendNode!=null) trendNode.quality.textQuality = q1;
958         if (trendNode!=null) trendNode.quality.lineQuality = q2;
959         
960     }
961
962     private void setInterval(long redrawInterval, long autoscaleInterval) {
963         redrawInterval = Math.max(1, redrawInterval);
964         long pulse = Math.min(50, redrawInterval);
965         pulse = Math.min(pulse, autoscaleInterval);
966         IHintContext h = canvas.getCanvasContext().getDefaultHintContext();
967         h.setHint(TimeParticipant.KEY_TIME_PULSE_INTERVAL, pulse);
968         h.setHint(TrendParticipant.KEY_TREND_DRAW_INTERVAL, redrawInterval);
969         h.setHint(TrendParticipant.KEY_TREND_AUTOSCALE_INTERVAL, autoscaleInterval);        
970     }
971
972     private void setDrawSamples(boolean value) {
973         trendNode.drawSamples = value;
974         trendNode.layout();
975         tp.setDirty();
976     }
977
978     private void setTimeFormat( TimeFormat tf ) {
979         if (trendNode.timeFormat == tf) return;
980         trendNode.timeFormat = tf;
981         trendNode.layout();
982         tp.setDirty();
983     }
984
985     private void setValueFormat( ValueFormat vf ) {
986         if (trendNode.valueFormat == vf) return;
987         trendNode.valueFormat = vf;
988         trendNode.layout();
989         tp.setDirty();
990     }
991
992     @SuppressWarnings("rawtypes")
993     @Override
994     public Object getAdapter(Class adapter) {
995         if (adapter == INode.class) {
996             ICanvasContext ctx = cvsCtx;
997             if (ctx != null)
998                 return ctx.getSceneGraph();
999         }
1000         if (adapter == IPropertyPage.class)
1001             return new StandardPropertyPage(getSite(), getPropertyPageContexts());
1002         if (adapter == ICanvasContext.class)
1003             return cvsCtx;
1004         return super.getAdapter(adapter);
1005     }
1006
1007     protected Set<String> getPropertyPageContexts() {
1008         try {
1009             return BrowseContext.getBrowseContextClosure(Simantics.getSession(), Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT));
1010         } catch (DatabaseException e) {
1011             ExceptionUtils.logAndShowError("Failed to load modeled browse contexts for property page, see exception for details.", e);
1012             return Collections.singleton(ROOT_PROPERTY_BROWSE_CONTEXT);
1013         }
1014     }
1015
1016     /**
1017      * Add from, end, (scale x) to argument array
1018      * @param fromEnd array of 2 or 3
1019      */
1020     public void getFromEnd(ChartLinkData data) {
1021         data.sender = this;
1022         TrendNode tn = trendNode;
1023         data.valueTipTime = tn.valueTipTime;
1024         HorizRuler hr = tn!=null ? tn.horizRuler : null;
1025         if ( hr != null ) {
1026                 data.from = hr.from;
1027                 data.end = hr.end;
1028                         double len = hr.end-hr.from; 
1029                         double wid = tn.plot.getWidth();
1030                         if ( wid==0.0 ) wid = 0.1;
1031                         data.sx = len/wid;
1032         }
1033     }
1034
1035     @SuppressWarnings("unused")
1036     private static boolean doubleEquals(double a, double b) {
1037         if (Double.isNaN(a) && Double.isNaN(b)) return true;
1038         return a==b;
1039     }
1040
1041     protected void resetViewAfterDataChange() {
1042         
1043         CanvasUtils.sendCommand(cvsCtx, Commands.CANCEL);
1044         CanvasUtils.sendCommand(cvsCtx, Commands.AUTOSCALE);
1045         
1046     }
1047
1048 }