Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX: updating progress for the multiple tasks

I'm writing a multithreaded fractal drawing program with JavaFX 2.2 and now I need some guidance.

What I'm trying to achieve is to create a Task or Service (haven't decided yet) which then fires up some other tasks that actually do the calculation and return sections of the whole image when ready. When all the pieces are returned to the initiating task it puts together the pieces and returns it to the main thread so it can be visualized.

Obviously, all this must happen without ever blocking the UI.

The problem is I can't figure out how these tasks could communicate with each other. For example, I need to update the progress property of the initiating task based on the average progress of the tasks inside it (or something like this), so their progress properties should be bound to the progress property of the initiating task somehow. The image pieces should be put in a list or some container and redrawn on a separate image when all of them are available.

I have already written a simpler (though still experimental) version of this program that creates only one task that calculates the whole fractal. The progress is bound to the progressBar of the GUI. The return value is handled by an EventHandler on success of the task.

I'm not asking for a complete solution but some ideas with maybe a little bit of example code would really help me.

This is the class that should be modified:

package fractal;

import fractalUtil.DefaultPalette;
import fractalUtil.PaletteInterface;
import javafx.concurrent.Task;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import org.apache.commons.math3.complex.Complex;

/**
 *
 * @author athelionas
 */
public abstract class AbstractFractal extends Task implements FractalInterface {

    private PaletteInterface palette;
    protected final int width, height, order, iterations;
    protected final double scale, xReal, xIm, xCenter, yCenter, zoom;
    protected final boolean julia;

    protected AbstractFractal(final int width, final int height, final double xReal, final double xIm, final double xCenter, final double yCenter, final int order, final boolean julia, final int iterations, final double zoom) {
        this.width = width;
        this.height = height;
        this.xReal = xReal;
        this.xIm = xIm;
        this.xCenter = xCenter;
        this.yCenter = yCenter;
        this.order = order;
        this.julia = julia;
        this.iterations = iterations;
        this.zoom = zoom;
        this.scale = (double) width / (double) height;
        palette = new DefaultPalette();
    }

    @Override
    public final void setPalette(final PaletteInterface palette) {
        this.palette = palette;
    }

    @Override
    public abstract Complex formula(final Complex z, final Complex c, final int order, final Complex center);

    @Override
    public final Color calculatePoint(final Complex z, final Complex c, final int order, final Complex center, final int iterations) {
        Complex zTemp = z;
        int iter = iterations;
        while (zTemp.abs() <= 2.0 && iter > 0) {
            zTemp = formula(zTemp, c, order, center);
            iter--;
        }
        if (iter == 0) {
            return Color.rgb(0, 0, 0);
        } else {
            return palette.pickColor((double) (iterations - iter) / (double) iterations);
        }
    }

    @Override
    public final WritableImage call() {
        Complex z;
        Complex c;
        Complex center = new Complex(xCenter, yCenter);
        final WritableImage image = new WritableImage(width, height);

        if (julia) {
            c = new Complex(xReal, xIm);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {

                    z = new Complex(((double) x) / (double) (width - 1) * 2.0 * scale * (1.0 / zoom) - scale * (1.0 / zoom), ((double) y) / (double) (height - 1) * 2.0 * (1.0 / zoom) - 1.0 * (1.0 / zoom));

                    image.getPixelWriter().setColor(x, y, calculatePoint(z, c, order, center, iterations));
                }

            }
        } else {
            z = new Complex(xReal, xIm);
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {

                    c = new Complex(((double) x) / (double) (width - 1) * 2.0 * scale * (1.0 / zoom) - scale * (1.0 / zoom), ((double) y) / (double) (height - 1) * 2.0 * (1.0 / zoom) - 1.0 * (1.0 / zoom));

                    image.getPixelWriter().setColor(x, y, calculatePoint(z, c, order, center, iterations));
                }
                updateProgress(y, height);
            }
        }
        return image;
    }
}
like image 410
Athelionas Avatar asked Oct 20 '12 09:10

Athelionas


2 Answers

Use binding and Task. This way you don't need to care about threading at all. All you need is to create a binding which will normalize each progress according to threads number and summ them up. E.g.

progressBar.progressProperty().bind(
    task1.progressProperty().multiply(0.5).add(
         task2.progressProperty().multiply(0.5)));

It's a bit trickier for unknown number of threads. See next example:

public class MultiProgressTask extends Application {
    private static final int THREADS_NUM = 10;

    // this is our Task which produces a Node and track progress
    private static class MyTask extends Task<Node> {

        private final int delay = new Random().nextInt(1000) + 100;
        { System.out.println("I update progress every " + delay); }

        @Override
        protected Node call() throws Exception {
            updateProgress(0, 5);
            for (int i = 0; i < 5; i++) {
                System.out.println(i);
                Thread.sleep(delay); // imitating activity
                updateProgress(i+1, 5);
            }
            System.out.println("done");
            return new Rectangle(20, 20, Color.RED);
        }
    };

    @Override
    public void start(Stage primaryStage) {
        ProgressBar pb = new ProgressBar(0);
        pb.setMinWidth(300);

        final VBox root = new VBox();
        root.getChildren().add(pb);

        Scene scene = new Scene(root, 300, 250);

        primaryStage.setScene(scene);
        primaryStage.show();

        DoubleBinding progress = null;

        for (int i = 0; i < THREADS_NUM; i++) {
            final MyTask mt = new MyTask();

            // here goes binding creation
            DoubleBinding scaledProgress = mt.progressProperty().divide(THREADS_NUM);
            if (progress == null) {
                progress = scaledProgress;
            } else {
                progress = progress.add(scaledProgress);
            }
            // here you process the result of MyTask
            mt.setOnSucceeded(new EventHandler<WorkerStateEvent>() {

                @Override
                public void handle(WorkerStateEvent t) {
                    root.getChildren().add((Node)t.getSource().getValue());
                }
            });
            new Thread(mt).start();
        }
        pb.progressProperty().bind(progress);
    }


    public static void main(String[] args) { launch(args); }
}
like image 194
Sergey Grinev Avatar answered Nov 14 '22 23:11

Sergey Grinev


This is a pretty interesting problem :)

If we remove the issue of thread safety for a moment, you could pass in a double property (or whatever the progress property is bound to) and update that with the progress which would then update the progress indicator. Two problems with that:

  1. Multiple tasks could increment the property at the same time.
  2. The changes must be fired on the javafx thread.

I would wrap the property in it's own class with a simple API:

class ProgressModel {
    private final SimpleDoubleProperty progress;
    public void increment(finally double increment) {
        Platform.runLater(new Runnable() {
            progress.set(progress.doubleValue() + increment);
        }
    }

    public void bindPropertyToProgress(DoubleProperty property) {
        property.bind(progress);
    }
}

In the above code, all updates will run on the javafx thread sequentially so it is thread safe plus no locks. I have done similar background tasks like this and performance has been good (realtime to the user's eyes) although if you're updating thousands of times a second this might not be the case! You will just need to measure. I've not shown the boiler plate code to make it a bit more readable.

like image 33
Andy Till Avatar answered Nov 14 '22 22:11

Andy Till