blob: 3ea67eb8a8a9ecf67ac60266aca56e9806f30aae [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2007, 2019 Intel Corporation, Ericsson
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Intel Corporation - Initial API and implementation
* Ruslan A. Scherbakov, Intel - Initial API and implementation
* Alvaro Sanchez-Leon - Udpated for TMF
* Patrick Tasse - Refactoring
* Marc-Andre Laperle - Add time zone preference
*****************************************************************************/
package org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets;
import java.util.Collections;
import java.util.Iterator;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.common.core.format.LongToPercentFormat;
import org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry;
import com.google.common.collect.Iterables;
/**
* General utilities and definitions used by the time graph widget
*
* @author Alvaro Sanchez-Leon
* @author Patrick Tasse
*/
public class Utils {
private Utils() {
}
/**
* Time format for dates and timestamp
*/
public enum TimeFormat {
/** Relative to the start of the trace */
RELATIVE(FormatTimeUtils.TimeFormat.RELATIVE),
/**
* Absolute timestamp (ie, relative to the Unix epoch)
*/
CALENDAR(FormatTimeUtils.TimeFormat.CALENDAR),
/**
* Timestamp displayed as a simple number
*/
NUMBER(FormatTimeUtils.TimeFormat.NUMBER),
/**
* Timestamp displayed as cycles
*/
CYCLES((FormatTimeUtils.TimeFormat.CYCLES)),
/**
* Timestamp displayed as percentages, where 100% maps to the long value
* {@link LongToPercentFormat#MAX_PERCENT_VALUE}
*
* @since 4.0
*/
PERCENTAGE((FormatTimeUtils.TimeFormat.PERCENTAGE));
private final FormatTimeUtils.TimeFormat tf;
private TimeFormat(FormatTimeUtils.TimeFormat tf) {
this.tf = tf;
}
/**
* Convert this {@link TimeFormat} to a
* {@link org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils.TimeFormat}
*
* @return the converted time format
* @since 3.3
*/
public FormatTimeUtils.TimeFormat convert() {
return tf;
}
/**
* Convert the specified
* {@link org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils.TimeFormat}
* to a {@link TimeFormat}
*
* @param timeFormat
* the time format
* @return the converted time format
* @since 5.0
*/
public static TimeFormat convert(FormatTimeUtils.TimeFormat timeFormat) {
for (TimeFormat format : values()) {
if (format.tf == timeFormat) {
return format;
}
}
return null;
}
}
/**
* Timestamp resolution
*/
public enum Resolution {
/** seconds */
SECONDS(FormatTimeUtils.Resolution.SECONDS),
/** milliseconds */
MILLISEC(FormatTimeUtils.Resolution.MILLISEC),
/** microseconds */
MICROSEC(FormatTimeUtils.Resolution.MICROSEC),
/** nanoseconds */
NANOSEC(FormatTimeUtils.Resolution.NANOSEC);
private final FormatTimeUtils.Resolution res;
private Resolution(FormatTimeUtils.Resolution res) {
this.res = res;
}
/**
* Convert the {@link Resolution} to a {@link org.eclipse.tracecompass.tmf.ui.views.FormatTimeUtils.Resolution}
* @return the converted resolution
* @since 3.3
*/
public FormatTimeUtils.Resolution convert() {
return res;
}
}
/**
* Ellipsis character, used to shorten strings that don't fit in their
* target area.
*
* @since 2.1
*/
public static final String ELLIPSIS = "…"; //$NON-NLS-1$
static Rectangle clone(Rectangle source) {
return new Rectangle(source.x, source.y, source.width, source.height);
}
/**
* Initialize a Rectangle object to default values (all equal to 0)
*
* @param rect
* The Rectangle to initialize
*/
public static void init(Rectangle rect) {
rect.x = 0;
rect.y = 0;
rect.width = 0;
rect.height = 0;
}
/**
* Initialize a Rectangle object with all the given values
*
* @param rect
* The Rectangle object to initialize
* @param x
* The X coordinate
* @param y
* The Y coordinate
* @param width
* The width of the rectangle
* @param height
* The height of the rectangle
*/
public static void init(Rectangle rect, int x, int y, int width, int height) {
rect.x = x;
rect.y = y;
rect.width = width;
rect.height = height;
}
/**
* Initialize a Rectangle object to another existing Rectangle's values.
*
* @param rect
* The Rectangle to initialize
* @param source
* The reference Rectangle to copy
*/
public static void init(Rectangle rect, Rectangle source) {
rect.x = source.x;
rect.y = source.y;
rect.width = source.width;
rect.height = source.height;
}
/**
* Reduce the size of a given rectangle by the given amounts.
*
* @param rect
* The rectangle to modify
* @param x
* The reduction in width
* @param y
* The reduction in height
*/
public static void deflate(Rectangle rect, int x, int y) {
rect.x += x;
rect.y += y;
rect.width -= x + x;
rect.height -= y + y;
}
/**
* Increase the size of a given rectangle by the given amounts.
*
* @param rect
* The rectangle to modify
* @param x
* The augmentation in width
* @param y
* The augmentation in height
*/
public static void inflate(Rectangle rect, int x, int y) {
rect.x -= x;
rect.y -= y;
rect.width += x + x;
rect.height += y + y;
}
static void dispose(Color col) {
if (null != col) {
col.dispose();
}
}
/**
* Get the resulting color from a mix of two existing ones for a given
* display.
*
* @param display
* The display device (which might affect the color conversion)
* @param c1
* The first color
* @param c2
* The second color
* @param w1
* The gamma level for color 1
* @param w2
* The gamma level for color 2
* @return The resulting color
*/
public static Color mixColors(Device display, Color c1, Color c2, int w1,
int w2) {
return new Color(display, (w1 * c1.getRed() + w2 * c2.getRed())
/ (w1 + w2), (w1 * c1.getGreen() + w2 * c2.getGreen())
/ (w1 + w2), (w1 * c1.getBlue() + w2 * c2.getBlue())
/ (w1 + w2));
}
/**
* Get the system color with the given ID.
*
* @param id
* The color ID
* @return The resulting color
*/
public static Color getSysColor(int id) {
Color col = Display.getCurrent().getSystemColor(id);
return new Color(col.getDevice(), col.getRGB());
}
/**
* Get the resulting color from a mix of two existing ones for the current
* display.
*
* @param col1
* The first color
* @param col2
* The second color
* @param w1
* The gamma level for color 1
* @param w2
* The gamma level for color 2
* @return The resulting color
*/
public static Color mixColors(Color col1, Color col2, int w1, int w2) {
return mixColors(Display.getCurrent(), col1, col2, w1, w2);
}
/**
* Get a distinct color from the specified RGB color, based on its
* relative luminance.
*
* @param rgb
* An RGB color
* @return The black or white system color, whichever is more distinct.
* @since 3.0
*/
public static Color getDistinctColor(RGB rgb) {
/* Calculate the relative luminance of the color, high value is bright */
final int luminanceThreshold = 160;
/* Relative luminance (Y) coefficients as defined in ITU.R Rec. 709 */
final double redCoefficient = 0.2126;
final double greenCoefficient = 0.7152;
final double blueCoefficient = 0.0722;
int luminance = (int) (redCoefficient * rgb.red + greenCoefficient * rgb.green + blueCoefficient * rgb.blue);
/* Use black over bright colors and white over dark colors */
return Display.getDefault().getSystemColor(
luminance > luminanceThreshold ? SWT.COLOR_BLACK : SWT.COLOR_WHITE);
}
/**
* Draw text in a rectangle.
*
* @param gc
* The SWT GC object
* @param text
* The text to draw
* @param rect
* The rectangle object which is being drawn
* @param transp
* If true the background will be transparent
* @return The width of the written text
*/
public static int drawText(GC gc, String text, Rectangle rect, boolean transp) {
Point size = gc.stringExtent(text);
gc.drawText(text, rect.x, rect.y, transp);
return size.x;
}
/**
* Draw text at a given location.
*
* @param gc
* The SWT GC object
* @param text
* The text to draw
* @param x
* The X coordinate of the starting point
* @param y
* the Y coordinate of the starting point
* @param transp
* If true the background will be transparent
* @return The width of the written text
*/
public static int drawText(GC gc, String text, int x, int y, boolean transp) {
Point size = gc.stringExtent(text);
gc.drawText(text, x, y, transp);
return size.x;
}
/**
* Draw text in a rectangle, trimming the text to prevent exceeding the specified width.
*
* @param gc
* The SWT GC object
* @param text
* The string to be drawn
* @param x
* The x coordinate of the top left corner of the rectangular area where the text is to be drawn
* @param y
* The y coordinate of the top left corner of the rectangular area where the text is to be drawn
* @param width
* The width of the area to be drawn
* @param height
* The height of the area to be drawn
* @param isCentered
* If <code>true</code> the text will be centered in the available area if space permits
* @param isTransparent
* If <code>true</code> the background will be transparent, otherwise it will be opaque
* @return The number of characters written
* @since 2.0
*/
public static int drawText(GC gc, String text, int x, int y, int width, int height, boolean isCentered, boolean isTransparent) {
if (width < 1 || text.isEmpty()) {
return 0;
}
String stringToDisplay;
int len;
boolean isCenteredWidth = isCentered;
int realX = x;
int realY = y;
/* First check if the whole string fits */
Point textExtent = gc.textExtent(text);
if (textExtent.x <= width) {
len = text.length();
stringToDisplay = text;
} else {
/*
* The full string doesn't fit, try to find the longest one with
* "..." at the end that does fit.
*
* Iterate on the string length sizes, starting from 1 going up,
* until we find a string that does not fit. Once we do, we keep the
* one just before that did fit.
*/
isCenteredWidth = false;
int prevLen = 0;
len = 1;
while (len <= text.length()) {
textExtent = gc.textExtent(text.substring(0, len) + ELLIPSIS);
if (textExtent.x > width) {
/*
* Here is the first length that does not fit, the one from
* the previous iteration is the one we will use.
*/
len = prevLen;
break;
}
/* This string would fit, try the next one */
prevLen = len;
len++;
}
stringToDisplay = text.substring(0, len) + ELLIPSIS;
}
if (len <= 0) {
/* Nothing fits, we end up drawing nothing */
return 0;
}
if (isCenteredWidth) {
realX += (width - textExtent.x) / 2;
}
if (isCentered) {
realY += (height - textExtent.y) / 2 - 1;
}
gc.drawText(stringToDisplay, realX, realY, isTransparent);
return len;
}
/**
* FIXME Currently does nothing.
*
* @param opt
* The option name
* @param def
* The option value
* @param min
* The minimal accepted value
* @param max
* The maximal accepted value
* @return The value that was read
*/
public static int loadIntOption(String opt, int def, int min, int max) {
return def;
}
/**
* FIXME currently does nothing
*
* @param opt
* The option name
* @param val
* The option value
*/
public static void saveIntOption(String opt, int val) {
}
static ITimeEvent getFirstEvent(ITimeGraphEntry entry) {
if (null == entry || ! entry.hasTimeEvents()) {
return null;
}
Iterator<? extends ITimeEvent> iterator = entry.getTimeEventsIterator();
if (iterator != null && iterator.hasNext()) {
return iterator.next();
}
return null;
}
/**
* Gets the {@link ITimeEvent} at the given time from the given
* {@link ITimeGraphEntry}.
*
* @param entry
* a {@link ITimeGraphEntry}
* @param time
* a timestamp
* @param n
* this parameter means: <list> <li>-1: Previous Event</li> <li>
* 0: Current Event</li> <li>
* 1: Next Event</li> <li>2: Previous Event when located in a non
* Event Area </list>
* @return a {@link ITimeEvent}, or <code>null</code>.
*/
public static ITimeEvent findEvent(ITimeGraphEntry entry, long time, int n) {
if (null == entry || ! entry.hasTimeEvents()) {
return null;
}
Iterator<@NonNull ? extends ITimeEvent> iterator = entry.getTimeEventsIterator();
if (iterator == null) {
return null;
}
ITimeEvent nextEvent = null;
ITimeEvent currEvent = null;
ITimeEvent prevEvent = null;
while (iterator.hasNext()) {
nextEvent = iterator.next();
long nextStartTime = nextEvent.getTime();
if (nextStartTime > time) {
break;
}
if (currEvent == null || currEvent.getTime() != nextStartTime ||
(nextStartTime != time && currEvent.getDuration() != nextEvent.getDuration())) {
prevEvent = currEvent;
currEvent = nextEvent;
}
}
if (n == -1) { //previous
if (currEvent != null && currEvent.getTime() + currEvent.getDuration() >= time) {
return prevEvent;
}
return currEvent;
} else if (n == 0) { //current
if (currEvent != null && currEvent.getTime() + currEvent.getDuration() >= time) {
return currEvent;
}
return null;
} else if (n == 1) { //next
if (nextEvent != null && nextEvent.getTime() > time) {
return nextEvent;
}
return null;
} else if (n == 2) { //current or previous when in empty space
return currEvent;
}
return null;
}
/**
* Returns the time of the next event state change starting from the given time,
* which is the end of the current event, or the beginning of the next event, or
* {@link Long#MAX_VALUE}.
*
* @param entry
* the entry
* @param time
* the time to start from
* @return the time of the next event state change, or {@link Long#MAX_VALUE}
* @since 3.1
*/
public static long nextChange(ITimeGraphEntry entry, long time) {
if (null == entry || ! entry.hasTimeEvents()) {
return Long.MAX_VALUE;
}
Iterator<@NonNull ? extends ITimeEvent> iterator = entry.getTimeEventsIterator();
if (iterator == null) {
return Long.MAX_VALUE;
}
while (iterator.hasNext()) {
ITimeEvent event = iterator.next();
long start = event.getTime();
if (start > time) {
return start;
}
long end = start + event.getDuration();
if (end > time) {
return end;
}
}
return Long.MAX_VALUE;
}
/**
* Returns the time of the previous event state change starting from the given
* time, which is the start of the current event, or the end of the previous
* event, or {@link Long#MIN_VALUE}.
*
* @param entry
* the entry
* @param time
* the time to start from
* @return the time of the previous event state change, or {@link Long#MIN_VALUE}
* @since 3.1
*/
public static long prevChange(ITimeGraphEntry entry, long time) {
if (null == entry || ! entry.hasTimeEvents()) {
return Long.MIN_VALUE;
}
Iterator<@NonNull ? extends ITimeEvent> iterator = entry.getTimeEventsIterator();
if (iterator == null) {
return Long.MIN_VALUE;
}
long prevEnd = Long.MIN_VALUE;
while (iterator.hasNext()) {
ITimeEvent event = iterator.next();
long start = event.getTime();
if (start >= time) {
return prevEnd;
}
long end = start + event.getDuration();
if (end >= time) {
return start;
}
prevEnd = end;
}
return prevEnd;
}
/**
* Pretty-print a method signature.
*
* @param origSig
* The original signature
* @return The pretty signature
*/
public static String fixMethodSignature(String origSig) {
String sig = origSig;
int pos = sig.indexOf('(');
if (pos >= 0) {
String ret = sig.substring(0, pos);
sig = sig.substring(pos);
sig = sig + " " + ret; //$NON-NLS-1$
}
return sig;
}
/**
* Restore an original method signature from a pretty-printed one.
*
* @param ppSig
* The pretty-printed signature
* @return The original method signature
*/
public static String restoreMethodSignature(String ppSig) {
String ret = ""; //$NON-NLS-1$
String sig = ppSig;
int pos = sig.indexOf('(');
if (pos >= 0) {
ret = sig.substring(0, pos);
sig = sig.substring(pos + 1);
}
pos = sig.indexOf(')');
if (pos >= 0) {
sig = sig.substring(0, pos);
}
String args[] = sig.split(","); //$NON-NLS-1$
StringBuffer result = new StringBuffer("("); //$NON-NLS-1$
for (int i = 0; i < args.length; i++) {
String arg = args[i].trim();
if (arg.length() == 0 && args.length == 1) {
break;
}
result.append(getTypeSignature(arg));
}
result.append(")").append(getTypeSignature(ret)); //$NON-NLS-1$
return result.toString();
}
/**
* Get the mangled type information from an array of types.
*
* @param typeStr
* The types to convert. See method implementation for what it
* expects.
* @return The mangled string of types
*/
public static String getTypeSignature(String typeStr) {
int dim = 0;
String type = typeStr;
for (int j = 0; j < type.length(); j++) {
if (type.charAt(j) == '[') {
dim++;
}
}
int pos = type.indexOf('[');
if (pos >= 0) {
type = type.substring(0, pos);
}
StringBuffer sig = new StringBuffer(""); //$NON-NLS-1$
for (int j = 0; j < dim; j++)
{
sig.append("["); //$NON-NLS-1$
}
if (type.equals("boolean")) { //$NON-NLS-1$
sig.append('Z');
} else if (type.equals("byte")) { //$NON-NLS-1$
sig.append('B');
} else if (type.equals("char")) { //$NON-NLS-1$
sig.append('C');
} else if (type.equals("short")) { //$NON-NLS-1$
sig.append('S');
} else if (type.equals("int")) { //$NON-NLS-1$
sig.append('I');
} else if (type.equals("long")) { //$NON-NLS-1$
sig.append('J');
} else if (type.equals("float")) { //$NON-NLS-1$
sig.append('F');
} else if (type.equals("double")) { //$NON-NLS-1$
sig.append('D');
} else if (type.equals("void")) { //$NON-NLS-1$
sig.append('V');
}
else {
sig.append('L').append(type.replace('.', '/')).append(';');
}
return sig.toString();
}
/**
* Compare two doubles together.
*
* @param d1
* First double
* @param d2
* Second double
* @return 1 if they are different, and 0 if they are *exactly* the same.
* Because of the way doubles are stored, it's possible for the
* same number obtained in two different ways to actually look
* different.
*/
public static int compare(double d1, double d2) {
if (d1 > d2) {
return 1;
}
if (d1 < d2) {
return 1;
}
return 0;
}
/**
* Compare two character strings alphabetically. This is simply a wrapper
* around String.compareToIgnoreCase but that will handle cases where
* strings can be null
*
* @param s1
* The first string
* @param s2
* The second string
* @return A number below, equal, or greater than zero if the first string
* is smaller, equal, or bigger (alphabetically) than the second
* one.
*/
public static int compare(String s1, String s2) {
if (s1 != null && s2 != null) {
return s1.compareToIgnoreCase(s2);
}
if (s1 != null) {
return 1;
}
if (s2 != null) {
return -1;
}
return 0;
}
/**
* Calculates the square of the distance between two points.
*
* @param x1
* x-coordinate of point 1
* @param y1
* y-coordinate of point 1
* @param x2
* x-coordinate of point 2
* @param y2
* y-coordinate of point 2
*
* @return the square of the distance in pixels^2
*/
public static double distance2(int x1, int y1, int x2, int y2) {
int dx = x2 - x1;
int dy = y2 - y1;
int d2 = dx * dx + dy * dy;
return d2;
}
/**
* Calculates the distance between a point and a line segment. If the point
* is in the perpendicular region between the segment points, return the
* distance from the point to its projection on the segment. Otherwise
* return the distance from the point to its closest segment point.
*
* @param px
* x-coordinate of the point
* @param py
* y-coordinate of the point
* @param x1
* x-coordinate of segment point 1
* @param y1
* y-coordinate of segment point 1
* @param x2
* x-coordinate of segment point 2
* @param y2
* y-coordinate of segment point 2
*
* @return the distance in pixels
*/
public static double distance(int px, int py, int x1, int y1, int x2, int y2) {
double length2 = distance2(x1, y1, x2, y2);
if (length2 == 0) {
return Math.sqrt(distance2(px, py, x1, y1));
}
// 'r' is the ratio of the position, between segment point 1 and segment
// point 2, of the projection of the point on the segment
double r = ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / length2;
if (r <= 0.0) {
// the projection is before segment point 1, return distance from
// the point to segment point 1
return Math.sqrt(distance2(px, py, x1, y1));
}
if (r >= 1.0) {
// the projection is after segment point 2, return distance from
// the point to segment point 2
return Math.sqrt(distance2(px, py, x2, y2));
}
// the projection is between the segment points, return distance from
// the point to its projection on the segment
int x = (int) (x1 + r * (x2 - x1));
int y = (int) (y1 + r * (y2 - y1));
return Math.sqrt(distance2(px, py, x, y));
}
/**
* Flatten a {@link TimeGraphEntry} tree for easier iteration.
*
* @param root
* root entry from which to flatten the tree.
* @return an {@link Iterable} over the entries.
* @since 3.3
*/
public static @NonNull Iterable<@NonNull TimeGraphEntry> flatten(TimeGraphEntry root) {
Iterable<Iterable<TimeGraphEntry>> transform = Iterables.transform(root.getChildren(), Utils::flatten);
return Iterables.concat(Collections.singleton(root), Iterables.concat(transform));
}
}