Commit 028d0748 authored by Nico Mack's avatar Nico Mack

Added WordCloud corona

parent 5f52d736
......@@ -25,6 +25,7 @@ CENTRE_NODE=centre
CENTRED_NODE=centred
CIRCLE_SIZE_NODE=circleSize
COLOUR_NODE=colour
COLOUR_PALETTE_NODE=colourPalette
COLOURSC_NODE=colourscheme
CONDITION_NODE=condition
CONDITION_BUILDER_NAMESPACE=lu.list.itis.dkd.tui.event.conditional.builder
......@@ -68,6 +69,7 @@ FADE_IN_TIME_NODE=fadeInTime
FADE_OUT_TIME_NODE=fadeOutTime
FADE_WITH_HANDLE_NODE=fadeWithHandle
FADING_ENABLED_NODE=fadingEnabled
FILE_ATTRIBUTE=file
FILL_COLOUR_ELEMENT=fillColour
FILL_COLOUR_NODE=fillColour
FILL_COLOUR_NUANCE_NODE=fillColourNuance
......@@ -217,12 +219,15 @@ TRIGGER_ELEMENT=trigger
TRIGGERS_NODE=triggers
TULIP_NAMESPACE=lu.list.itis.dkd.tui
TYPE_NODE=type
TYPE_ATTRIBUTE=type
UNIT_NODE=unit
UPPER_BOUND_NODE=upperBound
VERSION_NODE=version
VOLUME_NODE=volume
WIDGET_BUILDER_NAMESPACE=lu.list.itis.dkd.tui.widget.builder
WIDTH_NODE=width
WORD_FREQUENCIES_NODE=wordFrequencies
WORD_LENGTH_NODE=wordLength
X_NODE=x
Y_NODE=y
Z_NODE=z
......
......@@ -201,6 +201,11 @@
<artifactId>filters</artifactId>
<version>2.0.235-1</version>
</dependency>
<dependency>
<groupId>com.kennycason</groupId>
<artifactId>kumo-core</artifactId>
<version>1.17</version>
</dependency>
</dependencies>
<repositories>
......
......@@ -451,6 +451,44 @@ public class BootstrappingUtils {
throw (exception);
}
// ---------------------------------------------------------------------------
/**
*
* @param rootElement
* @param childName
* @param optional
* @return
* @throws BuildException
*/
// ---------------------------------------------------------------------------
public static Element getChild(Element rootElement, String childName, boolean optional) throws BuildException {
Element child = rootElement.getChild(childName);
if (!optional && child == null) {
logAndThrowBuildException(new BuildException(StringUtils.build(MANDATORY_NODE_TEMPLATE, childName)), rootElement, childName);
}
return child;
}
// ---------------------------------------------------------------------------
/**
*
* @param rootElement
* @param childName
* @param optional
* @return
* @throws BuildException
*/
// ---------------------------------------------------------------------------
public static List<Element> getChildren(Element rootElement, String childName, boolean optional) throws BuildException {
List<Element> children = rootElement.getChildren(childName);
if (!optional && ((children == null) || children.isEmpty())) {
logAndThrowBuildException(new BuildException(StringUtils.build(MANDATORY_NODE_TEMPLATE, childName)), rootElement, childName);
}
return children;
}
// ---------------------------------------------------------------------------
/**
* returns the content of a child node as a string
......
......@@ -60,6 +60,7 @@ public class Externalization extends NLS {
public static String CENTRED_NODE;
public static String CIRCLE_SIZE_NODE;
public static String COLOUR_NODE;
public static String COLOUR_PALETTE_NODE;
public static String COLOURSC_NODE;
public static String CONDITION_NODE;
public static String CONDITION_BUILDER_NAMESPACE;
......@@ -103,6 +104,7 @@ public class Externalization extends NLS {
public static String FADE_OUT_TIME_NODE;
public static String FADE_WITH_HANDLE_NODE;
public static String FADING_ENABLED_NODE;
public static String FILE_ATTRIBUTE;
public static String FILL_COLOUR_ELEMENT;
public static String FILL_COLOUR_NODE;
public static String FILL_COLOUR_NUANCE_NODE;
......@@ -252,12 +254,15 @@ public class Externalization extends NLS {
public static String TRIGGERS_NODE;
public static String TULIP_NAMESPACE;
public static String TYPE_NODE;
public static String TYPE_ATTRIBUTE;
public static String UNIT_NODE;
public static String UPPER_BOUND_NODE;
public static String VERSION_NODE;
public static String VOLUME_NODE;
public static String WIDGET_BUILDER_NAMESPACE;
public static String WIDTH_NODE;
public static String WORD_FREQUENCIES_NODE;
public static String WORD_LENGTH_NODE;
public static String X_NODE;
public static String Y_NODE;
public static String Z_NODE;
......
/**
* Copyright Luxembourg Institute of Science and Technology, 2018. All rights reserved.
*
* This file is part of TULIP.
*
* TULIP is free software: you can redistribute it and/or modify it under the terms of the GNU
* Lesser General Public License as published by the Free Software Foundation, version 3 of the
* License.
*
* TULIP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with TULIP. If
* not, see <http://www.gnu.org/licenses/lgpl-3.0.html>.
*/
package lu.list.itis.dkd.tui.widget.corona;
import lu.list.itis.dkd.tui.content.InformationReceiver;
import lu.list.itis.dkd.tui.utility.Point;
import lu.list.itis.dkd.tui.utility.ScreenCoordinates;
import lu.list.itis.dkd.tui.widget.corona.builder.BaseWordCloudBuilder;
import lu.list.itis.dkd.tui.widget.corona.builder.ImageBuilder;
import com.kennycason.kumo.CollisionMode;
import com.kennycason.kumo.WordFrequency;
import com.kennycason.kumo.bg.Background;
import com.kennycason.kumo.collide.Collidable;
import com.kennycason.kumo.font.scale.FontScalar;
import com.kennycason.kumo.nlp.FrequencyAnalyzer;
import com.kennycason.kumo.palette.ColorPalette;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* @author Nico Mack [nico.mack@list.lu]
* @since 2.5.0
* @version 1.0
*/
// ***************************************************************************
// * Class Definition and Members *
// ***************************************************************************
public class WordCloud extends Corona implements InformationReceiver<String> {
protected String textContent;
protected int width;
protected int height;
protected FrequencyAnalyzer analyzer;
protected ColorPalette palette;
protected FontScalar scaler;
protected com.kennycason.kumo.WordCloud cloud;
private Point cloudCentre;
// ***************************************************************************
// * Constants
// ***************************************************************************
private static final Color TRANSPARENT = new Color(0, 0, 0, 0);
private static final Logger LOGGER = LoggerFactory.getLogger(WordCloud.class.getSimpleName());
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Inner Class *
// ***************************************************************************
// ---------------------------------------------------------------------------
private class ShapeBackground implements Background {
private Shape background;
public ShapeBackground(Shape shape) {
this.background = shape;
}
/** {@inheritDoc} */
@Override
public boolean isInBounds(Collidable collidable) {
final java.awt.Point position = collidable.getPosition();
return this.background.contains(position);
}
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Constructor(s) *
// ***************************************************************************
// ---------------------------------------------------------------------------
/**
* Constructor building all fields and calling the implicit super constructor using a
* {@link ImageBuilder} instance holding all values.
*
* @param builder
* The {@link ImageBuilder} instance holding all values.
*/
// ---------------------------------------------------------------------------
public WordCloud(BaseWordCloudBuilder<?> builder) {
super(builder);
this.textContent = builder.textContent;
this.width = builder.width;
this.height = builder.height;
this.analyzer = builder.analyzer;
this.palette = builder.palette;
this.scaler = builder.scaler;
this.buildFromProperties();
this.setInformation(this.textContent);
}
// ---------------------------------------------------------------------------
/**
* @param original
*/
// ---------------------------------------------------------------------------
public WordCloud(WordCloud original) {
super(original);
this.textContent = original.textContent;
this.width = original.width;
this.height = original.height;
this.analyzer = original.analyzer;
this.palette = original.palette;
this.scaler = original.scaler;
this.buildFromProperties();
this.setInformation(this.textContent);
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Primitive(s) *
// ***************************************************************************
// ---------------------------------------------------------------------------
private void buildFromProperties() {
if ((this.width <= 0) || (this.height <= 0)) {
Rectangle2D bounds = this.shape.getBounds2D();
this.width = (int) bounds.getWidth();
this.height = (int) bounds.getHeight();
}
this.cloud = new com.kennycason.kumo.WordCloud(new Dimension(this.width, this.height), CollisionMode.PIXEL_PERFECT);
this.cloud.setBackgroundColor(TRANSPARENT);
this.cloud.setBackground(new ShapeBackground(this.shape));
this.cloud.setColorPalette(this.palette);
this.cloud.setFontScalar(this.scaler);
}
// ---------------------------------------------------------------------------
private void renderInformation(String information) {
List<WordFrequency> frequencies = null;
this.textContent = information;
try (InputStream input = IOUtils.toInputStream(information)) {
frequencies = this.analyzer.load(input);
} catch (IOException e) {
LOGGER.error("Failed to read information!", e); //$NON-NLS-1$
}
this.cloud.build(frequencies);
BufferedImage image = this.cloud.getBufferedImage();
this.cloudCentre = (image != null) ? new Point(image.getWidth(null) / 2, image.getHeight(null) / 2, 0, ScreenCoordinates.class)
: new Point();
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Class Body *
// ***************************************************************************
// ---------------------------------------------------------------------------
/** {@inheritDoc} */
@Override
public void paint(Graphics2D canvas) {
if (!active) {
return;
}
AffineTransform transform = this.getTransform(this.cloudCentre);
canvas.drawImage(this.cloud.getBufferedImage(), transform, null);
}
// ---------------------------------------------------------------------------
/** {@inheritDoc} */
// ---------------------------------------------------------------------------
@Override
public WordCloud clone() {
return new WordCloud(this);
}
// ---------------------------------------------------------------------------
/** {@inheritDoc} */
// ---------------------------------------------------------------------------
@Override
public void setInformation(String information) {
this.renderInformation(information);
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * End of Class *
// ***************************************************************************
// ---------------------------------------------------------------------------
}
/**
* Copyright Luxembourg Institute of Science and Technology, 2016.
*
* This file is part of TULIP.
*
* TULIP is licensed under a dual-licensing scheme. For non-commercial purposes, the LGPL version 3,
* as stated below, is applicable. For all commercial purposes TULIP is licensed under a LIST
* proprietary license. Please contact LIST at tto@list.lu to obtain a commercial license.
*
* For all non-commercial purposes, TULIP is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, version 3 of the License.
*
* TULIP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with TULIP. If
* not, see <http://www.gnu.org/licenses/lgpl-3.0.html>.
*/
package lu.list.itis.dkd.tui.widget.corona.builder;
import lu.list.itis.dkd.dbc.annotation.NonNullByDefault;
import lu.list.itis.dkd.dbc.annotation.Nullable;
import lu.list.itis.dkd.tui.bootstrapping.BootstrapCallback;
import lu.list.itis.dkd.tui.bootstrapping.BootstrapContext;
import lu.list.itis.dkd.tui.bootstrapping.BootstrappingUtils;
import lu.list.itis.dkd.tui.exception.BuildException;
import lu.list.itis.dkd.tui.utility.Externalization;
import lu.list.itis.dkd.tui.utility.Point;
import lu.list.itis.dkd.tui.utility.StringUtils;
import lu.list.itis.dkd.tui.widget.corona.Image;
import lu.list.itis.dkd.tui.widget.corona.WordCloud;
import com.google.common.base.Strings;
import com.kennycason.kumo.font.scale.FontScalar;
import com.kennycason.kumo.font.scale.LinearFontScalar;
import com.kennycason.kumo.nlp.FrequencyAnalyzer;
import com.kennycason.kumo.palette.ColorPalette;
import org.jdom2.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/**
* Builder serving as abstract super class for all {@link Image} builders.
*
* @author Nico Mack [nico.mack@list.lu]
* @since 2.5.0
* @version 1.0
* @param <B>
* The concrete builder.
*/
@NonNullByDefault
// ***************************************************************************
// * Class Definition and Members *
// ***************************************************************************
public abstract class BaseWordCloudBuilder<B extends BaseWordCloudBuilder<B>> extends CoronaBuilder<B> {
/** Field holding the image to display. */
@Nullable
public String textContent;
/** */
public int width;
/** */
public int height;
public FrequencyAnalyzer analyzer;
public ColorPalette palette;
public FontScalar scaler;
// ***************************************************************************
// * Constants *
// ***************************************************************************
private static Logger LOGGER = LoggerFactory.getLogger(BaseWordCloudBuilder.class.getSimpleName());
private static final int DEFAULT_MIN_SCALE = 10;
private static final int DEFAULT_MAX_SCALE = 40;
private static final ColorPalette DEFAULT_PALETTE = new ColorPalette(Color.LIGHT_GRAY, Color.DARK_GRAY);
private static final FontScalar DEFAULT_SCALER = new LinearFontScalar(DEFAULT_MIN_SCALE, DEFAULT_MAX_SCALE);
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Constructor(s) *
// ***************************************************************************
// ---------------------------------------------------------------------------
/**
* Constructor setting the centre of the corona.
*
* @param centre
* The centre of the corona, usually the centre of the handle.
*/
// ---------------------------------------------------------------------------
protected BaseWordCloudBuilder(Point centre) {
super(centre);
}
// ---------------------------------------------------------------------------
/**
* Constructor initializing the centre of the corona as well as a possible image and information
* provider as given by the children of a provided element.
*
* @param rootElement
* The element harbouring, on child nodes, the necessary information to initialize all fields
* of the builder.
* @throws BuildException
* Exception raised when the building of a corona instance cannot complete successfully due
* to the violation of one or more contracts associated with the instance, including
* missing, incomplete, or erroneous parameters or values thereof.
*/
// ---------------------------------------------------------------------------
protected BaseWordCloudBuilder(Element rootElement) throws BuildException {
super(rootElement);
this.buildFromBootstrap(rootElement, null, null);
}
// ---------------------------------------------------------------------------
/**
*
* @param rootElement
* @param context
* @param callback
* @throws BuildException
*/
// ---------------------------------------------------------------------------
protected BaseWordCloudBuilder(Element rootElement, BootstrapContext context, BootstrapCallback callback) throws BuildException {
super(rootElement, context, callback);
this.buildFromBootstrap(rootElement, context, callback);
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Primitive(s) *
// ***************************************************************************
// ---------------------------------------------------------------------------
/**
*
* @param rootElement
* @param context
* @param callback
* @throws BuildException
*/
// ---------------------------------------------------------------------------
private void buildFromBootstrap(@Nullable Element rootElement, BootstrapContext context, BootstrapCallback callback) throws BuildException {
textContent = BootstrappingUtils.getContentAsString(rootElement, Externalization.TEXT_NODE, BootstrappingUtils.OPTIONAL, Externalization.EMPTY_STRING, context);
if (Strings.isNullOrEmpty(textContent)) {
Element textNode = BootstrappingUtils.getChild(rootElement, Externalization.TEXT_NODE, BootstrappingUtils.MANDATORY);
if (textNode != null) {
String textPath = BootstrappingUtils.getAttributeAsString(textNode, Externalization.FILE_ATTRIBUTE, BootstrappingUtils.MANDATORY, null);
URL textUrl;
try {
File urlFile = new File(textPath);
textUrl = urlFile.toURI().toURL();
} catch (MalformedURLException e) {
throw new BuildException(StringUtils.build("Incorrectly formatted file location {}", textPath)); //$NON-NLS-1$
}
try (BufferedReader in = new BufferedReader(new InputStreamReader(textUrl.openStream()))) {
StringBuilder builder = new StringBuilder();
String inputLine;
while ((inputLine = in.readLine()) != null) {
builder.append(inputLine);
}
this.textContent = builder.toString();
} catch (IOException e) {
throw new BuildException(StringUtils.build("Failed to read text content from {}", textPath)); //$NON-NLS-1$
}
}
}
width = BootstrappingUtils.getContentAsInteger(rootElement, Externalization.WIDTH_NODE, BootstrappingUtils.OPTIONAL, -1, context);
height = BootstrappingUtils.getContentAsInteger(rootElement, Externalization.HEIGHT_NODE, BootstrappingUtils.OPTIONAL, -1, context);
analyzer = new FrequencyAnalyzer();
int wordFrequencies = BootstrappingUtils.getContentAsInteger(rootElement, Externalization.WORD_FREQUENCIES_NODE, BootstrappingUtils.OPTIONAL, FrequencyAnalyzer.DEFAULT_WORD_FREQUENCIES_TO_RETURN, context);
Element wordLengthNode = BootstrappingUtils.getChild(rootElement, Externalization.WORD_LENGTH_NODE, BootstrappingUtils.OPTIONAL);
int minWordLength = FrequencyAnalyzer.DEFAULT_WORD_MIN_LENGTH;
int maxWordLength = FrequencyAnalyzer.DEFAULT_WORD_MAX_LENGTH;
if (wordLengthNode != null) {
minWordLength = BootstrappingUtils.getContentAsInteger(wordLengthNode, Externalization.LOWER_BOUND_NODE, BootstrappingUtils.OPTIONAL, FrequencyAnalyzer.DEFAULT_WORD_MIN_LENGTH, context);
maxWordLength = BootstrappingUtils.getContentAsInteger(wordLengthNode, Externalization.UPPER_BOUND_NODE, BootstrappingUtils.OPTIONAL, FrequencyAnalyzer.DEFAULT_WORD_MAX_LENGTH, context);
}
analyzer.setWordFrequenciesToReturn(wordFrequencies);
analyzer.setMinWordLength(minWordLength);
analyzer.setMaxWordLength(maxWordLength);
Element paletteNode = BootstrappingUtils.getChild(rootElement, Externalization.COLOUR_PALETTE_NODE, BootstrappingUtils.OPTIONAL);
palette = (paletteNode != null) ? this.buildColorPalette(paletteNode, context) : DEFAULT_PALETTE;
Element fontSizeNode = BootstrappingUtils.getChild(rootElement, Externalization.FONT_SIZE_NODE, BootstrappingUtils.OPTIONAL);
scaler = (fontSizeNode != null) ? this.buildFontScaler(fontSizeNode, context) : DEFAULT_SCALER;
}
// ---------------------------------------------------------------------------
private ColorPalette buildColorPalette(Element paletteElement, BootstrapContext context) throws BuildException {
ColorPalette colourPalette = null;
List<Element> colourNodes = BootstrappingUtils.getChildren(paletteElement, Externalization.COLOUR_NODE, BootstrappingUtils.MANDATORY);
if (colourNodes != null) {
List<Color> colours = new ArrayList<>();
for (Element colourNode : colourNodes) {
colours.add(BootstrappingUtils.getContentAsColour(colourNode, null, BootstrappingUtils.MANDATORY, Color.BLACK, context));
}
colourPalette = new ColorPalette(colours);
}
return colourPalette;
}
// ---------------------------------------------------------------------------
private FontScalar buildFontScaler(Element fontSizeNode, BootstrapContext context) throws BuildException {
int lowerBound = BootstrappingUtils.getContentAsInteger(fontSizeNode, Externalization.LOWER_BOUND_NODE, BootstrappingUtils.OPTIONAL, DEFAULT_MIN_SCALE, context);
int upperBound = BootstrappingUtils.getContentAsInteger(fontSizeNode, Externalization.UPPER_BOUND_NODE, BootstrappingUtils.OPTIONAL, DEFAULT_MAX_SCALE, context);
return new LinearFontScalar(lowerBound, upperBound);
}
// ---------------------------------------------------------------------------
// ***************************************************************************
// * Class Body *
// ***************************************************************************
// ---------------------------------------------------------------------------
/** {@inheritDoc} */
@Override
public abstract WordCloud build();
}
\ No newline at end of file
/**
* Copyright Luxembourg Institute of Science and Technology, 2016.
*
* This file is part of TULIP.
*
* TULIP is licensed under a dual-licensing scheme. For non-commercial purposes, the LGPL version 3,
* as stated below, is applicable. For all commercial purposes TULIP is licensed under a LIST
* proprietary license. Please contact LIST at tto@list.lu to obtain a commercial license.
*
* For all non-commercial purposes, TULIP is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the Free Software
* Foundation, version 3 of the License.
*
* TULIP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
* General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with TULIP. If
* not, see <http://www.gnu.org/licenses/lgpl-3.0.html>.
*/
package lu.list.itis.dkd.tui.widget.corona.builder;
import lu.list.itis.dkd.dbc.annotation.NonNullByDefault;
import lu.list.itis.dkd.tui.bootstrapping.BootstrapCallback;
import lu.list.itis.dkd.tui.bootstrapping.BootstrapContext;
import lu.list.itis.dkd.tui.exception.BuildException;
import lu.list.itis.dkd.tui.utility.Point;
import lu.list.itis.dkd.tui.widget.corona.Image;
import lu.list.itis.dkd.tui.widget.corona.WordCloud;
import org.jdom2.Element;
/**
* {@link WordCloudBuilder} class used to construct an {@link Image} corona by providing methods to
* set all parameters and permutations thereof.
*
* @author Eric Tobias [eric.tobias@list.lu]
* @since 1.0
* @version 2.3.0
*/
@NonNullByDefault
public final class WordCloudBuilder extends BaseWordCloudBuilder<WordCloudBuilder> {
/**
* Constructor setting the centre of the corona.
*
* @param centre
* The centre of the corona, usually the centre of the handle.
*/
public WordCloudBuilder(Point centre) {
super(centre);
}
/**
* Constructor initializing the centre of the corona as well as a possible image and information
* provider as given by the children of a provided element.
*
* @param rootElement
* The element harbouring, on child nodes, the necessary information to initialize all fields
* of the builder.
* @throws BuildException
* Exception raised when the building of a corona instance cannot complete successfully due
* to the violation of one or more contracts associated with the instance, including
* missing, incomplete, or erroneous parameters or values thereof.
*/
public WordCloudBuilder(Element rootElement) throws BuildException {
super(rootElement);
}
/**
* Constructor initializing the centre of the corona as well as a possible image and information
* provider as given by the children of a provided element.
*
* @param rootElement
* The element harbouring, on child nodes, the necessary information to initialize all fields
* of the builder.
* @param context
* @param callback
* @throws BuildException
* Exception raised when the building of a corona instance cannot complete successfully due
* to the violation of one or more contracts associated with the instance, including
* missing, incomplete, or erroneous parameters or values thereof.
*/
public WordCloudBuilder(Element rootElement, BootstrapContext context, BootstrapCallback callback) throws BuildException {
super(rootElement, context, callback);
}
/** {@inheritDoc} */
@Override
public WordCloud build() {
return new WordCloud(this);
}
}
\ No newline at end of file