Merge branch 'subtree'
[nikiroo-utils.git] / src / be / nikiroo / utils / Progress.java
1 package be.nikiroo.utils;
2
3 import java.util.ArrayList;
4 import java.util.EventListener;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.Map.Entry;
9
10 /**
11 * Progress reporting system, possibly nested.
12 * <p>
13 * A {@link Progress} can have a name, and that name will be reported through
14 * the event system (it will report the first non-null name in the stack from
15 * the {@link Progress} from which the event originated to the parent the event
16 * is listened on).
17 * <p>
18 * The {@link Progress} also has a table of keys/values shared amongst all the
19 * hierarchy (note that when adding a {@link Progress} to others, its values
20 * will be prioritized if some with the same keys were already present in the
21 * hierarchy).
22 *
23 * @author niki
24 */
25 public class Progress {
26 /**
27 * This event listener is designed to report progress events from
28 * {@link Progress}.
29 *
30 * @author niki
31 */
32 public interface ProgressListener extends EventListener {
33 /**
34 * A progression event.
35 *
36 * @param progress
37 * the {@link Progress} object that generated it, not
38 * necessarily the same as the one where the listener was
39 * attached (it could be a child {@link Progress} of this
40 * {@link Progress}).
41 * @param name
42 * the first non-null name of the {@link Progress} step that
43 * generated this event
44 */
45 public void progress(Progress progress, String name);
46 }
47
48 private Map<Object, Object> map = new HashMap<Object, Object>();
49 private Progress parent = null;
50 private Object lock = new Object();
51 private String name;
52 private Map<Progress, Double> children;
53 private List<ProgressListener> listeners;
54 private int min;
55 private int max;
56 private double relativeLocalProgress;
57 private double relativeProgress; // children included
58
59 /**
60 * Create a new default unnamed {@link Progress}, from 0 to 100.
61 */
62 public Progress() {
63 this(null);
64 }
65
66 /**
67 * Create a new default {@link Progress}, from 0 to 100.
68 *
69 * @param name
70 * the name of this {@link Progress} step
71 */
72 public Progress(String name) {
73 this(name, 0, 100);
74 }
75
76 /**
77 * Create a new unnamed {@link Progress}, from min to max.
78 *
79 * @param min
80 * the minimum progress value (and starting value) -- must be
81 * non-negative
82 * @param max
83 * the maximum progress value
84 */
85 public Progress(int min, int max) {
86 this(null, min, max);
87 }
88
89 /**
90 * Create a new {@link Progress}, from min to max.
91 *
92 * @param name
93 * the name of this {@link Progress} step
94 * @param min
95 * the minimum progress value (and starting value) -- must be
96 * non-negative
97 * @param max
98 * the maximum progress value
99 */
100 public Progress(String name, int min, int max) {
101 this.name = name;
102 this.children = new HashMap<Progress, Double>();
103 this.listeners = new ArrayList<Progress.ProgressListener>();
104 setMinMax(min, max);
105 setProgress(min);
106 }
107
108 /**
109 * The name of this {@link Progress} step.
110 *
111 * @return the name
112 */
113 public String getName() {
114 return name;
115 }
116
117 /**
118 * The name of this {@link Progress} step.
119 *
120 * @param name
121 * the new name
122 */
123 public void setName(String name) {
124 this.name = name;
125 changed(this, name);
126 }
127
128 /**
129 * The minimum progress value.
130 *
131 * @return the min
132 */
133 public int getMin() {
134 return min;
135 }
136
137 /**
138 * The minimum progress value.
139 *
140 * @param min
141 * the min to set
142 *
143 *
144 * @throws RuntimeException
145 * if min &lt; 0 or if min &gt; max
146 */
147 public void setMin(int min) {
148 if (min < 0) {
149 throw new RuntimeException("negative values not supported");
150 }
151
152 synchronized (lock) {
153 if (min > max) {
154 throw new RuntimeException(
155 "The minimum progress value must be <= the maximum progress value");
156 }
157
158 this.min = min;
159 }
160 }
161
162 /**
163 * The maximum progress value.
164 *
165 * @return the max
166 */
167 public int getMax() {
168 return max;
169 }
170
171 /**
172 * The maximum progress value (must be >= the minimum progress value).
173 *
174 * @param max
175 * the max to set
176 *
177 *
178 * @throws RuntimeException
179 * if max &lt; min
180 */
181 public void setMax(int max) {
182 synchronized (lock) {
183 if (max < min) {
184 throw new Error(
185 "The maximum progress value must be >= the minimum progress value");
186 }
187
188 this.max = max;
189 }
190 }
191
192 /**
193 * Set both the minimum and maximum progress values.
194 *
195 * @param min
196 * the min
197 * @param max
198 * the max
199 *
200 * @throws RuntimeException
201 * if min &lt; 0 or if min &gt; max
202 */
203 public void setMinMax(int min, int max) {
204 if (min < 0) {
205 throw new RuntimeException("negative values not supported");
206 }
207
208 if (min > max) {
209 throw new RuntimeException(
210 "The minimum progress value must be <= the maximum progress value");
211 }
212
213 synchronized (lock) {
214 this.min = min;
215 this.max = max;
216 }
217 }
218
219 /**
220 * Get the total progress value (including the optional children
221 * {@link Progress}) on a {@link Progress#getMin()} to
222 * {@link Progress#getMax()} scale.
223 *
224 * @return the progress the value
225 */
226 public int getProgress() {
227 return (int) Math.round(relativeProgress * (max - min));
228 }
229
230 /**
231 * Set the local progress value (not including the optional children
232 * {@link Progress}), on a {@link Progress#getMin()} to
233 * {@link Progress#getMax()} scale.
234 *
235 * @param progress
236 * the progress to set
237 */
238 public void setProgress(int progress) {
239 synchronized (lock) {
240 double childrenProgress = relativeProgress - relativeLocalProgress;
241
242 relativeLocalProgress = ((double) progress) / (max - min);
243
244 setRelativeProgress(this, name, relativeLocalProgress
245 + childrenProgress);
246 }
247 }
248
249 /**
250 * Get the total progress value (including the optional children
251 * {@link Progress}) on a 0.0 to 1.0 scale.
252 *
253 * @return the progress
254 */
255 public double getRelativeProgress() {
256 return relativeProgress;
257 }
258
259 /**
260 * Set the total progress value (including the optional children
261 * {@link Progress}), on a 0 to 1 scale.
262 *
263 * @param pg
264 * the {@link Progress} to report as the progression emitter
265 * @param name
266 * the current name (if it is NULL, the first non-null name in
267 * the hierarchy will overwrite it) of the {@link Progress} who
268 * emitted this change
269 * @param relativeProgress
270 * the progress to set
271 */
272 private void setRelativeProgress(Progress pg, String name,
273 double relativeProgress) {
274 synchronized (lock) {
275 relativeProgress = Math.max(0, relativeProgress);
276 relativeProgress = Math.min(1, relativeProgress);
277 this.relativeProgress = relativeProgress;
278
279 changed(pg, name);
280 }
281 }
282
283 /**
284 * Get the total progress value (including the optional children
285 * {@link Progress}) on a 0 to 1 scale.
286 *
287 * @return the progress the value
288 */
289 private int getLocalProgress() {
290 return (int) Math.round(relativeLocalProgress * (max - min));
291 }
292
293 /**
294 * Add some value to the current progression of this {@link Progress}.
295 *
296 * @param step
297 * the amount to add
298 */
299 public void add(int step) {
300 synchronized (lock) {
301 setProgress(getLocalProgress() + step);
302 }
303 }
304
305 /**
306 * Check if the action corresponding to this {@link Progress} is done (i.e.,
307 * if its progress value == its max value).
308 *
309 * @return TRUE if it is
310 */
311 public boolean isDone() {
312 return getProgress() == max;
313 }
314
315 /**
316 * Mark the {@link Progress} as done by setting its value to max.
317 */
318 public void done() {
319 synchronized (lock) {
320 double childrenProgress = relativeProgress - relativeLocalProgress;
321 relativeLocalProgress = 1 - childrenProgress;
322 setRelativeProgress(this, name, 1d);
323 }
324 }
325
326 /**
327 * Return the list of direct children of this {@link Progress}.
328 *
329 * @return the children (Who will think of the children??)
330 */
331 public List<Progress> getChildren() {
332 synchronized (lock) {
333 return new ArrayList<Progress>(children.keySet());
334 }
335 }
336
337 /**
338 * Notify the listeners that this {@link Progress} changed value.
339 *
340 * @param pg
341 * the emmiter, that is, the (sub-){link Progress} that just
342 * reported some change, not always the same as <tt>this</tt>
343 * @param name
344 * the current name (if it is NULL, the first non-null name in
345 * the hierarchy will overwrite it) of the {@link Progress} who
346 * emitted this change
347 */
348 private void changed(Progress pg, String name) {
349 if (pg == null) {
350 pg = this;
351 }
352
353 if (name == null) {
354 name = this.name;
355 }
356
357 synchronized (lock) {
358 for (ProgressListener l : listeners) {
359 l.progress(pg, name);
360 }
361 }
362 }
363
364 /**
365 * Add a {@link ProgressListener} that will trigger on progress changes.
366 * <p>
367 * Note: the {@link Progress} that will be reported will be the active
368 * progress, not necessarily the same as the current one (it could be a
369 * child {@link Progress} of this {@link Progress}).
370 *
371 * @param l
372 * the listener
373 */
374 public void addProgressListener(ProgressListener l) {
375 synchronized (lock) {
376 this.listeners.add(l);
377 }
378 }
379
380 /**
381 * Remove a {@link ProgressListener} that would trigger on progress changes.
382 *
383 * @param l
384 * the listener
385 *
386 * @return TRUE if it was found (and removed)
387 */
388 public boolean removeProgressListener(ProgressListener l) {
389 synchronized (lock) {
390 return this.listeners.remove(l);
391 }
392 }
393
394 /**
395 * Add a child {@link Progress} of the given weight.
396 *
397 * @param progress
398 * the child {@link Progress} to add
399 * @param weight
400 * the weight (on a {@link Progress#getMin()} to
401 * {@link Progress#getMax()} scale) of this child
402 * {@link Progress} in relation to its parent
403 *
404 * @throws RuntimeException
405 * if weight exceed {@link Progress#getMax()} or if progress
406 * already has a parent
407 */
408 public void addProgress(Progress progress, double weight) {
409 if (weight < min || weight > max) {
410 throw new RuntimeException(String.format(
411 "Progress object %s cannot have a weight of %f, "
412 + "it is outside of its parent (%s) range (%d)",
413 progress.name, weight, name, max));
414 }
415
416 if (progress.parent != null) {
417 throw new RuntimeException(String.format(
418 "Progress object %s cannot be added to %s, "
419 + "as it already has a parent (%s)", progress.name,
420 name, progress.parent.name));
421 }
422
423 ProgressListener progressListener = new ProgressListener() {
424 @Override
425 public void progress(Progress pg, String name) {
426 synchronized (lock) {
427 double total = relativeLocalProgress;
428 for (Entry<Progress, Double> entry : children.entrySet()) {
429 total += (entry.getValue() / (max - min))
430 * entry.getKey().getRelativeProgress();
431 }
432
433 setRelativeProgress(pg, name, total);
434 }
435 }
436 };
437
438 synchronized (lock) {
439 // Should not happen but just in case
440 if (this.map != progress.map) {
441 this.map.putAll(progress.map);
442 }
443 progress.map = this.map;
444 progress.parent = this;
445 this.children.put(progress, weight);
446 progress.addProgressListener(progressListener);
447 }
448 }
449
450 /**
451 * Set the given value for the given key on this {@link Progress} and it's
452 * children.
453 *
454 * @param key
455 * the key
456 * @param value
457 * the value
458 */
459 public void put(Object key, Object value) {
460 map.put(key, value);
461 }
462
463 /**
464 * Return the value associated with this key as a {@link String} if any,
465 * NULL if not.
466 * <p>
467 * If the value is not NULL but not a {@link String}, it will be converted
468 * via {@link Object#toString()}.
469 *
470 * @param key
471 * the key to check
472 *
473 * @return the value or NULL
474 */
475 public String getString(Object key) {
476 Object value = map.get(key);
477 if (value == null) {
478 return null;
479 }
480
481 return value.toString();
482 }
483
484 /**
485 * Return the value associated with this key if any, NULL if not.
486 *
487 * @param key
488 * the key to check
489 *
490 * @return the value or NULL
491 */
492 public Object get(Object key) {
493 return map.get(key);
494 }
495 }