/******************************************************************************
* Copyright (c) 2011, 2016 Stephan Schwiebert and others.
* 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:
* Stephan Schwiebert - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.cloudio.internal.ui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EventListener;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.gef.cloudio.internal.ui.layout.DefaultLayouter;
import org.eclipse.gef.cloudio.internal.ui.layout.ILayouter;
import org.eclipse.gef.cloudio.internal.ui.util.CloudMatrix;
import org.eclipse.gef.cloudio.internal.ui.util.RectTree;
import org.eclipse.gef.cloudio.internal.ui.util.SmallRect;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
/**
*
* @author sschwieb
*
*/
public class TagCloud extends Canvas {
/**
* Minimum 'resolution' of the {@link RectTree} used for collision handling.
*/
private final int accuracy;
/**
* Maximum size of the {@link RectTree} used for collision handling.
*/
private final int maxSize;
/**
* Draw area.
*/
private final Rectangle cloudArea;
/**
* Maximum Font Size.
*/
private int maxFontSize = 100;
private final GC gc;
/**
* Highlight color.
*/
private Color highlightColor;
/**
* Used to detect mousehover, -enter and -exit.
*/
private Word currentWord;
/**
* Opacity of the rendered strings.
*/
private int opacity = 255;
/**
* Required for scroll bars
*/
private final Point origin = new Point(0, 0);
/**
* Main image, on which all strings are rendered.
*/
private Image textLayerImage;
/**
* Second level image: All elements plus selected elements in highlight
* color.
*/
private Image selectionLayerImage;
/**
* Last level image: All + selected elements, zoomed. This is the image
* which will be displayed.
*/
private Image zoomLayerImage;
/**
* The list of words to render.
*/
private List<Word> wordsToUse;
private boolean initialized = false;
/**
* Current zoom factor.
*/
private double currentZoom = 1;
/**
* Minimum font size.
*/
private int minFontSize = 12;
/**
* Set of selected words
*/
private Set<Word> selection = new HashSet<>();
private CloudMatrix cloudMatrix;
/**
* Executor service to process the creation of {@link RectTree} objects in
* parallel.
*/
private ExecutorService executors;
private ILayouter layouter;
/**
* The <code>boost</code> words with highest weight will be further
* increased in size. Eye-Candy only.
*/
private int boost;
/**
* Offset of the region which surrounds the placed words, required to
* translate between mouse position and underlying words.
*/
private Point regionOffset;
private int antialias = SWT.ON;
private float boostFactor;
private Listener hBarListener;
private Listener resizeListener;
private Listener paintListener;
private Listener mouseTrackListener;
private Listener mouseMoveListener;
private Listener mouseUpListener;
private Listener mouseDCListener;
private Listener mouseDownListener;
private Listener mouseWheelListener;
private Listener vBarListener;
private Set<EventListener> mouseWheelListeners = new HashSet<>();
private Set<EventListener> mouseTrackListeners = new HashSet<>();
private Set<EventListener> mouseMoveListeners = new HashSet<>();
private Set<EventListener> mouseListeners = new HashSet<>();
private Set<SelectionListener> selectionListeners = new HashSet<>();
private ImageData mask;
/**
* Creates a new Tag cloud on the given parent. When using this constructor,
* please read the following carefully: <br>
* Parameter <code>accuracy</code> defines the size of the raster used when
* placing strings, and must be a value greater than <code>0</code>. An
* accuracy of <code>1</code> will theoretically give best results, as the
* drawable area is analyzed most detailed, but this will also be very slow.
* <br>
* Parameter <code>maxSize</code> defines the maximum size of the drawable
* area and <strong>must</strong> be a power of <code>accuracy</code>,
* such that <code>accuracy^n=maxSize</code> holds. <br>
* To add scroll bars to the cloud, use {@link SWT#HORIZONTAL} and
* {@link SWT#VERTICAL}.
*
* @param accuracy
* @param maxSize
* @param parent
* @param style
*/
public TagCloud(Composite parent, int style, int accuracy, int maxSize) {
super(parent, style);
Assert.isLegal(accuracy > 0, "Parameter accuracy must be greater than 0, but was " + accuracy);
Assert.isLegal(maxSize > 0, "Parameter maxSize must be greater than 0, but was " + maxSize);
int tmp = maxSize;
while (tmp > accuracy) {
tmp /= 2;
}
Assert.isLegal(tmp == accuracy, "Parameter maxSize must be a power of accuracy");
this.accuracy = accuracy;
this.maxSize = maxSize;
cloudArea = new Rectangle(0, 0, maxSize, maxSize);
highlightColor = new Color(getDisplay(), Display.getDefault().getSystemColor(SWT.COLOR_RED).getRGB());
gc = new GC(this);
layouter = new DefaultLayouter(accuracy, accuracy);
setBackground(new Color(getDisplay(), Display.getDefault().getSystemColor(SWT.COLOR_WHITE).getRGB()));
initListeners();
textLayerImage = new Image(getDisplay(), 100, 100);
zoomFit();
addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
internalDispose();
}
});
}
/**
* Creates a new Tag cloud on the given parent. To add scroll bars to the
* cloud, use {@link SWT#HORIZONTAL} and {@link SWT#VERTICAL}. This is a
* shortcut to {@link #TagCloud(Composite, int, int, int)}, which sets the
* accuracy to <code>5</code> and the maximum size of the drawable area to
* <code>5120</code>.
*
* @param parent
* @param style
*/
public TagCloud(Composite parent, int style) {
this(parent, style, 5, 5120);
}
/**
* Disposes all system resources created in this class. Resources which were
* provided through a {@link ICloudLabelProvider} etc are not disposed.
*/
private void internalDispose() {
removeListeners();
textLayerImage.dispose();
if (selectionLayerImage != null) {
selectionLayerImage.dispose();
}
if (zoomLayerImage != null) {
zoomLayerImage.dispose();
}
if (!this.isDisposed()) {
gc.dispose();
}
super.dispose();
}
private void removeListeners() {
if (isDisposed())
return;
removeListener(SWT.Paint, paintListener);
if (hBarListener != null) {
removeListener(SWT.H_SCROLL, hBarListener);
}
if (vBarListener != null) {
removeListener(SWT.V_SCROLL, vBarListener);
}
removeListener(SWT.MouseDoubleClick, mouseDCListener);
removeListener(SWT.MouseDown, mouseDownListener);
removeListener(SWT.MouseMove, mouseTrackListener);
removeListener(SWT.MouseUp, mouseUpListener);
removeListener(SWT.Resize, resizeListener);
removeListener(SWT.MouseMove, mouseMoveListener);
removeListener(SWT.MouseWheel, mouseWheelListener);
}
/**
* Resets the zoom to 100 % (original size)
*/
public void zoomReset() {
checkWidget();
if (selectionLayerImage == null)
return;
zoomLayerImage = new Image(getDisplay(), selectionLayerImage.getBounds().width,
selectionLayerImage.getBounds().height);
GC gc = new GC(zoomLayerImage);
gc.drawImage(selectionLayerImage, 0, 0);
gc.dispose();
currentZoom = 1;
updateScrollbars();
redraw();
}
public double getZoom() {
checkWidget();
return currentZoom;
}
/**
* Resets the zoom such that the generated cloud will fit exactly into the
* available space (unless the zoom factor is too small or too large).
*/
public void zoomFit() {
checkWidget();
if (selectionLayerImage == null)
return;
Rectangle imageBound = selectionLayerImage.getBounds();
Rectangle destRect = getClientArea();
double sx = (double) destRect.width / (double) imageBound.width;
double sy = (double) destRect.height / (double) imageBound.height;
currentZoom = Math.min(sx, sy);
zoom(currentZoom);
}
private void zoom(double s) {
checkWidget();
if (selectionLayerImage == null)
return;
if (s < 0.1)
s = 0.1;
if (s > 3)
s = 3;
int width = (int) (selectionLayerImage.getBounds().width * s);
int height = (int) (selectionLayerImage.getBounds().height * s);
if (width == 0 || height == 0)
return;
zoomLayerImage = new Image(getDisplay(), width, height);
Transform tf = new Transform(getDisplay());
tf.scale((float) s, (float) s);
GC gc = new GC(zoomLayerImage);
gc.setTransform(tf);
gc.drawImage(selectionLayerImage, 0, 0);
gc.dispose();
tf.dispose();
currentZoom = s;
updateScrollbars();
redraw();
}
/**
* Zooms in, by the factor of 10 percent.
*/
public void zoomIn() {
checkWidget();
zoom(currentZoom * 1.1);
redraw();
}
/**
* Zooms out, by the factor of 10 percent.
*/
public void zoomOut() {
checkWidget();
zoom(currentZoom * 0.9);
redraw();
}
/**
* Returns the maximum cloud area.
*
* @return the maximum cloud area
*/
protected Rectangle getCloudArea() {
return cloudArea;
}
/**
* Returns the font size of the given word. By default, this is calculated
* as <code>8 + (word.weight * maxFontSize)</code>.
*
* @param word
* @return
*/
private float getFontSize(Word word) {
float size = (float) (word.weight * maxFontSize);
size += minFontSize;
return size;
}
/**
* Draws a word with the given color.
*
* @param gc
* @param word
* @param color
*/
private void drawWord(final GC gc, final Word word, final Color color) {
gc.setForeground(color);
Font font = new Font(gc.getDevice(), word.getFontData());
gc.setFont(font);
gc.setAntialias(antialias);
gc.setAlpha(opacity);
Point stringExtent = word.stringExtent;
gc.setForeground(color);
int xOffset = word.x - regionOffset.x;
int yOffset = word.y - regionOffset.y;
double radian = Math.toRadians(word.angle);
final double sin = Math.abs(Math.sin(radian));
final double cos = Math.abs(Math.cos(radian));
int y = (int) ((cos * stringExtent.y) + (sin * stringExtent.x));
Transform t = new Transform(gc.getDevice());
if (word.angle < 0) {
t.translate(xOffset, yOffset + y - (int) (cos * stringExtent.y));
} else {
t.translate(xOffset + (int) (sin * stringExtent.y), yOffset);
}
t.rotate(word.angle);
gc.setTransform(t);
gc.drawString(word.string, 0, 0, true);
gc.setTransform(null);
t.dispose();
font.dispose();
}
/**
* Calculates the bounds of each word, by determining the {@link Rectangle}
* a path would require to render an element.
*
* @param monitor
*/
protected void calcExtents(IProgressMonitor monitor) {
checkWidget();
if (monitor != null) {
monitor.subTask("Calculating word boundaries...");
}
if (wordsToUse == null)
return;
double step = 80D / wordsToUse.size();
double current = 0;
int next = 10;
executors = Executors.newFixedThreadPool(getNumberOfThreads());
final Color color = gc.getDevice().getSystemColor(SWT.COLOR_BLACK);
for (final Word word : wordsToUse) {
FontData[] fontData = word.getFontData();
int fontSize = (int) getFontSize(word);
for (FontData data : fontData) {
data.setHeight((int) fontSize);
}
final Font font = new Font(gc.getDevice(), fontData);
gc.setFont(font);
final Point stringExtent = gc.stringExtent(word.string);
FontMetrics fm = gc.getFontMetrics();
stringExtent.y = fm.getHeight();
executors.execute(new Runnable() {
@Override
public void run() {
double radian = Math.toRadians(word.angle);
final double sin = Math.abs(Math.sin(radian));
final double cos = Math.abs(Math.cos(radian));
final int x = (int) ((cos * stringExtent.x) + (sin * stringExtent.y));
final int y = (int) ((cos * stringExtent.y) + (sin * stringExtent.x));
ImageData id = createImageData(word, font, stringExtent, sin, cos, x, y, color);
calcWordExtents(word, id);
font.dispose();
}
});
if (monitor != null) {
current += step;
if (current > next) {
monitor.worked(5);
next += 5;
}
}
}
executors.shutdown();
try {
executors.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
Collections.sort(wordsToUse, new Comparator<Word>() {
@Override
public int compare(Word o1, Word o2) {
return (o2.width * o2.height) - (o1.width * o1.height);
}
});
short i = 1;
for (Word word : wordsToUse) {
word.id = i++;
}
}
private ImageData createImageData(final Word word, Font font, Point stringExtent, final double sin,
final double cos, int x, int y, Color color) {
Image img = new Image(null, x, y);
word.width = x;
word.height = y;
word.stringExtent = stringExtent;
GC g = new GC(img);
g.setAntialias(antialias);
g.setForeground(color);
Transform t = new Transform(img.getDevice());
if (word.angle < 0) {
t.translate(0, img.getBounds().height - (int) (cos * stringExtent.y));
} else {
t.translate((int) (sin * stringExtent.y), 0);
}
t.rotate(word.angle);
g.setTransform(t);
g.setFont(font);
// Why is drawString so slow? between 30 and 90 percent of the whole
// draw time...
g.drawString(word.string, 0, 0, false);
int max = Math.max(x, y);
int tmp = maxSize;
while (max < tmp) {
tmp = tmp / 2;
}
tmp = tmp * 2;
SmallRect root = new SmallRect(0, 0, tmp, tmp);
word.tree = new RectTree(root, accuracy);
final ImageData id = img.getImageData();
g.dispose();
img.dispose();
return id;
}
/**
* Calculates the extents of a word, based on its rendered image.
*/
private void calcWordExtents(final Word word, final ImageData id) {
final int[] pixels = new int[id.width];
final PaletteData palette = id.palette;
Set<SmallRect> inserted = new HashSet<>();
for (int y = 0; y < id.height; y++) {
id.getPixels(0, y, id.width, pixels, 0);
for (int i = 0; i < pixels.length; i++) {
int pixel = pixels[i];
// Extracting color values as in PaletteData.getRGB(int pixel):
int r = pixel & palette.redMask;
r = (palette.redShift < 0) ? r >>> -palette.redShift : r << palette.redShift;
int g = pixel & palette.greenMask;
g = (palette.greenShift < 0) ? g >>> -palette.greenShift : g << palette.greenShift;
int b = pixel & palette.blueMask;
b = (palette.blueShift < 0) ? b >>> -palette.blueShift : b << palette.blueShift;
if (r < 250 || g < 250 || b < 250) {
SmallRect rect = new SmallRect((i / accuracy) * accuracy, (y / accuracy) * accuracy, accuracy,
accuracy);
if (!inserted.contains(rect)) {
word.tree.insert(rect, word.id);
inserted.add(rect);
}
i += accuracy - 1;
}
}
}
word.tree.releaseRects();
}
/**
* Generates the layout of the given words.
*
* @param wordsToUse
* @param monitor
* may be <code>null</code>.
* @return the number of words which could be placed
*/
protected int layoutWords(Collection<Word> wordsToUse, IProgressMonitor monitor) {
checkWidget();
if (monitor != null) {
monitor.subTask("Placing words...");
}
Rectangle r = new Rectangle(Integer.MAX_VALUE, Integer.MAX_VALUE, 0, 0);
final Rectangle cloudArea = getCloudArea();
int w = cloudArea.width;
int h = cloudArea.height;
double current = 0;
int next = 10;
final Image tmpImage = new Image(getDisplay(), w, h);
GC gc = new GC(tmpImage);
gc.setBackground(getBackground());
gc.setTextAntialias(SWT.ON);
gc.setBackground(getBackground());
gc.fillRectangle(tmpImage.getBounds());
executors = Executors.newFixedThreadPool(1);
int success = 0;
if (wordsToUse != null) {
double step = 100D / wordsToUse.size();
final GC g = gc;
for (Word word : wordsToUse) {
Point point = layouter.getInitialOffset(word, cloudArea);
boolean result = layouter.layout(point, word, cloudArea, cloudMatrix);
if (!result) {
System.err.println("Failed to place " + word.string);
continue;
}
success++;
if (word.x < r.x) {
r.x = word.x;
}
if (word.y < r.y) {
r.y = word.y;
}
if (word.x + word.width > r.width) {
r.width = word.x + word.width;
}
if (word.y + word.height > r.height) {
r.height = word.y + word.height;
}
final Word wrd = word;
executors.execute(new Runnable() {
@Override
public void run() {
drawWord(g, wrd, wrd.getColor());
}
});
current += step;
if (current > next) {
next += 5;
if (monitor != null) {
monitor.worked(5);
}
}
}
executors.shutdown();
try {
executors.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// drawRects(gc);
gc.dispose();
if (success == 0)
return success;
if (textLayerImage != null) {
textLayerImage.dispose();
}
textLayerImage = new Image(getDisplay(), r.width - r.x, r.height - r.y);
gc = new GC(textLayerImage);
gc.drawImage(tmpImage, r.x, r.y, r.width - r.x, r.height - r.y, 0, 0, textLayerImage.getBounds().width,
textLayerImage.getBounds().height);
this.regionOffset = new Point(r.x, r.y);
tmpImage.dispose();
gc.dispose();
selectionLayerImage = new Image(getDisplay(), textLayerImage.getBounds());
gc = new GC(selectionLayerImage);
gc.drawImage(textLayerImage, 0, 0);
gc.dispose();
zoomFit();
if (monitor != null) {
monitor.worked(10);
}
return success;
}
/**
* Sets the given list as input of the tag cloud, replacing any previous
* content. By default, available word positions will be determined
* in-order, starting with the element at position 0.
*
* @param values
* @param monitor
*/
public int setWords(List<Word> values, IProgressMonitor monitor) {
checkWidget();
Assert.isLegal(values != null, "List must not be null!");
for (Word word : values) {
Assert.isLegal(word != null, "Word must not be null!");
Assert.isLegal(word.string != null, "Word must define a string!");
Assert.isLegal(word.getColor() != null, "A word must define a color");
Assert.isLegal(word.getFontData() != null, "A word must define a fontdata array");
Assert.isLegal(word.weight >= 0,
"Word weight must be between 0 and 1 (inclusive), but value was " + word.weight);
Assert.isLegal(word.weight <= 1,
"Word weight must be between 0 and 1 (inclusive), but value was " + word.weight);
Assert.isLegal(word.angle >= -90, "Angle must be between -90 and +90 (inclusive), but was " + word.angle);
Assert.isLegal(word.angle <= 90, "Angle must be between -90 and +90 (inclusive), but was " + word.angle);
}
this.wordsToUse = new ArrayList<>(values);
if (boost > 0) {
double factor = boostFactor;
int i = boost;
for (Word word : values) {
if (factor <= 1) {
break;
}
word.weight *= factor;
factor -= 0.2;
i--;
if (i == 0)
break;
}
}
return layoutCloud(monitor, true);
}
/**
* Reset the initial matrix
*/
private void resetLayout() {
if (cloudMatrix == null) {
cloudMatrix = new CloudMatrix(maxSize, accuracy);
} else {
cloudMatrix.reset();
}
if (mask != null) {
resetMask();
}
}
/**
* Set a background mask to define the drawable area of the cloud. The image
* must be a square containing black and white pixels only. It is scaled to
* the full size of the drawable region. Black pixels are interpreted as
* used, such that strings will be drawn on white areas only. If parameter
* <code>bgData</code> is <code>null</code>, the old mask will be removed.
*
* @param bgData
* a square containing black and white pixels only
*/
public void setBackgroundMask(ImageData bgData) {
if (mask != null) {
mask = null;
}
if (bgData != null) {
Image img = new Image(null, cloudArea.width, cloudArea.height);
GC gc = new GC(img);
Image tmp = new Image(null, bgData);
gc.drawImage(tmp, 0, 0, tmp.getBounds().width, tmp.getBounds().height, 0, 0, cloudArea.width,
cloudArea.height);
ImageData id = img.getImageData();
tmp.dispose();
img.dispose();
gc.dispose();
mask = id;
}
}
private void resetMask() {
Word word = new Word("mask");
word.tree = new RectTree(new SmallRect(0, 0, cloudArea.width, cloudArea.height), accuracy);
calcWordExtents(word, mask);
word.tree.place(cloudMatrix, RectTree.BACKGROUND);
}
private int getNumberOfThreads() {
return Runtime.getRuntime().availableProcessors();
}
/**
* Initialize internal listeners (scrollbar, mouse, paint...).
*/
private void initListeners() {
if (initialized)
return;
initialized = true;
final ScrollBar hBar = this.getHorizontalBar();
if (hBar != null) {
hBarListener = new Listener() {
@Override
public void handleEvent(Event e) {
int hSelection = hBar.getSelection();
int destX = -hSelection - origin.x;
Rectangle rect = zoomLayerImage.getBounds();
TagCloud.this.scroll(destX, 0, 0, 0, rect.width, rect.height, false);
origin.x = -hSelection;
}
};
hBar.addListener(SWT.Selection, hBarListener);
}
final ScrollBar vBar = this.getVerticalBar();
if (vBar != null) {
vBarListener = new Listener() {
@Override
public void handleEvent(Event e) {
int vSelection = vBar.getSelection();
int destY = -vSelection - origin.y;
Rectangle rect = zoomLayerImage.getBounds();
TagCloud.this.scroll(0, destY, 0, 0, rect.width, rect.height, false);
origin.y = -vSelection;
}
};
vBar.addListener(SWT.Selection, vBarListener);
}
resizeListener = new Listener() {
@Override
public void handleEvent(Event e) {
updateScrollbars();
TagCloud.this.redraw();
}
};
this.addListener(SWT.Resize, resizeListener);
paintListener = new Listener() {
@Override
public void handleEvent(Event e) {
GC gc = e.gc;
if (zoomLayerImage == null)
return;
Rectangle rect = zoomLayerImage.getBounds();
Rectangle client = TagCloud.this.getClientArea();
int marginWidth = client.width - rect.width;
gc.setBackground(getBackground());
if (marginWidth > 0) {
gc.fillRectangle(rect.width, 0, marginWidth, client.height);
}
int marginHeight = client.height - rect.height;
if (marginHeight > 0) {
gc.fillRectangle(0, rect.height, client.width, marginHeight);
}
gc.drawImage(zoomLayerImage, origin.x, origin.y);
}
};
this.addListener(SWT.Paint, paintListener);
mouseTrackListener = new Listener() {
@Override
public void handleEvent(Event event) {
Word word = getWordAt(new Point(event.x, event.y));
MouseEvent me = createMouseEvent(event, word);
if (currentWord != null) {
if (word == currentWord) {
fireMouseEvent(me, SWT.MouseHover, mouseTrackListeners);
} else {
currentWord = null;
fireMouseEvent(me, SWT.MouseExit, mouseTrackListeners);
}
}
if (currentWord == null && word != null) {
currentWord = word;
fireMouseEvent(me, SWT.MouseEnter, mouseTrackListeners);
}
}
};
this.addListener(SWT.MouseMove, mouseTrackListener);
mouseMoveListener = new Listener() {
@Override
public void handleEvent(Event event) {
Word word = getWordAt(new Point(event.x, event.y));
MouseEvent me = createMouseEvent(event, word);
fireMouseEvent(me, SWT.MouseMove, mouseMoveListeners);
}
};
this.addListener(SWT.MouseMove, mouseMoveListener);
mouseUpListener = new Listener() {
@Override
public void handleEvent(Event event) {
Word word = getWordAt(new Point(event.x, event.y));
MouseEvent me = createMouseEvent(event, word);
fireMouseEvent(me, SWT.MouseUp, mouseListeners);
}
};
this.addListener(SWT.MouseUp, mouseUpListener);
mouseDCListener = new Listener() {
@Override
public void handleEvent(Event event) {
Word word = getWordAt(new Point(event.x, event.y));
MouseEvent me = createMouseEvent(event, word);
fireMouseEvent(me, SWT.MouseDoubleClick, mouseListeners);
}
};
this.addListener(SWT.MouseDoubleClick, mouseDCListener);
mouseDownListener = new Listener() {
@Override
public void handleEvent(Event event) {
Word word = getWordAt(new Point(event.x, event.y));
MouseEvent me = createMouseEvent(event, word);
fireMouseEvent(me, SWT.MouseDown, mouseListeners);
}
};
this.addListener(SWT.MouseDown, mouseDownListener);
mouseWheelListener = new Listener() {
@Override
public void handleEvent(Event event) {
Word word = getWordAt(new Point(event.x, event.y));
MouseEvent me = createMouseEvent(event, word);
fireMouseEvent(me, SWT.MouseWheel, mouseWheelListeners);
}
};
this.addListener(SWT.MouseWheel, mouseWheelListener);
}
/**
* Translates the given point in screen coordinates to the corresponding
* point in the (zoomed and scrolled) image and returns the {@link Word} at
* this position, or <code>null</code>, if no word exists at this position.
*
* @param point
* @return
*/
private Word getWordAt(Point point) {
if (cloudMatrix == null || regionOffset == null)
return null;
Point translatedMousePos = translateMousePos(point.x, point.y);
translatedMousePos.x += regionOffset.x;
translatedMousePos.y += regionOffset.y;
int x = translatedMousePos.x / accuracy;
int y = translatedMousePos.y / accuracy;
if (x >= maxSize || y >= maxSize) {
return null;
}
short wordId = cloudMatrix.get(x, y);
if (wordId > 0) {
Word clicked = wordsToUse.get(wordId - 1);
return clicked;
}
return null;
}
/**
* Translates the current mouse position, such that it corresponds to scroll
* bars and zoom.
*
* @param x
* @param y
* @return
*/
private Point translateMousePos(final int x, final int y) {
final Point point = new Point(x - origin.x, y - origin.y);
point.x /= currentZoom;
point.y /= currentZoom;
return point;
}
@Override
public void addMouseListener(MouseListener listener) {
checkWidget();
Assert.isLegal(listener != null);
mouseListeners.add(listener);
}
@Override
public void addMouseMoveListener(MouseMoveListener listener) {
checkWidget();
Assert.isLegal(listener != null);
mouseMoveListeners.add(listener);
}
@Override
public void addMouseTrackListener(MouseTrackListener listener) {
checkWidget();
Assert.isLegal(listener != null);
mouseTrackListeners.add(listener);
}
@Override
public void addMouseWheelListener(MouseWheelListener listener) {
checkWidget();
Assert.isLegal(listener != null);
mouseWheelListeners.add(listener);
}
public void addSelectionListener(SelectionListener listener) {
checkWidget();
Assert.isLegal(listener != null);
selectionListeners.add(listener);
}
@Override
public void removeMouseListener(MouseListener listener) {
checkWidget();
mouseListeners.remove(listener);
}
@Override
public void removeMouseMoveListener(MouseMoveListener listener) {
checkWidget();
mouseMoveListeners.remove(listener);
}
@Override
public void removeMouseTrackListener(MouseTrackListener listener) {
checkWidget();
mouseTrackListeners.remove(listener);
}
@Override
public void removeMouseWheelListener(MouseWheelListener listener) {
checkWidget();
mouseWheelListeners.remove(listener);
}
public void removeSelectionListener(SelectionListener listener) {
checkWidget();
selectionListeners.remove(listener);
}
private MouseEvent createMouseEvent(Event event, Word word) {
MouseEvent me = new MouseEvent(event);
me.x = event.x - origin.x;
me.y = event.y - origin.y;
me.data = word;
me.widget = TagCloud.this;
me.display = Display.getCurrent();
return me;
}
private void fireMouseEvent(MouseEvent me, int type, Set<EventListener> listeners) {
for (EventListener listener : listeners) {
if (listener instanceof MouseListener) {
MouseListener ml = (MouseListener) listener;
switch (type) {
case SWT.MouseUp:
ml.mouseUp(me);
break;
case SWT.MouseDoubleClick:
ml.mouseDoubleClick(me);
break;
case SWT.MouseDown:
ml.mouseDown(me);
break;
}
}
if (listener instanceof MouseTrackListener) {
MouseTrackListener ml = (MouseTrackListener) listener;
switch (type) {
case SWT.MouseEnter:
ml.mouseEnter(me);
break;
case SWT.MouseExit:
ml.mouseExit(me);
break;
case SWT.MouseHover:
ml.mouseHover(me);
break;
}
}
if (listener instanceof MouseMoveListener) {
MouseMoveListener ml = (MouseMoveListener) listener;
switch (type) {
case SWT.MouseMove:
ml.mouseMove(me);
break;
}
}
if (listener instanceof MouseWheelListener) {
MouseWheelListener ml = (MouseWheelListener) listener;
switch (type) {
case SWT.MouseWheel:
ml.mouseScrolled(me);
break;
}
}
}
}
/**
* Marks the set of elements as selected.
*
* @param words
* must not be <code>null</code>.
*/
public void setSelection(Set<Word> words) {
checkWidget();
Assert.isNotNull(words, "Selection must not be null!");
if (wordsToUse == null)
return;
Set<Word> selection = new HashSet<>(words);
selection.retainAll(wordsToUse);
int w = textLayerImage.getBounds().width;
int h = textLayerImage.getBounds().height;
if (selectionLayerImage != null) {
selectionLayerImage.dispose();
}
selectionLayerImage = new Image(getDisplay(), w, h);
GC gc = new GC(selectionLayerImage);
gc.drawImage(textLayerImage, 0, 0);
for (Word word : selection) {
drawWord(gc, word, highlightColor);
}
if (!selection.equals(this.selection)) {
this.selection = selection;
fireSelectionChanged();
}
gc.dispose();
zoom(currentZoom);
redraw();
}
private void fireSelectionChanged() {
Event e = new Event();
e.widget = this;
final SelectionEvent event = new SelectionEvent(e);
event.data = getSelection();
event.widget = this;
event.display = Display.getCurrent();
for (SelectionListener listener : selectionListeners) {
listener.widgetSelected(event);
}
}
public void redrawTextLayerImage() {
if (wordsToUse == null)
return;
GC gc = new GC(textLayerImage);
gc.setBackground(getBackground());
gc.fillRectangle(0, 0, textLayerImage.getBounds().width, textLayerImage.getBounds().height);
for (Word word : wordsToUse) {
drawWord(gc, word, word.getColor());
}
gc.dispose();
setSelection(getSelection());
}
/**
* Returns the set of selected elements. Never returns <code>null</code>.
*
* @return the set of selected words
*/
public Set<Word> getSelection() {
checkWidget();
Set<Word> copy = new HashSet<>(selection);
return copy;
}
/**
* Sets the highlight color of the cloud. Default color is red.
*
* @param color
*/
public void setSelectionColor(Color color) {
checkWidget();
Assert.isLegal(color != null, "Color must not be null!");
this.highlightColor = color;
}
@Override
public void setBackground(Color color) {
checkWidget();
Assert.isLegal(color != null, "Color must not be null!");
super.setBackground(color);
}
/**
* Does a full relayout of all displayed elements.
*
* @param monitor
* @return the number of words that could be placed
*/
public int layoutCloud(IProgressMonitor monitor, boolean recalc) {
checkWidget();
resetLayout();
if (selectionLayerImage != null) {
selectionLayerImage.dispose();
selectionLayerImage = null;
}
regionOffset = new Point(0, 0);
if (textLayerImage != null)
textLayerImage.dispose();
int placedWords = 0;
try {
if (recalc) {
calcExtents(monitor);
}
placedWords = layoutWords(wordsToUse, monitor);
} catch (Exception e) {
MessageDialog.openError(getShell(), "Exception while layouting data",
"An exception occurred while layouting: " + e.getMessage());
e.printStackTrace();
}
// zoomFit();
redraw();
updateScrollbars();
return placedWords;
}
private void updateScrollbars() {
if (zoomLayerImage == null) {
return;
}
Rectangle rect = zoomLayerImage.getBounds();
Rectangle client = getClientArea();
ScrollBar hBar = getHorizontalBar();
ScrollBar vBar = getVerticalBar();
if (hBar != null) {
hBar.setMaximum(rect.width);
hBar.setThumb(Math.min(rect.width, client.width));
int hPage = rect.width - client.width;
int hSelection = hBar.getSelection();
if (hSelection >= hPage) {
if (hPage <= 0)
hSelection = 0;
origin.x = -hSelection;
}
}
if (vBar != null) {
vBar.setMaximum(rect.height);
vBar.setThumb(Math.min(rect.height, client.height));
int vPage = rect.height - client.height;
int vSelection = vBar.getSelection();
if (vSelection >= vPage) {
if (vPage <= 0)
vSelection = 0;
origin.y = -vSelection;
}
}
}
/**
* Sets the maximum font size (which must be a value greater 0). Note that
* strings which are too large to fit into the cloud region will be skipped.
* By default, this value is 500.
*
* @param maxSize
*/
public void setMaxFontSize(int maxSize) {
checkWidget();
Assert.isLegal(maxSize > 0, "Font Size must be greater than zero, but was " + maxSize + "!");
maxFontSize = maxSize;
}
/**
* Sets the opacity of the words, which must be a value between 0 and 255
* (inclusive). Currently not very useful...
*
* @param opacity
*/
public void setOpacity(int opacity) {
checkWidget();
Assert.isLegal(opacity > 0, "Opacity must be greater than zero: " + opacity);
Assert.isLegal(opacity < 256, "Opacity must be less than 256: " + opacity);
this.opacity = opacity;
}
/**
* Sets the minimum font size. Should be a reasonable value > 0 (twice of
* {@link TagCloud#accuracy} is recommended). By default, this value is 12.
*
* @param size
*/
public void setMinFontSize(int size) {
checkWidget();
Assert.isLegal(size > 0, "Font Size must be greater zero: " + size);
this.minFontSize = size;
}
/**
* Returns the {@link ImageData} of the text layer image (all rendered
* elements, unscaled, without highlighted selection). Can be used to print
* or export the cloud.
*
* @return the image data of the text layer image
*/
public ImageData getImageData() {
checkWidget();
if (textLayerImage == null)
return null;
return textLayerImage.getImageData();
}
/**
* Enable boosting for the first <code>boost</code> elements. By default, no
* elements are boosted.
*
* @param boost
*/
public void setBoost(int boost) {
checkWidget();
Assert.isLegal(boost >= 0, "Boost cannot be negative");
this.boost = boost;
}
/**
* Enable or disable antialiasing. Enabled by default.
*
* @param enabled
*/
public void setAntiAlias(boolean enabled) {
checkWidget();
if (enabled) {
antialias = SWT.ON;
} else {
antialias = SWT.OFF;
}
}
// /**
// * Work in progress - still broken positioning
// * @param w
// * @throws IOException
// */
// public void toSVG(Writer w) throws IOException {
// int counter = 1;
// w.append("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
// +
// "<!-- Created with Eclipse Tag Cloud -->\n" +
// "<svg\n" +
// "xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n" +
// "xmlns:cc=\"http://creativecommons.org/ns#\"\n" +
// "xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n" +
// "xmlns:svg=\"http://www.w3.org/2000/svg\"\n" +
// "xmlns=\"http://www.w3.org/2000/svg\"\n" +
// "version=\"1.1\"\n" +
// "width=\"" + textLayerImage.getBounds().width + "\"\n" +
// "height=\"" + textLayerImage.getBounds().height + "\"\n" +
// "id=\"svg2\">\n" +
// "<defs\n" +
// "id=\"defs4\" />\n" +
// "<metadata\n" +
// "id=\"metadata7\">\n" +
// "<rdf:RDF>\n" +
// "<cc:Work\n" +
// "rdf:about=\"\">\n" +
// "<dc:format>image/svg+xml</dc:format>\n" +
// "<dc:type\n" +
// "rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" />\n" +
// "<dc:title></dc:title>\n" +
// "</cc:Work>\n" +
// "</rdf:RDF>\n" +
// "</metadata>\n" +
// "<g\n" +
// "id=\"layer1\">\n");
// GC tmp = new GC(Display.getDefault());
// String bg = Integer.toHexString(getBackground().getRed()) +
// Integer.toHexString(getBackground().getGreen()) +
// Integer.toHexString(getBackground().getBlue());
// w.append("<rect x=\"" + 0 + "\" y=\"" + 0 + "\" width=\"" +
// textLayerImage.getBounds().width + "\" height=\"" +
// textLayerImage.getBounds().height + "\" style=\"fill:" + bg +
// ";stroke:#006600;\"/>");
// for (Word word : wordsToUse) {
// String id = "text" + counter++;
// FontData fd = word.fontData[0];
// String style = "font-size:" + fd.getHeight()+"px;" +
// "font-family:" + fd.getName() + ";";
// String text = word.string;
// int x = 0;
// int y = 0;
// double radian = Math.toRadians(word.angle);
// final double sin = Math.abs(Math.sin(radian));
// final double cos = Math.abs(Math.cos(radian));
// float fontSize = getFontSize(word);
// Font font = new Font(tmp.getDevice(), word.fontData);
// Path p = new Path(tmp.getDevice());
// p.addString(word.string, 0, 0, font);
// float[] bounds = new float[4];
// p.getBounds(bounds);
// p.dispose();
// gc.setFont(font);
// //Point stringExtent = gc.stringExtent(word.string);
// font.dispose();
// if(word.angle < 0) {
// y = word.height - (int) ( cos * fontSize);
// } else {
// x = (int) (sin * fontSize);
// }
// x += word.x - regionOffset.x;
// y += word.y - regionOffset.y;
//
// // w.append("<rect x=\"" + 0 + "\" y=\"" + 0 + "\" width=\"" +
// stringExtent.x + "\" height=\"" + stringExtent.y +
// "\" style=\"fill:none;stroke:#006600;\"" +
// // " transform=\"translate(" + x + "," + y + ") rotate(" + word.angle +
// ")\"/>");
//
// int xOff = (int) (-bounds[0] + bounds[2]/2);
// int yOff = (int)(bounds[3] - bounds[1]);
// String color = Integer.toHexString(word.color.getRed()) +
// Integer.toHexString(word.color.getGreen()) +
// Integer.toHexString(word.color.getBlue());
// String fullString = "\n<text "
// + "x=\"" + xOff + "\"\n"
// + "y=\"" + yOff + "\"\n"
// + "text-anchor=\"middle\"\n"
// + "transform = \"translate(" + x + "," + y + ") rotate(" +
// word.angle+")\"\n"
// + "id=\"" + id + "\"\n"
// + "xml:space=\"preserve\"\n"
// + "style=\"font-size:40px;fill:#" + color +
// ";fill-opacity:1;stroke:none;font-family:Sans\">\n"
// + "<tspan "
// + "style=\""+style+"\">"
// + text + "</tspan>\n"
// +"</text>\n";
//
// w.append(fullString);
// }
// tmp.dispose();
// w.append("</g>\n</svg>\n");
// }
public void setBoostFactor(float boostFactor) {
Assert.isLegal(boostFactor != 0);
this.boostFactor = boostFactor;
}
public Color getSelectionColor() {
return highlightColor;
}
public void setLayouter(ILayouter layouter) {
checkWidget();
Assert.isLegal(layouter != null, "Layouter must not be null!");
this.layouter = layouter;
}
public int getMaxFontSize() {
checkWidget();
return maxFontSize;
}
public int getMinFontSize() {
checkWidget();
return minFontSize;
}
public int getBoost() {
checkWidget();
return boost;
}
public float getBoostFactor() {
checkWidget();
return boostFactor;
}
public List<Word> getWords() {
return wordsToUse;
}
public ILayouter getLayouter() {
return layouter;
}
}