Created
March 2, 2013 19:24
-
-
Save jewelsea/5072743 to your computer and use it in GitHub Desktop.
Render 300 charts off screen and save them to files in JavaFX.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
Answer to the Oracle JavaFX forum question:
Render charts in background
The solution makes use of multiple Tasks executed in parallel and interleaves work on and off the JavaFX application thread to provide an efficient and responsive solution which adheres to JavaFX threading requirements for thread safety.