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; | |
b7156f6b MJ |
174 | |
175 | private String fXLabel; | |
176 | private @Nullable String fXUnits; | |
177 | ||
178 | private String fYLabel; | |
179 | private @Nullable String fYUnits; | |
4208b510 AM |
180 | |
181 | private boolean fSelected; | |
182 | private Set<Integer> fSelection; | |
183 | ||
7710e6ed JR |
184 | private final ToolBar fToolBar; |
185 | ||
4208b510 AM |
186 | /** |
187 | * Creates a Viewer instance based on SWTChart. | |
188 | * | |
189 | * @param parent | |
190 | * The parent composite to draw in. | |
191 | * @param resultTable | |
192 | * The result table containing the data from which to build the | |
193 | * chart | |
194 | * @param chartModel | |
195 | * The information about the chart to build | |
196 | */ | |
197 | public LamiXYChartViewer(Composite parent, LamiResultTable resultTable, LamiChartModel chartModel) { | |
198 | super(parent); | |
199 | ||
200 | fParent = parent; | |
201 | fResultTable = resultTable; | |
202 | fChartModel = chartModel; | |
203 | fSelection = new HashSet<>(); | |
204 | ||
b7156f6b MJ |
205 | fXLabel = ""; //$NON-NLS-1$ |
206 | fYLabel = ""; //$NON-NLS-1$ | |
207 | ||
4208b510 AM |
208 | fChart = new Chart(parent, SWT.NONE); |
209 | fChart.addListener(SWT.Resize, fResizeListener); | |
210 | ||
211 | /* Set Chart title */ | |
212 | fChartTitle = fResultTable.getTableClass().getTableTitle(); | |
213 | ||
214 | /* Set X axis title */ | |
215 | if (fChartModel.getXSeriesColumns().size() == 1) { | |
216 | /* | |
217 | * There is only 1 series in the chart, we will use its name as the | |
b7156f6b | 218 | * X axis. |
4208b510 | 219 | */ |
b7156f6b | 220 | innerSetXTitle(getXAxisAspects().get(0).getName(), getXAxisAspects().get(0).getUnits()); |
4208b510 AM |
221 | } else { |
222 | /* | |
223 | * There are multiple series in the chart, if they all share the same | |
224 | * units, display that. | |
225 | */ | |
8d9b1c04 | 226 | long nbDiffAspectsUnits = getXAxisAspects().stream() |
4208b510 AM |
227 | .map(aspect -> aspect.getUnits()) |
228 | .distinct() | |
229 | .count(); | |
230 | ||
8d9b1c04 JR |
231 | long nbDiffAspectName = getXAxisAspects().stream() |
232 | .map(aspect -> aspect.getName()) | |
233 | .distinct() | |
234 | .count(); | |
235 | ||
236 | String xBaseTitle = Messages.LamiViewer_DefaultValueName; | |
237 | if (nbDiffAspectName == 1) { | |
238 | xBaseTitle = getXAxisAspects().get(0).getName(); | |
239 | } | |
240 | ||
b7156f6b MJ |
241 | String units = null; |
242 | if (nbDiffAspectsUnits == 1) { | |
4208b510 | 243 | /* All aspects use the same unit type */ |
b7156f6b | 244 | units = getXAxisAspects().get(0).getUnits(); |
4208b510 | 245 | } |
b7156f6b MJ |
246 | |
247 | innerSetXTitle(xBaseTitle, units); | |
4208b510 AM |
248 | } |
249 | ||
250 | /* Set Y axis title */ | |
251 | if (fChartModel.getYSeriesColumns().size() == 1) { | |
252 | /* | |
253 | * There is only 1 series in the chart, we will use its name as the | |
254 | * Y axis (and hide the legend). | |
255 | */ | |
b7156f6b MJ |
256 | innerSetYTitle(getYAxisAspects().get(0).getName(), getYAxisAspects().get(0).getUnits()); |
257 | ||
258 | /* Hide the legend */ | |
4208b510 AM |
259 | fChart.getLegend().setVisible(false); |
260 | } else { | |
261 | /* | |
262 | * There are multiple series in the chart, if they all share the same | |
263 | * units, display that. | |
264 | */ | |
8d9b1c04 | 265 | long nbDiffAspectsUnits = getYAxisAspects().stream() |
4208b510 AM |
266 | .map(aspect -> aspect.getUnits()) |
267 | .distinct() | |
268 | .count(); | |
269 | ||
8d9b1c04 JR |
270 | long nbDiffAspectName = getYAxisAspects().stream() |
271 | .map(aspect -> aspect.getName()) | |
272 | .distinct() | |
273 | .count(); | |
274 | ||
275 | String yBaseTitle = Messages.LamiViewer_DefaultValueName; | |
276 | if (nbDiffAspectName == 1) { | |
277 | yBaseTitle = getYAxisAspects().get(0).getName(); | |
278 | } | |
279 | ||
b7156f6b MJ |
280 | String units = null; |
281 | if (nbDiffAspectsUnits == 1) { | |
4208b510 | 282 | /* All aspects use the same unit type */ |
b7156f6b | 283 | units = getYAxisAspects().get(0).getUnits(); |
4208b510 AM |
284 | } |
285 | ||
b7156f6b MJ |
286 | innerSetYTitle(yBaseTitle, units); |
287 | ||
4208b510 AM |
288 | /* Put legend at the bottom */ |
289 | fChart.getLegend().setPosition(SWT.BOTTOM); | |
290 | } | |
291 | ||
292 | /* Set all titles and labels font color to black */ | |
293 | fChart.getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
294 | fChart.getAxisSet().getXAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
295 | fChart.getAxisSet().getYAxis(0).getTitle().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
296 | fChart.getAxisSet().getXAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
297 | fChart.getAxisSet().getYAxis(0).getTick().setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); | |
298 | ||
299 | /* Set X label 90 degrees */ | |
300 | fChart.getAxisSet().getXAxis(0).getTick().setTickLabelAngle(90); | |
301 | ||
302 | /* Refresh the titles to fit the current chart size */ | |
303 | refreshDisplayTitles(); | |
304 | ||
7710e6ed JR |
305 | fToolBar = createChartToolBar(); |
306 | ||
4208b510 AM |
307 | fChart.addDisposeListener(e -> { |
308 | /* Dispose resources of this class */ | |
309 | LamiXYChartViewer.super.dispose(); | |
310 | }); | |
311 | } | |
312 | ||
b7156f6b MJ |
313 | /** |
314 | * Set the Y axis title and refresh the chart. | |
315 | * | |
316 | * @param label | |
317 | * the label string. | |
318 | * @param units | |
319 | * the units string. | |
320 | */ | |
321 | protected void setYTitle(@Nullable String label, @Nullable String units) { | |
322 | innerSetYTitle(label, units); | |
323 | } | |
324 | ||
325 | private void innerSetYTitle(@Nullable String label, @Nullable String units) { | |
326 | fYLabel = nullToEmptyString(label); | |
327 | innerSetYUnits(units); | |
328 | refreshDisplayTitles(); | |
329 | } | |
330 | ||
331 | /** | |
332 | * Set the units on the Y Axis title and refresh the chart. | |
333 | * | |
334 | * @param units | |
335 | * the units string. | |
336 | */ | |
337 | protected void setYUnits(@Nullable String units) { | |
338 | innerSetYUnits(units); | |
339 | } | |
340 | ||
341 | private void innerSetYUnits(@Nullable String units) { | |
342 | /* | |
343 | * All time durations in the Lami protocol are nanoseconds, on the | |
344 | * charts we use an axis formater that converts back to seconds as a | |
345 | * base unit and then uses prefixes like nano and milli depending on the | |
346 | * range. | |
347 | * | |
348 | * So set the units to seconds in the title to match the base unit of | |
349 | * the formater. | |
350 | */ | |
351 | if (NANOSECONDS_SYMBOL.equals(units)) { | |
352 | fYUnits = SECONDS_SYMBOL; | |
353 | } else { | |
354 | fYUnits = units; | |
355 | } | |
356 | refreshDisplayTitles(); | |
357 | } | |
358 | ||
359 | /** | |
360 | * Get the Y axis title string. | |
361 | * | |
362 | * If the units is non-null, the title will be: "label (units)" | |
363 | * | |
364 | * If the units is null, the title will be: "label" | |
365 | * | |
366 | * @return the title of the Y axis. | |
367 | */ | |
368 | protected String getYTitle() { | |
369 | if (fYUnits == null) { | |
370 | return fYLabel; | |
371 | } | |
372 | return fYLabel + " (" + fYUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$ | |
373 | } | |
374 | ||
375 | /** | |
376 | * Set the X axis title and refresh the chart. | |
377 | * | |
378 | * @param label | |
379 | * the label string. | |
380 | * @param units | |
381 | * the units string. | |
382 | */ | |
383 | protected void setXTitle(@Nullable String label, @Nullable String units) { | |
384 | innerSetXTitle(label, units); | |
385 | } | |
386 | ||
387 | private void innerSetXTitle(@Nullable String label, @Nullable String units) { | |
388 | fXLabel = nullToEmptyString(label); | |
389 | innerSetXUnits(units); | |
390 | refreshDisplayTitles(); | |
391 | } | |
392 | ||
393 | /** | |
394 | * Set the units on the X Axis title. | |
395 | * | |
396 | * @param units | |
397 | * the units string | |
398 | */ | |
399 | protected void setXUnits(@Nullable String units) { | |
400 | innerSetXUnits(units); | |
401 | } | |
402 | ||
403 | private void innerSetXUnits(@Nullable String units) { | |
404 | /* The time duration formatter converts ns to s on the axis */ | |
405 | if (NANOSECONDS_SYMBOL.equals(units)) { | |
406 | fXUnits = SECONDS_SYMBOL; | |
407 | } else { | |
408 | fXUnits = units; | |
409 | } | |
410 | refreshDisplayTitles(); | |
411 | } | |
412 | ||
413 | /** | |
414 | * Get the X axis title string. | |
415 | * | |
416 | * If the units is non-null, the title will be: "label (units)" | |
417 | * | |
418 | * If the units is null, the title will be: "label" | |
419 | * | |
420 | * @return the title of the Y axis. | |
421 | */ | |
422 | protected String getXTitle() { | |
423 | if (fXUnits == null) { | |
424 | return fXLabel; | |
425 | } | |
426 | return fXLabel + " (" + fXUnits + ")"; //$NON-NLS-1$ //$NON-NLS-2$ | |
427 | } | |
428 | ||
4208b510 AM |
429 | /** |
430 | * Util method to check if a list of aspects are all continuous. | |
431 | * | |
432 | * @param axisAspects | |
433 | * The list of aspects to check. | |
434 | * @return true is all aspects are continuous, otherwise false. | |
435 | */ | |
436 | protected static boolean areAspectsContinuous(List<LamiTableEntryAspect> axisAspects) { | |
437 | return axisAspects.stream().allMatch(aspect -> aspect.isContinuous()); | |
438 | } | |
439 | ||
440 | /** | |
441 | * Util method to check if a list of aspects are all time stamps. | |
442 | * | |
443 | * @param axisAspects | |
444 | * The list of aspects to check. | |
445 | * @return true is all aspects are time stamps, otherwise false. | |
446 | */ | |
447 | protected static boolean areAspectsTimeStamp(List<LamiTableEntryAspect> axisAspects) { | |
448 | return axisAspects.stream().allMatch(aspect -> aspect.isTimeStamp()); | |
449 | } | |
450 | ||
451 | /** | |
452 | * Util method to check if a list of aspects are all time durations. | |
453 | * | |
454 | * @param axisAspects | |
455 | * The list of aspects to check. | |
456 | * @return true is all aspects are time durations, otherwise false. | |
457 | */ | |
458 | protected static boolean areAspectsTimeDuration(List<LamiTableEntryAspect> axisAspects) { | |
459 | return axisAspects.stream().allMatch(aspect -> aspect.isTimeDuration()); | |
460 | } | |
461 | ||
462 | /** | |
463 | * Util method that will return a formatter based on the aspects linked to an axis | |
464 | * | |
465 | * If all aspects are time stamps, return a timestamp formatter tuned to the interval. | |
466 | * If all aspects are time durations, return the nanoseconds to seconds formatter. | |
467 | * Otherwise, return the generic decimal formatter. | |
468 | * | |
469 | * @param axisAspects | |
470 | * The list of aspects of the axis. | |
471 | * @param entries | |
472 | * The list of entries of the chart. | |
5b973e7c JR |
473 | * @param internalRange |
474 | * The internal range for value transformation | |
475 | * @param externalRange | |
476 | * The external range for value transformation | |
4208b510 AM |
477 | * @return a formatter for the axis. |
478 | */ | |
5b973e7c JR |
479 | protected static Format getContinuousAxisFormatter(List<LamiTableEntryAspect> axisAspects, List<LamiTableEntry> entries , @Nullable LamiGraphRange internalRange, @Nullable LamiGraphRange externalRange) { |
480 | ||
481 | Format formatter = DECIMAL_FORMATTER; | |
4208b510 AM |
482 | |
483 | if (areAspectsTimeStamp(axisAspects)) { | |
484 | /* Set a TimeStamp formatter depending on the duration between the first and last value */ | |
5b973e7c JR |
485 | BigDecimal max = new BigDecimal(Long.MIN_VALUE); |
486 | BigDecimal min = new BigDecimal(Long.MAX_VALUE); | |
4208b510 AM |
487 | |
488 | for (LamiTableEntry entry : entries) { | |
489 | for (LamiTableEntryAspect aspect : axisAspects) { | |
5b973e7c JR |
490 | @Nullable Number number = aspect.resolveNumber(entry); |
491 | if (number != null) { | |
492 | BigDecimal current = new BigDecimal(number.toString()); | |
493 | max = current.max(max); | |
494 | min = current.min(min); | |
4208b510 AM |
495 | } |
496 | } | |
497 | } | |
4208b510 | 498 | |
5b973e7c | 499 | long duration = max.subtract(min).longValue(); |
4208b510 | 500 | if (duration > TimeUnit.DAYS.toNanos(1)) { |
5b973e7c | 501 | formatter = DAYS_FORMATTER; |
4208b510 | 502 | } else if (duration > TimeUnit.HOURS.toNanos(1)) { |
5b973e7c | 503 | formatter = HOURS_FORMATTER; |
4208b510 | 504 | } else if (duration > TimeUnit.MINUTES.toNanos(1)) { |
5b973e7c | 505 | formatter = MINUTES_FORMATTER; |
4208b510 | 506 | } else if (duration > TimeUnit.SECONDS.toNanos(15)) { |
5b973e7c | 507 | formatter = SECONDS_FORMATTER; |
4208b510 | 508 | } else { |
5b973e7c | 509 | formatter = MILLISECONDS_FORMATTER; |
4208b510 | 510 | } |
5b973e7c JR |
511 | ((LamiTimeStampFormat) formatter).setInternalRange(internalRange); |
512 | ((LamiTimeStampFormat) formatter).setExternalRange(externalRange); | |
513 | ||
4208b510 | 514 | } else if (areAspectsTimeDuration(axisAspects)) { |
5b973e7c JR |
515 | /* Set the time duration formatter. */ |
516 | formatter = NANO_TO_SECS_FORMATTER; | |
517 | ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange); | |
518 | ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange); | |
4208b510 AM |
519 | |
520 | } else { | |
5b973e7c JR |
521 | /* |
522 | * For other numeric aspects, use the default lami decimal unit | |
523 | * formatter. | |
524 | */ | |
525 | formatter = DECIMAL_FORMATTER; | |
526 | ((LamiDecimalUnitFormat) formatter).setInternalRange(internalRange); | |
527 | ((LamiDecimalUnitFormat) formatter).setExternalRange(externalRange); | |
4208b510 | 528 | } |
5b973e7c JR |
529 | |
530 | return formatter; | |
4208b510 AM |
531 | } |
532 | ||
533 | /** | |
534 | * Get the chart result table. | |
535 | * | |
536 | * @return The chart result table. | |
537 | */ | |
538 | protected LamiResultTable getResultTable() { | |
539 | return fResultTable; | |
540 | } | |
541 | ||
542 | /** | |
543 | * Get the chart model. | |
544 | * | |
545 | * @return The chart model. | |
546 | */ | |
547 | protected LamiChartModel getChartModel() { | |
548 | return fChartModel; | |
549 | } | |
550 | ||
551 | /** | |
552 | * Get the chart object. | |
553 | * @return The chart object. | |
554 | */ | |
555 | protected Chart getChart() { | |
556 | return fChart; | |
557 | } | |
558 | ||
7710e6ed JR |
559 | /** |
560 | * @return the toolBar | |
561 | */ | |
562 | public ToolBar getToolBar() { | |
563 | return fToolBar; | |
564 | } | |
565 | ||
4208b510 AM |
566 | /** |
567 | * Is a selection made in the chart. | |
568 | * | |
569 | * @return true if there is a selection. | |
570 | */ | |
571 | protected boolean isSelected() { | |
572 | return fSelected; | |
573 | } | |
574 | ||
575 | /** | |
576 | * Set the selection index. | |
577 | * | |
578 | * @param selection the index to select. | |
579 | */ | |
580 | protected void setSelection(Set<Integer> selection) { | |
581 | fSelection = selection; | |
582 | fSelected = !selection.isEmpty(); | |
583 | } | |
584 | ||
585 | /** | |
586 | * Unset the chart selection. | |
587 | */ | |
588 | protected void unsetSelection() { | |
589 | fSelection.clear(); | |
590 | fSelected = false; | |
591 | } | |
592 | ||
593 | /** | |
594 | * Get the current selection index. | |
595 | * | |
596 | * @return the current selection index. | |
597 | */ | |
598 | protected Set<Integer> getSelection() { | |
599 | return fSelection; | |
600 | } | |
601 | ||
602 | @Override | |
603 | public @Nullable Control getControl() { | |
604 | return fChart.getParent(); | |
605 | } | |
606 | ||
607 | @Override | |
608 | public void refresh() { | |
609 | Display.getDefault().asyncExec(() -> { | |
610 | if (!fChart.isDisposed()) { | |
611 | fChart.redraw(); | |
612 | } | |
613 | }); | |
614 | } | |
615 | ||
616 | @Override | |
617 | public void dispose() { | |
618 | fChart.dispose(); | |
619 | /* The control's DisposeListener will call super.dispose() */ | |
620 | } | |
621 | ||
622 | /** | |
623 | * Get a list of all the aspect of the Y axis. | |
624 | * | |
625 | * @return The aspects for the Y axis | |
626 | */ | |
627 | protected List<LamiTableEntryAspect> getYAxisAspects() { | |
628 | ||
629 | List<LamiTableEntryAspect> yAxisAspects = new ArrayList<>(); | |
630 | ||
631 | for (String colName : getChartModel().getYSeriesColumns()) { | |
632 | yAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); | |
633 | } | |
634 | ||
635 | return yAxisAspects; | |
636 | } | |
637 | ||
638 | /** | |
639 | * Get a list of all the aspect of the X axis. | |
640 | * | |
641 | * @return The aspects for the X axis | |
642 | */ | |
643 | protected List<LamiTableEntryAspect> getXAxisAspects() { | |
644 | ||
645 | List<LamiTableEntryAspect> xAxisAspects = new ArrayList<>(); | |
646 | ||
647 | for (String colName : getChartModel().getXSeriesColumns()) { | |
648 | xAxisAspects.add(checkNotNull(getAspectFromName(getResultTable().getTableClass().getAspects(), colName))); | |
649 | } | |
650 | ||
651 | return xAxisAspects; | |
652 | } | |
653 | ||
654 | /** | |
655 | * Set the ITitle object text to a substring of canonicalTitle that when | |
656 | * rendered in the chart will fit maxPixelLength. | |
657 | */ | |
658 | private void refreshDisplayTitle(ITitle title, String canonicalTitle, int maxPixelLength) { | |
659 | if (title.isVisible()) { | |
660 | ||
661 | String newTitle = canonicalTitle; | |
662 | ||
663 | /* Get the title font */ | |
664 | Font font = title.getFont(); | |
665 | ||
666 | GC gc = new GC(fParent); | |
667 | gc.setFont(font); | |
668 | ||
669 | /* Get the length and height of the canonical title in pixels */ | |
670 | Point pixels = gc.stringExtent(canonicalTitle); | |
671 | ||
672 | /* | |
673 | * If the title is too long, generate a shortened version based on the | |
674 | * average character width of the current font. | |
675 | */ | |
676 | if (pixels.x > maxPixelLength) { | |
677 | int charwidth = gc.getFontMetrics().getAverageCharWidth(); | |
678 | ||
679 | int minimum = 3; | |
680 | ||
681 | int strLen = ((maxPixelLength / charwidth) - minimum); | |
682 | ||
683 | if (strLen > minimum) { | |
684 | newTitle = canonicalTitle.substring(0, strLen) + ELLIPSIS; | |
685 | } else { | |
686 | newTitle = ELLIPSIS; | |
687 | } | |
688 | } | |
689 | ||
690 | title.setText(newTitle); | |
691 | ||
692 | // Cleanup | |
693 | gc.dispose(); | |
694 | } | |
695 | } | |
696 | ||
697 | /** | |
698 | * Refresh the Chart, XAxis and YAxis titles to fit the current | |
699 | * chart size. | |
700 | */ | |
701 | private void refreshDisplayTitles() { | |
702 | Rectangle chartRect = fChart.getClientArea(); | |
703 | Rectangle plotRect = fChart.getPlotArea().getClientArea(); | |
704 | ||
705 | ITitle chartTitle = checkNotNull(fChart.getTitle()); | |
706 | refreshDisplayTitle(chartTitle, fChartTitle, chartRect.width); | |
707 | ||
708 | ITitle xTitle = checkNotNull(fChart.getAxisSet().getXAxis(0).getTitle()); | |
b7156f6b | 709 | refreshDisplayTitle(xTitle, getXTitle(), plotRect.width); |
4208b510 AM |
710 | |
711 | ITitle yTitle = checkNotNull(fChart.getAxisSet().getYAxis(0).getTitle()); | |
b7156f6b | 712 | refreshDisplayTitle(yTitle, getYTitle(), plotRect.height); |
4208b510 AM |
713 | } |
714 | ||
715 | /** | |
716 | * Get the aspect with the given name | |
717 | * | |
718 | * @param aspects | |
719 | * The list of aspects to search into | |
720 | * @param aspectName | |
721 | * The name of the aspect we are looking for | |
722 | * @return The corresponding aspect | |
723 | */ | |
724 | protected static @Nullable LamiTableEntryAspect getAspectFromName(List<LamiTableEntryAspect> aspects, String aspectName) { | |
725 | for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { | |
726 | ||
727 | if (lamiTableEntryAspect.getLabel().equals(aspectName)) { | |
728 | return lamiTableEntryAspect; | |
729 | } | |
730 | } | |
731 | ||
732 | return null; | |
733 | } | |
734 | ||
735 | /** | |
736 | * Refresh the axis labels to fit the current chart size. | |
737 | */ | |
738 | protected abstract void refreshDisplayLabels(); | |
739 | ||
740 | /** | |
741 | * Redraw the chart. | |
742 | */ | |
743 | protected void redraw() { | |
744 | refresh(); | |
745 | } | |
746 | ||
747 | /** | |
748 | * Signal handler for selection update. | |
749 | * | |
750 | * @param signal | |
751 | * The selection update signal | |
752 | */ | |
753 | @TmfSignalHandler | |
754 | public void updateSelection(LamiSelectionUpdateSignal signal) { | |
755 | if (getResultTable().hashCode() != signal.getSignalHash() || equals(signal.getSource())) { | |
756 | /* The signal is not for us */ | |
757 | return; | |
758 | } | |
759 | setSelection(signal.getEntryIndex()); | |
760 | ||
761 | redraw(); | |
762 | } | |
7710e6ed JR |
763 | |
764 | /** | |
765 | * Create a tool bar on top right of the chart. Contained actions: | |
766 | * <ul> | |
767 | * <li>Dispose the current viewer, also known as "Close the chart"</li> | |
768 | * </ul> | |
769 | * | |
770 | * This tool bar should only appear when the mouse enters the composite. | |
771 | * | |
772 | * @return the tool bar | |
773 | */ | |
774 | protected ToolBar createChartToolBar() { | |
775 | Image removeImage = PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_ELCL_REMOVE); | |
776 | ToolBar toolBar = new ToolBar(getChart(), SWT.HORIZONTAL); | |
777 | ||
778 | /* Default state */ | |
779 | toolBar.moveAbove(null); | |
780 | toolBar.setVisible(false); | |
781 | ||
782 | /* | |
783 | * Close chart button | |
784 | */ | |
785 | ToolItem closeButton = new ToolItem(toolBar, SWT.PUSH); | |
786 | closeButton.setImage(removeImage); | |
787 | closeButton.setToolTipText(Messages.LamiXYChartViewer_CloseChartToolTip); | |
788 | closeButton.addSelectionListener(new SelectionListener() { | |
789 | @Override | |
790 | public void widgetSelected(@Nullable SelectionEvent e) { | |
791 | Composite parent = getParent(); | |
792 | dispose(); | |
793 | parent.layout(); | |
794 | } | |
795 | ||
796 | @Override | |
797 | public void widgetDefaultSelected(@Nullable SelectionEvent e) { | |
798 | } | |
799 | }); | |
800 | ||
801 | toolBar.pack(); | |
802 | toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); | |
803 | ||
804 | /* Visibility toggle filter */ | |
805 | Listener toolBarVisibilityToggleListener = e -> { | |
806 | if (e.widget instanceof Control) { | |
807 | Control control = (Control) e.widget; | |
808 | Point display = control.toDisplay(e.x, e.y); | |
809 | Point location = getChart().getParent().toControl(display); | |
810 | ||
811 | /* | |
812 | * Only set to visible if we are at the right location, in the | |
813 | * right shell. | |
814 | */ | |
815 | boolean visible = getChart().getBounds().contains(location) && | |
816 | control.getShell().equals(getChart().getShell()); | |
817 | getToolBar().setVisible(visible); | |
818 | } | |
819 | }; | |
820 | ||
821 | /* Filter to make sure we hide the toolbar if we exit the window */ | |
822 | Listener hideToolBarListener = (e -> getToolBar().setVisible(false)); | |
823 | ||
824 | /* | |
825 | * Add the filters to the main Display, and remove them when we dispose | |
826 | * the chart. | |
827 | */ | |
828 | Display display = getChart().getDisplay(); | |
829 | display.addFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); | |
830 | display.addFilter(SWT.MouseExit, hideToolBarListener); | |
831 | ||
832 | getChart().addDisposeListener(e -> { | |
833 | display.removeFilter(SWT.MouseEnter, toolBarVisibilityToggleListener); | |
834 | display.removeFilter(SWT.MouseExit, hideToolBarListener); | |
835 | }); | |
836 | ||
837 | /* Reposition the tool bar on resize */ | |
838 | getChart().addListener(SWT.Resize, new Listener() { | |
839 | @Override | |
840 | public void handleEvent(@Nullable Event event) { | |
841 | toolBar.setLocation(new Point(getChart().getSize().x - toolBar.getSize().x, 0)); | |
842 | } | |
843 | }); | |
844 | ||
845 | return toolBar; | |
846 | } | |
5b973e7c JR |
847 | |
848 | /** | |
849 | * Get a {@link LamiGraphRange} that covers all data points in the result | |
850 | * table. | |
851 | * <p> | |
852 | * The returned range will be the minimum and maximum of the resolved values | |
853 | * of the passed aspects for all result entries. If <code>clampToZero</code> | |
854 | * is true, a positive minimum value will be clamped down to zero. | |
855 | * | |
856 | * @param aspects | |
857 | * The aspects that the range will represent | |
858 | * @param clampToZero | |
859 | * If true, a positive minimum value will be clamped down to zero | |
860 | * @return the range | |
861 | */ | |
862 | protected LamiGraphRange getRange(List<LamiTableEntryAspect> aspects, boolean clampToZero) { | |
863 | /* Find the minimum and maximum values */ | |
864 | BigDecimal min = new BigDecimal(Long.MAX_VALUE); | |
865 | BigDecimal max = new BigDecimal(Long.MIN_VALUE); | |
866 | for (LamiTableEntryAspect lamiTableEntryAspect : aspects) { | |
867 | for (LamiTableEntry entry : getResultTable().getEntries()) { | |
868 | @Nullable Number number = lamiTableEntryAspect.resolveNumber(entry); | |
869 | if (number != null) { | |
870 | BigDecimal current = new BigDecimal(number.toString()); | |
871 | min = current.min(min); | |
872 | max = current.max(max); | |
873 | } | |
874 | } | |
875 | } | |
876 | ||
877 | if (clampToZero) { | |
9c8b3806 | 878 | min = min.min(BigDecimal.ZERO); |
5b973e7c JR |
879 | } |
880 | ||
881 | /* Do not allow a range with a zero delta default to 1 */ | |
882 | if (max.equals(min)) { | |
883 | max = min.add(BigDecimal.ONE); | |
884 | } | |
885 | ||
886 | return new LamiGraphRange(checkNotNull(min), checkNotNull(max)); | |
887 | } | |
888 | ||
889 | /** | |
890 | * Transform an external value into an internal value. Since SWTChart only | |
891 | * support Double and Lami can pass Long values, loss of precision might | |
892 | * happen. To minimize this, transform the raw values to an internal | |
893 | * representation based on a linear transformation. | |
894 | * | |
895 | * The internal value = | |
896 | * | |
897 | * ((rawValue - rawMinimum) * (internalRangeDelta/rawRangeDelta)) + | |
898 | * internalMinimum | |
899 | * | |
900 | * @param number | |
901 | * The number to transform | |
902 | * @param internalRange | |
903 | * The internal range definition to be used | |
904 | * @param externalRange | |
905 | * The external range definition to be used | |
906 | * @return the transformed value in Double comprised inside the internal | |
907 | * range | |
908 | */ | |
909 | protected static double getInternalDoubleValue(Number number, LamiGraphRange internalRange, LamiGraphRange externalRange) { | |
910 | BigDecimal value = new BigDecimal(number.toString()); | |
911 | ||
912 | if (externalRange.getDelta().compareTo(BigDecimal.ZERO) == 0) { | |
913 | return internalRange.getMinimum().doubleValue(); | |
914 | } | |
915 | ||
916 | BigDecimal internalValue = value | |
917 | .subtract(externalRange.getMinimum()) | |
918 | .multiply(internalRange.getDelta()) | |
919 | .divide(externalRange.getDelta(), BIG_DECIMAL_DIVISION_SCALE, BigDecimal.ROUND_DOWN) | |
920 | .add(internalRange.getMinimum()); | |
921 | ||
922 | return internalValue.doubleValue(); | |
923 | } | |
4208b510 | 924 | } |