import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberExpression;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.SnapshotResult;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.control.Pagination;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
import javax.imageio.ImageIO;
public class OffScreenOffThreadCharts extends Application {
private static final String CHART_FILE_PREFIX = "chart_";
private static final String WORKING_DIR = System.getProperty("user.dir");
private final SimpleDateFormat dateFormat = new SimpleDateFormat(
"HH:mm:ss.SSS");
private final Random random = new Random();
private final int N_CHARTS = 300;
private final int PREVIEW_SIZE = 600;
private final int CHART_SIZE = 600;
final ExecutorService saveChartsExecutor = createExecutor("SaveCharts");
@Override
public void start(Stage stage) throws IOException {
stage.setTitle("Chart Export Sample");
final SaveChartsTask saveChartsTask = new SaveChartsTask(N_CHARTS);
final VBox layout = new VBox(10);
layout.getChildren().addAll(createProgressPane(saveChartsTask),
createChartImagePagination(saveChartsTask));
layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 15;");
stage.setOnCloseRequest(new EventHandler() {
@Override
public void handle(Event event) {
saveChartsTask.cancel();
}
});
stage.setScene(new Scene(layout));
stage.show();
saveChartsExecutor.execute(saveChartsTask);
}
@Override
public void stop() throws Exception {
saveChartsExecutor.shutdown();
saveChartsExecutor.awaitTermination(5, TimeUnit.SECONDS);
}
private Pagination createChartImagePagination(
final SaveChartsTask saveChartsTask) {
final Pagination pagination = new Pagination(N_CHARTS);
pagination.setMinSize(PREVIEW_SIZE + 100, PREVIEW_SIZE + 100);
pagination.setPageFactory(new Callback<Integer, Node>() {
@Override
public Node call(final Integer chartNumber) {
final StackPane page = new StackPane();
page.setStyle("-fx-background-color: antiquewhite;");
if (chartNumber < saveChartsTask.getWorkDone()) {
page.getChildren().setAll(
createImageViewForChartFile(chartNumber));
} else {
ProgressIndicator progressIndicator = new ProgressIndicator();
progressIndicator.setMaxSize(PREVIEW_SIZE * 1 / 4,
PREVIEW_SIZE * 1 / 4);
page.getChildren().setAll(progressIndicator);
final ChangeListener<Number> WORK_DONE_LISTENER = new ChangeListener<Number>() {
@Override
public void changed(
ObservableValue<? extends Number> observable,
Number oldValue, Number newValue) {
if (chartNumber < saveChartsTask.getWorkDone()) {
page.getChildren()
.setAll(createImageViewForChartFile(chartNumber));
saveChartsTask.workDoneProperty()
.removeListener(this);
}
}
};
saveChartsTask.workDoneProperty().addListener(
WORK_DONE_LISTENER);
}
return page;
}
});
return pagination;
}
private ImageView createImageViewForChartFile(Integer chartNumber) {
ImageView imageView = new ImageView(new Image("file:///"
+ getChartFilePath(chartNumber)));
imageView.setFitWidth(PREVIEW_SIZE);
imageView.setPreserveRatio(true);
return imageView;
}
private Pane createProgressPane(SaveChartsTask saveChartsTask) {
GridPane progressPane = new GridPane();
progressPane.setHgap(5);
progressPane.setVgap(5);
progressPane.addRow(0, new Label("Create:"),
createBoundProgressBar(saveChartsTask
.chartsCreationProgressProperty()));
progressPane.addRow(1, new Label("Snapshot:"),
createBoundProgressBar(saveChartsTask
.chartsSnapshotProgressProperty()));
progressPane.addRow(2, new Label("Save:"),
createBoundProgressBar(saveChartsTask
.imagesExportProgressProperty()));
progressPane.addRow(
3,
new Label("Processing:"),
createBoundProgressBar(Bindings
.when(saveChartsTask.stateProperty().isEqualTo(
Worker.State.SUCCEEDED))
.then(new SimpleDoubleProperty(1))
.otherwise(
new SimpleDoubleProperty(
ProgressBar.INDETERMINATE_PROGRESS))));
return progressPane;
}
private ProgressBar createBoundProgressBar(NumberExpression progressProperty) {
ProgressBar progressBar = new ProgressBar();
progressBar.setMaxWidth(Double.MAX_VALUE);
progressBar.progressProperty().bind(progressProperty);
GridPane.setHgrow(progressBar, Priority.ALWAYS);
return progressBar;
}
class ChartsCreationTask extends Task<Void> {
private final int nCharts;
private final BlockingQueue<Parent> charts;
ChartsCreationTask(BlockingQueue<Parent> charts, final int nCharts) {
this.charts = charts;
this.nCharts = nCharts;
updateProgress(0, nCharts);
}
@Override
protected Void call() throws Exception {
int i = nCharts;
while (i > 0) {
if (isCancelled()) {
break;
}
charts.put(createChart());
i--;
updateProgress(nCharts - i, nCharts);
}
return null;
}
private Parent createChart() {
final Pane chartContainer = new Pane();
try {
// create a chart.
final PieChart chart = new PieChart();
ObservableList<PieChart.Data> pieChartData = FXCollections
.observableArrayList(new PieChart.Data("Grapefruit",
random.nextInt(30)), new PieChart.Data(
"Oranges", random.nextInt(30)),
new PieChart.Data("Plums", random.nextInt(30)),
new PieChart.Data("Pears", random.nextInt(30)),
new PieChart.Data("Apples", random.nextInt(30)));
chart.setData(pieChartData);
chart.setTitle("Imported Fruits - "
+ dateFormat.format(new Date()));
// Place the chart in a container pane.
chartContainer.getChildren().add(chart);
chart.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
chart.setPrefSize(CHART_SIZE, CHART_SIZE);
chart.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
chart.setStyle("-fx-font-size: 16px;");
TextField txt = new TextField("Test");
Tooltip tip = new Tooltip("Sample");
txt.setTooltip(tip);
chartContainer.getChildren().add(txt);
} catch (Exception e) {
e.printStackTrace();
}
return chartContainer;
}
}
class ChartsSnapshotTask extends Task<Void> {
private final int nCharts;
private final BlockingQueue<Parent> charts;
private final BlockingQueue<BufferedImage> images;
ChartsSnapshotTask(BlockingQueue<Parent> charts,
BlockingQueue<BufferedImage> images, final int nCharts) {
this.charts = charts;
this.images = images;
this.nCharts = nCharts;
updateProgress(0, nCharts);
}
@Override
protected Void call() throws Exception {
int i = nCharts;
while (i > 0) {
if (isCancelled()) {
break;
}
images.put(snapshotChart(charts.take()));
i--;
updateProgress(nCharts - i, nCharts);
}
return null;
}
private BufferedImage snapshotChart(final Parent chartContainer)
throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
// render the chart in an offscreen scene (scene is used to allow
// css processing) and snapshot it to an image.
// the snapshot is done in runlater as it must occur on the javafx
// application thread.
final SimpleObjectProperty<BufferedImage> imageProperty = new SimpleObjectProperty();
Platform.runLater(new Runnable() {
@Override
public void run() {
Scene snapshotScene = new Scene(chartContainer);
final SnapshotParameters params = new SnapshotParameters();
params.setFill(Color.ALICEBLUE);
chartContainer.snapshot(
new Callback<SnapshotResult, Void>() {
@Override
public Void call(SnapshotResult result) {
imageProperty.set(SwingFXUtils.fromFXImage(
result.getImage(), null));
latch.countDown();
return null;
}
}, params, null);
}
});
latch.await();
return imageProperty.get();
}
}
class PngsExportTask extends Task<Void> {
private final int nImages;
private final BlockingQueue<BufferedImage> images;
PngsExportTask(BlockingQueue<BufferedImage> images, final int nImages) {
this.images = images;
this.nImages = nImages;
updateProgress(0, nImages);
}
@Override
protected Void call() throws Exception {
int i = nImages;
while (i > 0) {
if (isCancelled()) {
break;
}
exportPng(images.take(), getChartFilePath(nImages - i));
i--;
updateProgress(nImages - i, nImages);
}
return null;
}
private void exportPng(BufferedImage image, String filename) {
try {
ImageIO.write(image, "png", new File(filename));
} catch (IOException ex) {
Logger.getLogger(OffScreenOffThreadCharts.class.getName()).log(
Level.SEVERE, null, ex);
}
}
}
class SaveChartsTask<Void> extends Task {
private final BlockingQueue<Parent> charts = new ArrayBlockingQueue(10);
private final BlockingQueue<BufferedImage> bufferedImages = new ArrayBlockingQueue(
10);
private final ExecutorService chartsCreationExecutor = createExecutor("CreateCharts");
private final ExecutorService chartsSnapshotExecutor = createExecutor("TakeSnapshots");
private final ExecutorService imagesExportExecutor = createExecutor("ExportImages");
private final ChartsCreationTask chartsCreationTask;
private final ChartsSnapshotTask chartsSnapshotTask;
private final PngsExportTask imagesExportTask;
SaveChartsTask(final int nCharts) {
chartsCreationTask = new ChartsCreationTask(charts, nCharts);
chartsSnapshotTask = new ChartsSnapshotTask(charts, bufferedImages,
nCharts);
imagesExportTask = new PngsExportTask(bufferedImages, nCharts);
setOnCancelled(new EventHandler() {
@Override
public void handle(Event event) {
chartsCreationTask.cancel();
chartsSnapshotTask.cancel();
imagesExportTask.cancel();
}
});
imagesExportTask.workDoneProperty().addListener(
new ChangeListener<Number>() {
@Override
public void changed(
ObservableValue<? extends Number> observable,
Number oldValue, Number workDone) {
updateProgress(workDone.intValue(), nCharts);
}
});
}
ReadOnlyDoubleProperty chartsCreationProgressProperty() {
return chartsCreationTask.progressProperty();
}
ReadOnlyDoubleProperty chartsSnapshotProgressProperty() {
return chartsSnapshotTask.progressProperty();
}
ReadOnlyDoubleProperty imagesExportProgressProperty() {
return imagesExportTask.progressProperty();
}
@Override
protected Void call() throws Exception {
chartsCreationExecutor.execute(chartsCreationTask);
chartsSnapshotExecutor.execute(chartsSnapshotTask);
imagesExportExecutor.execute(imagesExportTask);
chartsCreationExecutor.shutdown();
chartsSnapshotExecutor.shutdown();
imagesExportExecutor.shutdown();
try {
imagesExportExecutor.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
/** no action required */
}
return null;
}
}
private String getChartFilePath(int chartNumber) {
return new File(WORKING_DIR, CHART_FILE_PREFIX + chartNumber + ".png")
.getPath();
}
private ExecutorService createExecutor(final String name) {
ThreadFactory factory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName(name);
t.setDaemon(true);
return t;
}
};
return Executors.newSingleThreadExecutor(factory);
}
public static void main(String[] args) {
launch(args);
}
}