Commit | Line | Data |
---|---|---|
4208b510 AM |
1 | /******************************************************************************* |
2 | * Copyright (c) 2015, 2016 EfficiOS Inc., Michael Jeanson | |
3 | * | |
4 | * All rights reserved. This program and the accompanying materials are | |
5 | * made available under the terms of the Eclipse Public License v1.0 which | |
6 | * accompanies this distribution, and is available at | |
7 | * http://www.eclipse.org/legal/epl-v10.html | |
8 | *******************************************************************************/ | |
9 | ||
10 | package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.viewers; | |
11 | ||
12 | import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull; | |
13 | import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; | |
14 | ||
5b973e7c | 15 | import java.math.BigDecimal; |
4208b510 AM |
16 | import java.text.Format; |
17 | import java.util.ArrayList; | |
18 | import java.util.HashSet; | |
19 | import java.util.List; | |
20 | import java.util.Set; | |
21 | import java.util.concurrent.TimeUnit; | |
22 | import java.util.function.ToDoubleFunction; | |
23 | ||
24 | import org.eclipse.jdt.annotation.NonNull; | |
25 | import org.eclipse.jdt.annotation.Nullable; | |
26 | import org.eclipse.swt.SWT; | |
7710e6ed JR |
27 | import org.eclipse.swt.events.SelectionEvent; |
28 | import org.eclipse.swt.events.SelectionListener; | |
4208b510 AM |
29 | import org.eclipse.swt.graphics.Color; |
30 | import org.eclipse.swt.graphics.Font; | |
31 | import org.eclipse.swt.graphics.GC; | |
7710e6ed | 32 | import org.eclipse.swt.graphics.Image; |
4208b510 AM |
33 | import org.eclipse.swt.graphics.Point; |
34 | import org.eclipse.swt.graphics.Rectangle; | |
35 | import org.eclipse.swt.widgets.Composite; | |
36 | import org.eclipse.swt.widgets.Control; | |
37 | import org.eclipse.swt.widgets.Display; | |
7710e6ed | 38 | import org.eclipse.swt.widgets.Event; |
4208b510 | 39 | import org.eclipse.swt.widgets.Listener; |
7710e6ed JR |
40 | import org.eclipse.swt.widgets.ToolBar; |
41 | import org.eclipse.swt.widgets.ToolItem; | |
4208b510 AM |
42 | import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat; |
43 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.aspect.LamiTableEntryAspect; | |
44 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiChartModel; | |
45 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable; | |
46 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry; | |
5b973e7c JR |
47 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiDecimalUnitFormat; |
48 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.format.LamiTimeStampFormat; | |
4208b510 AM |
49 | import org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.signals.LamiSelectionUpdateSignal; |
50 | import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler; | |
51 | import org.eclipse.tracecompass.tmf.ui.viewers.TmfViewer; | |
7710e6ed JR |
52 | import org.eclipse.ui.ISharedImages; |
53 | import org.eclipse.ui.PlatformUI; | |
4208b510 AM |
54 | import org.swtchart.Chart; |
55 | import org.swtchart.ITitle; | |
56 | ||
57 | import com.google.common.collect.ImmutableList; | |
58 | ||
59 | /** | |
60 | * Abstract XYChart Viewer for LAMI views. | |
61 | * | |
62 | * @author Michael Jeanson | |
63 | * | |
64 | */ | |
65 | public abstract class LamiXYChartViewer extends TmfViewer implements ILamiViewer { | |
66 | ||
67 | /** Ellipsis character */ | |
68 | protected static final String ELLIPSIS = "…"; //$NON-NLS-1$ | |
69 | ||
70 | /** | |
71 | * String representing unknown values. Can be present even in numerical | |
72 | * aspects! | |
73 | */ | |
74 | protected static final String UNKNOWN = "?"; //$NON-NLS-1$ | |
75 | ||
5b973e7c JR |
76 | /** Zero long value */ |
77 | protected static final long ZERO_LONG = 0L; | |
78 | /** Zero double value */ | |
79 | protected static final double ZERO_DOUBLE = 0.0; | |
4208b510 AM |
80 | |
81 | /** | |
82 | * Function to use to map Strings read from the data table to doubles for | |
83 | * use in SWTChart series. | |
84 | */ | |
85 | protected static final ToDoubleFunction<@Nullable String> DOUBLE_MAPPER = str -> { | |
86 | if (str == null || str.equals(UNKNOWN)) { | |
5b973e7c | 87 | return ZERO_LONG; |
4208b510 AM |
88 | } |
89 | return Double.parseDouble(str); | |
90 | }; | |
91 | ||
92 | /** | |
93 | * List of standard colors | |
94 | */ | |
95 | protected static final List<@NonNull Color> COLORS = ImmutableList.of( | |
96 | new Color(Display.getDefault(), 72, 120, 207), | |
97 | new Color(Display.getDefault(), 106, 204, 101), | |
98 | new Color(Display.getDefault(), 214, 95, 95), | |
99 | new Color(Display.getDefault(), 180, 124, 199), | |
100 | new Color(Display.getDefault(), 196, 173, 102), | |
101 | new Color(Display.getDefault(), 119, 190, 219) | |
102 | ); | |
103 | ||
104 | /** | |
105 | * List of "light" colors (when unselected) | |
106 | */ | |
107 | protected static final List<@NonNull Color> LIGHT_COLORS = ImmutableList.of( | |
108 | new Color(Display.getDefault(), 173, 195, 233), | |
109 | new Color(Display.getDefault(), 199, 236, 197), | |
110 | new Color(Display.getDefault(), 240, 196, 196), | |
111 | new Color(Display.getDefault(), 231, 213, 237), | |
112 | new Color(Display.getDefault(), 231, 222, 194), | |
113 | new Color(Display.getDefault(), 220, 238, 246) | |
114 | ); | |
115 | ||
116 | /** | |
117 | * Time stamp formatter for intervals in the days range. | |
118 | */ | |
119 | protected static final LamiTimeStampFormat DAYS_FORMATTER = new LamiTimeStampFormat("dd HH:mm"); //$NON-NLS-1$ | |
120 | ||
121 | /** | |
122 | * Time stamp formatter for intervals in the hours range. | |
123 | */ | |
124 | protected static final LamiTimeStampFormat HOURS_FORMATTER = new LamiTimeStampFormat("HH:mm"); //$NON-NLS-1$ | |
125 | ||
126 | /** | |
127 | * Time stamp formatter for intervals in the minutes range. | |
128 | */ | |
129 | protected static final LamiTimeStampFormat MINUTES_FORMATTER = new LamiTimeStampFormat("mm:ss"); //$NON-NLS-1$ | |
130 | ||
131 | /** | |
132 | * Time stamp formatter for intervals in the seconds range. | |
133 | */ | |
134 | protected static final LamiTimeStampFormat SECONDS_FORMATTER = new LamiTimeStampFormat("ss"); //$NON-NLS-1$ | |
135 | ||
136 | /** | |
137 | * Time stamp formatter for intervals in the milliseconds range. | |
138 | */ | |
139 | protected static final LamiTimeStampFormat MILLISECONDS_FORMATTER = new LamiTimeStampFormat("ss.SSS"); //$NON-NLS-1$ | |
140 | ||
141 | /** | |
142 | * Decimal formatter to display nanoseconds as seconds. | |
143 | */ | |
5b973e7c | 144 | protected static final DecimalUnitFormat NANO_TO_SECS_FORMATTER = new LamiDecimalUnitFormat(0.000000001); |
4208b510 AM |
145 | |
146 | /** | |
147 | * Default decimal formatter. | |
148 | */ | |
5b973e7c JR |
149 | protected static final DecimalUnitFormat DECIMAL_FORMATTER = new LamiDecimalUnitFormat(); |
150 | ||
151 | /** Symbol for seconds (used in the custom ns -> s conversion) */ | |
152 | private static final String SECONDS_SYMBOL = "s"; //$NON-NLS-1$ | |
153 | ||
154 | /** Symbol for nanoseconds (used in the custom ns -> s conversion) */ | |
155 | private static final String NANOSECONDS_SYMBOL = "ns"; //$NON-NLS-1$ | |
156 | ||
157 | /** Maximum amount of digits that can be represented into a double */ | |
158 | private static final int BIG_DECIMAL_DIVISION_SCALE = 22; | |
4208b510 AM |
159 | |
160 | private final Listener fResizeListener = event -> { | |
161 | /* Refresh the titles to fit the current chart size */ | |
162 | refreshDisplayTitles(); | |
163 | ||
164 | /* Refresh the Axis labels to fit the current chart size */ | |
165 | refreshDisplayLabels(); | |
166 | }; | |
167 | ||
168 | private final LamiResultTable fResultTable; | |
169 | private final LamiChartModel fChartModel; | |
170 | ||
171 | private final Chart fChart; | |
172 | ||
173 | private final String fChartTitle; | |
174 | private final String fXTitle; | |
175 | private final String fYTitle; | |
176 | ||
177 | private boolean fSelected; | |
178 | private Set<Integer> fSelection; | |
179 | ||
7710e6ed JR |
180 | private final ToolBar fToolBar; |
181 | ||
4208b510 AM |
182 | /** |
183 | * Creates a Viewer instance based on SWTChart. | |
184 | * | |
185 | * @param parent | |
186 | * The parent composite to draw in. | |
187 | * @param resultTable | |
188 | * The result table containing the data from which to build the | |
189 | * chart | |
190 | * @param chartModel | |
191 | * The information about the chart to build | |
192 | */ | |
193 | public LamiXYChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) { | |
194 | super(parent); | |
195 | ||
196 | fParent = parent; | |
197 | fResultTable = resultTable; | |
198 | fChartModel = chartModel; | |
199 | fSelection = new HashSet<>(); | |
200 | ||
201 | fChart = new Chart(parent, SWT.NONE); | |
202 | fChart.addListener(SWT.Resize, fResizeListener); | |
203 | ||
204 | /* Set Chart title */ | |
205 | fChartTitle = fResultTable.getTableClass().getTableTitle(); | |
206 | ||
207 | /* Set X axis title */ | |
208 | if (fChartModel.getXSeriesColumns().size() == 1) { | |
209 | /* | |
210 | * There is only 1 series in the chart, we will use its name as the | |
211 | * Y axis (and hide the legend). | |
212 | */ | |
213 | String seriesName = getChartModel().getXSeriesColumns().get(0); | |
214 | // The time duration formatter converts ns to s on the axis | |
215 | if (NANOSECONDS_SYMBOL.equals(getXAxisAspects().get(0).getUnits())) { | |
216 | seriesName = getXAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$ | |
217 | } | |
218 | fXTitle = seriesName; | |
219 | } else { | |
220 | /* | |
221 | * There are multiple series in the chart, if they all share the same | |
222 | * units, display that. | |
223 | */ | |
8d9b1c04 | 224 | long nbDiffAspectsUnits = getXAxisAspects().stream() |
4208b510 AM |
225 | .map(aspect -> aspect.getUnits()) |
226 | .distinct() | |
227 | .count(); | |
228 | ||
8d9b1c04 JR |
229 | long nbDiffAspectName = getXAxisAspects().stream() |
230 | .map(aspect -> aspect.getName()) | |
231 | .distinct() | |
232 | .count(); | |
233 | ||
234 | String xBaseTitle = Messages.LamiViewer_DefaultValueName; | |
235 | if (nbDiffAspectName == 1) { | |
236 | xBaseTitle = getXAxisAspects().get(0).getName(); | |
237 | } | |
238 | ||
4208b510 | 239 | String units = getXAxisAspects().get(0).getUnits(); |
8d9b1c04 | 240 | if (nbDiffAspectsUnits == 1 && units != null) { |
4208b510 AM |
241 | /* All aspects use the same unit type */ |
242 | ||
243 | // The time duration formatter converts ns to s on the axis | |
244 | if (NANOSECONDS_SYMBOL.equals(units)) { | |
245 | units = SECONDS_SYMBOL; | |
246 | } | |
8d9b1c04 | 247 | fXTitle = xBaseTitle + " (" + units + ')'; //$NON-NLS-1$ |
4208b510 AM |
248 | } else { |
249 | /* Various unit types, just say "Value" */ | |
8d9b1c04 | 250 | fXTitle = nullToEmptyString(xBaseTitle); |
4208b510 AM |
251 | } |
252 | } | |
253 | ||
254 | /* Set Y axis title */ | |
255 | if (fChartModel.getYSeriesColumns().size() == 1) { | |
256 | /* | |
257 | * There is only 1 series in the chart, we will use its name as the | |
258 | * Y axis (and hide the legend). | |
259 | */ | |
260 | String seriesName = getChartModel().getYSeriesColumns().get(0); | |
261 | // The time duration formatter converts ns to s on the axis | |
262 | if (NANOSECONDS_SYMBOL.equals(getYAxisAspects().get(0).getUnits())) { | |
263 | seriesName = getYAxisAspects().get(0).getName() + " (" + SECONDS_SYMBOL + ')'; //$NON-NLS-1$ | |
264 | } | |
265 | fYTitle = seriesName; | |
266 | fChart.getLegend().setVisible(false); | |
267 | } else { | |
268 | /* | |
269 | * There are multiple series in the chart, if they all share the same | |
270 | * units, display that. | |
271 | */ | |
8d9b1c04 | 272 | long nbDiffAspectsUnits = getYAxisAspects().stream() |
4208b510 AM |
273 | .map(aspect -> aspect.getUnits()) |
274 | .distinct() | |
275 | .count(); | |
276 | ||
8d9b1c04 JR |
277 | long nbDiffAspectName = getYAxisAspects().stream() |
278 | .map(aspect -> aspect.getName()) | |
279 | .distinct() | |
280 | .count(); | |
281 | ||
282 | String yBaseTitle = Messages.LamiViewer_DefaultValueName; | |
283 | if (nbDiffAspectName == 1) { | |
284 | yBaseTitle = getYAxisAspects().get(0).getName(); | |
285 | } | |
286 | ||
4208b510 | 287 | String units = getYAxisAspects().get(0).getUnits(); |
8d9b1c04 | 288 | if (nbDiffAspectsUnits == 1 && units != null) { |
4208b510 AM |
289 | /* All aspects use the same unit type */ |
290 | ||
291 | // The time duration formatter converts ns to s on the axis | |
292 | if (NANOSECONDS_SYMBOL.equals(units)) { | |
293 | units = SECONDS_SYMBOL; | |
294 | } | |
8d9b1c04 | 295 | fYTitle = yBaseTitle + " (" + units + ')'; //$NON-NLS-1$ |
4208b510 | 296 | } else { |
8d9b1c04 JR |
297 | /* Various unit types, don't display any units */ |
298 | fYTitle = nullToEmptyString(yBaseTitle); | |
4208b510 AM |
299 | } |
300 | ||
301 | /* Put legend at the bottom */ | |
302 | fChart.getLegend().setPosition(SWT.BOTTOM); | |
303 | } | |
304 | ||
305 | /* Set all titles and labels font color to black */ | |
306 | fChart.getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
307 | fChart.getAxisSet().getXAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
308 | fChart.getAxisSet().getYAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
309 | fChart.getAxisSet().getXAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
310 | fChart.getAxisSet().getYAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
311 | ||
312 | /* Set X label 90 degrees */ | |
313 | fChart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90); | |
314 | ||
315 | /* Refresh the titles to fit the current chart size */ | |
316 | refreshDisplayTitles(); | |
317 | ||
7710e6ed JR |
318 | fToolBar = createChartToolBar(); |
319 | ||
4208b510 AM |
320 | fChart.addDisposeListener(e -> { |
321 | /* Dispose resources of this class */ | |
322 | LamiXYChartViewer.super.dispose(); | |
323 | }); | |
324 | } | |
325 | ||
326 | /** | |
327 | * Util method to check if a list of aspects are all continuous. | |
328 | * | |
329 | * @param axisAspects | |
330 | * The list of aspects to check. | |
331 | * @return true is all aspects are continuous, otherwise false. | |
332 | */ | |
333 | protected static boolean areAspectsContinuous(List<LamiTableEntryAspect> axisAspects) { | |
334 | return axisAspects.stream().allMatch(aspect -> aspect.isContinuous()); | |
335 | } | |
336 | ||
337 | /** | |
338 | * Util method to check if a list of aspects are all time stamps. | |
339 | * | |
340 | * @param axisAspects | |
341 | * The list of aspects to check. | |
342 | * @return true is all aspects are time stamps, otherwise false. | |
343 | */ | |
344 | protected static boolean areAspectsTimeStamp(List<LamiTableEntryAspect> axisAspects) { | |
345 | return axisAspects.stream().allMatch(aspect -> aspect.isTimeStamp()); | |
346 | } | |
347 | ||
348 | /** | |
349 | * Util method to check if a list of aspects are all time durations. | |
350 | * | |
351 | * @param axisAspects | |
352 | * The list of aspects to check. | |
353 | * @return true is all aspects are time durations, otherwise false. | |
354 | */ | |
355 | protected static boolean areAspectsTimeDuration(List<LamiTableEntryAspect> axisAspects) { | |
356 | return axisAspects.stream().allMatch(aspect -> aspect.isTimeDuration()); | |
357 | } | |
358 | ||
359 | /** | |
360 | * Util method that will return a formatter based on the aspects linked to an axis | |
361 | * | |
362 | * If all aspects are time stamps, return a timestamp formatter tuned to the interval. | |
363 | * If all aspects are time durations, return the nanoseconds to seconds formatter. | |
364 | * Otherwise, return the generic decimal formatter. | |
365 | * | |
366 | * @param axisAspects | |
367 | * The list of aspects of the axis. | |
368 | * @param entries | |
369 | * The list of entries of the chart. | |
5b973e7c JR |
370 | * @param internalRange |
371 | * The internal range for value transformation | |
372 | * @param externalRange | |
373 | * The external range for value transformation | |
4208b510 AM |
374 | * @return a formatter for the axis. |
375 | */ | |
5b973e7c JR |
376 | protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries , @Nullable LamiGraphRange internalRange, @Nullable LamiGraphRange externalRange) { |
377 | ||
378 | Format formatter = DECIMAL_FORMATTER; | |
4208b510 AM |
379 | |
380 | if (areAspectsTimeStamp(axisAspects)) { | |
381 | /* Set a TimeStamp formatter depending on the duration between the first and last value */ | |
5b973e7c JR |
382 | BigDecimal max = new BigDecimal(Long.MIN_VALUE); |
383 | BigDecimal min = new BigDecimal(Long.MAX_VALUE); | |
4208b510 AM |
384 | |
385 | for (LamiTableEntry entry : entries) { | |
386 | for (LamiTableEntryAspect aspect : axisAspects) { | |
5b973e7c JR |
387 | @Nullable Number number = aspect.resolveNumber(entry); |
388 | if (number != null) { | |
389 | BigDecimal current = new BigDecimal(number.toString()); | |
390 | max = current.max(max); | |
391 | min = current.min(min); | |
4208b510 AM |
392 | } |
393 | } | |
394 | } | |
4208b510 | 395 | |
5b973e7c | 396 | long duration = max.subtract(min).longValue(); |
4208b510 | 397 | if (duration > TimeUnit.DAYS.toNanos(1)) { |
5b973e7c | 398 | formatter = DAYS_FORMATTER; |
4208b510 | 399 | } else if (duration > TimeUnit.HOURS.toNanos(1)) { |
5b973e7c | 400 | formatter = HOURS_FORMATTER; |
4208b510 | 401 | } else if (duration > TimeUnit.MINUTES.toNanos(1)) { |
5b973e7c | 402 | formatter = MINUTES_FORMATTER; |
4208b510 | 403 | } else if (duration > TimeUnit.SECONDS.toNanos(15)) { |
5b973e7c | 404 | formatter = SECONDS_FORMATTER; |
4208b510 | 405 | } else { |
5b973e7c | 406 | formatter = MILLISECONDS_FORMATTER; |
4208b510 | 407 | } |
5b973e7c JR |
408 | ((LamiTimeStampFormat) formatter).setInternalRange(internalRange); |
409 | ((LamiTimeStampFormat) formatter).setExternalRange(externalRange); | |
410 | ||
4208b510 | 411 | } else if (areAspectsTimeDuration(axisAspects)) { |
5b973e7c JR |
412 | /* Set the time duration formatter. */ |
413 | formatter = NANO_TO_SECS_FORMATTER; | |
414 | ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange); | |
415 | ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange); | |
4208b510 AM |
416 | |
417 | } else { | |
5b973e7c JR |
418 | /* |
419 | * For other numeric aspects, use the default lami decimal unit | |
420 | * formatter. | |
421 | */ | |
422 | formatter = DECIMAL_FORMATTER; | |
423 | ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange); | |
424 | ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange); | |
4208b510 | 425 | } |
5b973e7c JR |
426 | |
427 | return formatter; | |
4208b510 AM |
428 | } |
429 | ||
430 | /** | |
431 | * Get the chart result table. | |
432 | * | |
433 | * @return The chart result table. | |
434 | */ | |
435 | protected LamiResultTable getResultTable() { | |
436 | return fResultTable; | |
437 | } | |
438 | ||
439 | /** | |
440 | * Get the chart model. | |
441 | * | |
442 | * @return The chart model. | |
443 | */ | |
444 | protected LamiChartModel getChartModel() { | |
445 | return fChartModel; | |
446 | } | |
447 | ||
448 | /** | |
449 | * Get the chart object. | |
450 | * @return The chart object. | |
451 | */ | |
452 | protected Chart getChart() { | |
453 | return fChart; | |
454 | } | |
455 | ||
7710e6ed JR |
456 | /** |
457 | * @return the toolBar | |
458 | */ | |
459 | public ToolBar getToolBar() { | |
460 | return fToolBar; | |
461 | } | |
462 | ||
4208b510 AM |
463 | /** |
464 | * Is a selection made in the chart. | |
465 | * | |
466 | * @return true if there is a selection. | |
467 | */ | |
468 | protected boolean isSelected() { | |
469 | return fSelected; | |
470 | } | |
471 | ||
472 | /** | |
473 | * Set the selection index. | |
474 | * | |
475 | * @param selection the index to select. | |
476 | */ | |
477 | protected void setSelection(Set<Integer> selection) { | |
478 | fSelection = selection; | |
479 | fSelected = !selection.isEmpty(); | |
480 | } | |
481 | ||
482 | /** | |
483 | * Unset the chart selection. | |
484 | */ | |
485 | protected void unsetSelection() { | |
486 | fSelection.clear(); | |
487 | fSelected = false; | |
488 | } | |
489 | ||
490 | /** | |
491 | * Get the current selection index. | |
492 | * | |
493 | * @return the current selection index. | |
494 | */ | |
495 | protected Set<Integer> getSelection() { | |
496 | return fSelection; | |
497 | } | |
498 | ||
499 | @Override | |
500 | public @Nullable Control getControl() { | |
501 | return fChart.getParent(); | |
502 | } | |
503 | ||
504 | @Override | |
505 | public void refresh() { | |
506 | Display.getDefault().asyncExec(() -> { | |
507 | if (!fChart.isDisposed()) { | |
508 | fChart.redraw(); | |
509 | } | |
510 | }); | |
511 | } | |
512 | ||
513 | @Override | |
514 | public void dispose() { | |
515 | fChart.dispose(); | |
516 | /* The control's DisposeListener will call super.dispose() */ | |
517 | } | |
518 | ||
519 | /** | |
520 | * Get a list of all the aspect of the Y axis. | |
521 | * | |
522 | * @return The aspects for the Y axis | |
523 | */ | |
524 | protected List<LamiTableEntryAspect> getYAxisAspects() { | |
525 | ||
526 | List<LamiTableEntryAspect> yAxisAspects = new ArrayList<>(); | |
527 | ||
528 | for (String colName : getChartModel().getYSeriesColumns()) { | |
529 | yAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); | |
530 | } | |
531 | ||
532 | return yAxisAspects; | |
533 | } | |
534 | ||
535 | /** | |
536 | * Get a list of all the aspect of the X axis. | |
537 | * | |
538 | * @return The aspects for the X axis | |
539 | */ | |
540 | protected List<LamiTableEntryAspect> getXAxisAspects() { | |
541 | ||
542 | List<LamiTableEntryAspect> xAxisAspects = new ArrayList<>(); | |
543 | ||
544 | for (String colName : getChartModel().getXSeriesColumns()) { | |
545 | xAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); | |
546 | } | |
547 | ||
548 | return xAxisAspects; | |
549 | } | |
550 | ||
551 | /** | |
552 | * Set the ITitle object text to a substring of canonicalTitle that when | |
553 | * rendered in the chart will fit maxPixelLength. | |
554 | */ | |
555 | private void refreshDisplayTitle(ITitle title, String canonicalTitle, int maxPixelLength) { | |
556 | if (title.isVisible()) { | |
557 | ||
558 | String newTitle = canonicalTitle; | |
559 | ||
560 | /* Get the title font */ | |
561 | Font font = title.getFont(); | |
562 | ||
563 | GC gc = new GC(fParent); | |
564 | gc.setFont(font); | |
565 | ||
566 | /* Get the length and height of the canonical title in pixels */ | |
567 | Point pixels = gc.stringExtent(canonicalTitle); | |
568 | ||
569 | /* | |
570 | * If the title is too long, generate a shortened version based on the | |
571 | * average character width of the current font. | |
572 | */ | |
573 | if (pixels.x > maxPixelLength) { | |
574 | int charwidth = gc.getFontMetrics().getAverageCharWidth(); | |
575 | ||
576 | int minimum = 3; | |
577 | ||
578 | int strLen = ((maxPixelLength / charwidth) - minimum); | |
579 | ||
580 | if (strLen > minimum) { | |
581 | newTitle = canonicalTitle.substring(0, strLen) + ELLIPSIS; | |
582 | } else { | |
583 | newTitle = ELLIPSIS; | |
584 | } | |
585 | } | |
586 | ||
587 | title.setText(newTitle); | |
588 | ||
589 | // Cleanup | |
590 | gc.dispose(); | |
591 | } | |
592 | } | |
593 | ||
594 | /** | |
595 | * Refresh the Chart, XAxis and YAxis titles to fit the current | |
596 | * chart size. | |
597 | */ | |
598 | private void refreshDisplayTitles() { | |
599 | Rectangle chartRect = fChart.getClientArea(); | |
600 | Rectangle plotRect = fChart.getPlotArea().getClientArea(); | |
601 | ||
602 | ITitle chartTitle = checkNotNull(fChart.getTitle()); | |
603 | refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width); | |
604 | ||
605 | ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle()); | |
606 | refreshDisplayTitle(xTitle, fXTitle, plotRect.width); | |
607 | ||
608 | ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle()); | |
609 | refreshDisplayTitle(yTitle, fYTitle, plotRect.height); | |
610 | } | |
611 | ||
612 | /** | |
613 | * Get the aspect with the given name | |
614 | * | |
615 | * @param aspects | |
616 | * The list of aspects to search into | |
617 | * @param aspectName | |
618 | * The name of the aspect we are looking for | |
619 | * @return The corresponding aspect | |
620 | */ | |
621 | protected static @Nullable LamiTableEntryAspect getAspectFromName(List<LamiTableEntryAspect> aspects, String aspectName) { | |
622 | for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { | |
623 | ||
624 | if (lamiTableEntryAspect.getLabel().equals(aspectName)) { | |
625 | return lamiTableEntryAspect; | |
626 | } | |
627 | } | |
628 | ||
629 | return null; | |
630 | } | |
631 | ||
632 | /** | |
633 | * Refresh the axis labels to fit the current chart size. | |
634 | */ | |
635 | protected abstract void refreshDisplayLabels(); | |
636 | ||
637 | /** | |
638 | * Redraw the chart. | |
639 | */ | |
640 | protected void redraw() { | |
641 | refresh(); | |
642 | } | |
643 | ||
644 | /** | |
645 | * Signal handler for selection update. | |
646 | * | |
647 | * @param signal | |
648 | * The selection update signal | |
649 | */ | |
650 | @TmfSignalHandler | |
651 | public void updateSelection(LamiSelectionUpdateSignal signal) { | |
652 | if (getResultTable().hashCode() != signal.getSignalHash() || equals(signal.getSource())) { | |
653 | /* The signal is not for us */ | |
654 | return; | |
655 | } | |
656 | setSelection(signal.getEntryIndex()); | |
657 | ||
658 | redraw(); | |
659 | } | |
7710e6ed JR |
660 | |
661 | /** | |
662 | * Create a tool bar on top right of the chart. Contained actions: | |
663 | * <ul> | |
664 | * <li>Dispose the current viewer, also known as "Close the chart"</li> | |
665 | * </ul> | |
666 | * | |
667 | * This tool bar should only appear when the mouse enters the composite. | |
668 | * | |
669 | * @return the tool bar | |
670 | */ | |
671 | protected ToolBar createChartToolBar() { | |
672 | Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE); | |
673 | ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL); | |
674 | ||
675 | /* Default state */ | |
676 | toolBar.moveAbove(null); | |
677 | toolBar.setVisible(false); | |
678 | ||
679 | /* | |
680 | * Close chart button | |
681 | */ | |
682 | ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH); | |
683 | closeButton.setImage(removeImage); | |
684 | closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip); | |
685 | closeButton.addSelectionListener(new SelectionListener() { | |
686 | @Override | |
687 | public void widgetSelected(@Nullable SelectionEvent e) { | |
688 | Composite parent = getParent(); | |
689 | dispose(); | |
690 | parent.layout(); | |
691 | } | |
692 | ||
693 | @Override | |
694 | public void widgetDefaultSelected(@Nullable SelectionEvent e) { | |
695 | } | |
696 | }); | |
697 | ||
698 | toolBar.pack(); | |
699 | toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); | |
700 | ||
701 | /* Visibility toggle filter */ | |
702 | Listener toolBarVisibilityToggleListener = e -> { | |
703 | if (e.widget instanceof Control) { | |
704 | Control control = (Control) e.widget; | |
705 | Point display = control.toDisplay(e.x, e.y); | |
706 | Point location = getChart().getParent().toControl(display); | |
707 | ||
708 | /* | |
709 | * Only set to visible if we are at the right location, in the | |
710 | * right shell. | |
711 | */ | |
712 | boolean visible = getChart().getBounds().contains(location) && | |
713 | control.getShell().equals(getChart().getShell()); | |
714 | getToolBar().setVisible(visible); | |
715 | } | |
716 | }; | |
717 | ||
718 | /* Filter to make sure we hide the toolbar if we exit the window */ | |
719 | Listener hideToolBarListener = (e -> getToolBar().setVisible(false)); | |
720 | ||
721 | /* | |
722 | * Add the filters to the main Display, and remove them when we dispose | |
723 | * the chart. | |
724 | */ | |
725 | Display display = getChart().getDisplay(); | |
726 | display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); | |
727 | display.addFilter(SWT.MouseExit, hideToolBarListener); | |
728 | ||
729 | getChart().addDisposeListener(e -> { | |
730 | display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); | |
731 | display.removeFilter(SWT.MouseExit, hideToolBarListener); | |
732 | }); | |
733 | ||
734 | /* Reposition the tool bar on resize */ | |
735 | getChart().addListener(SWT.Resize, new Listener() { | |
736 | @Override | |
737 | public void handleEvent(@Nullable Event event) { | |
738 | toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); | |
739 | } | |
740 | }); | |
741 | ||
742 | return toolBar; | |
743 | } | |
5b973e7c JR |
744 | |
745 | /** | |
746 | * Get a {@link LamiGraphRange} that covers all data points in the result | |
747 | * table. | |
748 | * <p> | |
749 | * The returned range will be the minimum and maximum of the resolved values | |
750 | * of the passed aspects for all result entries. If <code>clampToZero</code> | |
751 | * is true, a positive minimum value will be clamped down to zero. | |
752 | * | |
753 | * @param aspects | |
754 | * The aspects that the range will represent | |
755 | * @param clampToZero | |
756 | * If true, a positive minimum value will be clamped down to zero | |
757 | * @return the range | |
758 | */ | |
759 | protected LamiGraphRange getRange(List<LamiTableEntryAspect> aspects, boolean clampToZero) { | |
760 | /* Find the minimum and maximum values */ | |
761 | BigDecimal min = new BigDecimal(Long.MAX_VALUE); | |
762 | BigDecimal max = new BigDecimal(Long.MIN_VALUE); | |
763 | for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { | |
764 | for (LamiTableEntry entry : getResultTable().getEntries()) { | |
765 | @Nullable Number number = lamiTableEntryAspect.resolveNumber(entry); | |
766 | if (number != null) { | |
767 | BigDecimal current = new BigDecimal(number.toString()); | |
768 | min = current.min(min); | |
769 | max = current.max(max); | |
770 | } | |
771 | } | |
772 | } | |
773 | ||
774 | if (clampToZero) { | |
775 | min.min(BigDecimal.ZERO); | |
776 | } | |
777 | ||
778 | /* Do not allow a range with a zero delta default to 1 */ | |
779 | if (max.equals(min)) { | |
780 | max = min.add(BigDecimal.ONE); | |
781 | } | |
782 | ||
783 | return new LamiGraphRange(checkNotNull(min), checkNotNull(max)); | |
784 | } | |
785 | ||
786 | /** | |
787 | * Transform an external value into an internal value. Since SWTChart only | |
788 | * support Double and Lami can pass Long values, loss of precision might | |
789 | * happen. To minimize this, transform the raw values to an internal | |
790 | * representation based on a linear transformation. | |
791 | * | |
792 | * The internal value = | |
793 | * | |
794 | * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) + | |
795 | * internalMinimum | |
796 | * | |
797 | * @param number | |
798 | * The number to transform | |
799 | * @param internalRange | |
800 | * The internal range definition to be used | |
801 | * @param externalRange | |
802 | * The external range definition to be used | |
803 | * @return the transformed value in Double comprised inside the internal | |
804 | * range | |
805 | */ | |
806 | protected static double getInternalDoubleValue(Number number, LamiGraphRange internalRange, LamiGraphRange externalRange) { | |
807 | BigDecimal value = new BigDecimal(number.toString()); | |
808 | ||
809 | if (externalRange.getDelta().compareTo(BigDecimal.ZERO) == 0) { | |
810 | return internalRange.getMinimum().doubleValue(); | |
811 | } | |
812 | ||
813 | BigDecimal internalValue = value | |
814 | .subtract(externalRange.getMinimum()) | |
815 | .multiply(internalRange.getDelta()) | |
816 | .divide(externalRange.getDelta(), BIG_DECIMAL_DIVISION_SCALE, BigDecimal.ROUND_DOWN) | |
817 | .add(internalRange.getMinimum()); | |
818 | ||
819 | return internalValue.doubleValue(); | |
820 | } | |
4208b510 | 821 | } |