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