/*****************************************************************************
* Copyright (c) 2007, 2016 Intel Corporation, Ericsson
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* 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.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNull;
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.Rectangle;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.common.core.format.DecimalUnitFormat;
import org.eclipse.tracecompass.internal.tmf.ui.Messages;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimePreferences;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeGraphEntry;
/**
* 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,
/**
* Absolute timestamp (ie, relative to the Unix epoch)
*/
CALENDAR,
/**
* Timestamp displayed as a simple number
*/
NUMBER,
/**
* Timestamp displayed as cycles
*/
CYCLES
}
/**
* Timestamp resolution
*/
public static enum Resolution {
/** seconds */
SECONDS,
/** milliseconds */
MILLISEC,
/** microseconds */
MICROSEC,
/** nanoseconds */
NANOSEC
}
/**
* 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$
private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); //$NON-NLS-1$
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); //$NON-NLS-1$
private static final long HOURS_PER_DAY = 24;
private static final long MIN_PER_HOUR = 60;
private static final long SEC_PER_MIN = 60;
private static final long SEC_IN_NS = 1000000000;
private static final long MILLISEC_IN_NS = 1000000;
/**
* Update the time and date formats to use the current time zone
*/
public static void updateTimeZone() {
TimeZone timeZone = TmfTimePreferences.getTimeZone();
TIME_FORMAT.setTimeZone(timeZone);
DATE_FORMAT.setTimeZone(timeZone);
}
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);
}
/**
* 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 isCentered
* If <code>true</code> the text will be centered in the available width 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
* @deprecated Use {@link #drawText(GC, String, int, int, int, int, boolean, boolean)} instead.
*/
@Deprecated
public static int drawText(GC gc, String text, int x, int y, int width, boolean isCentered, boolean isTransparent) {
if (width < 1) {
return 0;
}
int len = text.length();
int textWidth = 0;
boolean isReallyCentered = isCentered;
int realX = x;
while (len > 0) {
textWidth = gc.stringExtent(text.substring(0, len)).x;
if (textWidth <= width) {
break;
}
isReallyCentered = false;
len--;
}
if (len > 0) {
if (isReallyCentered) {
realX += (width - textWidth) / 2;
}
gc.drawText(text.substring(0, len), realX, y, isTransparent);
}
return len;
}
/**
* 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;
}
/**
* Formats time in format: MM:SS:NNN
*
* @param time time
* @param format 0: MMMM:ss:nnnnnnnnn, 1: HH:MM:ss MMM.mmmm.nnn
* @param resolution the resolution
* @return the formatted time
*/
public static String formatTime(long time, TimeFormat format, Resolution resolution) {
switch (format) {
case CALENDAR:
return formatTimeAbs(time, resolution);
case NUMBER:
return NumberFormat.getInstance().format(time);
case CYCLES:
return NumberFormat.getInstance().format(time) + Messages.Utils_ClockCyclesUnit;
case RELATIVE:
default:
}
StringBuffer str = new StringBuffer();
long t = time;
boolean neg = t < 0;
if (neg) {
t = -t;
str.append('-');
}
long sec = t / SEC_IN_NS;
str.append(sec);
String ns = formatNs(t, resolution);
if (!ns.equals("")) { //$NON-NLS-1$
str.append('.');
str.append(ns);
}
return str.toString();
}
/**
* From input time in nanoseconds, convert to Date format YYYY-MM-dd
*
* @param absTime
* The source time, in ns
* @return the formatted date
*/
public static String formatDate(long absTime) {
String sdate = DATE_FORMAT.format(new Date(absTime / MILLISEC_IN_NS));
return sdate;
}
/**
* Formats time in ns to Calendar format: HH:MM:SS MMM.mmm.nnn
*
* @param time
* The source time, in ns
* @param res
* The resolution to use
* @return the formatted time
*/
public static String formatTimeAbs(long time, Resolution res) {
StringBuffer str = new StringBuffer();
// format time from nanoseconds to calendar time HH:MM:SS
String stime = TIME_FORMAT.format(new Date(time / MILLISEC_IN_NS));
str.append(stime);
str.append('.');
// append the Milliseconds, MicroSeconds and NanoSeconds as specified in
// the Resolution
str.append(formatNs(time, res));
return str.toString();
}
/**
* Formats time delta
*
* @param delta
* The time delta, in ns
* @param format
* The time format to use
* @param resolution
* The resolution to use
* @return the formatted time delta
*/
public static String formatDelta(long delta, TimeFormat format, Resolution resolution) {
if (format == TimeFormat.CALENDAR) {
return formatDeltaAbs(delta, resolution);
}
return formatTime(delta, format, resolution);
}
/**
* Formats time delta in ns to Calendar format, only formatting the years,
* days, hours or minutes if necessary.
*
* @param delta
* The time delta, in ns
* @param resolution
* The resolution to use
* @return the formatted time delta
*/
public static String formatDeltaAbs(long delta, Resolution resolution) {
StringBuffer str = new StringBuffer();
if (delta < 0) {
str.append('-');
}
long ns = Math.abs(delta);
long seconds = TimeUnit.NANOSECONDS.toSeconds(ns);
long minutes = TimeUnit.NANOSECONDS.toMinutes(ns);
long hours = TimeUnit.NANOSECONDS.toHours(ns);
long days = TimeUnit.NANOSECONDS.toDays(ns);
if (days > 0) {
str.append(days);
str.append("d "); //$NON-NLS-1$
}
if (hours > 0) {
str.append(hours % HOURS_PER_DAY);
str.append("h "); //$NON-NLS-1$
}
if (minutes > 0) {
str.append(minutes % MIN_PER_HOUR);
str.append("m "); //$NON-NLS-1$
}
str.append(seconds % SEC_PER_MIN);
str.append('.');
// append the ms, us and ns as specified in the resolution
str.append(formatNs(delta, resolution));
str.append("s"); //$NON-NLS-1$
if (seconds == 0) {
str.append(" ("); //$NON-NLS-1$
str.append(new DecimalUnitFormat(1.0 / SEC_IN_NS).format(delta));
str.append("s)"); //$NON-NLS-1$
}
return str.toString();
}
/**
* Obtains the remainder fraction on unit Seconds of the entered value in
* nanoseconds. e.g. input: 1241207054171080214 ns The number of fraction
* seconds can be obtained by removing the last 9 digits: 1241207054 the
* fractional portion of seconds, expressed in ns is: 171080214
*
* @param srcTime
* The source time in ns
* @param res
* The Resolution to use
* @return the formatted nanosec
*/
public static String formatNs(long srcTime, Resolution res) {
StringBuffer str = new StringBuffer();
long ns = Math.abs(srcTime % SEC_IN_NS);
String nanos = Long.toString(ns);
str.append("000000000".substring(nanos.length())); //$NON-NLS-1$
str.append(nanos);
if (res == Resolution.MILLISEC) {
return str.substring(0, 3);
} else if (res == Resolution.MICROSEC) {
return str.substring(0, 6);
} else if (res == Resolution.NANOSEC) {
return str.substring(0, 9);
}
return ""; //$NON-NLS-1$
}
/**
* 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;
}
/**
* 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));
}
}