ruby_wordcram 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ }