Skip to content

Instantly share code, notes, and snippets.

@EasyG0ing1
Forked from jewelsea/OffScreenOffThreadCharts.java
Last active November 25, 2021 02:55
Show Gist options
  • Save EasyG0ing1/c4a5f5fcf021f431487ac431534e6b2f to your computer and use it in GitHub Desktop.
Save EasyG0ing1/c4a5f5fcf021f431487ac431534e6b2f to your computer and use it in GitHub Desktop.
Render 300 charts off screen and save them to files in JavaFX.
import java.awt.image.BufferedImage;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.*;
import javafx.application.*;
import javafx.beans.binding.*;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.concurrent.*;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.*;
import javafx.scene.*;
import javafx.stage.Stage;
import javafx.scene.chart.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
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() {
// 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.
final Pane chartContainer = new 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;");
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); }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment