jFreeChart custom domain axis labels - java

I'm hoping someone can help me with setting a custom label for the domain axis tick labels within a jFreeChart being created by Jasper Reports. I've tried everything that I've found online and still no dice. Here's my code:
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.util.List;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.SymbolAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.labels.CategoryItemLabelGenerator;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.data.Range;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.text.TextBlock;
import org.jfree.text.TextUtilities;
import org.jfree.ui.RectangleEdge;
import net.sf.jasperreports.engine.JRChart;
import net.sf.jasperreports.engine.JRChartCustomizer;
public class ChartCustomizer implements JRChartCustomizer{
public class CustomColorRenderer extends BarRenderer {
private static final long serialVersionUID = -9045170581109026224L;
#Override
public Paint getItemPaint(int row, int col) {
CategoryDataset currentDataset = getPlot().getDataset();
String columnKey = (String) currentDataset.getColumnKey(col);
String[] columnKeyValues = columnKey.split(":");
if(columnKeyValues.length < 2) return getSeriesPaint(row);
String columnActualEstimated = columnKeyValues[2];
if(columnActualEstimated.equals("A")) {
return Color.RED;
} else if(columnActualEstimated.equals("E")) {
return Color.BLUE;
}
return getSeriesPaint(row);
}
}
public void customize(JFreeChart chart, JRChart jasperChart)
{
if(jasperChart.getChartType() == JRChart.CHART_TYPE_BAR) {
CategoryPlot plot = chart.getCategoryPlot();
CategoryDataset currentDataset = plot.getDataset();
double maxValue = Double.MIN_VALUE;
// Scan to get total max value for the chart in order to set chart height appropriately
for(int i = 0; i < currentDataset.getRowCount(); i++) {
//System.out.println(i);
for(int j = 0; j < currentDataset.getColumnCount(); j++) {
Number numberValue = currentDataset.getValue(i, j);
//System.out.println("Column " + j + " key: " + currentDataset.getColumnKey(j));
double value = numberValue == null ? Double.NaN : numberValue.doubleValue();
if(value > maxValue) {
maxValue = value;
}
}
}
// Add 10% to top margin
double tenPercent = maxValue * 0.1;
maxValue = (Math.round((maxValue * 1.1) / tenPercent) * tenPercent) + tenPercent;
// Set max bar height to max value
ValueAxis yAxis = plot.getRangeAxis();
yAxis.setAutoRange(false);
yAxis.setRange(0, maxValue);
CategoryAxis xAxis = plot.getDomainAxis();
// Set label font size
xAxis.setTickLabelFont(new Font("Arial", Font.PLAIN, 4));
// Will set single bar colors by value with a custom renderer
CustomColorRenderer customRenderer = new CustomColorRenderer();
// Set the chart to apply the custom renderer
plot.setRenderer(customRenderer);
}
}
}
Here is what my chart looks like currently:
Note that the domain axis is displaying keys such as "1:N:A". In this case, 1 refers to the order, N refers to November, A refers to the value being "Actual" vs. "Estimated" which are the two series. All I'd like to do is change the visible tick label to "Nov" for the "1:N:A" example. Things like custom label generators change the labels for other parts of the chart and not the tick labels. I can set the tick label fonts successfully but just cannot seem to get the labels themselves to change.
Edit: The other tricky part about this situation is that the requirement is to display 13 months comprising the previous 11, current, and the upcoming. The upcoming month is always an estimated value, hence the "A" and "E" series). This makes it painful since that means there's always a duplicate month therefore columns that will want to merge.
Any help would be appreciated. Let me know if more info is needed.
Crossposted to http://www.jfree.org/forum/viewtopic.php?f=3&t=117811

A custom CategoryItemLabelGenerator, which is typically used to label the bars, is probably not the right choice for this. As shown here, a CategoryAxis obtains the text of the category labels from the column keys of the CategoryDataset via the plot's getCategoriesForAxis() method. You can specify the desired keys when you create the dataset.

Related

JavaFX: Give Line Chart Series of multiple tables different colors

I have an application that displays several Line Charts with several Series like this:
I'd like to change the color of each Series but haven't found a way to achieve this. The only thing I found is how to change the default colors but that doesn't solve my problem.
Is there really now way to achieve individual colors for chart series?
The JavaFX CSS Reference Guide says the following for LineChart:
Style class
Comments
Properties
"chart-series-line series<i> default-color<j>"
Where <i> is the index of the series and <j> is the series’ color index
Node
"chart-line-symbol series<i> data<j> default-color<k>"
Where <i> is the index of the series, <j> is the index of the data within the series, and <k> is the series’ color index
Node
"chart-line-symbol series<i> default-color<j>"
Where <i> is the index of the series and <j> is the series’ color index
LegendItem
Note: Although the line is only documented as a Node, by default it is actually a javafx.scene.shape.Path.
If you want to target a specific series' line, use .chart-series-line.series<i>, where <i> is replaced with the index of the series in the chart's data. And if you want to give a series of a specific chart a certain color, then simply give the chart an ID and use that in the CSS selector.
Here's an example. It uses so-called looked-up colors, which makes the CSS a little more scalable and organized. Also, they can have their values changed via the setStyle method in code, allowing you to dynamically change the color programmatically. Another approach for that is to use a "data URL", showcased in this other Stack Overflow answer.
style.css:
/*
* The '-fx-stroke' is used to set the color of the line. The line is
* targeted by the '.chart-series-line.series<i>' selectors.
*
* The '-fx-background-color' is used to set the color of the legend
* symbol so it matches the line. This would also color the symbols
* on the line if they were shown. These symbol nodes are targeted
* by the '.chart-line-symbol.series<i>' selectors.
*
* Both the '-fx-series0-color' and '-fx-series1-color' "properties"
* are looked-up colors. You can change the value of a looked-up color
* in code by calling 'setStyle(...)' on the appropriate node.
*/
#firstChart {
-fx-series0-color: magenta;
-fx-series1-color: dodgerblue;
}
#secondChart {
-fx-series0-color: red;
-fx-series1-color: black;
}
#firstChart .chart-series-line.series0,
#firstChart .chart-line-symbol.series0 {
-fx-stroke: -fx-series0-color;
-fx-background-color: -fx-series0-color;
}
#firstChart .chart-series-line.series1,
#firstChart .chart-line-symbol.series1 {
-fx-stroke: -fx-series1-color;
-fx-background-color: -fx-series1-color;
}
#secondChart .chart-series-line.series0,
#secondChart .chart-line-symbol.series0 {
-fx-stroke: -fx-series0-color;
-fx-background-color: -fx-series0-color;
}
#secondChart .chart-series-line.series1,
#secondChart .chart-line-symbol.series1 {
-fx-stroke: -fx-series1-color;
-fx-background-color: -fx-series1-color;
}
Main.java:
import java.util.function.UnaryOperator;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var chart1 = createLineChart("Chart 1");
chart1.setId("firstChart");
chart1.getData().add(createSeries("f(x) = x", x -> x));
chart1.getData().add(createSeries("f(x) = 2x", x -> 2 * x));
/*
* Uncomment the line of code below to demonstrate dynamically changing
* the value of a looked-up color.
*/
// chart1.setStyle("-fx-series0-color: goldenrod;");
var chart2 = createLineChart("Chart 2");
chart2.setId("secondChart");
chart2.getData().add(createSeries("f(x) = x^2", x -> x * x));
chart2.getData().add(createSeries("f(x) = x^3", x -> x * x * x));
var root = new VBox(10, chart1, chart2);
root.setPadding(new Insets(10));
primaryStage.setScene(new Scene(root, 1000, 720));
primaryStage.getScene()
.getStylesheets()
.add(Main.class.getResource("/style.css").toString());
primaryStage.show();
}
private LineChart<Number, Number> createLineChart(String title) {
var chart = new LineChart<>(new NumberAxis(), new NumberAxis());
chart.setTitle(title);
chart.getXAxis().setLabel("x");
chart.getYAxis().setLabel("f(x)");
chart.setCreateSymbols(false);
return chart;
}
private XYChart.Series<Number, Number> createSeries(
String name,
UnaryOperator<Double> func) {
var series = new XYChart.Series<Number, Number>();
series.setName(name);
for (int x = 0; x < 20; x++) {
series.getData().add(new XYChart.Data<>(x, func.apply((double) x)));
}
return series;
}
}

JavaFX LineChart with Values

I want to show values on top of the linechart. I've seen this answer which is quite helpful but it changes the linechart nodes. What I want is same idea but showing values not on nodes, but near them (maybe right and above of them) something like:
53
O
/ \
/ \
/42 \ 21 21
O O------------O
EDIT:
I've tried to code below but sadly only lines appear, doesnt show nodes.
for (int index = 0; index < value.getData().size(); index++) {
XYChart.Data dataPoint = value.getData().get(index);
Node lineSymbol = dataPoint.getNode().lookup(".chart-line-symbol");
lineSymbol.setStyle("-fx-background-color: " + definedColor + " , white;");
Label label = new Label(value.getName());
label.toFront();
Pane nodeWithText = new Pane();
label.setStyle("-fx-font-size: 20; -fx-font-weight: bold;");
StackPane stackPane = new StackPane();
Group group = new Group(label);
group.getChildren().add(lineSymbol);
group.toFront();
group.setLayoutX(-10);
group.setLayoutY(-30);
StackPane.setAlignment(group, Pos.TOP_RIGHT);
StackPane.setMargin(group,new Insets(0,0,5,0));
nodeWithText.getChildren().add(group);
nodeWithText.getChildren().add(lineSymbol);
dataPoint.setNode(nodeWithText);
}
One option is to set a custom Node for each Data in your chart. Here's a crude example:
import javafx.application.Application;
import javafx.beans.binding.ObjectExpression;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
var chart = new LineChart<>(new NumberAxis(), new NumberAxis());
chart.getData().add(new Series<>(createData()));
primaryStage.setScene(new Scene(chart, 600, 400));
primaryStage.show();
}
private static ObservableList<Data<Number, Number>> createData() {
var list = FXCollections.<Data<Number, Number>>observableArrayList();
for (int x = 0; x < 10; x++) {
var data = new Data<Number, Number>(x, Math.pow(x, 2));
data.setNode(createDataNode(data.YValueProperty()));
list.add(data);
}
return list;
}
private static Node createDataNode(ObjectExpression<Number> value) {
var label = new Label();
label.textProperty().bind(value.asString("%,.2f"));
var pane = new Pane(label);
pane.setShape(new Circle(6.0));
pane.setScaleShape(false);
label.translateYProperty().bind(label.heightProperty().divide(-1.5));
return pane;
}
}
The above doesn't do anything complex when it comes to positioning the text. For instance, it won't take into account where the line is nor whether or not some of the text is cut off by the chart's clip. Basically, it's a proof-of-concept; the result looks like:
Some other possible options for adding text to the chart include:
Put the LineChart in a Group or Pane where the chart is the first child. Then, for each Data in your chart, you'd add a Label or Text to the parent and position it relative to the position of the Data's node (when it becomes available). You can calculate these positions using methods such as Node#localToScene and Node#sceneToLocal.
Subclass LineChart and add the Text or Label to the chart directly. As I've never subclassed a chart before, I'm not sure how to implement this option.
Something else I'm not thinking of.
Note that, no matter what you do, if your chart has a lot of data points very close to each other then displaying all the text in a visually pleasing way will be difficult—if not impossible.

JavaFX How Can I Center a Tooltip on a Node?

I'm currently working on a form that features validation upon changing focus from one node to another and wish to display a tooltip centered above a node containing an error if one exists. I'm using the tooltip's show(Node ownerNode, double anchorX, double anchorY) method signature to specify which node I would like to attach it to and where to position it.
I've tried the following code:
Tooltip tooltip = new Tooltip("Error");
tooltip.show(node, 0, 0);
double tooltipMiddle = tooltip.getWidth() / 2;
tooltip.hide();
double nodeMiddle = node.getWidth() / 2;
//Gets the node's x and y position within the window
Bounds bounds = node.localToScene(node.getBoundsInLocal());
//Tooltip's bottom-right corner is set to the anchor points, so I set x to
//the node's x coordinate plus half the width of the node plus half the width
//of the tooltip
tooltip.show(node,
bounds.getMinX() + nodeMiddle + tooltipMiddle, bounds.getMinY());
This has gotten me very close to the center, but it's still off. I've been all over the internet trying to find help, but I'm just not finding any, so I figured I'd ask here.
Any chance I could get some insight into why I'm not able to get this working how I'd like it?
Code which I bring provides correct positioning of tooltip but is far from being perfect. It would take a lot of work to bring comprehensive solution (if you want we can discuss it).
Going to the bottom I think you have a math problem and Tooltip class may not be the best option.
import javafx.application.Application;
import javafx.geometry.Bounds;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class TooltipApp extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
TextField textField = new TextField();
textField.setPrefWidth(100.);
Button button = new Button("Tooltip");
HBox hBox = new HBox(textField, button);
hBox.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
Label label = new Label("Empty!");
label.setVisible(false);
Pane tooltipPane = new Pane(label);
tooltipPane.setMouseTransparent(true);
StackPane stackPane = new StackPane(hBox, tooltipPane);
stackPane.setPrefSize(600., 400.);
Scene scene = new Scene(stackPane);
stage.setScene(scene);
stage.show();
button.setOnMouseClicked(event -> {
if (label.isVisible()) {
label.setVisible(false);
} else {
Bounds sceneBounds = textField.localToScene(textField.getBoundsInLocal());
label.setLayoutX((sceneBounds.getMinX() + (sceneBounds.getWidth() / 2)) - (label.getWidth() / 2));
label.setLayoutY(sceneBounds.getMinY() - label.getHeight());
label.setVisible(true);
}
});
}
}
First you should use Bounds bounds = node.localToScreen(node.getBoundsInLocal()); as it might not be clear what the relevant Scene is.
Then calling tooltip.show(node, bounds.getMinX(), bounds.getMinY()); should give you a tooltip with an upper left corner identical to the upper upper left corner of node.
Now, tooltip.show(node, bounds.getMinX() + nodeMiddle - tooltipMiddle, bounds.getMinY() - tooltipHeight); should produce the desired result and it does for
double tooltipMiddle = (tooltip.getWidth()-18) / 2;
double tooltipHeight = tooltip.getHeight()-18;
double nodeMiddle = getWidth() / 2;
The 18 came from a little experimenting and I'm not sure why the width and height are off by that amount, but it seems to be indpendent of the length or number of lines of the text in the tooltip.

JFreechart Boxplot changing size of boxes when coloring the boxes

I am using JFreeChart to make a boxplot (code at the bottom). When I don't add a a coloring per box they are drawn wide and correctly centered (as I want them):
However, when I then color them by x-axis label, they get smaller and are not centered correctly anymore:
How can I get the coloring of the second figure but with the box sizes of the first?
package test;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.labels.BoxAndWhiskerToolTipGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.renderer.category.BoxAndWhiskerRenderer;
import org.jfree.data.statistics.DefaultBoxAndWhiskerCategoryDataset;
public class test {
public static void main(String[] args) throws Exception {
DefaultBoxAndWhiskerCategoryDataset dataset = new DefaultBoxAndWhiskerCategoryDataset();
// example data
HashMap<String, ArrayList<Double>> test = new HashMap<String, ArrayList<Double>>();
test.put("A",new ArrayList<Double>(Arrays.asList(0.8, 1.4, 0.8, 1.9, 1.2)));
test.put("B",new ArrayList<Double>(Arrays.asList(0.8, 1.4, 0.8, 1.9, 1.2)));
test.put("C",new ArrayList<Double>(Arrays.asList(0.8, 1.4, 0.8, 1.9, 1.2)));
test.put("D",new ArrayList<Double>(Arrays.asList(0.8, 1.4, 0.8, 1.9, 1.2)));
test.put("E",new ArrayList<Double>(Arrays.asList(0.8, 1.4, 0.8, 1.9, 1.2)));
for (String k : test.keySet()){
/* change to
* String xAxisLabel = "";
* to get wide plot
*/
String xAxisLabel = k;
dataset.add(test.get(k), xAxisLabel, k);// + beta of interactionterm");
}
final CategoryAxis xAxis = new CategoryAxis("Example x-axis");
final NumberAxis yAxis = new NumberAxis("Example y-axis");
yAxis.setAutoRangeIncludesZero(false);
final BoxAndWhiskerRenderer renderer = new BoxAndWhiskerRenderer();
renderer.setFillBox(true);
renderer.setSeriesToolTipGenerator(1, new BoxAndWhiskerToolTipGenerator());
renderer.setMeanVisible(false);
final CategoryPlot plot = new CategoryPlot(dataset, xAxis, yAxis, renderer);
final JFreeChart chart = new JFreeChart(
"Example",
plot
);
final ChartPanel chartPanel = new ChartPanel(chart);
chartPanel.setPreferredSize(new java.awt.Dimension(3000,1800));
ChartUtilities.saveChartAsPNG(new File("test.png"), chart, 1000, 600);
}
}
The difference is that your first picture has one series, but your second picture has five series. Instead of adding lots of series, add one series with five items like your top picture. You can use a custom BoxAndWhiskerRenderer that overrides getItemPaint() to get different colors like they show here for XYLineAndShapeRenderer.
Edit: To get a matching legend, you need a new DrawingSupplier like this.

How load javafx.scene.image.Image by demand?

Is it possible to discard loaded content of Image and later load it again? Is it possible to load it on demand?
Can I have ImageView which loads it's image only on show?
The Image class is essentially immutable with respect to its image data, in the sense that you can specify a source for the image data at construction time, and then cannot modify it via the API subsequently.
The ImageView class provides functionality for displaying an image in the UI. The ImageView class is mutable, in the sense that you can change the image it displays.
The basic strategy you need to implement "tiled images" functionality is to create a virtualized container, which has a collection of "cells" or "tiles" which are reused to display different content. This is essentially how controls such as ListView, TableView, and TreeView are implemented in JavaFX. You may also be interested in Tomas Mikula's Flowless implementation of the same kind of idea.
So to implement "tiled images" functionality, you could use an array of ImageViews as the "cells" or "tiles". You can place these in a pane and implement panning/scrolling in the pane, and when image views scroll out of view, reuse the ImageViews by moving the images from one image view to another, loading new images only for the tiles that need it. Obviously, images that are no longer referenced by any image view will be eligible for garbage collection in the usual way.
There are probably other ways to achieve this, such as using WritableImages and using a PixelWriter to update the pixel data when needed. Which works best probably depends somewhat on which is most convenient for the actual format you have for the image data; there is probably little performance difference between different strategies.
If you are loading the images from a server or database, you should do so in the background. If the image is loaded from a URL, the Image class provides functionality to do this directly. If you are loading from an input stream (e.g. from a database BLOB field), you will need to implement the background threading yourself.
Here is the basic idea (no threading):
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class PanningTilesExample extends Application {
private static final int TILE_WIDTH = 100;
private static final int TILE_HEIGHT = 100;
private static final int PANE_WIDTH = 800;
private static final int PANE_HEIGHT = 800;
// amount scrolled left, in pixels:
private final DoubleProperty xOffset = new SimpleDoubleProperty();
// amount scrolled right, in pixels:
private final DoubleProperty yOffset = new SimpleDoubleProperty();
// number of whole tiles shifted to left:
private final IntegerProperty tileXOffset = new SimpleIntegerProperty();
// number of whole tiles shifted up:
private final IntegerProperty tileYOffset = new SimpleIntegerProperty();
private final Pane pane = new Pane();
// for enabling dragging:
private double mouseAnchorX;
private double mouseAnchorY;
// array of ImageViews:
private ImageView[][] tiles;
private final Random rng = new Random();
#Override
public void start(Stage primaryStage) {
// update number of tiles offset when number of pixels offset changes:
tileXOffset.bind(xOffset.divide(TILE_WIDTH));
tileYOffset.bind(yOffset.divide(TILE_HEIGHT));
// create the images views, etc. This method could be called
// when the pane size changes, if you want a resizable pane with fixed size tiles:
build();
// while tile offsets change, allocate new images to existing image views:
tileXOffset.addListener(
(obs, oldOffset, newOffset) -> rotateHorizontal(oldOffset.intValue() - newOffset.intValue()));
tileYOffset.addListener(
(obs, oldOffset, newOffset) -> rotateVertical(oldOffset.intValue() - newOffset.intValue()));
// Simple example just has a fixed size pane:
pane.setMinSize(PANE_WIDTH, PANE_HEIGHT);
pane.setPrefSize(PANE_WIDTH, PANE_HEIGHT);
pane.setMaxSize(PANE_WIDTH, PANE_HEIGHT);
// enable panning on pane (just update offsets when dragging):
pane.setOnMousePressed(e -> {
mouseAnchorX = e.getSceneX();
mouseAnchorY = e.getSceneY();
});
pane.setOnMouseDragged(e -> {
double deltaX = e.getSceneX() - mouseAnchorX;
double deltaY = e.getSceneY() - mouseAnchorY;
xOffset.set(xOffset.get() + deltaX);
yOffset.set(yOffset.get() + deltaY);
mouseAnchorX = e.getSceneX();
mouseAnchorY = e.getSceneY();
});
// display in stage:
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
}
private void build() {
// create array of image views:
int numTileCols = (int) (PANE_WIDTH / TILE_WIDTH + 2);
int numTileRows = (int) (PANE_HEIGHT / TILE_HEIGHT + 2);
tiles = new ImageView[numTileCols][numTileRows];
// populate array:
for (int colIndex = 0; colIndex < numTileCols; colIndex++) {
final int col = colIndex;
for (int rowIndex = 0; rowIndex < numTileRows; rowIndex++) {
final int row = rowIndex;
// create actual image view and initialize image:
ImageView tile = new ImageView();
tile.setImage(getImage(col - tileXOffset.get(), row - tileYOffset.get()));
tile.setFitWidth(TILE_WIDTH);
tile.setFitHeight(TILE_HEIGHT);
// position image by offset, and register listeners to keep it updated:
xOffset.addListener((obs, oldOffset, newOffset) -> {
double offset = newOffset.intValue() % TILE_WIDTH + (col - 1) * TILE_WIDTH;
tile.setLayoutX(offset);
});
tile.setLayoutX(xOffset.intValue() % TILE_WIDTH + (col - 1) * TILE_WIDTH);
yOffset.addListener((obs, oldOffset, newOffset) -> {
double offset = newOffset.intValue() % TILE_HEIGHT + (row - 1) * TILE_HEIGHT;
tile.setLayoutY(offset);
});
tile.setLayoutY(yOffset.intValue() % TILE_HEIGHT + (row - 1) * TILE_HEIGHT);
// add image view to pane:
pane.getChildren().add(tile);
// store image view in array:
tiles[col][row] = tile;
}
}
}
// tiles have been shifted off-screen in vertical direction
// need to reallocate images to image views, and get new images
// for tiles that have moved into view:
// delta represents the number of tiles we have shifted, positive for up
private void rotateVertical(int delta) {
for (int colIndex = 0; colIndex < tiles.length; colIndex++) {
if (delta > 0) {
// top delta rows have shifted off-screen
// shift top row images by delta
// add new images to bottom rows:
for (int rowIndex = 0; rowIndex + delta < tiles[colIndex].length; rowIndex++) {
// stop any background loading we no longer need
if (rowIndex < delta) {
Image current = tiles[colIndex][rowIndex].getImage();
if (current != null) {
current.cancel();
}
}
// move image up from lower rows:
tiles[colIndex][rowIndex].setImage(tiles[colIndex][rowIndex + delta].getImage());
}
// fill lower rows with new images:
for (int rowIndex = tiles[colIndex].length - delta; rowIndex < tiles[colIndex].length; rowIndex++) {
tiles[colIndex][rowIndex].setImage(getImage(-tileXOffset.get() + colIndex, -tileYOffset.get() + rowIndex));
}
}
if (delta < 0) {
// similar to previous case...
}
}
}
// similarly, rotate images horizontally:
private void rotateHorizontal(int delta) {
// similar to rotateVertical....
}
// get a new image for tile represented by column, row
// this implementation just snapshots a label, but this could be
// retrieved from a file, server, or database, etc
private Image getImage(int column, int row) {
Label label = new Label(String.format("Tile [%d,%d]", column, row));
label.setPrefSize(TILE_WIDTH, TILE_HEIGHT);
label.setMaxSize(TILE_WIDTH, TILE_HEIGHT);
label.setAlignment(Pos.CENTER);
label.setBackground(new Background(new BackgroundFill(randomColor(), CornerRadii.EMPTY , Insets.EMPTY)));
// must add label to a scene for background to work:
new Scene(label);
return label.snapshot(null, null);
}
private Color randomColor() {
return Color.rgb(rng.nextInt(256), rng.nextInt(256), rng.nextInt(256));
}
public static void main(String[] args) {
launch(args);
}
}
Complete code (with thread handling) here, complete version without threading in a previous revision
There is obviously more functionality (and performance enhancements) that could be added here, for example you could allow for resizing the pane (update: the latest version of the gist linked above does this), and create or remove tiles when the pane changes size, etc. But this should function as a basic template for this functionality.
It is best practice to have your images loaded before they are displayed!
If you wish to get rid of the image simply set the image to null! But you will then have ro reinitialize that image in order to be able to view! I do not recommend this!
If you will reuse that image just keep it memory!
Load it once and use it on unlimited imageViews!
No, there is no such functionality in Image contract. Image can load in background, but once loaded, it cannot be unloaded.
If using ImageView, then you should assign Image to it explicitly, but JavaFX doesn't provide a way for you to know when ImageView is actually shown.
To implement required close to ImageView, I was to fork it and highly utilize deprecated API with Prism, including NGImageView class.

Categories

Resources