ruby_wordcram 1.0.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.mvn/extensions.xml +8 -0
  4. data/.mvn/wrapper/maven-wrapper.properties +1 -0
  5. data/Rakefile +28 -5
  6. data/docs/_posts/2017-03-07-getting_started.md +3 -2
  7. data/docs/_posts/2017-03-07-under_the_hood.md +33 -0
  8. data/lib/WordCram.jar +0 -0
  9. data/lib/jsoup-1.10.2.jar +0 -0
  10. data/lib/ruby_wordcram/version.rb +1 -1
  11. data/lib/ruby_wordcram.rb +1 -2
  12. data/pom.rb +53 -0
  13. data/pom.xml +87 -0
  14. data/ruby_wordcram.gemspec +1 -2
  15. data/src/cue/lang/Counter.java +141 -0
  16. data/src/cue/lang/IterableText.java +10 -0
  17. data/src/cue/lang/NGramIterator.java +151 -0
  18. data/src/cue/lang/SentenceIterator.java +86 -0
  19. data/src/cue/lang/WordIterator.java +60 -0
  20. data/src/cue/lang/stop/StopWords.java +114 -0
  21. data/src/cue/lang/stop/arabic +351 -0
  22. data/src/cue/lang/stop/armenian +45 -0
  23. data/src/cue/lang/stop/catalan +219 -0
  24. data/src/cue/lang/stop/croatian +2024 -0
  25. data/src/cue/lang/stop/czech +256 -0
  26. data/src/cue/lang/stop/danish +94 -0
  27. data/src/cue/lang/stop/dutch +107 -0
  28. data/src/cue/lang/stop/english +183 -0
  29. data/src/cue/lang/stop/esperanto +180 -0
  30. data/src/cue/lang/stop/farsi +966 -0
  31. data/src/cue/lang/stop/finnish +235 -0
  32. data/src/cue/lang/stop/french +543 -0
  33. data/src/cue/lang/stop/german +231 -0
  34. data/src/cue/lang/stop/greek +637 -0
  35. data/src/cue/lang/stop/hebrew +220 -0
  36. data/src/cue/lang/stop/hindi +97 -0
  37. data/src/cue/lang/stop/hungarian +202 -0
  38. data/src/cue/lang/stop/italian +279 -0
  39. data/src/cue/lang/stop/latin +1 -0
  40. data/src/cue/lang/stop/norwegian +176 -0
  41. data/src/cue/lang/stop/polish +138 -0
  42. data/src/cue/lang/stop/portuguese +204 -0
  43. data/src/cue/lang/stop/romanian +284 -0
  44. data/src/cue/lang/stop/russian +652 -0
  45. data/src/cue/lang/stop/slovak +110 -0
  46. data/src/cue/lang/stop/slovenian +448 -0
  47. data/src/cue/lang/stop/spanish +308 -0
  48. data/src/cue/lang/stop/swedish +114 -0
  49. data/src/cue/lang/stop/turkish +117 -0
  50. data/src/cue/lang/unicode/BlockUtil.java +103 -0
  51. data/src/cue/lang/unicode/Normalizer.java +55 -0
  52. data/src/cue/lang/unicode/Normalizer6.java +32 -0
  53. data/src/license.txt +201 -0
  54. data/src/wordcram/Anglers.java +137 -0
  55. data/src/wordcram/BBTree.java +133 -0
  56. data/src/wordcram/BBTreeBuilder.java +61 -0
  57. data/src/wordcram/Colorers.java +52 -0
  58. data/src/wordcram/EngineWord.java +73 -0
  59. data/src/wordcram/Fonters.java +17 -0
  60. data/src/wordcram/HsbWordColorer.java +28 -0
  61. data/src/wordcram/ImageShaper.java +91 -0
  62. data/src/wordcram/Observer.java +9 -0
  63. data/src/wordcram/PlacerHeatMap.java +134 -0
  64. data/src/wordcram/Placers.java +74 -0
  65. data/src/wordcram/PlottingWordNudger.java +38 -0
  66. data/src/wordcram/PlottingWordPlacer.java +36 -0
  67. data/src/wordcram/ProcessingWordRenderer.java +42 -0
  68. data/src/wordcram/RandomWordNudger.java +44 -0
  69. data/src/wordcram/RenderOptions.java +10 -0
  70. data/src/wordcram/ShapeBasedPlacer.java +66 -0
  71. data/src/wordcram/Sizers.java +54 -0
  72. data/src/wordcram/SketchCallbackObserver.java +70 -0
  73. data/src/wordcram/SpiralWordNudger.java +31 -0
  74. data/src/wordcram/SvgWordRenderer.java +110 -0
  75. data/src/wordcram/SwirlWordPlacer.java +25 -0
  76. data/src/wordcram/UpperLeftWordPlacer.java +27 -0
  77. data/src/wordcram/WaveWordPlacer.java +25 -0
  78. data/src/wordcram/Word.java +357 -0
  79. data/src/wordcram/WordAngler.java +20 -0
  80. data/src/wordcram/WordArray.java +18 -0
  81. data/src/wordcram/WordBag.java +31 -0
  82. data/src/wordcram/WordColorer.java +25 -0
  83. data/src/wordcram/WordCounter.java +96 -0
  84. data/src/wordcram/WordCram.java +920 -0
  85. data/src/wordcram/WordCramEngine.java +196 -0
  86. data/src/wordcram/WordFonter.java +24 -0
  87. data/src/wordcram/WordNudger.java +44 -0
  88. data/src/wordcram/WordPlacer.java +44 -0
  89. data/src/wordcram/WordRenderer.java +10 -0
  90. data/src/wordcram/WordShaper.java +78 -0
  91. data/src/wordcram/WordSizer.java +46 -0
  92. data/src/wordcram/WordSkipReason.java +42 -0
  93. data/src/wordcram/WordSorterAndScaler.java +31 -0
  94. data/src/wordcram/WordSource.java +5 -0
  95. data/src/wordcram/text/Html.java +15 -0
  96. data/src/wordcram/text/Html2Text.java +17 -0
  97. data/src/wordcram/text/Text.java +15 -0
  98. data/src/wordcram/text/TextFile.java +23 -0
  99. data/src/wordcram/text/TextSource.java +5 -0
  100. data/src/wordcram/text/WebPage.java +23 -0
  101. metadata +94 -5
  102. data/lib/cue.language.jar +0 -0
  103. data/lib/jsoup-1.7.2.jar +0 -0
  104. data/vendors/Rakefile +0 -51
@@ -0,0 +1,20 @@
1
+ package wordcram;
2
+
3
+ /**
4
+ * A WordAngler tells WordCram what angle to draw a word at, in radians.
5
+ * <p>
6
+ * Some useful implementations are available in {@link Anglers}.
7
+ *
8
+ * @author Dan Bernier
9
+ */
10
+ public interface WordAngler {
11
+
12
+ /**
13
+ * What angle should this {@link Word} be rotated at?
14
+ *
15
+ * @param word
16
+ * The Word that WordCram is about to draw, and wants to rotate
17
+ * @return the rotation angle for the Word, in radians
18
+ */
19
+ public float angleFor(Word word);
20
+ }
@@ -0,0 +1,18 @@
1
+ package wordcram;
2
+
3
+ /*
4
+ * This is just here so WordCram.fromWords(Word[]) has something to use.
5
+ */
6
+
7
+ class WordArray implements WordSource {
8
+ Word[] words;
9
+
10
+ WordArray(Word[] words) {
11
+ this.words = words;
12
+ }
13
+
14
+ @Override
15
+ public Word[] getWords() {
16
+ return words;
17
+ }
18
+ }
@@ -0,0 +1,31 @@
1
+ package wordcram;
2
+
3
+ public class WordBag implements WordSource {
4
+
5
+ int numWords;
6
+ String[] wordStrings;
7
+ double weightDistributionPower = 2;
8
+
9
+ public WordBag(int numWords, String... wordStrings) {
10
+ this.numWords = numWords;
11
+ this.wordStrings = wordStrings;
12
+ }
13
+
14
+ public WordBag weightDistributionPower(float wdp) {
15
+ this.weightDistributionPower = wdp;
16
+ return this;
17
+ }
18
+
19
+ @Override
20
+ public Word[] getWords() {
21
+ Word[] words = new Word[numWords];
22
+ java.util.Random rand = new java.util.Random();
23
+
24
+ for (int i = 0, wi = 0; i < words.length; i++, wi = (wi + 1) % wordStrings.length) {
25
+ String word = wordStrings[wi];
26
+ double weight = Math.pow(rand.nextDouble(), weightDistributionPower);
27
+ words[i] = new Word(word, (float)weight);
28
+ }
29
+ return words;
30
+ }
31
+ }
@@ -0,0 +1,25 @@
1
+ package wordcram;
2
+
3
+ /**
4
+ * A WordColorer tells WordCram what color to render a word in.
5
+ * <p>
6
+ * <b>Note:</b> if you implement your own WordColorer, you should be familiar
7
+ * with how <a href="http://processing.org/reference/color_datatype.html"
8
+ * target="blank">Processing represents colors</a> -- or just make sure it uses
9
+ * Processing's <a href="http://processing.org/reference/color_.html"
10
+ * target="blank">color</a> method.
11
+ * <p>
12
+ * Some useful implementations are available in {@link Colorers}.
13
+ *
14
+ * @author Dan Bernier
15
+ */
16
+ public interface WordColorer {
17
+
18
+ /**
19
+ * What color should this {@link Word} be?
20
+ *
21
+ * @param word the word to pick the color for
22
+ * @return the color for the word
23
+ */
24
+ public int colorFor(Word word);
25
+ }
@@ -0,0 +1,96 @@
1
+ package wordcram;
2
+
3
+ import java.util.*;
4
+ import java.util.Map.Entry;
5
+
6
+ import cue.lang.Counter;
7
+ import cue.lang.WordIterator;
8
+ import cue.lang.stop.StopWords;
9
+
10
+ class WordCounter {
11
+
12
+ private StopWords cueStopWords;
13
+ private Set<String> extraStopWords = new HashSet<>();
14
+ private boolean excludeNumbers;
15
+
16
+ public WordCounter() {
17
+ this(null);
18
+ }
19
+ public WordCounter(StopWords cueStopWords) {
20
+ this.cueStopWords = cueStopWords;
21
+ }
22
+
23
+ public WordCounter withExtraStopWords(String extraStopWordsString) {
24
+ String[] stopWordsArray = extraStopWordsString.toLowerCase().split(" ");
25
+ extraStopWords = new HashSet<>(Arrays.asList(stopWordsArray));
26
+ return this;
27
+ }
28
+
29
+ public WordCounter shouldExcludeNumbers(boolean shouldExcludeNumbers) {
30
+ excludeNumbers = shouldExcludeNumbers;
31
+ return this;
32
+ }
33
+
34
+ public Word[] count(String text, RenderOptions renderOptions) {
35
+ if (cueStopWords == null) {
36
+ cueStopWords = StopWords.guess(text);
37
+
38
+ if (cueStopWords == StopWords.Arabic ||
39
+ cueStopWords == StopWords.Farsi ||
40
+ cueStopWords == StopWords.Hebrew) {
41
+ renderOptions.rightToLeft = true;
42
+ }
43
+
44
+ tellScripterAboutTheGuess(cueStopWords);
45
+ }
46
+ return countWords(text);
47
+ }
48
+
49
+ private void tellScripterAboutTheGuess(StopWords stopWords) {
50
+ // TODO Find a better way to do this; it prints out during the tests. =p
51
+ if (stopWords == null) {
52
+ System.out.println("cue.language can't guess what language your text is in.");
53
+ } else {
54
+ System.out.println("cue.language guesses your text is in " + stopWords);
55
+ }
56
+ }
57
+
58
+ private Word[] countWords(String text) {
59
+ Counter<String> counter = new Counter<>();
60
+
61
+ for (String word : new WordIterator(text)) {
62
+ if (shouldCountWord(word)) {
63
+ counter.note(word);
64
+ }
65
+ }
66
+
67
+ List<Word> words = new ArrayList<>();
68
+
69
+ counter.entrySet().forEach((entry) -> {
70
+ words.add(new Word(entry.getKey(), (int)entry.getValue()));
71
+ });
72
+
73
+ return words.toArray(new Word[0]);
74
+ }
75
+
76
+ private boolean shouldCountWord(String word) {
77
+ return !isStopWord(word) && !(excludeNumbers && isNumeric(word));
78
+ }
79
+
80
+ private boolean isNumeric(String word) {
81
+ try {
82
+ Double.parseDouble(word);
83
+ return true;
84
+ }
85
+ catch (NumberFormatException x) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ private boolean isStopWord(String word) {
91
+ boolean cueSaysStopWord = cueStopWords != null && cueStopWords.isStopWord(word);
92
+ boolean extraSaysStopWord = extraStopWords.contains(word.toLowerCase());
93
+ return cueSaysStopWord || extraSaysStopWord;
94
+ }
95
+
96
+ }
@@ -0,0 +1,920 @@
1
+ package wordcram;
2
+
3
+ import processing.core.*;
4
+ import wordcram.text.*;
5
+ import java.util.ArrayList;
6
+
7
+ /**
8
+ * The main API for WordCram.
9
+ *
10
+ * <p>There are three steps to making a WordCram:
11
+ * <ol>
12
+ * <li>weight your words
13
+ * <li>style your words
14
+ * <li>draw your WordCram
15
+ * </ol>
16
+ * You start with a <code>new WordCram(this)</code>, and then...
17
+ *
18
+ * <h2>Step One: Weight Your Words</h2>
19
+ *
20
+ * Give WordCram some text to chew on, or an array of Words you've
21
+ * weighted yourself.
22
+ *
23
+ * <h3>Let WordCram Weight Your Words</h3>
24
+ *
25
+ * <p>WordCram weights your words by the number of times they appear
26
+ * in a document. It can load the document a few ways:
27
+ *
28
+ * <ul>
29
+ * <li>{@link #fromWebPage(String)} and {@link #fromHtmlFile(String)} load the HTML and scrape out the words</li>
30
+ * <li>{@link #fromHtmlString(String...)} takes a String (or String[]), assumes it's HTML, and scrapes out its text</li>
31
+ * <li>{@link #fromTextFile(String)} loads a file (from the filesystem or the network), and counts the words</li>
32
+ * <li>{@link #fromTextString(String...)} takes a String (or String[]), and counts the words</li>
33
+ * </ul>
34
+ *
35
+ * <p>If you need some other way to load your text, pass your own
36
+ * TextSource to {@link #fromText(TextSource)}, and WordCram get its
37
+ * text via {@link TextSource#getText()}.
38
+ *
39
+ * <p>Once the text is loaded, you can control how WordCram counts up the words.
40
+ *
41
+ * <p><b>Case sensitivity:</b> If your text contains "hello", "HELLO",
42
+ * and "Hello",
43
+ *
44
+ * <ul><li>{@link #lowerCase()} will count them all as "hello"</li>
45
+ * <li>{@link #upperCase()} will count them all as "HELLO"</li>
46
+ * <li>{@link #keepCase()}, the default, will count them separately, as three different words</li></ul>
47
+ *
48
+ * <p><b>Numbers:</b> If your text contains words like "42" or
49
+ * "3.14159", you can remove them with {@link #excludeNumbers()} (the
50
+ * default), or include them with {@link #includeNumbers()}.
51
+ *
52
+ * <p><b>Stop words:</b> WordCram uses <a
53
+ * href="https://github.com/jdf/cue.language">cue.language</a> to remove common words from the text by default, but you can
54
+ * add your own stop words with {@link #withStopWords(String)}.
55
+ *
56
+ *
57
+ * <h3>Weight Your Own Words</h3>
58
+ *
59
+ * <p>If you have some other way to weight your words, you can pass
60
+ * them to {@link #fromWords(Word[])}, and in that case, you can use
61
+ * {@link Word#setColor(int)}, {@link Word#setFont(PFont)}, {@link
62
+ * Word#setAngle(float)}, and/or {@link Word#setPlace(PVector)} to
63
+ * control how any (or all) of your Words are drawn.
64
+ *
65
+ *
66
+ *
67
+ * <h2>Step Two: Style Your Words</h2>
68
+ *
69
+ * There are six questions you have to answer when drawing a word on the WordCram:
70
+ *
71
+ * <h3>How big should it be?</h3>
72
+ * A word can be
73
+ * {@link #sizedByWeight(int, int)},
74
+ * {@link #sizedByRank(int, int)}, or
75
+ * {@link #withSizer(WordSizer)}
76
+ *
77
+ * <h3>How should it be angled?</h3>
78
+ * It can be
79
+ * {@link #angledAt(float...)},
80
+ * {@link #angledBetween(float, float)}, or
81
+ * {@link #withAngler(WordAngler)}
82
+ *
83
+ * <h3>What font should it be in?</h3> You can render words {@link
84
+ * #withFont(String)} or {@link #withFonts(String...)} (those both can
85
+ * also take PFonts), or {@link #withFonter(WordFonter)}
86
+ *
87
+ * <h3>How should it be colored?</h3>
88
+ * {@link #withColor(int)},
89
+ * {@link #withColors(int...)}, or
90
+ * {@link #withColorer(WordColorer)}
91
+ *
92
+ * <h3>Where on the image should it go?</h3>
93
+ * {@link #withPlacer(WordPlacer)}
94
+ *
95
+ * <h3>If it doesn't fit at first, how should I nudge it?</h3>
96
+ * {@link #withNudger(WordNudger)}
97
+ *
98
+ * <h2>Step Three: Draw Your WordCram</h2>
99
+ *
100
+ * <p>After all that, actually rendering the WordCram is simple.
101
+ *
102
+ * You can repeatedly call {@link #drawNext()} while the WordCram
103
+ * {@link #hasMore()} words to draw (probably once per Processing
104
+ * frame):
105
+ *
106
+ * <pre>
107
+ * void draw() {
108
+ * if (wordCram.hasMore()) {
109
+ * wordCram.drawNext();
110
+ * }
111
+ * }
112
+ * </pre>
113
+ *
114
+ * Or you can call {@link #drawAll()} once, and let it loop for you:
115
+ *
116
+ * <pre>
117
+ * void draw() {
118
+ * wordCram.drawAll();
119
+ * }
120
+ * </pre>
121
+ *
122
+ * <h2>Step Three-and-a-Half: How Did It Go?</h2>
123
+ *
124
+ * <p>If you're having trouble getting your words to show up, you
125
+ * might want to {@link #getSkippedWords()}. Knowing which words were
126
+ * skipped, and why (see {@link Word#wasSkippedBecause()}), can help
127
+ * you size and place your words better.
128
+ *
129
+ * <p>You can also {@link #getWords()} to see the whole list, and
130
+ * {@link #getWordAt(float,float)} to see which word covers a given pixel.
131
+ *
132
+ * @author Dan Bernier
133
+ */
134
+ public class WordCram {
135
+
136
+ /*
137
+ * This class is really only two parts: the fluent builder API, and
138
+ * pass-through calls to the WordCramEngine, where all the work happens.
139
+ * This separation keeps the classes focused on only one thing, but still
140
+ * gives the user a pretty nice API.
141
+ */
142
+ private Word[] words;
143
+ private WordSource wordSource;
144
+ private final ArrayList<TextSource> textSources = new ArrayList<>();
145
+ private String extraStopWords = "";
146
+ private boolean excludeNumbers = true;
147
+ private enum TextCase { Lower, Upper, Keep };
148
+ private TextCase textCase = TextCase.Keep;
149
+
150
+ private WordCramEngine wordCramEngine;
151
+
152
+ private final PApplet parent;
153
+
154
+ private WordFonter fonter;
155
+ private WordSizer sizer;
156
+ private WordColorer colorer;
157
+ private WordAngler angler;
158
+ private WordPlacer placer;
159
+ private WordNudger nudger;
160
+
161
+ private WordRenderer renderer;
162
+ private final RenderOptions renderOptions = new RenderOptions();
163
+ private Observer observer;
164
+
165
+ /**
166
+ * Make a new WordCram.
167
+ * <p>
168
+ * It's the starting point of the fluent API for building WordCrams.
169
+ *
170
+ * @param parent Your Processing sketch. Pass it as <code>this</code>.
171
+ */
172
+ public WordCram(PApplet parent) {
173
+ this.parent = parent;
174
+ this.renderer = new ProcessingWordRenderer(parent.g);
175
+ this.observer = new SketchCallbackObserver(parent);
176
+ }
177
+
178
+ /**
179
+ * Tells WordCram which words to ignore when it counts up the words in your text.
180
+ * These words won't show up in the image.
181
+ * <p>
182
+ * Stop-words are always case-insensitive: if your source text contains "The plane,
183
+ * the plane!", using "the" for a stop-word is enough to block both "the" and "The".
184
+ * <p>
185
+ * It doesn't matter whether this is called before or after the "for{text}" methods.
186
+ * <p>
187
+ * <b><i>Note:</i></b> Stop-words have no effect if you're passing in your own custom
188
+ * {@link Word} array, since WordCram won't do any text analysis on it (other than
189
+ * sorting the words and scaling their weights).
190
+ *
191
+ * @param extraStopWords a space-delimited String of words to ignore when counting the words in your text.
192
+ * @return The WordCram, for further setup or drawing.
193
+ */
194
+ public WordCram withStopWords(String extraStopWords) {
195
+ this.extraStopWords = extraStopWords;
196
+ return this;
197
+ }
198
+
199
+ /**
200
+ * Exclude numbers from the text in the WordCram. They're excluded by default.
201
+ * <p>
202
+ * Words that are all numbers, like 1, 3.14159, 42, or 1492, will be excluded.
203
+ * Words that have some letters and some numbers like 1A, U2, or funnyguy194 will be included.
204
+ *
205
+ * @see #includeNumbers()
206
+ * @return The WordCram, for further setup or drawing.
207
+ */
208
+ public WordCram excludeNumbers() {
209
+ this.excludeNumbers = true;
210
+ return this;
211
+ }
212
+
213
+ /**
214
+ * Include numbers from the text in the WordCram. They're excluded by default.
215
+ *
216
+ * @see #excludeNumbers()
217
+ * @return The WordCram, for further setup or drawing.
218
+ */
219
+ public WordCram includeNumbers() {
220
+ this.excludeNumbers = false;
221
+ return this;
222
+ }
223
+
224
+ /**
225
+ * Make the WordCram change all words to lower-case.
226
+ * Stop-words are unaffected; they're always case-insensitive.
227
+ * The default is to keep words as they appear in the text.
228
+ *
229
+ * @return The WordCram, for further setup or drawing.
230
+ */
231
+ public WordCram lowerCase() {
232
+ this.textCase = TextCase.Lower;
233
+ return this;
234
+ }
235
+
236
+ /**
237
+ * Make the WordCram change all words to upper-case.
238
+ * Stop-words are unaffected; they're always case-insensitive.
239
+ * The default is to keep words as they appear in the text.
240
+ *
241
+ * @return The WordCram, for further setup or drawing.
242
+ */
243
+ public WordCram upperCase() {
244
+ this.textCase = TextCase.Upper;
245
+ return this;
246
+ }
247
+
248
+ /**
249
+ * Make the WordCram leave all words cased as they appear in the text.
250
+ * Stop-words are unaffected; they're always case-insensitive.
251
+ * This is the default.
252
+ *
253
+ * @return The WordCram, for further setup or drawing.
254
+ */
255
+ public WordCram keepCase() {
256
+ this.textCase = TextCase.Keep;
257
+ return this;
258
+ }
259
+
260
+ /**
261
+ * Make a WordCram from the text on a web page.
262
+ * Just before the WordCram is drawn, it'll load the web page's HTML, scrape out the text,
263
+ * and count and sort the words.
264
+ *
265
+ * @param webPageAddress the URL of the web page to load
266
+ * @return The WordCram, for further setup or drawing.
267
+ */
268
+ public WordCram fromWebPage(String webPageAddress) {
269
+ return fromWebPage(webPageAddress, null);
270
+ }
271
+
272
+ /**
273
+ * Make a WordCram from the text in any elements on a web page that match the
274
+ * <tt>cssSelector</tt>.
275
+ * Just before the WordCram is drawn, it'll load the web page's HTML, scrape
276
+ * out the text, and count and sort the words.
277
+ *
278
+ * HTML parsing is handled by Jsoup, so see
279
+ * <a href="http://jsoup.org/cookbook/extracting-data/selector-syntax">the
280
+ * Jsoup selector documentation</a> if you're having trouble writing your
281
+ * selector.
282
+ *
283
+ * @param webPageAddress the URL of the web page to load
284
+ * @param cssSelector a CSS selector to filter the HTML by, before extracting
285
+ * text
286
+ * @return The WordCram, for further setup or drawing.
287
+ */
288
+ public WordCram fromWebPage(String webPageAddress, String cssSelector) {
289
+ return fromText(new WebPage(webPageAddress, cssSelector, parent));
290
+ }
291
+
292
+ /**
293
+ * Make a WordCram from the text in a HTML file.
294
+ * Just before the WordCram is drawn, it'll load the file's HTML, scrape out the text,
295
+ * and count and sort the words.
296
+ *
297
+ * @param htmlFilePath the path of the html file to load
298
+ * @return The WordCram, for further setup or drawing.
299
+ */
300
+ public WordCram fromHtmlFile(String htmlFilePath) {
301
+ return fromHtmlFile(htmlFilePath, null);
302
+ }
303
+
304
+ /**
305
+ * Make a WordCram from the text in any elements on a web page that match the
306
+ * <tt>cssSelector</tt>.
307
+ * Just before the WordCram is drawn, it'll load the file's HTML, scrape out the text,
308
+ * and count and sort the words.
309
+ *
310
+ * HTML parsing is handled by Jsoup, so see
311
+ * <a href="http://jsoup.org/cookbook/extracting-data/selector-syntax">the
312
+ * Jsoup selector documentation</a> if you're having trouble writing your
313
+ * selector.
314
+ *
315
+ * @param htmlFilePath the path of the html file to load
316
+ * @param cssSelector a CSS selector to filter the HTML by, before extracting
317
+ * text
318
+ * @return The WordCram, for further setup or drawing.
319
+ */
320
+ public WordCram fromHtmlFile(String htmlFilePath, String cssSelector) {
321
+ return fromText(new WebPage(htmlFilePath, cssSelector, parent));
322
+ }
323
+
324
+ // TODO from an inputstream! or reader, anyway
325
+
326
+ /**
327
+ * Makes a WordCram from a String of HTML. Just before the
328
+ * WordCram is drawn, it'll scrape out the text from the HTML,
329
+ * and count and sort the words. It takes one String, or any
330
+ * number of Strings, or an array of Strings, so you can
331
+ * easily use it with <a
332
+ * href="http://processing.org/reference/loadStrings_.html"
333
+ * target="blank">loadStrings()</a>.
334
+ *
335
+ * @deprecated because its signature is annoying, and makes it hard to
336
+ * pass a CSS Selector. If you love this method, and want it to stick around,
337
+ * let me know: <a href="http://github.com/danbernier/WordCram/issues">open
338
+ * a github issue</a>, send me a
339
+ * <a href="http://twitter.com/wordcram">tweet</a>,
340
+ * or say hello at wordcram at gmail.
341
+ * Otherwise, it'll be deleted in a future release, probably 0.6.
342
+ *
343
+ * @param html the String(s) of HTML
344
+ * @return The WordCram, for further setup or drawing.
345
+ */
346
+ @Deprecated
347
+ public WordCram fromHtmlString(String... html) {
348
+ return fromText(new Html(PApplet.join(html, "")));
349
+ }
350
+
351
+ /**
352
+ * Makes a WordCram from a text file, either on the filesystem
353
+ * or the network. Just before the WordCram is drawn, it'll
354
+ * load the file, and count and sort its words.
355
+ *
356
+ * @param textFilePathOrUrl the path of the text file
357
+ * @return The WordCram, for further setup or drawing.
358
+ */
359
+ public WordCram fromTextFile(String textFilePathOrUrl) {
360
+ return fromText(new TextFile(textFilePathOrUrl, parent));
361
+ }
362
+
363
+ /**
364
+ * Makes a WordCram from a String of text. It takes one
365
+ * String, or any number of Strings, or an array of Strings,
366
+ * so you can easily use it with <a
367
+ * href="http://processing.org/reference/loadStrings_.html"
368
+ * target="blank">loadStrings()</a>.
369
+ *
370
+ * @param text the String of text to get the words from
371
+ * @return The WordCram, for further setup or drawing.
372
+ */
373
+ //example fromTextString(loadStrings("my.txt"))
374
+ //example fromTextString("one", "two", "three")
375
+ //example fromTextString("Hello there!")
376
+ public WordCram fromTextString(String... text) {
377
+ return fromText(new Text(PApplet.join(text, " ")));
378
+ }
379
+
380
+ /**
381
+ * Makes a WordCram from any TextSource.
382
+ *
383
+ * <p> It only caches the TextSource - it won't load the text
384
+ * from it until {@link #drawAll()} or {@link #drawNext()} is
385
+ * called.
386
+ *
387
+ * @param textSource the TextSource to get the text from.
388
+ * @return The WordCram, for further setup or drawing.
389
+ */
390
+ public WordCram fromText(TextSource textSource) {
391
+ this.textSources.add(textSource);
392
+ return this;
393
+ }
394
+
395
+ /**
396
+ * Makes a WordCram from your own custom Word array. The
397
+ * Words can be ordered and weighted arbitrarily - WordCram
398
+ * will sort them by weight, and then divide their weights by
399
+ * the weight of the heaviest Word, so the heaviest Word will
400
+ * end up with a weight of 1.0.
401
+ *
402
+ * <p>Note: WordCram won't do any text analysis on the words;
403
+ * stop-words will have no effect, etc. These words are
404
+ * supposed to be ready to go.
405
+ *
406
+ * @param words
407
+ * @return The WordCram, for further setup or drawing.
408
+ */
409
+ public WordCram fromWords(Word[] words) {
410
+ return fromWords(new WordArray(words));
411
+ }
412
+
413
+ public WordCram fromWords(WordSource wordSource) {
414
+ this.wordSource = wordSource;
415
+ return this;
416
+ }
417
+
418
+ //----------------------------------------------
419
+
420
+ /**
421
+ * This WordCram will get a
422
+ * <a href="http://processing.org/reference/PFont.html" target="blank">PFont</a>
423
+ * for each fontName, via
424
+ * <a href="http://processing.org/reference/createFont_.html" target="blank">createFont</a>,
425
+ * and will render words in one of those PFonts.
426
+ *
427
+ * @param fontNames
428
+ * @return The WordCram, for further setup or drawing.
429
+ */
430
+ public WordCram withFonts(String... fontNames) {
431
+ PFont[] fonts = new PFont[fontNames.length];
432
+ for (int i = 0; i < fontNames.length; i++) {
433
+ fonts[i] = parent.createFont(fontNames[i], 1);
434
+ }
435
+
436
+ return withFonts(fonts);
437
+ }
438
+
439
+ /**
440
+ * Make the WordCram render all words in the font that matches
441
+ * the given name, via Processing's
442
+ * <a href="http://processing.org/reference/createFont_.html" target="blank">createFont</a>.
443
+ *
444
+ * @param fontName the font name to pass to createFont.
445
+ * @return The WordCram, for further setup or drawing.
446
+ */
447
+ public WordCram withFont(String fontName) {
448
+ PFont font = parent.createFont(fontName, 1);
449
+ return withFont(font);
450
+ }
451
+
452
+ /**
453
+ * This WordCram will render words in one of the given
454
+ * <a href="http://processing.org/reference/PFont.html" target="blank">PFonts</a>.
455
+ *
456
+ * @param fonts
457
+ * @return The WordCram, for further setup or drawing.
458
+ */
459
+ public WordCram withFonts(PFont... fonts) {
460
+ return withFonter(Fonters.pickFrom(fonts));
461
+ }
462
+
463
+ /**
464
+ * Make the WordCram render all words in the given
465
+ * <a href="http://processing.org/reference/PFont.html" target="blank">PFont</a>.
466
+ *
467
+ * @param font the PFont to render the words in.
468
+ * @return The WordCram, for further setup or drawing.
469
+ */
470
+ public WordCram withFont(PFont font) {
471
+ return withFonter(Fonters.pickFrom(font));
472
+ }
473
+
474
+ /**
475
+ * Use the given WordFonter to pick fonts for each word.
476
+ * You can make your own, or use a pre-fab one from {@link Fonters}.
477
+ *
478
+ * @see WordFonter
479
+ * @see Fonters
480
+ * @param fonter the WordFonter to use.
481
+ * @return The WordCram, for further setup or drawing.
482
+ */
483
+ /*=
484
+ * Here is a bit of a play-ground for now, to see how
485
+ * this might work. See docgen.rb.
486
+ * example withFonter({your WordFonter})
487
+ * example withFonter(Fonters.alwaysUse("Comic Sans"))
488
+ * example withFonter(new WordFonter() { ... (how to doc-gen this?)
489
+ =*/
490
+ public WordCram withFonter(WordFonter fonter) {
491
+ this.fonter = fonter;
492
+ return this;
493
+ }
494
+
495
+ /**
496
+ * Make the WordCram size words by their weight, where the
497
+ * "heaviest" word will be sized at <code>maxSize</code>.
498
+ *
499
+ * <p>Specifically, it makes the WordCram use {@link
500
+ * Sizers#byWeight(int, int)}.
501
+ *
502
+ * @param minSize the size to draw a Word of weight 0
503
+ * @param maxSize the size to draw a Word of weight 1
504
+ * @return The WordCram, for further setup or drawing.
505
+ */
506
+ /*=example sizedByWeight(int minSize, int maxSize)=*/
507
+ public WordCram sizedByWeight(int minSize, int maxSize) {
508
+ return withSizer(Sizers.byWeight(minSize, maxSize));
509
+ }
510
+
511
+ /**
512
+ * Make the WordCram size words by their rank. The first
513
+ * word will be sized at <code>maxSize</code>.
514
+ *
515
+ * <p>Specifically, it makes the WordCram use {@link
516
+ * Sizers#byRank(int, int)}.
517
+ *
518
+ * @param minSize the size to draw the last Word
519
+ * @param maxSize the size to draw the first Word
520
+ * @return The WordCram, for further setup or drawing.
521
+ */
522
+ /*=example sizedByRank(int minSize, int maxSize)=*/
523
+ public WordCram sizedByRank(int minSize, int maxSize) {
524
+ return withSizer(Sizers.byRank(minSize, maxSize));
525
+ }
526
+
527
+ /**
528
+ * Use the given WordSizer to pick fonts for each word.
529
+ * You can make your own, or use a pre-fab one from {@link Sizers}.
530
+ *
531
+ * @see WordSizer
532
+ * @see Sizers
533
+ * @param sizer the WordSizer to use.
534
+ * @return The WordCram, for further setup or drawing.
535
+ */
536
+ public WordCram withSizer(WordSizer sizer) {
537
+ this.sizer = sizer;
538
+ return this;
539
+ }
540
+
541
+ /**
542
+ * Render words by randomly choosing from the given colors.
543
+ * Uses {@link Colorers#pickFrom(int...)}.
544
+ *
545
+ * <p> Note: if you want all your words to be, say, red,
546
+ * <i>don't</i> do this:
547
+ *
548
+ * <pre>
549
+ * ...withColors(255, 0, 0)... // Not what you want!
550
+ * </pre>
551
+ *
552
+ * You'll just see a blank WordCram. Since <a
553
+ * href="http://processing.org/reference/color_datatype.html"
554
+ * target="blank">Processing stores colors as integers</a>,
555
+ * WordCram will see each integer as a different color, and
556
+ * it'll color about 1/3 of your words with the color
557
+ * represented by the integer 255, and the other 2/3 with the
558
+ * color represented by the integer 0. The punchline is,
559
+ * Processing stores opacity (or alpha) in the highest bits
560
+ * (the ones used for storing really big numbers, from
561
+ * 2<sup>24</sup> to 2<sup>32</sup>), so your colors 0 and 255
562
+ * have, effectively, 0 opacity -- they're completely
563
+ * transparent. Oops.
564
+ *
565
+ * <p> Use this instead, and you'll get what you're after:
566
+ *
567
+ * <pre>
568
+ * ...withColors(color(255, 0, 0))... // Much better!
569
+ * </pre>
570
+ *
571
+ * @param colors the colors to randomly choose from.
572
+ * @return The WordCram, for further setup or drawing.
573
+ */
574
+ public WordCram withColors(int... colors) {
575
+ return withColorer(Colorers.pickFrom(colors));
576
+ }
577
+
578
+ /**
579
+ * Renders all words in the given color.
580
+ * @see #withColors(int...)
581
+ * @param color the color for each word.
582
+ * @return The WordCram, for further setup or drawing.
583
+ */
584
+ public WordCram withColor(int color) {
585
+ return withColors(color);
586
+ }
587
+
588
+ /**
589
+ * Use the given WordColorer to pick colors for each word.
590
+ * You can make your own, or use a pre-fab one from {@link Colorers}.
591
+ *
592
+ * @see WordColorer
593
+ * @see Colorers
594
+ * @param colorer the WordColorer to use.
595
+ * @return The WordCram, for further setup or drawing.
596
+ */
597
+ public WordCram withColorer(WordColorer colorer) {
598
+ this.colorer = colorer;
599
+ return this;
600
+ }
601
+
602
+ // TODO need more overloads!
603
+
604
+ /**
605
+ * Make the WordCram rotate each word at one of the given angles.
606
+ * @param anglesInRadians The list of possible rotation angles, in radians
607
+ * @return The WordCram, for further setup or drawing.
608
+ */
609
+ public WordCram angledAt(float... anglesInRadians) {
610
+ return withAngler(Anglers.pickFrom(anglesInRadians));
611
+ }
612
+
613
+ /**
614
+ * Make the WordCram rotate words randomly, between the min and max angles.
615
+ * @param minAngleInRadians The minimum rotation angle, in radians
616
+ * @param maxAngleInRadians The maximum rotation angle, in radians
617
+ * @return The WordCram, for further setup or drawing.
618
+ */
619
+ public WordCram angledBetween(float minAngleInRadians, float maxAngleInRadians) {
620
+ return withAngler(Anglers.randomBetween(minAngleInRadians, maxAngleInRadians));
621
+ }
622
+
623
+ /**
624
+ * Use the given WordAngler to pick angles for each word.
625
+ * You can make your own, or use a pre-fab one from {@link Anglers}.
626
+ *
627
+ * @see WordAngler
628
+ * @see Anglers
629
+ * @param angler the WordAngler to use.
630
+ * @return The WordCram, for further setup or drawing.
631
+ */
632
+ public WordCram withAngler(WordAngler angler) {
633
+ this.angler = angler;
634
+ return this;
635
+ }
636
+
637
+ /**
638
+ * Use the given WordPlacer to pick locations for each word.
639
+ * You can make your own, or use a pre-fab one from {@link Placers}.
640
+ *
641
+ * @see WordPlacer
642
+ * @see Placers
643
+ * @see PlottingWordPlacer
644
+ * @param placer the WordPlacer to use.
645
+ * @return The WordCram, for further setup or drawing.
646
+ */
647
+ public WordCram withPlacer(WordPlacer placer) {
648
+ this.placer = placer;
649
+ return this;
650
+ }
651
+
652
+ /**
653
+ * Use the given WordNudger to pick angles for each word.
654
+ * You can make your own, or use a pre-fab one.
655
+ *
656
+ * @see WordNudger
657
+ * @see SpiralWordNudger
658
+ * @see RandomWordNudger
659
+ * @see PlottingWordNudger
660
+ * @param nudger the WordNudger to use.
661
+ * @return The WordCram, for further setup or drawing.
662
+ */
663
+ public WordCram withNudger(WordNudger nudger) {
664
+ this.nudger = nudger;
665
+ return this;
666
+ }
667
+
668
+ /**
669
+ * How many attempts should be used to place a word. Higher
670
+ * values ensure that more words get placed, but will make
671
+ * algorithm slower.
672
+ * @param maxAttempts
673
+ * @return The WordCram, for further setup or drawing.
674
+ */
675
+ public WordCram maxAttemptsToPlaceWord(int maxAttempts) {
676
+ renderOptions.maxAttemptsToPlaceWord = maxAttempts;
677
+ return this;
678
+ }
679
+
680
+ /**
681
+ * The maximum number of Words WordCram should try to draw.
682
+ * This might be useful if you have a whole bunch of words,
683
+ * and need an artificial way to cut down the list (for
684
+ * speed). By default, it's unlimited.
685
+ * @param maxWords can be any value from 0 to Integer.MAX_VALUE. Values < 0 are treated as unlimited.
686
+ * @return The WordCram, for further setup or drawing.
687
+ */
688
+ public WordCram maxNumberOfWordsToDraw(int maxWords) {
689
+ renderOptions.maxNumberOfWordsToDraw = maxWords;
690
+ return this;
691
+ }
692
+
693
+ /**
694
+ * The smallest-sized Shape the WordCram should try to draw.
695
+ * By default, it's 7.
696
+ * @param minShapeSize the size of the smallest Shape.
697
+ * @return The WordCram, for further setup or drawing.
698
+ */
699
+ public WordCram minShapeSize(int minShapeSize) {
700
+ renderOptions.minShapeSize = minShapeSize;
701
+ return this;
702
+ }
703
+
704
+ /**
705
+ * Use a custom canvas instead of the applet's default one.
706
+ * This may be needed if rendering in background or in other
707
+ * dimensions than the applet size is needed.
708
+ * @deprecated for more consistent naming. Use {@link #toCanvas(PGraphics canvas)} instead.
709
+ * @param canvas the canvas to draw to
710
+ * @return The WordCram, for further setup or drawing.
711
+ */
712
+ public WordCram withCustomCanvas(PGraphics canvas) {
713
+ return toCanvas(canvas);
714
+ }
715
+
716
+ /**
717
+ * Use a custom canvas instead of the applet's default one.
718
+ * This may be needed if rendering in background or in other
719
+ * dimensions than the applet size is needed.
720
+ * @param canvas the canvas to draw to
721
+ * @return The WordCram, for further setup or drawing.
722
+ */
723
+ public WordCram toCanvas(PGraphics canvas) {
724
+ this.renderer = new ProcessingWordRenderer(canvas);
725
+ return this;
726
+ }
727
+
728
+ public WordCram toSvg(String filename, int width, int height) throws java.io.FileNotFoundException {
729
+ this.renderer = new SvgWordRenderer(parent.sketchPath(filename), width, height);
730
+ return this;
731
+ }
732
+
733
+
734
+ /**
735
+ * Add padding around each word, so they stand out from each other more.
736
+ * If you call this multiple times, the last value will be used.
737
+ *
738
+ * WordCram uses a tree of java.awt.Rectangle objects to detect whether two
739
+ * words overlap. What this method actually does is call
740
+ * <code>Rectangle.grow(padding)</code> on the leaves of that tree.
741
+ *
742
+ * @param padding The number of pixels to grow each rectangle by. Defaults to zero.
743
+ * @return The WordCram, for further setup or drawing.
744
+ */
745
+ public WordCram withWordPadding(int padding) {
746
+ renderOptions.wordPadding = padding;
747
+ return this;
748
+ }
749
+
750
+ /**
751
+ * Render a heatmap of the locations where your WordPlacer
752
+ * places words. This is pretty accurate: it renders all your words
753
+ * according to your sizer, fonter, angler, and placer, without
754
+ * nudging them, to an in-memory buffer. Then it splits your sketch
755
+ * into 10x10 pixel squares, and counts how many words overlap each
756
+ * square, and renders a heatmap: black for 0 words, green for 1,
757
+ * and red for more than 8. Rendering too many words at the same
758
+ * spot will make your WordCram run slower, and skip more words,
759
+ * so learning where your hotspots are can be helpful.
760
+ *
761
+ * This is very experimental, and could be changed or removed in a
762
+ * future release.
763
+ */
764
+ public void testPlacer() {
765
+ initComponents();
766
+ WordShaper shaper = new WordShaper(renderOptions.rightToLeft);
767
+ PlacerHeatMap heatMap = new PlacerHeatMap(words, fonter, sizer, angler, placer, nudger, shaper);
768
+ heatMap.draw(parent);
769
+ }
770
+
771
+ private WordCramEngine getWordCramEngine() {
772
+ if (wordCramEngine == null) {
773
+ initComponents();
774
+ WordShaper shaper = new WordShaper(renderOptions.rightToLeft);
775
+ wordCramEngine = new WordCramEngine(renderer,
776
+ words,
777
+ fonter,
778
+ sizer,
779
+ colorer,
780
+ angler,
781
+ placer,
782
+ nudger,
783
+ shaper,
784
+ new BBTreeBuilder(),
785
+ renderOptions,
786
+ observer);
787
+ }
788
+ return wordCramEngine;
789
+ }
790
+
791
+ private void initComponents() {
792
+
793
+ if (words == null && wordSource != null) {
794
+ words = wordSource.getWords();
795
+ }
796
+
797
+ if (words == null && !textSources.isEmpty()) {
798
+ String text = joinTextSources();
799
+
800
+ text = textCase == TextCase.Lower ? text.toLowerCase()
801
+ : textCase == TextCase.Upper ? text.toUpperCase()
802
+ : text;
803
+
804
+ words = new WordCounter().withExtraStopWords(extraStopWords).shouldExcludeNumbers(excludeNumbers).count(text, renderOptions);
805
+ observer.wordsCounted(words);
806
+ if (words.length == 0) {
807
+ warnScripterAboutEmptyWordArray();
808
+ }
809
+ }
810
+ words = new WordSorterAndScaler().sortAndScale(words);
811
+
812
+ if (fonter == null) fonter = Fonters.alwaysUse(parent.createFont("sans", 1));
813
+ if (sizer == null) sizer = Sizers.byWeight(5, 70);
814
+ if (colorer == null) colorer = Colorers.alwaysUse(parent.color(0));
815
+ if (angler == null) angler = Anglers.mostlyHoriz();
816
+ if (placer == null) placer = Placers.horizLine();
817
+ if (nudger == null) nudger = new SpiralWordNudger();
818
+ }
819
+
820
+ private String joinTextSources() {
821
+ StringBuilder buffer = new StringBuilder();
822
+ textSources.stream().map((textSource) -> {
823
+ buffer.append(textSource.getText());
824
+ return textSource;
825
+ }).forEachOrdered((_item) -> {
826
+ buffer.append("\n");
827
+ });
828
+ return buffer.toString();
829
+ }
830
+
831
+ private void warnScripterAboutEmptyWordArray() {
832
+ System.out.println();
833
+ System.out.println("cue.language can't find any non-stop words in your text. This could be because your file encoding is wrong, or because all your words are single characters, among other things.");
834
+ System.out.println("Since cue.language can't find any words in your text, WordCram won't display any, but your Processing sketch will continue as normal.");
835
+ System.out.println("See https://github.com/danbernier/WordCram/issues/8 for more information.");
836
+ }
837
+
838
+
839
+ /**
840
+ * If you're drawing the words one-at-a-time using {@link
841
+ * #drawNext()}, this will tell you whether the WordCram has
842
+ * any words left to draw.
843
+ * @return true if the WordCram has any words left to draw; false otherwise.
844
+ * @see #drawNext()
845
+ */
846
+ public boolean hasMore() {
847
+ return getWordCramEngine().hasMore();
848
+ }
849
+
850
+ /**
851
+ * If the WordCram has any more words to draw, draw the next
852
+ * one.
853
+ * @see #hasMore()
854
+ * @see #drawAll()
855
+ */
856
+ public void drawNext() {
857
+ getWordCramEngine().drawNext();
858
+ }
859
+
860
+ /**
861
+ * Just like it sounds: draw all the words. Once the WordCram
862
+ * has everything set, call this and wait just a bit.
863
+ * @see #drawNext()
864
+ */
865
+ public void drawAll() {
866
+ getWordCramEngine().drawAll();
867
+ }
868
+
869
+
870
+ /**
871
+ * Get the Words that WordCram is drawing. This can be useful
872
+ * if you want to inspect exactly how the words were weighted,
873
+ * or see how they were colored, fonted, sized, angled, or
874
+ * placed, or why they were skipped.
875
+ * @return
876
+ */
877
+ public Word[] getWords() {
878
+ Word[] wordsCopy = new Word[words.length];
879
+ System.arraycopy(words, 0, wordsCopy, 0, words.length);
880
+ return wordsCopy;
881
+ }
882
+
883
+ /**
884
+ * Get the Word at the given (x,y) coordinates.
885
+ *
886
+ * <p>This can be called while the WordCram is rendering, or
887
+ * after it's done. If a Word is too small to render, or
888
+ * hasn't been placed yet, it will never be returned by this
889
+ * method.
890
+ *
891
+ * @param x the X coordinate
892
+ * @param y the Y coordinate
893
+ * @return the Word that covers those coordinates, or null if there isn't one
894
+ */
895
+ public Word getWordAt(float x, float y) {
896
+ return getWordCramEngine().getWordAt(x, y);
897
+ }
898
+
899
+ /**
900
+ * Returns an array of words that could not be placed.
901
+ * @return An array of the skipped words
902
+ */
903
+ public Word[] getSkippedWords() {
904
+ return getWordCramEngine().getSkippedWords();
905
+ }
906
+
907
+ /**
908
+ * How far through the words are we? Useful for when drawing
909
+ * to a custom PGraphics.
910
+ * @return The current point of progress through the list, as a float between 0 and 1.
911
+ */
912
+ public float getProgress() {
913
+ return getWordCramEngine().getProgress();
914
+ }
915
+
916
+ public WordCram withObserver(Observer observer) {
917
+ this.observer = observer;
918
+ return this;
919
+ }
920
+ }