Javafx 2.2: How to implement a new Chart that extends LineChart - java

I'm trying to implement a new chart which supports drawing minor grid lines. Earlier I tried implementing it by plotting lines JavaFx 2.x: How to draw minor grid lines. I would like to go the native route of drawing Path's instead of plotting lines.
I'm new to JavaFx and any help/guidance will be appreciated. The current code just concentrates on drawing minor horizontal grid lines.
public class CTChart extends LineChart
{
private final Path horizontalMinorGridLines = new Path();
public CTChart(final NumberAxis xAxis, final NumberAxis yAxis)
{
super(xAxis, yAxis);
this.setAnimated(false);
this.setCreateSymbols(false);
this.setAlternativeRowFillVisible(false);
this.setLegendVisible(false);
this.setHorizontalGridLinesVisible(true);
this.setVerticalGridLinesVisible(true);
getPlotChildren().add(horizontalMinorGridLines);
horizontalMinorGridLines.getStyleClass().setAll("chart-horizontal-minor-grid-lines");
int lowerBound = (int) yAxis.getLowerBound();
int upperBound = (int) yAxis.getUpperBound();
int tickUnit = (int) yAxis.getTickUnit();
int minorTickCount = yAxis.getMinorTickCount();
int minorTickUnit = tickUnit / minorTickCount;
horizontalMinorGridLines.getElements().clear();
for (int i = lowerBound; i < upperBound; i = i + minorTickUnit) {
ObservableList<TickMark<Number>> tickMarks = yAxis.getTickMarks();
for (TickMark<Number> tickMark : tickMarks) {
tickMark.getPosition();
// horizontalMinorGridLines.getElements().add(new MoveTo(zero, tickMark.getPosition()));
// horizontalMinorGridLines.getElements().add(new LineTo(zero + xAxis.getWidth(), tickMark.getPosition()));
}
}
}
}
Things I'm not able to figure out
Am I heading in the right direction?
How to get the X,Y coordinates for the MoveTo and LineTo API's?
How to render/which code renders the minor grid lines?
Should everything be in the constructor?

yourObject.setOnMouseMoved(event -> {
double x = event.getX();
double y = event.getY();
}
Example. Use this in CSS:
.chart-alternative-row-fill {
-fx-fill: transparent;
-fx-stroke: transparent;
-fx-stroke-width: 0;
}
Setting properties of the LineChart must be in constructor. Adding series does not have to be in constructor.

Related

Making a zoomable coordinate system in JavaFX [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I want to make a kind of "zoomable graphing calculator" in javafx so basically a coordinate system where you can zoom in with the mouse wheel. Im drawing everything on a canvas but Im not sure how to do the zooming part... I can think of three ways of doing it:
I could apply a transformation matrix to everything with GraphicsContext.transform()
I could make some sort of coordinate transformation method that I pass all my coordinates through, and that moves them to the correct positions on the screen
Do all the maths manually for everything I draw on the canvas (this seems like its gonna be super tedious and very hard to maintain)
What would you guys suggest I do?
EDIT: Also if I go for the first approach or something similar, do I need to worry about elements that are "drawn" outside of the canvas? Also will the lines stay nice and crisp or will they get blurry because of anti-aliasing? (I'd perfer the former)
I updated the graphing solution from this answer to add zooming functionality:
Draw Cartesian Plane Graphi with canvas in JavaFX
To add interactive zooming I added a handler for the scroll event. In the scroll event handler, I calculate new values for low and high values for axes and plot coordinates, then apply them to the axes and plot.
I use the scroll event handler that works on the mouse scroll wheel or touchpad or touchscreen scroll gestures. But you could also (or instead) use a zoom event handler that utilizes zoom (pinching) gestures on touch surfaces.
When a scroll is detected, I just zoom in or out on a fixed amount (10% of the current zoom factor) up to a min or max zoom value. A more sophisticated solution could query the delta values of the scroll or zoom events to achieve inertial scrolling and greater or less scrolling based on the speed of the scroll event.
To implement the zoom, I recreate the zoomed nodes rather than updating the properties of existing nodes, which is probably not all that efficient. But, in the simple test case I had, performance seemed fine so I didn't think it was worth additional effort to optimize.
This is just one of the numerous potential solution strategies for this question (I won't discuss other potential solutions here). The particular solution offered in this answer appeared to be a good fit for me.
Also, note that this solution does not use a canvas, it is based on a scene graph. I recommend using the scene graph for this task, though you could use a canvas if you wish. With a canvas solution, the solution might be quite different from the one presented here (I don't offer any advice on how to create a canvas-based solution).
Event handler for handling the zoom
This handler is attached to the parent pane which holds the graph node children.
private class ZoomHandler implements EventHandler<ScrollEvent> {
private static final double MAX_ZOOM = 2;
private static final double MIN_ZOOM = 0.5;
private double zoomFactor = 1;
#Override
public void handle(ScrollEvent event) {
if (event.getDeltaY() == 0) {
return;
} else if (event.getDeltaY() < 0) {
zoomFactor = Math.max(MIN_ZOOM, zoomFactor * 0.9);
} else if (event.getDeltaY() > 0) {
zoomFactor = Math.min(MAX_ZOOM, zoomFactor * 1.1);
}
Plot plot = plotChart(zoomFactor);
Pane parent = (Pane) event.getSource();
parent.getChildren().setAll(plot);
}
}
Sample zoomed chart images
zoomed all the way out
default zoom level
zoomed all the way in
Complete sample solution code
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import java.util.function.Function;
public class ZoomableCartesianPlot extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(final Stage stage) {
Plot plot = plotChart(1);
StackPane layout = new StackPane(
plot
);
layout.setPadding(new Insets(20));
layout.setStyle("-fx-background-color: rgb(35, 39, 50);");
layout.setOnScroll(new ZoomHandler());
stage.setTitle("y = \u00BC(x+4)(x+1)(x-2)");
stage.setScene(new Scene(layout, Color.rgb(35, 39, 50)));
stage.show();
}
private Plot plotChart(double zoomFactor) {
Axes axes = new Axes(
400, 300,
-8 * zoomFactor, 8 * zoomFactor, 1,
-6 * zoomFactor, 6 * zoomFactor, 1
);
Plot plot = new Plot(
x -> .25 * (x + 4) * (x + 1) * (x - 2),
-8 * zoomFactor, 8 * zoomFactor, 0.1,
axes
);
return plot;
}
class Axes extends Pane {
private NumberAxis xAxis;
private NumberAxis yAxis;
public Axes(
int width, int height,
double xLow, double xHi, double xTickUnit,
double yLow, double yHi, double yTickUnit
) {
setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
setPrefSize(width, height);
setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
xAxis = new NumberAxis(xLow, xHi, xTickUnit);
xAxis.setSide(Side.BOTTOM);
xAxis.setMinorTickVisible(false);
xAxis.setPrefWidth(width);
xAxis.setLayoutY(height / 2);
yAxis = new NumberAxis(yLow, yHi, yTickUnit);
yAxis.setSide(Side.LEFT);
yAxis.setMinorTickVisible(false);
yAxis.setPrefHeight(height);
yAxis.layoutXProperty().bind(
Bindings.subtract(
(width / 2) + 1,
yAxis.widthProperty()
)
);
getChildren().setAll(xAxis, yAxis);
}
public NumberAxis getXAxis() {
return xAxis;
}
public NumberAxis getYAxis() {
return yAxis;
}
}
class Plot extends Pane {
public Plot(
Function<Double, Double> f,
double xMin, double xMax, double xInc,
Axes axes
) {
Path path = new Path();
path.setStroke(Color.ORANGE.deriveColor(0, 1, 1, 0.6));
path.setStrokeWidth(2);
path.setClip(
new Rectangle(
0, 0,
axes.getPrefWidth(),
axes.getPrefHeight()
)
);
double x = xMin;
double y = f.apply(x);
path.getElements().add(
new MoveTo(
mapX(x, axes), mapY(y, axes)
)
);
x += xInc;
while (x < xMax) {
y = f.apply(x);
path.getElements().add(
new LineTo(
mapX(x, axes), mapY(y, axes)
)
);
x += xInc;
}
setMinSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
setPrefSize(axes.getPrefWidth(), axes.getPrefHeight());
setMaxSize(Pane.USE_PREF_SIZE, Pane.USE_PREF_SIZE);
getChildren().setAll(axes, path);
}
private double mapX(double x, Axes axes) {
double tx = axes.getPrefWidth() / 2;
double sx = axes.getPrefWidth() /
(axes.getXAxis().getUpperBound() -
axes.getXAxis().getLowerBound());
return x * sx + tx;
}
private double mapY(double y, Axes axes) {
double ty = axes.getPrefHeight() / 2;
double sy = axes.getPrefHeight() /
(axes.getYAxis().getUpperBound() -
axes.getYAxis().getLowerBound());
return -y * sy + ty;
}
}
private class ZoomHandler implements EventHandler<ScrollEvent> {
private static final double MAX_ZOOM = 2;
private static final double MIN_ZOOM = 0.5;
private double zoomFactor = 1;
#Override
public void handle(ScrollEvent event) {
if (event.getDeltaY() == 0) {
return;
} else if (event.getDeltaY() < 0) {
zoomFactor = Math.max(MIN_ZOOM, zoomFactor * 0.9);
} else if (event.getDeltaY() > 0) {
zoomFactor = Math.min(MAX_ZOOM, zoomFactor * 1.1);
}
Plot plot = plotChart(zoomFactor);
Pane parent = (Pane) event.getSource();
parent.getChildren().setAll(plot);
}
}
}

Android piechart with icons between bars

I want to create a pie chart with images in between the legend bars. I am adding the screenshot below for better understanding, i tried using one canvas and then created one arc and tried to add images to it, but it was not working. For now i am using below pie chart library to show the bars with center text. Any suggestion will be helpful. Thanks :)
https://github.com/PhilJay/MPAndroidChart
enter image description here
Regards,
Rohit Garg
You can do that by extending PieChartRenderer.
If you look at the implementation of PieChartRenderer.drawRoundedSlices(Canvas c) you can get an example of how to get the starting coordinates of each slice.
Then just use drawBitmap or drawPicture to render your image between the pie slices. (I used Utils.drawImage in the example to mimic the source of PieChartRenderer)
As an example, i copied drawRoundedSlices and renamed it drawImageBeforeSlice. Instead of drawing the arcs, i draw bitmaps.
To make the renderer use the new method, i override drawExtras and stick a call to the new method on the end.
class PieChartRendererWithImages extends PieChartRenderer
{
protected Drawable mImage;
public PieChartRendererWithImages(PieChart chart, ChartAnimator animator, ViewPortHandler viewPortHandler, Drawable image) {
super(chart, animator, viewPortHandler);
mImage = image;
}
/**
* This draws an image before all pie-slices
*
* #param c
*/
protected void drawImageBeforeSlice(Canvas c) {
IPieDataSet dataSet = mChart.getData().getDataSet();
if (!dataSet.isVisible())
return;
float phaseX = mAnimator.getPhaseX();
float phaseY = mAnimator.getPhaseY();
MPPointF center = mChart.getCenterCircleBox();
float r = mChart.getRadius();
// calculate the radius of the "slice-circle"
float circleRadius = (r - (r * mChart.getHoleRadius() / 100f)) / 2f;
float[] drawAngles = mChart.getDrawAngles();
float angle = mChart.getRotationAngle();
for (int j = 0; j < dataSet.getEntryCount(); j++) {
float sliceAngle = drawAngles[j];
Entry e = dataSet.getEntryForIndex(j);
// draw only if the value is greater than zero
if ((Math.abs(e.getY()) > Utils.FLOAT_EPSILON)) {
float x = (float) ((r - circleRadius)
* Math.cos(Math.toRadians((angle + sliceAngle)
* phaseY)) + center.x);
float y = (float) ((r - circleRadius)
* Math.sin(Math.toRadians((angle + sliceAngle)
* phaseY)) + center.y);
// draw image instead of arcs
Utils.drawImage(
c,
mImage,
(int)x,
(int)y,
mImage.getIntrinsicWidth(),
mImage.getIntrinsicHeight());
}
angle += sliceAngle * phaseX;
}
MPPointF.recycleInstance(center);
}
#Override
public void drawExtras(Canvas c) {
super.drawExtras(c);
// use drawImageBeforeSlice in last step of rendering process
drawImageBeforeSlice(c);
}
}
Don't forget to set your new renderer on your PieChart:
myPieChart.setRenderer(new PieChartRendererWithImages(myPieChart, myPieChart.getAnimator(), myPieChart.getViewPortHandler(), getResources().getDrawable(R.drawable.my_image)));
Verified to work by putting it in the MPAndroidChart example:

What is the correct way of creating textures for a map with Libgdx without tmx?

I am making a top down style game using LibGdx that I want to be randomly generated each new game load.
I am using a sprite sheet with 8 x 8 sprites inside that need to be combined into 16 x 16 tiles. (I'm doing this to get more natural looking levels. I can explain this more if needed.)
I already have the algorithm to generate the array for what tile should be what.
But i'm stuck on how I should handle the tile classes.
I have this class called Tile
import com.badlogic.gdx.graphics.g2d.Sprite;
public class Tile extends Sprite{ // Should I use sprite??? Seems wrong..
public byte id;
public static Tile[] tiles = new Tile[256];
public static Tile grass = new GrassTile(0);
public Tile(int id) {
this.id = (byte) id;
if (tiles[id] != null)
throw new RuntimeException("Tile Already Exists....");
tiles[id] = this;
}
}
And I want to have multiple classes that exened this class for each tile. For example this is grass.
import com.badlogic.gdx.graphics.g2d.Sprite;
public class Tile extends Sprite{
public byte id;
public static Tile[] tiles = new Tile[256];
public static Tile grass = new GrassTile(0);
public Tile(int id) {
this.id = (byte) id;
if (tiles[id] != null)
throw new RuntimeException("Tile Already Exists....");
tiles[id] = this;
}
}
Should these classes be extended off of a sprite? Or some other class from libgdx?
Also Keep in mind these tiles are going to be 16 x 16 that are made up of 4 smaller 8x8 sections of the sprite sheet.
A great example provided by the Libgdx github page shows you how to achieve this by generating a TileMapLayer. Assigning cells to the layer and then adding the layer to the map.
This snippet of code will help you achieve what you want. The original documentation is here
More documentation here: Libgdx Tile-maps
public void create () {
float w = Gdx.graphics.getWidth();
float h = Gdx.graphics.getHeight();
camera = new OrthographicCamera();
camera.setToOrtho(false, (w / h) * 320, 320);
camera.update();
cameraController = new OrthoCamController(camera);
Gdx.input.setInputProcessor(cameraController);
font = new BitmapFont();
batch = new SpriteBatch();
{
tiles = new Texture(Gdx.files.internal("data/maps/tiled/tiles.png"));
TextureRegion[][] splitTiles = TextureRegion.split(tiles, 32, 32);
map = new TiledMap();
MapLayers layers = map.getLayers();
for (int l = 0; l < 20; l++) {
TiledMapTileLayer layer = new TiledMapTileLayer(150, 100, 32, 32);
for (int x = 0; x < 150; x++) {
for (int y = 0; y < 100; y++) {
int ty = (int)(Math.random() * splitTiles.length);
int tx = (int)(Math.random() * splitTiles[ty].length);
Cell cell = new Cell();
cell.setTile(new StaticTiledMapTile(splitTiles[ty][tx]));
layer.setCell(x, y, cell);
}
}
layers.add(layer);
}
}
// for top down camera view
renderer = new OrthogonalTiledMapRenderer(map);
}

How can I can insert an image or stamp on a pdf where there is free space available like a density scanner

I have a pdf file where-in I am adding a stamp to all it's pages.
But, the problem is, the stamp is added to the upper-left corner of each page. If, the page has text in that part, the stamp appears on the text.
My question is, is there any method by which I can read each page and if there is no text in that part add the stamp else search for nearest available free space, just like what a density scanner does?
I am using IText and Java 1.7.
The free space fider class and the distance calculation function are the same that is there in the accepted answer.
Following is the edited code I am using:
// The resulting PDF file
String RESULT = "K:\\DCIN_TER\\DCIN_EPU2\\CIRCUIT FROM BRANCH\\RAINBOW ORDERS\\" + jtfSONo.getText().trim() + "\\PADR Release\\Final PADR Release 1.pdf";
// Create a reader
PdfReader reader = new PdfReader("K:\\DCIN_TER\\DCIN_EPU2\\CIRCUIT FROM BRANCH\\RAINBOW ORDERS\\" + jtfSONo.getText().trim() + "\\PADR Release\\Final PADR Release.pdf");
// Create a stamper
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(RESULT));
// Loop over the pages and add a footer to each page
int n = reader.getNumberOfPages();
for(int i = 1; i <= n; i++)
{
Collection<Rectangle2D> rectangles = find(reader, 300, 100, n, stamper); // minimum width & height of a rectangle
Iterator itr = rectangles.iterator();
while(itr.hasNext())
{
System.out.println(itr.next());
}
if(!(rectangles.isEmpty()) && (rectangles.size() != 0))
{
Rectangle2D best = null;
double bestDist = Double.MAX_VALUE;
Point2D.Double point = new Point2D.Double(200, 400);
float x = 0, y = 0;
for(Rectangle2D rectangle: rectangles)
{
double distance = distance(rectangle, point);
if(distance < bestDist)
{
best = rectangle;
bestDist = distance;
x = (float) best.getX();
y = (float) best.getY();
int left = (int) best.getMinX();
int right = (int) best.getMaxX();
int top = (int) best.getMaxY();
int bottom = (int) best.getMinY();
System.out.println("x : " + x);
System.out.println("y : " + y);
System.out.println("left : " + left);
System.out.println("right : " + right);
System.out.println("top : " + top);
System.out.println("bottom : " + bottom);
}
}
getFooterTable(i, n).writeSelectedRows(0, -1, x, y, stamper.getOverContent(i)); // 0, -1 indicates 1st row, 1st column upto last row and last column
}
else
getFooterTable(i, n).writeSelectedRows(0, -1, 94, 140, stamper.getOverContent(i)); // bottom left corner
}
// Close the stamper
stamper.close();
// Close the reader
reader.close();
public Collection<Rectangle2D> find(PdfReader reader, float minWidth, float minHeight, int page, PdfStamper stamper) throws IOException
{
Rectangle cropBox = reader.getCropBox(page);
Rectangle2D crop = new Rectangle2D.Float(cropBox.getLeft(), cropBox.getBottom(), cropBox.getWidth(), cropBox.getHeight());
FreeSpaceFinder finder = new FreeSpaceFinder(crop, minWidth, minHeight);
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
parser.processContent(page, finder);
System.out.println("finder.freeSpaces : " + finder.freeSpaces);
return finder.freeSpaces;
}
// Create a table with page X of Y, #param x the page number, #param y the total number of pages, #return a table that can be used as footer
public static PdfPTable getFooterTable(int x, int y)
{
java.util.Date date = new java.util.Date();
SimpleDateFormat sdf = new SimpleDateFormat("dd MMM yyyy");
String month = sdf.format(date);
System.out.println("Month : " + month);
PdfPTable table = new PdfPTable(1);
table.setTotalWidth(120);
table.setLockedWidth(true);
table.getDefaultCell().setFixedHeight(20);
table.getDefaultCell().setBorder(Rectangle.TOP);
table.getDefaultCell().setBorder(Rectangle.LEFT);
table.getDefaultCell().setBorder(Rectangle.RIGHT);
table.getDefaultCell().setBorderColorTop(BaseColor.BLUE);
table.getDefaultCell().setBorderColorLeft(BaseColor.BLUE);
table.getDefaultCell().setBorderColorRight(BaseColor.BLUE);
table.getDefaultCell().setBorderWidthTop(1f);
table.getDefaultCell().setBorderWidthLeft(1f);
table.getDefaultCell().setBorderWidthRight(1f);
table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
Font font1 = new Font(FontFamily.HELVETICA, 10, Font.BOLD, BaseColor.BLUE);
table.addCell(new Phrase("CONTROLLED COPY", font1));
table.getDefaultCell().setFixedHeight(20);
table.getDefaultCell().setBorder(Rectangle.LEFT);
table.getDefaultCell().setBorder(Rectangle.RIGHT);
table.getDefaultCell().setBorderColorLeft(BaseColor.BLUE);
table.getDefaultCell().setBorderColorRight(BaseColor.BLUE);
table.getDefaultCell().setBorderWidthLeft(1f);
table.getDefaultCell().setBorderWidthRight(1f);
table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
Font font = new Font(FontFamily.HELVETICA, 10, Font.BOLD, BaseColor.RED);
table.addCell(new Phrase(month, font));
table.getDefaultCell().setFixedHeight(20);
table.getDefaultCell().setBorder(Rectangle.LEFT);
table.getDefaultCell().setBorder(Rectangle.RIGHT);
table.getDefaultCell().setBorder(Rectangle.BOTTOM);
table.getDefaultCell().setBorderColorLeft(BaseColor.BLUE);
table.getDefaultCell().setBorderColorRight(BaseColor.BLUE);
table.getDefaultCell().setBorderColorBottom(BaseColor.BLUE);
table.getDefaultCell().setBorderWidthLeft(1f);
table.getDefaultCell().setBorderWidthRight(1f);
table.getDefaultCell().setBorderWidthBottom(1f);
table.getDefaultCell().setHorizontalAlignment(Element.ALIGN_CENTER);
table.addCell(new Phrase("BLR DESIGN DEPT.", font1));
return table;
}
is there any method by which I can read each page and if there is no text in that part add the stamp else search for nearest available free space, just like what a density scanner does?
iText does not offer that functionality out of the box. Depending of what kind of content you want to evade, though, you might consider either rendering the page to an image and looking for white spots in the image or doing text extraction with a strategy that tries to find locations without text.
The first alternative, analyzing a rendered version of the page, would be the focus of a separate question as an image processing library would have to be chosen first.
There are a number of situations, though, in which that first alternative is not the best way to go. E.g. if you only want to evade text but not necessarily graphics (like watermarks), or if you also want to evade invisible text (which usually can be marked in a PDF viewer and, therefore, interfere with your addition).
The second alternative (using text and image extraction abilities of iText) can be the more appropriate approach in such situations.
Here a sample RenderListener for such a task:
public class FreeSpaceFinder implements RenderListener
{
//
// constructors
//
public FreeSpaceFinder(Rectangle2D initialBox, float minWidth, float minHeight)
{
this(Collections.singleton(initialBox), minWidth, minHeight);
}
public FreeSpaceFinder(Collection<Rectangle2D> initialBoxes, float minWidth, float minHeight)
{
this.minWidth = minWidth;
this.minHeight = minHeight;
freeSpaces = initialBoxes;
}
//
// RenderListener implementation
//
#Override
public void renderText(TextRenderInfo renderInfo)
{
Rectangle2D usedSpace = renderInfo.getAscentLine().getBoundingRectange();
usedSpace.add(renderInfo.getDescentLine().getBoundingRectange());
remove(usedSpace);
}
#Override
public void renderImage(ImageRenderInfo renderInfo)
{
Matrix imageMatrix = renderInfo.getImageCTM();
Vector image00 = rect00.cross(imageMatrix);
Vector image01 = rect01.cross(imageMatrix);
Vector image10 = rect10.cross(imageMatrix);
Vector image11 = rect11.cross(imageMatrix);
Rectangle2D usedSpace = new Rectangle2D.Float(image00.get(Vector.I1), image00.get(Vector.I2), 0, 0);
usedSpace.add(image01.get(Vector.I1), image01.get(Vector.I2));
usedSpace.add(image10.get(Vector.I1), image10.get(Vector.I2));
usedSpace.add(image11.get(Vector.I1), image11.get(Vector.I2));
remove(usedSpace);
}
#Override
public void beginTextBlock() { }
#Override
public void endTextBlock() { }
//
// helpers
//
void remove(Rectangle2D usedSpace)
{
final double minX = usedSpace.getMinX();
final double maxX = usedSpace.getMaxX();
final double minY = usedSpace.getMinY();
final double maxY = usedSpace.getMaxY();
final Collection<Rectangle2D> newFreeSpaces = new ArrayList<Rectangle2D>();
for (Rectangle2D freeSpace: freeSpaces)
{
final Collection<Rectangle2D> newFragments = new ArrayList<Rectangle2D>();
if (freeSpace.intersectsLine(minX, minY, maxX, minY))
newFragments.add(new Rectangle2D.Double(freeSpace.getMinX(), freeSpace.getMinY(), freeSpace.getWidth(), minY-freeSpace.getMinY()));
if (freeSpace.intersectsLine(minX, maxY, maxX, maxY))
newFragments.add(new Rectangle2D.Double(freeSpace.getMinX(), maxY, freeSpace.getWidth(), freeSpace.getMaxY() - maxY));
if (freeSpace.intersectsLine(minX, minY, minX, maxY))
newFragments.add(new Rectangle2D.Double(freeSpace.getMinX(), freeSpace.getMinY(), minX - freeSpace.getMinX(), freeSpace.getHeight()));
if (freeSpace.intersectsLine(maxX, minY, maxX, maxY))
newFragments.add(new Rectangle2D.Double(maxX, freeSpace.getMinY(), freeSpace.getMaxX() - maxX, freeSpace.getHeight()));
if (newFragments.isEmpty())
{
add(newFreeSpaces, freeSpace);
}
else
{
for (Rectangle2D fragment: newFragments)
{
if (fragment.getHeight() >= minHeight && fragment.getWidth() >= minWidth)
{
add(newFreeSpaces, fragment);
}
}
}
}
freeSpaces = newFreeSpaces;
}
void add(Collection<Rectangle2D> rectangles, Rectangle2D addition)
{
final Collection<Rectangle2D> toRemove = new ArrayList<Rectangle2D>();
boolean isContained = false;
for (Rectangle2D rectangle: rectangles)
{
if (rectangle.contains(addition))
{
isContained = true;
break;
}
if (addition.contains(rectangle))
toRemove.add(rectangle);
}
rectangles.removeAll(toRemove);
if (!isContained)
rectangles.add(addition);
}
//
// members
//
public Collection<Rectangle2D> freeSpaces = null;
final float minWidth;
final float minHeight;
final static Vector rect00 = new Vector(0, 0, 1);
final static Vector rect01 = new Vector(0, 1, 1);
final static Vector rect10 = new Vector(1, 0, 1);
final static Vector rect11 = new Vector(1, 1, 1);
}
Using this FreeSpaceFinder you can find empty areas with given minimum dimensions in a method like this:
public Collection<Rectangle2D> find(PdfReader reader, float minWidth, float minHeight, int page) throws IOException
{
Rectangle cropBox = reader.getCropBox(page);
Rectangle2D crop = new Rectangle2D.Float(cropBox.getLeft(), cropBox.getBottom(), cropBox.getWidth(), cropBox.getHeight());
FreeSpaceFinder finder = new FreeSpaceFinder(crop, minWidth, minHeight);
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
parser.processContent(page, finder);
return finder.freeSpaces;
}
For your task you now have to choose from the returned rectangles the one which suits you best.
Beware, this code still may have to be tuned to your requirements:
It ignores clip paths, rendering modes, colors, and covering objects. Thus, it considers all text and all bitmap images, whether they are actually visible or not.
It does not consider vector graphics (because the iText parser package does not consider them).
It is not very optimized.
Applied to this PDF page:
with minimum width 200 and height 50, you get these rectangles:
x y w h
000,000 000,000 595,000 056,423
000,000 074,423 595,000 168,681
000,000 267,304 314,508 088,751
000,000 503,933 351,932 068,665
164,296 583,598 430,704 082,800
220,803 583,598 374,197 096,474
220,803 583,598 234,197 107,825
000,000 700,423 455,000 102,396
000,000 700,423 267,632 141,577
361,348 782,372 233,652 059,628
or, more visually, here as rectangles on the page:
The paper plane is a vector graphic and, therefore, ignored.
Of course you could also change the PDF rendering code to not draw stuff you want to ignore and to visibly draw originally invisible stuff which you want to ignore, and then use bitmap image analysis nonetheless...
EDIT
In his comments the OP asked how to find the rectangle in the rectangle collection returned by find which is nearest to a given point.
First of all there not necessarily is the nearest rectangle, there may be multiple.
That been said, one can choose a nearest rectangle as follows:
First one needs to calculate a distance between point and rectangle, e.g.:
double distance(Rectangle2D rectangle, Point2D point)
{
double x = point.getX();
double y = point.getY();
double left = rectangle.getMinX();
double right = rectangle.getMaxX();
double top = rectangle.getMaxY();
double bottom = rectangle.getMinY();
if (x < left) // point left of rect
{
if (y < bottom) // and below
return Point2D.distance(x, y, left, bottom);
if (y > top) // and top
return Point2D.distance(x, y, left, top);
return left - x;
}
if (x > right) // point right of rect
{
if (y < bottom) // and below
return Point2D.distance(x, y, right, bottom);
if (y > top) // and top
return Point2D.distance(x, y, right, top);
return x - right;
}
if (y < bottom) // and below
return bottom - y;
if (y > top) // and top
return y - top;
return 0;
}
Using this distance measurement one can select a nearest rectangle using code like this for a Collection<Rectangle2D> rectangles and a Point2D point:
Rectangle2D best = null;
double bestDist = Double.MAX_VALUE;
for (Rectangle2D rectangle: rectangles)
{
double distance = distance(rectangle, point);
if (distance < bestDist)
{
best = rectangle;
bestDist = distance;
}
}
After this best contains a best rectangle.
For the sample document used above, this method returns the colored rectangles for the page corners and left and right centers:
EDIT TWO
Since iText 5.5.6, the RenderListener interface has been extended as ExtRenderListener to also be signaled about Path construction and path drawing operations. Thus, the FreeSpaceFinder above could also be extended to handle paths:
//
// Additional ExtRenderListener methods
//
#Override
public void modifyPath(PathConstructionRenderInfo renderInfo)
{
List<Vector> points = new ArrayList<Vector>();
if (renderInfo.getOperation() == PathConstructionRenderInfo.RECT)
{
float x = renderInfo.getSegmentData().get(0);
float y = renderInfo.getSegmentData().get(1);
float w = renderInfo.getSegmentData().get(2);
float h = renderInfo.getSegmentData().get(3);
points.add(new Vector(x, y, 1));
points.add(new Vector(x+w, y, 1));
points.add(new Vector(x, y+h, 1));
points.add(new Vector(x+w, y+h, 1));
}
else if (renderInfo.getSegmentData() != null)
{
for (int i = 0; i < renderInfo.getSegmentData().size()-1; i+=2)
{
points.add(new Vector(renderInfo.getSegmentData().get(i), renderInfo.getSegmentData().get(i+1), 1));
}
}
for (Vector point: points)
{
point = point.cross(renderInfo.getCtm());
Rectangle2D.Float pointRectangle = new Rectangle2D.Float(point.get(Vector.I1), point.get(Vector.I2), 0, 0);
if (currentPathRectangle == null)
currentPathRectangle = pointRectangle;
else
currentPathRectangle.add(pointRectangle);
}
}
#Override
public Path renderPath(PathPaintingRenderInfo renderInfo)
{
if (renderInfo.getOperation() != PathPaintingRenderInfo.NO_OP)
remove(currentPathRectangle);
currentPathRectangle = null;
return null;
}
#Override
public void clipPath(int rule)
{
// TODO Auto-generated method stub
}
Rectangle2D.Float currentPathRectangle = null;
(FreeSpaceFinderExt.java)
Using this class the result above is improved to
As you see the paper plane and the table background colorations now also are taken into account.
My other answer focuses on the original question, i.e. how to find free space with given minimum dimensions on a page.
Since that answer had been written, the OP provided code trying to make use of that original answer.
This answer deals with that code.
The code has a number of shortcoming.
The choice of free space on a page depends on the number of pages in the document.
The reason for this is to be found at the start of the loop over the pages:
for(int i = 1; i <= n; i++)
{
Collection<Rectangle2D> rectangles = find(reader, 300, 100, n, stamper);
...
The OP surely meant i, not n there. The code as is always looks for free space on the last document page.
The rectangles are lower than they should be.
The reason for this is to be found in the retrieval and use of the rectangle coordinates:
x = (float) best.getX();
y = (float) best.getY();
...
getFooterTable(i, n).writeSelectedRows(0, -1, x, y, stamper.getOverContent(i));
The Rectangle2D methods getX and getY return the coordinates of the lower left rectangle corner; the PdfPTable methods writeSelectedRows, on the other hand, require the upper left rectangle corner. Thus, getMaxY should be used instead of getY.

How do I create a bar chart using the Java prefuse library?

I've currently got prefuse to plot a scatter graph, where the X axis is the computer name and the Y axis is its temperature. How do I get it to draw bars showing the values instead of discrete points?
I'm currently using the following code to render the points:
ShapeAction shape = new ShapeAction(group, Constants.SHAPE_RECTANGLE);
ColorAction strokeColor = new DataColorAction(group, dataType, Constants.NUMERICAL, VisualItem.STROKECOLOR, colorPalette);
ActionList draw = new ActionList();
draw.add(shape);
draw.add(strokeColor);
draw.add(new ColorAction(group, VisualItem.FILLCOLOR, 0));
draw.add(new RepaintAction());
m_vis.putAction("draw", draw);
How would I adapt this code to get, instead of a small square at each point, a thick bar going from th bottom of the graph to the point?
Thanks.
I think I should probably point out how I did this - Stack Overflow is supposed to be a repository too, after all. Earlier in the code was the following:
m_vis.setRendererFactory(new RendererFactory() {
Renderer yAxisRenderer = new AxisRenderer(Constants.LEFT, Constants.TOP);
Renderer xAxisRenderer = new AxisRenderer(Constants.CENTER, Constants.FAR_BOTTOM);
Renderer barRenderer = new ShapeRenderer();
public Renderer getRenderer(VisualItem item) {
return item.isInGroup("yAxis") ? yAxisRenderer :
item.isInGroup("xAxis") ? xAxisRenderer :
barRenderer;
}
});
I extended the shape renderer to always return a rectangle of the correct width and height, and positioned it half a bar to the left of where it was supposed to be. If you want to position your bars in the centre, you need to do that yourself - prefuse won't help you.
m_vis.setRendererFactory(new RendererFactory() {
Renderer yAxisRenderer = new AxisRenderer(Constants.LEFT, Constants.TOP);
Renderer xAxisRenderer = new AxisRenderer(Constants.CENTER, Constants.FAR_BOTTOM);
Renderer barRenderer = new ShapeRenderer() {
protected Shape getRawShape(VisualItem item) {
double x = item.getX();
double y = item.getY();
if (Double.isNaN(x) || Double.isInfinite(x))
x = getInsets().left + axisWidth + totalBarWidth / 2;
if (Double.isNaN(y) || Double.isInfinite(y))
y = 0;
double width = totalBarWidth / (barCount + 1) - barGap;
double height = getHeight() - getInsets().bottom - axisHeight - y;
x -= width / 2;
return rectangle(x, y, width, height);
}
};
public Renderer getRenderer(VisualItem item) {
return item.isInGroup("yAxis") ? yAxisRenderer :
item.isInGroup("xAxis") ? xAxisRenderer :
barRenderer;
}
});

Categories