Commit | Line | Data |
---|---|---|
b3aad1f9 | 1 | package be.nikiroo.utils; |
86057589 NR |
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. | |
69155ad5 NR |
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). | |
86057589 NR |
22 | * |
23 | * @author niki | |
24 | */ | |
25 | public class Progress { | |
d827da2a NR |
26 | /** |
27 | * This event listener is designed to report progress events from | |
28 | * {@link Progress}. | |
29 | * | |
30 | * @author niki | |
31 | */ | |
86057589 NR |
32 | public interface ProgressListener extends EventListener { |
33 | /** | |
34 | * A progression event. | |
35 | * | |
36 | * @param progress | |
da5bfa48 NR |
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}). | |
86057589 NR |
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 | ||
69155ad5 | 48 | private Map<Object, Object> map = new HashMap<Object, Object>(); |
0924c45d NR |
49 | private Progress parent = null; |
50 | private Object lock = new Object(); | |
86057589 NR |
51 | private String name; |
52 | private Map<Progress, Double> children; | |
53 | private List<ProgressListener> listeners; | |
54 | private int min; | |
55 | private int max; | |
11f9e5f3 NR |
56 | private double relativeLocalProgress; |
57 | private double relativeProgress; // children included | |
86057589 NR |
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 | ||
2998b78a NR |
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; | |
11f9e5f3 | 125 | changed(this, name); |
2998b78a NR |
126 | } |
127 | ||
86057589 NR |
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 | |
0924c45d NR |
142 | * |
143 | * | |
d827da2a | 144 | * @throws RuntimeException |
0924c45d | 145 | * if min < 0 or if min > max |
86057589 NR |
146 | */ |
147 | public void setMin(int min) { | |
148 | if (min < 0) { | |
d827da2a | 149 | throw new RuntimeException("negative values not supported"); |
86057589 NR |
150 | } |
151 | ||
96d4aebe | 152 | synchronized (lock) { |
0924c45d | 153 | if (min > max) { |
d827da2a | 154 | throw new RuntimeException( |
0924c45d NR |
155 | "The minimum progress value must be <= the maximum progress value"); |
156 | } | |
86057589 | 157 | |
0924c45d NR |
158 | this.min = min; |
159 | } | |
86057589 NR |
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 | |
0924c45d NR |
176 | * |
177 | * | |
d827da2a | 178 | * @throws RuntimeException |
0924c45d | 179 | * if max < min |
86057589 NR |
180 | */ |
181 | public void setMax(int max) { | |
96d4aebe | 182 | synchronized (lock) { |
0924c45d NR |
183 | if (max < min) { |
184 | throw new Error( | |
185 | "The maximum progress value must be >= the minimum progress value"); | |
186 | } | |
86057589 | 187 | |
0924c45d NR |
188 | this.max = max; |
189 | } | |
86057589 NR |
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 | |
0924c45d | 199 | * |
d827da2a | 200 | * @throws RuntimeException |
0924c45d | 201 | * if min < 0 or if min > max |
86057589 NR |
202 | */ |
203 | public void setMinMax(int min, int max) { | |
204 | if (min < 0) { | |
d827da2a | 205 | throw new RuntimeException("negative values not supported"); |
86057589 NR |
206 | } |
207 | ||
208 | if (min > max) { | |
d827da2a | 209 | throw new RuntimeException( |
86057589 NR |
210 | "The minimum progress value must be <= the maximum progress value"); |
211 | } | |
212 | ||
96d4aebe | 213 | synchronized (lock) { |
0924c45d NR |
214 | this.min = min; |
215 | this.max = max; | |
216 | } | |
86057589 NR |
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() { | |
11f9e5f3 | 227 | return (int) Math.round(relativeProgress * (max - min)); |
86057589 NR |
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) { | |
96d4aebe | 239 | synchronized (lock) { |
11f9e5f3 NR |
240 | double childrenProgress = relativeProgress - relativeLocalProgress; |
241 | ||
242 | relativeLocalProgress = ((double) progress) / (max - min); | |
243 | ||
244 | setRelativeProgress(this, name, relativeLocalProgress | |
245 | + childrenProgress); | |
0924c45d | 246 | } |
86057589 NR |
247 | } |
248 | ||
11f9e5f3 NR |
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) { | |
96d4aebe | 274 | synchronized (lock) { |
11f9e5f3 NR |
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 | ||
cac67ebc NR |
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) { | |
96d4aebe | 300 | synchronized (lock) { |
11f9e5f3 | 301 | setProgress(getLocalProgress() + step); |
cac67ebc NR |
302 | } |
303 | } | |
304 | ||
86057589 NR |
305 | /** |
306 | * Check if the action corresponding to this {@link Progress} is done (i.e., | |
2a35af0b | 307 | * if its progress value == its max value). |
86057589 NR |
308 | * |
309 | * @return TRUE if it is | |
310 | */ | |
311 | public boolean isDone() { | |
356c0336 | 312 | return getProgress() == max; |
86057589 NR |
313 | } |
314 | ||
cac67ebc NR |
315 | /** |
316 | * Mark the {@link Progress} as done by setting its value to max. | |
317 | */ | |
318 | public void done() { | |
96d4aebe | 319 | synchronized (lock) { |
356c0336 NR |
320 | double childrenProgress = relativeProgress - relativeLocalProgress; |
321 | relativeLocalProgress = 1 - childrenProgress; | |
322 | setRelativeProgress(this, name, 1d); | |
323 | } | |
cac67ebc NR |
324 | } |
325 | ||
88b36f83 NR |
326 | /** |
327 | * Return the list of direct children of this {@link Progress}. | |
328 | * | |
2a35af0b | 329 | * @return the children (Who will think of the children??) |
88b36f83 | 330 | */ |
f4053377 | 331 | public List<Progress> getChildren() { |
96d4aebe | 332 | synchronized (lock) { |
f4053377 NR |
333 | return new ArrayList<Progress>(children.keySet()); |
334 | } | |
88b36f83 NR |
335 | } |
336 | ||
86057589 | 337 | /** |
11f9e5f3 | 338 | * Notify the listeners that this {@link Progress} changed value. |
86057589 | 339 | * |
88b36f83 | 340 | * @param pg |
3766af24 NR |
341 | * the emmiter, that is, the (sub-){link Progress} that just |
342 | * reported some change, not always the same as <tt>this</tt> | |
86057589 NR |
343 | * @param name |
344 | * the current name (if it is NULL, the first non-null name in | |
88b36f83 NR |
345 | * the hierarchy will overwrite it) of the {@link Progress} who |
346 | * emitted this change | |
86057589 | 347 | */ |
11f9e5f3 | 348 | private void changed(Progress pg, String name) { |
2a35af0b NR |
349 | if (pg == null) { |
350 | pg = this; | |
351 | } | |
352 | ||
11f9e5f3 NR |
353 | if (name == null) { |
354 | name = this.name; | |
355 | } | |
356 | ||
96d4aebe | 357 | synchronized (lock) { |
0924c45d NR |
358 | for (ProgressListener l : listeners) { |
359 | l.progress(pg, name); | |
360 | } | |
86057589 NR |
361 | } |
362 | } | |
363 | ||
364 | /** | |
365 | * Add a {@link ProgressListener} that will trigger on progress changes. | |
da5bfa48 NR |
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}). | |
86057589 NR |
370 | * |
371 | * @param l | |
372 | * the listener | |
373 | */ | |
374 | public void addProgressListener(ProgressListener l) { | |
96d4aebe | 375 | synchronized (lock) { |
f4053377 NR |
376 | this.listeners.add(l); |
377 | } | |
86057589 NR |
378 | } |
379 | ||
2998b78a NR |
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) { | |
96d4aebe | 389 | synchronized (lock) { |
f4053377 NR |
390 | return this.listeners.remove(l); |
391 | } | |
2998b78a NR |
392 | } |
393 | ||
86057589 NR |
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 | |
0924c45d | 403 | * |
d827da2a | 404 | * @throws RuntimeException |
0924c45d NR |
405 | * if weight exceed {@link Progress#getMax()} or if progress |
406 | * already has a parent | |
86057589 NR |
407 | */ |
408 | public void addProgress(Progress progress, double weight) { | |
409 | if (weight < min || weight > max) { | |
d827da2a | 410 | throw new RuntimeException(String.format( |
0924c45d | 411 | "Progress object %s cannot have a weight of %f, " |
0988831f | 412 | + "it is outside of its parent (%s) range (%d)", |
0924c45d NR |
413 | progress.name, weight, name, max)); |
414 | } | |
415 | ||
416 | if (progress.parent != null) { | |
d827da2a | 417 | throw new RuntimeException(String.format( |
0924c45d NR |
418 | "Progress object %s cannot be added to %s, " |
419 | + "as it already has a parent (%s)", progress.name, | |
420 | name, progress.parent.name)); | |
86057589 NR |
421 | } |
422 | ||
96d4aebe | 423 | ProgressListener progressListener = new ProgressListener() { |
cd0c27d2 | 424 | @Override |
62c9ec78 | 425 | public void progress(Progress pg, String name) { |
96d4aebe | 426 | synchronized (lock) { |
11f9e5f3 | 427 | double total = relativeLocalProgress; |
96d4aebe NR |
428 | for (Entry<Progress, Double> entry : children.entrySet()) { |
429 | total += (entry.getValue() / (max - min)) | |
430 | * entry.getKey().getRelativeProgress(); | |
0924c45d NR |
431 | } |
432 | ||
11f9e5f3 | 433 | setRelativeProgress(pg, name, total); |
86057589 | 434 | } |
86057589 | 435 | } |
96d4aebe | 436 | }; |
0924c45d | 437 | |
0924c45d | 438 | synchronized (lock) { |
69155ad5 NR |
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; | |
96d4aebe NR |
444 | progress.parent = this; |
445 | this.children.put(progress, weight); | |
446 | progress.addProgressListener(progressListener); | |
0924c45d NR |
447 | } |
448 | } | |
69155ad5 | 449 | |
69155ad5 NR |
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 | /** | |
8b2627ce NR |
464 | * Return the value associated with this key as a {@link String} if any, |
465 | * NULL if not. | |
69155ad5 NR |
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 | */ | |
8b2627ce | 475 | public String getString(Object key) { |
69155ad5 NR |
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 | } | |
86057589 | 495 | } |