Creating a simple custom scroll view - java

This is somehow a general question about scroll views, I want to learn the basics of a scroll view and how to implement one on my own because it is essential as part of most dynamic GUI. You may ask, Why not simply use the one provided by the platform? My answer would be, aside from it's fun to learn new stuff, it's nice to see things customized the way you want it to be. Simply put, I want to create just a simple custom scroll view and try to understand how it is working behind the scene.
Moving on, what I currently have to present here is just the simplest example of the UI I came up with. Basically, it is a Pane that serves as the viewport for the entire content and contains a single vertical scrollbar on its right edge, just like normal scroll views, but I just added a little transition which animates the scrollbar's width on mouse hover.
ScrollContainer class
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
/**
* ScrollContainer
*
* A container for scrolling large content.
*/
public class ScrollContainer extends Pane {
private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
/**
* Construct a new ScrollContainer
*/
public ScrollContainer() {
super();
scrollBar = new VerticalScrollBar();
getChildren().add(scrollBar);
rectangle = new Rectangle();
rectangle.widthProperty().bind(widthProperty());
rectangle.heightProperty().bind(heightProperty());
setClip(rectangle);
}
#Override
protected void layoutChildren() {
super.layoutChildren();
// Layout scrollbar to the edge of container, and fit the viewport's height as well
scrollBar.resize(scrollBar.getWidth(), getHeight());
scrollBar.setLayoutX(getWidth() - scrollBar.getWidth());
}
/**
* VerticalScrollBar
*/
private class VerticalScrollBar extends Region {
// Temporary scrubber's height.
// TODO: Figure out the computation for scrubber's height.
private static final double SCRUBBER_LENGTH = 100;
private double initialY; // Initial mouse position when dragging the scrubber
private Timeline widthTransition; // Transforms width of scrollbar on hover
private Region scrubber; // Indicator about the content's visible area
/**
* Construct a new VerticalScrollBar
*/
private VerticalScrollBar() {
super();
// Scrollbar's initial width
setPrefWidth(7);
widthTransition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
);
scrubber = new Region();
scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
scrubber.setOnMousePressed(event -> initialY = event.getY());
scrubber.setOnMouseDragged(event -> {
// Moves the scrubber vertically within the scrollbar.
// TODO: Figure out the proper way of handling scrubber movement, an onScroll mouse wheel function, ect.
double initialScrollY = event.getSceneY() - initialY;
double maxScrollY = getHeight() - SCRUBBER_LENGTH;
double minScrollY = 0;
if (initialScrollY >= minScrollY && initialScrollY <= maxScrollY) {
scrubber.setTranslateY(initialScrollY);
}
});
getChildren().add(scrubber);
// Animate scrollbar's width on mouse enter and exit
setOnMouseEntered(event -> {
widthTransition.setRate(1);
widthTransition.play();
});
setOnMouseExited(event -> {
widthTransition.setRate(-1);
widthTransition.play();
});
}
#Override
protected void layoutChildren() {
super.layoutChildren();
// Layout scrubber to fit the scrollbar's width
scrubber.resize(getWidth(), SCRUBBER_LENGTH);
}
}
}
Main class
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
Label lorem = new Label();
lorem.setStyle("-fx-padding: 20px;");
lorem.setText("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
"Integer ut ornare enim, a rutrum nisl. " +
"Proin eros felis, rutrum at pharetra viverra, elementum quis lacus. " +
"Nam sit amet sollicitudin nibh, ac mattis lectus. " +
"Sed mattis ullamcorper sapien, a pulvinar turpis hendrerit vel. " +
"Fusce nec diam metus. In vel dui lacus. " +
"Sed imperdiet ipsum euismod aliquam rhoncus. " +
"Morbi sagittis mauris ac massa pretium, vel placerat purus porta. " +
"Suspendisse orci leo, sagittis eu orci vitae, porttitor sagittis odio. " +
"Proin iaculis enim sed ipsum sodales, at congue ante blandit. " +
"Etiam mattis erat nec dolor vestibulum, quis interdum sem pellentesque. " +
"Nullam accumsan ex non lacus sollicitudin interdum.");
lorem.setWrapText(true);
StackPane content = new StackPane();
content.setPrefSize(300, 300);
content.setMinSize(300, 300);
content.setMaxSize(300, 300);
content.setStyle("-fx-background-color: white;");
content.getChildren().add(lorem);
ScrollContainer viewport = new ScrollContainer();
viewport.setStyle("-fx-background-color: whitesmoke");
viewport.getChildren().add(0, content);
primaryStage.setScene(new Scene(viewport, 300, 150));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
I wanted to see a working example showing just the basic art of scrolling; like the proper way of handling the thumb's animation movement, computation of scrollbar's thumb length, and lastly, the required total unit or amount to move the content. I think these three parts are the keys to the core of a scroll view.
P.S
I also want to see the use of the onScroll event in JavaFX, right now all I know is the common used mouse events. Thank you in advance.
UPDATE
I've added a BlockIncrement function to the answer of sir #fabian below. It will basically just move the thumb to the current position of the pointer while keeping the [0, 1] range value. All credits and thanks goes to him.
This is for others who were looking for something like this idea of
custom scroll view, hope you might find this reference useful in the future.
public class ScrollContainer extends Region {
private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
/**
* Construct a new ScrollContainer
*/
public ScrollContainer() {
setOnScroll(evt -> {
double viewportHeight = getHeight();
double contentHeight = getContentHeight();
if (contentHeight > viewportHeight) {
double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
if (Double.isFinite(delta)) {
scrollBar.setValue(scrollBar.getValue() + delta);
}
}
});
scrollBar = new VerticalScrollBar();
getChildren().add(scrollBar);
rectangle = new Rectangle();
setClip(rectangle);
}
private Node content;
public void setContent(Node content) {
if (this.content != null) {
// remove old content
getChildren().remove(this.content);
}
if (content != null) {
// add new content
getChildren().add(0, content);
}
this.content = content;
}
private double getContentHeight() {
return content == null ? 0 : content.getLayoutBounds().getHeight();
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double w = getWidth();
double h = getHeight();
double sw = scrollBar.getWidth();
double viewportWidth = w - sw;
double viewportHeight = h;
if (content != null) {
double contentHeight = getContentHeight();
double vValue = scrollBar.getValue();
// position content according to scrollbar value
content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
}
// Layout scrollbar to the edge of container, and fit the viewport's height as well
scrollBar.resize(sw, h);
scrollBar.setLayoutX(viewportWidth);
// resize clip
rectangle.setWidth(w);
rectangle.setHeight(h);
}
/**
* VerticalScrollBar
*/
private class VerticalScrollBar extends Region {
private boolean thumbPressed; // Indicates that the scrubber was pressed
private double initialValue;
private double initialY; // Initial mouse position when dragging the scrubber
private Timeline widthTransition; // Transforms width of scrollbar on hover
private Region scrubber; // Indicator about the content's visible area
private double value;
private void setValue(double v) {
value = v;
}
private double getValue() {
return value;
}
private double calculateScrubberHeight() {
double h = getHeight();
return h * h / getContentHeight();
}
/**
* Construct a new VerticalScrollBar
*/
private VerticalScrollBar() {
// Scrollbar's initial width
setPrefWidth(7);
widthTransition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
);
scrubber = new Region();
scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
scrubber.setOnMousePressed(event -> {
initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
initialValue = value;
thumbPressed = true;
});
scrubber.setOnMouseDragged(event -> {
if (thumbPressed) {
double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
double sH = calculateScrubberHeight();
double h = getHeight();
// calculate value change and prevent errors
double delta = (currentY - initialY) / (h - sH);
if (!Double.isFinite(delta)) {
delta = 0;
}
// keep value in range [0, 1]
double newValue = Math.max(0, Math.min(1, initialValue + delta));
value = newValue;
// layout thumb
requestLayout();
}
});
scrubber.setOnMouseReleased(event -> thumbPressed = false);
getChildren().add(scrubber);
// Added BlockIncrement.
// Pressing the `track` or the scrollbar itself will move and position the
// scrubber to the pointer location, as well as the content prior to the
// value changes.
setOnMousePressed(event -> {
if (!thumbPressed) {
double sH = calculateScrubberHeight();
double h = getHeight();
double pointerY = event.getY();
double delta = pointerY / (h - sH);
double newValue = Math.max(0, Math.min(1, delta));
// keep value in range [0, 1]
if (delta > 1) {
newValue = 1;
}
value = newValue;
requestLayout();
}
});
// Animate scrollbar's width on mouse enter and exit
setOnMouseEntered(event -> {
widthTransition.setRate(1);
widthTransition.play();
});
setOnMouseExited(event -> {
widthTransition.setRate(-1);
widthTransition.play();
});
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double h = getHeight();
double cH = getContentHeight();
if (cH <= h) {
// full size, if content does not excede viewport size
scrubber.resize(getWidth(), h);
} else {
double sH = calculateScrubberHeight();
// move thumb to position
scrubber.setTranslateY(value * (h - sH));
// Layout scrubber to fit the scrollbar's width
scrubber.resize(getWidth(), sH);
}
}
}
}

There are a few equations that allow you to compute the layout (all assuming contentHeight > viewportHeight):
vValue denotes the position of the thumb in the vertical scroll bar in [0, 1] (0 = topmost position, 1 = bottom of the thumb is at bottom of the track).
topY = vValue * (contentHeight - viewportHeight)
thumbHeight / trackHeight = viewportHeight / contentHeight
thumbY = vValue * (trackHeight - thumbHeight)
Also note that providing access to the children and adding the content outside of the ScrollContainer is bad practice since it requires the user of this class to do modifications that should be reserved for the class itself. Doing this could easily lead to the following line which breaks the ScrollContainer (the content could hide the thumb):
// viewport.getChildren().add(0, content);
viewport.getChildren().add(content);
It's better extend Region directly and using a method to (re)place the content.
public class ScrollContainer extends Region {
private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content
/**
* Construct a new ScrollContainer
*/
public ScrollContainer() {
setOnScroll(evt -> {
double viewportHeight = getHeight();
double contentHeight = getContentHeight();
if (contentHeight > viewportHeight) {
double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
if (Double.isFinite(delta)) {
scrollBar.setValue(scrollBar.getValue() + delta);
}
}
});
scrollBar = new VerticalScrollBar();
getChildren().add(scrollBar);
rectangle = new Rectangle();
setClip(rectangle);
}
private Node content;
public void setContent(Node content) {
if (this.content != null) {
// remove old content
getChildren().remove(this.content);
}
if (content != null) {
// add new content
getChildren().add(0, content);
}
this.content = content;
}
private double getContentHeight() {
return content == null ? 0 : content.getLayoutBounds().getHeight();
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double w = getWidth();
double h = getHeight();
double sw = scrollBar.getWidth();
double viewportWidth = w - sw;
double viewportHeight = h;
if (content != null) {
double contentHeight = getContentHeight();
double vValue = scrollBar.getValue();
// position content according to scrollbar value
content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
}
// Layout scrollbar to the edge of container, and fit the viewport's height as well
scrollBar.resize(sw, h);
scrollBar.setLayoutX(viewportWidth);
// resize clip
rectangle.setWidth(w);
rectangle.setHeight(h);
}
/**
* VerticalScrollBar
*/
private class VerticalScrollBar extends Region {
private double initialValue;
private double initialY; // Initial mouse position when dragging the scrubber
private Timeline widthTransition; // Transforms width of scrollbar on hover
private Region scrubber; // Indicator about the content's visible area
private double value;
public double getValue() {
return value;
}
private double calculateScrubberHeight() {
double h = getHeight();
return h * h / getContentHeight();
}
/**
* Construct a new VerticalScrollBar
*/
private VerticalScrollBar() {
// Scrollbar's initial width
setPrefWidth(7);
widthTransition = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
);
scrubber = new Region();
scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
scrubber.setOnMousePressed(event -> {
initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
initialValue = value;
});
scrubber.setOnMouseDragged(event -> {
double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
double sH = calculateScrubberHeight();
double h = getHeight();
// calculate value change and prevent errors
double delta = (currentY - initialY) / (h - sH);
if (!Double.isFinite(delta)) {
delta = 0;
}
// keep value in range [0, 1]
double newValue = Math.max(0, Math.min(1, initialValue + delta));
value = newValue;
// layout thumb
requestLayout();
});
getChildren().add(scrubber);
// Animate scrollbar's width on mouse enter and exit
setOnMouseEntered(event -> {
widthTransition.setRate(1);
widthTransition.play();
});
setOnMouseExited(event -> {
widthTransition.setRate(-1);
widthTransition.play();
});
}
#Override
protected void layoutChildren() {
super.layoutChildren();
double h = getHeight();
double cH = getContentHeight();
if (cH <= h) {
// full size, if content does not excede viewport size
scrubber.resize(getWidth(), h);
} else {
double sH = calculateScrubberHeight();
// move thumb to position
scrubber.setTranslateY(value * (h - sH));
// Layout scrubber to fit the scrollbar's width
scrubber.resize(getWidth(), sH);
}
}
}
}

Related

How to implement bindings of multiple properties to all listen to each other?

I have two Circle nodes that are dragable, and I want to connect them with a Line that connects the nodes (not from their center, but from their perimeter). But as one Circle changes location as it is getting dragged, the Line's startX and startY values change so the Line becomes the shortest line between the two Circles, collinear with the line connecting their radii.
My problem is that it seems overly verbose to have the Line's startX, startY, endX, and endY each individually listen to or bind to both Circles' centerXProperty and centerYProperty (or rather, bind to a calculation with those properties' values), as that would result in 16 total bindings/listeners.
I was wondering if there exists an easier or more convenient way to accomplish this. I was considering creating a SimpleDoubleProperty that would be the slope of the Line object (y2 - y1)/(x2 - x1), binding to the two centerXPropertys and centerYPropertys, and have startX, startY, endX, and endY each listen to that property, but I'm also not sure how to have a single property bind to the resultant calculation of those four properties.
Here is how I'm currently constructing my Line. I was experimenting for the startXProperty and startYProperty binding and realized it properly updated the Line but ONLY when the source Circle source was moved, which prompted me to ask this question. The endXProperty and endYProperty still both have the Line anchored at the target Circle's center. I can provide my entire code if needed although I think this should suffice for what I'm trying to accomplish.
public GraphEdge(GraphNode source, GraphNode target) {
this.source = source;
this.target = target;
this.setFill(Color.BLACK);
this.startXProperty().bind(Bindings.createDoubleBinding(() -> {
slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
return source.getCenterX() + Math.cos(Math.atan(slope)) * source.getRadius();
}, source.boundsInParentProperty()));
this.startYProperty().bind(Bindings.createDoubleBinding(() -> {
slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
return source.getCenterY() + Math.sin(Math.atan(slope)) * source.getRadius();
}, source.boundsInParentProperty()));
this.endXProperty().bind(Bindings.createDoubleBinding(() -> {
Bounds b = target.getBoundsInParent();
return b.getMinX() + b.getWidth() / 2;
}, target.boundsInParentProperty()));
this.endYProperty().bind(Bindings.createDoubleBinding(() -> {
Bounds b = target.getBoundsInParent();
return b.getMinY() + b.getHeight() / 2;
}, target.boundsInParentProperty()));
}
createDoubleBinding accepts a list of dependencies. You should list all of the properties each line property depends on.
this.startXProperty().bind(Bindings.createDoubleBinding(
() -> {
double slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
return source.getCenterX() + Math.cos(Math.atan(slope)) * source.getRadius();
},
source.centerXProperty(),
source.centerYProperty(),
target.centerXProperty(),
target.centerYProperty(),
source.radiusProperty(),
));
Repeat for the three other line properties.
I agree with #John Kugelman answer.
I gave a quick try and looks like your actual formula (using sin/tan) is not working for me as expected.
So I gave a rethink about the logic and came up with the below solution. This solution is based on the concept of "Given line AB, find a point C on line which is at a distance d". Here A and B are the centers of the circles.
So the idea is :
We build a DoubleBinding to get the length(l) of the line between centers of circles.
Then we calculate the points which are at a distance of radius 'r' and 'l-r'. The points sits on the edges of the two circles.
Lastly, we build a line by binding with the new points.
Please find the below code:
class GraphEdge extends Line {
public GraphEdge(GraphNode source, GraphNode target) {
DoubleBinding lineLength = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (target.getCenterX() - source.getCenterX()) * (target.getCenterX() - source.getCenterX());
double yDiffSqu = (target.getCenterY() - source.getCenterY()) * (target.getCenterY() - source.getCenterY());
return Math.sqrt(xDiffSqu + yDiffSqu);
}, source.centerXProperty(), source.centerYProperty(), target.centerXProperty(), target.centerYProperty());
DoubleBinding sTx = pointBinding(source, target, lineLength, false, Circle::getCenterX);
DoubleBinding sTy = pointBinding(source, target, lineLength, false, Circle::getCenterY);
DoubleBinding eTx = pointBinding(source, target, lineLength, true, Circle::getCenterX);
DoubleBinding eTy = pointBinding(source, target, lineLength, true, Circle::getCenterY);
setStroke(Color.BLUE);
setStrokeWidth(2);
startXProperty().bind(sTx);
startYProperty().bind(sTy);
endXProperty().bind(eTx);
endYProperty().bind(eTy);
}
private DoubleBinding pointBinding(Circle startDot, Circle endDot, DoubleBinding lineLength, boolean isFarEnd, Function<Circle, Double> refPoint) {
return Bindings.createDoubleBinding(() -> {
double dt = isFarEnd ? lineLength.get() - endDot.getRadius() : startDot.getRadius();
double t = dt / lineLength.get();
double startPoint = refPoint.apply(startDot);
double endPoint = refPoint.apply(endDot);
double dy = ((1 - t) * startPoint) + (t * endPoint);
return dy;
}, lineLength);
}
}
Below is the complete working demo:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import java.util.function.Function;
public class DoubleBindingsDemo extends Application {
#Override
public void start(Stage stage) throws Exception {
StackPane root = new StackPane();
root.setPadding(new Insets(20));
Pane pane = new Pane();
pane.setStyle("-fx-border-width:1px;-fx-border-color:black;");
root.getChildren().add(pane);
Scene sc = new Scene(root, 600, 600);
stage.setScene(sc);
stage.show();
GraphNode greenNode = new GraphNode("green");
GraphNode redNode = new GraphNode("red");
GraphEdge edge = new GraphEdge(greenNode, redNode);
pane.getChildren().addAll(greenNode, redNode, edge);
}
class GraphNode extends Circle {
double sceneX, sceneY, centerX, centerY;
public GraphNode(String color) {
double radius = 30;
setRadius(radius);
setStyle("-fx-fill:" + color + ";-fx-stroke-width:2px;-fx-stroke:black;-fx-opacity:.5");
setCenterX(radius);
setCenterY(radius);
setOnMousePressed(e -> {
sceneX = e.getSceneX();
sceneY = e.getSceneY();
centerX = getCenterX();
centerY = getCenterY();
});
EventHandler<MouseEvent> dotOnMouseDraggedEventHandler = e -> {
// Offset of drag
double offsetX = e.getSceneX() - sceneX;
double offsetY = e.getSceneY() - sceneY;
// Taking parent bounds
Bounds parentBounds = getParent().getLayoutBounds();
double dotRadius = getRadius();
double maxCx = parentBounds.getWidth() - dotRadius;
double maxCy = parentBounds.getHeight() - dotRadius;
double cxOffset = centerX + offsetX;
double cyOffset = centerY + offsetY;
if (cxOffset < dotRadius) {
setCenterX(dotRadius);
} else if (cxOffset < maxCx) {
setCenterX(cxOffset);
} else {
setCenterX(maxCx);
}
if (cyOffset < dotRadius) {
setCenterY(dotRadius);
} else if (cyOffset < maxCy) {
setCenterY(cyOffset);
} else {
setCenterY(maxCy);
}
};
setOnMouseDragged(dotOnMouseDraggedEventHandler);
}
}
class GraphEdge extends Line {
public GraphEdge(GraphNode source, GraphNode target) {
DoubleBinding lineLength = Bindings.createDoubleBinding(() -> {
double xDiffSqu = (target.getCenterX() - source.getCenterX()) * (target.getCenterX() - source.getCenterX());
double yDiffSqu = (target.getCenterY() - source.getCenterY()) * (target.getCenterY() - source.getCenterY());
return Math.sqrt(xDiffSqu + yDiffSqu);
}, source.centerXProperty(), source.centerYProperty(), target.centerXProperty(), target.centerYProperty());
DoubleBinding sTx = pointBinding(source, target, lineLength, false, Circle::getCenterX);
DoubleBinding sTy = pointBinding(source, target, lineLength, false, Circle::getCenterY);
DoubleBinding eTx = pointBinding(source, target, lineLength, true, Circle::getCenterX);
DoubleBinding eTy = pointBinding(source, target, lineLength, true, Circle::getCenterY);
setStroke(Color.BLUE);
setStrokeWidth(2);
startXProperty().bind(sTx);
startYProperty().bind(sTy);
endXProperty().bind(eTx);
endYProperty().bind(eTy);
}
private DoubleBinding pointBinding(Circle startDot, Circle endDot, DoubleBinding lineLength, boolean isFarEnd, Function<Circle, Double> refPoint) {
return Bindings.createDoubleBinding(() -> {
double dt = isFarEnd ? lineLength.get() - endDot.getRadius() : startDot.getRadius();
double t = dt / lineLength.get();
double startPoint = refPoint.apply(startDot);
double endPoint = refPoint.apply(endDot);
double dy = ((1 - t) * startPoint) + (t * endPoint);
return dy;
}, lineLength);
}
}
}

JavaFX Border don't fit for node with custom shape

I'm trying to figure out, if it is possible to draw a border for a node with a custom shape. Currently the border doesn't fit the shape of the node.
This is what it currently looks like:
The shape is achieved by the following CSS:
.arrow-tail {
-fx-shape: "M 0 0 L 10 0 L 10 10 L 0 10 L 10 5 Z";
}
.arrow-head {
-fx-shape: "M 0 0 L 10 5 L 0 10 Z";
}
This is the important code of the arrow class where the CSS is used:
public class Arrow extends HBox {
public void Arrow(Node graphic, String title) {
getChildren().addAll(getArrowTail(), getArrowMiddlePart(graphic, title), getArrowHead());
}
private final Region getArrowTail() {
final Region arrowTail = new Region();
arrowTail.setMinWidth(10);
arrowTail.getStyleClass().add("arrow-tail");
return arrowTail;
}
private final Node getArrowMiddlePart(Node graphic, String text) {
labelTitle = new Label(text);
labelTitle.setGraphic(graphic);
labelTitle.idProperty().bind(idProperty());
final Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(labelTitle.textProperty());
Tooltip.install(labelTitle, tooltip);
final HBox arrowMiddlePart = new HBox(labelTitle);
arrowMiddlePart.minWidthProperty().bind(minWidthProperty());
arrowMiddlePart.setAlignment(Pos.CENTER);
return arrowMiddlePart;
}
private final Region getArrowHead() {
final Region arrowHead = new Region();
arrowHead.setMinWidth(10);
arrowHead.getStyleClass().add("arrow-head");
return arrowHead;
}
}
The Arrow class is a HBox, where I create a custom shaped region as arrow tail and arrow head and another HBox with an label in it as arrow middle part.
Unfortunately there doesn't seem to be a way to set the borders for the different sides of a Region with a shape applied to it independently.
I recommend extending Region directly adding a Path as first child and overriding layoutChildren resize this path.
public class Arrow extends Region {
private static final double ARROW_LENGTH = 10;
private static final Insets MARGIN = new Insets(1, ARROW_LENGTH, 1, ARROW_LENGTH);
private final HBox container;
private final HLineTo hLineTop;
private final LineTo tipTop;
private final LineTo tipBottom;
private final LineTo tailBottom;
public Arrow(Node graphic, String title) {
Path path = new Path(
new MoveTo(),
hLineTop = new HLineTo(),
tipTop = new LineTo(ARROW_LENGTH, 0),
tipBottom = new LineTo(-ARROW_LENGTH, 0),
new HLineTo(),
tailBottom = new LineTo(ARROW_LENGTH, 0),
new ClosePath());
tipTop.setAbsolute(false);
tipBottom.setAbsolute(false);
path.setManaged(false);
path.setStrokeType(StrokeType.INSIDE);
path.getStyleClass().add("arrow-shape");
Label labelTitle = new Label(title, graphic);
container = new HBox(labelTitle);
getChildren().addAll(path, container);
HBox.setHgrow(labelTitle, Priority.ALWAYS);
labelTitle.setAlignment(Pos.CENTER);
labelTitle.setMaxWidth(Double.POSITIVE_INFINITY);
}
#Override
protected void layoutChildren() {
// hbox layout
Insets insets = getInsets();
double left = insets.getLeft();
double top = insets.getTop();
double width = getWidth();
double height = getHeight();
layoutInArea(container,
left, top,
width - left - insets.getRight(), height - top - insets.getBottom(),
0, MARGIN, true, true, HPos.LEFT, VPos.TOP);
// adjust arrow shape
double length = width - ARROW_LENGTH;
double h2 = height / 2;
hLineTop.setX(length);
tipTop.setY(h2);
tipBottom.setY(h2);
tailBottom.setY(h2);
}
#Override
protected double computeMinWidth(double height) {
Insets insets = getInsets();
return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.minWidth(height);
}
#Override
protected double computeMinHeight(double width) {
Insets insets = getInsets();
return 2 + insets.getTop() + insets.getBottom() + container.minHeight(width);
}
#Override
protected double computePrefWidth(double height) {
Insets insets = getInsets();
return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.prefWidth(height);
}
#Override
protected double computePrefHeight(double width) {
Insets insets = getInsets();
return 2 + insets.getTop() + insets.getBottom() + container.prefHeight(width);
}
#Override
protected double computeMaxWidth(double height) {
Insets insets = getInsets();
return 2 * ARROW_LENGTH + insets.getLeft() + insets.getRight() + container.maxWidth(height);
}
#Override
protected double computeMaxHeight(double width) {
Insets insets = getInsets();
return 2 + insets.getTop() + insets.getBottom() + container.maxHeight(width);
}
}
CSS
.arrow-shape {
-fx-fill: dodgerblue;
-fx-stroke: black;
}
Note that the code would be simpler, if you extend HBox, but this would allow other classes access to the child list which could result in removal of the Path; extending Region allows us to keep the method protected preventing this kind of access but requires us to implement the compute... methods and the layouting of the children.

Zoom on a JScrollPane with headers

I hava a JFrame containing a table with row and column headers.
My table is a custom component made of 3 panels (row header, column header and grid).
The panels are regular JPanels, containing either JButton or JLabel, in a MigLayout.
I display this component inside a JScrollPane in order to scroll simultaneously my grid and my headers.
This part works fine.
Now, the user should be able to zoom on my component.
I tried to use the pbjar JXLayer but if I put my whole JScrollPane inside the layer, everything is zoomed, event the scrollbars.
I tried to use 3 JXLayers, one for each viewPort of my JScrollPane. But this solution just mess up with my layout as the panels inside the viewPorts get centered instead of being top-left aligned.
import org.jdesktop.jxlayer.JXLayer;
import org.pbjar.jxlayer.demo.TransformUtils;
import org.pbjar.jxlayer.plaf.ext.transform.DefaultTransformModel;
public class Matrix extends JScrollPane {
private Grid grid;
private Header rowHeader;
private Header columnHeader;
private DefaultTransformModel zoomTransformModel;
private double zoom = 1;
public Matrix() {
super(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
this.zoomTransformModel1 = new DefaultTransformModel();
this.zoomTransformModel1.setScaleToPreferredSize(true);
this.zoomTransformModel1.setScale(1);
this.zoomTransformModel2 = new DefaultTransformModel();
this.zoomTransformModel2.setScaleToPreferredSize(true);
this.zoomTransformModel2.setScale(1);
this.zoomTransformModel3 = new DefaultTransformModel();
this.zoomTransformModel3.setScaleToPreferredSize(true);
this.zoomTransformModel3.setScale(1);
this.grid = new Grid();
this.setViewportView(TransformUtils.createTransformJXLayer(this.grid,
zoomTransformModel1););
this.matrixRowHeader = new Header(Orientation.VERTICAL);
this.setRowHeader(new JViewport(
TransformUtils.createTransformJXLayer(
this.rowHeader, zoomTransformModel2)));
this.matrixColumnHeader = new Header(Orientation.HORIZONTAL);
this.setColumnHeader(new JViewport(
TransformUtils.createTransformJXLayer(
this.columnHeader, zoomTransformModel2)));
}
public void setScale(double scale) {
this.zoomTransformModel1.setScale(scale);
this.zoomTransformModel2.setScale(scale);
this.zoomTransformModel3.setScale(scale);
}
}
How could I handle the zoom on my JScrollPane without zooming on the scrollBars and without messing up my layout?
First, MigLayout seems to be incompatible with the JXLayer. When using both, the components in the panel using the MigLayout have a unpredictable behaviour.
Then, the original pbjar JXLayer only allows you to put your component in the center of the Layer pane.
Pbjar sources can be download on github. Note this is not the official Piet Blok repository.
The solution I found is to modify the TransformLayout, TransformUI, and the TranformModel classes:
Alignment enum give the possible alignment for the component in the layer.
public enum Alignment {
TOP,
BOTTOM,
LEFT,
RIGHT,
CENTER
}
In TransformLayout :
#Override
public void layoutContainer(Container parent) {
JXLayer<?> layer = (JXLayer<?>) parent;
LayerUI<?> layerUI = layer.getUI();
if (layerUI instanceof CustomTransformUI) {
JComponent view = (JComponent) layer.getView();
JComponent glassPane = layer.getGlassPane();
if (view != null) {
Rectangle innerArea = new Rectangle();
SwingUtilities.calculateInnerArea(layer, innerArea);
view.setSize(view.getPreferredSize());
Rectangle viewRect = new Rectangle(0, 0, view.getWidth(), view
.getHeight());
int x;
int y;
Alignment alignX = ((CustomTransformUI) layerUI).getAlignX();
Alignment alignY = ((CustomTransformUI) layerUI).getAlignY();
if(alignX == Alignment.LEFT) {
x = (int) (innerArea.getX() - viewRect.getX());
} else if(alignX == Alignment.RIGHT) {
x = (int) (innerArea.getX()+innerArea.getWidth()-viewRect.getWidth()-viewRect.getX());
} else {
x = (int) Math.round(innerArea.getCenterX()
- viewRect.getCenterX());
}
if(alignY == Alignment.TOP) {
y = (int) (innerArea.getY() - viewRect.getY());
} else if(alignY == Alignment.BOTTOM) {
y = (int) (innerArea.getY()+innerArea.getHeight()-viewRect.getHeight()-viewRect.getY());
} else {
y = (int) Math.round(innerArea.getCenterY()
- viewRect.getCenterY());
}
viewRect.translate(x, y);
view.setBounds(viewRect);
}
if (glassPane != null) {
glassPane.setLocation(0, 0);
glassPane.setSize(layer.getWidth(), layer.getHeight());
}
return;
}
super.layoutContainer(parent);
}
In TransformUI :
private Alignment alignX; // horizontal alignment
private Alignment alignY; // verticalalignment
public TransformUI(TransformModel model, Alignment alignX, Alignment alignY) {
super();
this.setModel(model);
this.alignX = alignX;
this.alignY = alignY;
}
public Alignment getAlignX() {
return alignX;
}
public Alignment getAlignY() {
return alignY;
}
In TransformModel:
private Alignment alignX = Alignment.CENTER;
private Alignment alignY = Alignment.CENTER;
public CustomTransformModel(Alignment alignX, Alignment alignY) {
super();
this.alignX = alignX;
this.alignY = alignY;
}
#Override
public AffineTransform getTransform(JXLayer<? extends JComponent> layer) {
JComponent view = (JComponent)layer.getView();
/*
* Set the current actual program values in addition to the user
* options.
*/
this.setValue(Type.LayerWidth, layer == null ? 0 : layer.getWidth());
this.setValue(Type.LayerHeight, layer == null ? 0 : layer.getHeight());
this.setValue(Type.ViewWidth, view == null ? 0 : view.getWidth());
this.setValue(Type.ViewHeight, view == null ? 0 : view.getHeight());
/*
* If any change to previous values, recompute the transform.
*/
if (!Arrays.equals(this.prevValues, this.values)) {
System.arraycopy(this.values, 0, this.prevValues, 0, this.values.length);
this.transform.setToIdentity();
if (view != null) {
double scaleX;
double scaleY;
double centerX;
if(this.alignX == Alignment.LEFT) {
centerX = 0.0;
} else if (this.alignX == Alignment.RIGHT){
centerX = layer == null ? 0.0 : (double)layer.getWidth();
} else {
centerX = layer == null ? 0.0 : (double)layer.getWidth() / 2.0;
}
double centerY;
if(this.alignY == Alignment.TOP) {
centerY = 0.0;
} else if(this.alignY == Alignment.BOTTOM){
centerY = layer == null ? 0.0 : (double)layer.getHeight();
} else {
centerY = layer == null ? 0.0 : (double)layer.getHeight() / 2.0;
}
AffineTransform nonScaledTransform = this.transformNoScale(centerX, centerY);
if (((Boolean)this.getValue(Type.ScaleToPreferredSize)).booleanValue()) {
scaleY = scaleX = ((Double)this.getValue(Type.PreferredScale)).doubleValue();
} else {
Area area = new Area(new Rectangle2D.Double(0.0, 0.0, view.getWidth(), view.getHeight()));
area.transform(nonScaledTransform);
Rectangle2D bounds = area.getBounds2D();
scaleX = layer == null ? 0.0 : (double)layer.getWidth() / bounds.getWidth();
scaleY = layer == null ? 0.0 : (double)layer.getHeight() / bounds.getHeight();
if (((Boolean)this.getValue(Type.PreserveAspectRatio)).booleanValue()) {
scaleY = scaleX = Math.min(scaleX, scaleY);
}
}
this.transform.translate(centerX, centerY);
this.transform.scale((Boolean)this.getValue(Type.Mirror) != false ? - scaleX : scaleX, scaleY);
this.transform.translate(- centerX, - centerY);
this.transform.concatenate(nonScaledTransform);
}
}
return this.transform;
}
You can now create a zoomable panel with configurable alignment using:
TransformModel model = new TransformModel(Alignment.LEFT, Alignment.TOP);
TransformUI ui = new TransformUI(model, Alignment.LEFT, Alignment.TOP);
new JXLayer((Component)component, (LayerUI)ui)
Note that's a quick fix. It can probably be improved.

How to determine remaining space on a pdf page with itext [duplicate]

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.

JFreeChart: how to set gradient paint for series in spider chart

I have a chart with this presentation:
But I require to do this:
How do I set correctly the gradient paint for series?. Here is what I have:
public class SpiderWebChartDemo1 extends ApplicationFrame {
public SpiderWebChartDemo1(String s) {
super(s);
JPanel jpanel = createDemoPanel();
jpanel.setPreferredSize(new Dimension(500, 270));
setContentPane(jpanel);
}
private static CategoryDataset createDataset() {
String s = "First";
String s3 = "Self leadership";
String s4 = "Organization leadership";
String s5 = "Team leadership";
DefaultCategoryDataset defaultcategorydataset = new DefaultCategoryDataset();
defaultcategorydataset.addValue(1.0D, s, s3);
defaultcategorydataset.addValue(4D, s, s4);
defaultcategorydataset.addValue(3D, s, s5);
return defaultcategorydataset;
}
private static JFreeChart createChart(CategoryDataset categorydataset) {
Color bckColor1 = Color.decode("#4282CE"); //Light blue
Color bckColor2 = Color.decode("#9BC1FF"); //Dark blue
Color axisColor = Color.decode("#DD0010"); //Red
SpiderWebPlot plot = new SpiderWebPlot(categorydataset);
Paint p = new GradientPaint(0,0,bckColor1,0,0,bckColor2);
plot.setSeriesPaint(p);
plot.setAxisLinePaint(axisColor);
JFreeChart chart = new JFreeChart("Spider Web Chart Demo 1"
, TextTitle.DEFAULT_FONT, plot, false);
LegendTitle legendtitle = new LegendTitle(plot);
legendtitle.setPosition(RectangleEdge.BOTTOM);
chart.addSubtitle(legendtitle);
return chart;
}
public static JPanel createDemoPanel() {
JFreeChart jfreechart = createChart(createDataset());
return new ChartPanel(jfreechart);
}
public static void main(String args[]) {
SpiderWebChartDemo1 spiderwebchartdemo1 = new SpiderWebChartDemo1("SpiderWebChartDemo1");
spiderwebchartdemo1.pack();
RefineryUtilities.centerFrameOnScreen(spiderwebchartdemo1);
spiderwebchartdemo1.setVisible(true);
}
}
I've seen gradient paint in bar charts, but not for spider charts. All I'm getting is transparent series.
Thanks.
You are setting the paint correctly however there are 2 things you should realize.
Gradient paints in java declare a start and end point. The first color will start at point 1 and transform into color 2 at point 2. If you use it to draw a polygon then the points are not relative to the polygons dimensions. Heres a picture to display, pt1 and pt2 in the picture are where your gradient start and end points are defined.
In an ideal world every setting is editable in a library but many times this just isnt the case. We can overcome that by overwriting methods in a subclass. You will need to override the SpiderWebPlot class and implement some of the painting methods. Heres a quick class I wrote up that does just that.
Take a look at the very end where it actually draws the polygon. I took this directly from the SpiderWebPlot source and altered the very end. To use this in your program call it like this
GradientSpiderWebPlot plot = new GradientSpiderWebPlot(categorydataset, Color.decode("#4282CE"), Color.decode("#9BC1FF"), .8f);
Here are the results
public class GradientSpiderWebPlot extends SpiderWebPlot {
private Color startColor, endColor;
private float alpha;
public GradientSpiderWebPlot(CategoryDataset data, Color startColor, Color endColor, float alpha) {
// TODO Auto-generated constructor stub
super(data);
this.startColor = startColor;
this.endColor = endColor;
this.alpha = alpha;
}
#Override
protected void drawRadarPoly(Graphics2D g2,
Rectangle2D plotArea,
Point2D centre,
PlotRenderingInfo info,
int series, int catCount,
double headH, double headW) {
Polygon polygon = new Polygon();
EntityCollection entities = null;
if (info != null) {
entities = info.getOwner().getEntityCollection();
}
// plot the data...
for (int cat = 0; cat < catCount; cat++) {
Number dataValue = getPlotValue(series, cat);
if (dataValue != null) {
double value = dataValue.doubleValue();
if (value >= 0) { // draw the polygon series...
// Finds our starting angle from the centre for this axis
double angle = getStartAngle()
+ (getDirection().getFactor() * cat * 360 / catCount);
// The following angle calc will ensure there isn't a top
// vertical axis - this may be useful if you don't want any
// given criteria to 'appear' move important than the
// others..
// + (getDirection().getFactor()
// * (cat + 0.5) * 360 / catCount);
// find the point at the appropriate distance end point
// along the axis/angle identified above and add it to the
// polygon
Point2D point = getWebPoint(plotArea, angle,
value / this.getMaxValue());
polygon.addPoint((int) point.getX(), (int) point.getY());
// put an elipse at the point being plotted..
Paint paint = getSeriesPaint(series);
Paint outlinePaint = getSeriesOutlinePaint(series);
Stroke outlineStroke = getSeriesOutlineStroke(series);
Ellipse2D head = new Ellipse2D.Double(point.getX()
- headW / 2, point.getY() - headH / 2, headW,
headH);
g2.setPaint(paint);
g2.fill(head);
g2.setStroke(outlineStroke);
g2.setPaint(outlinePaint);
g2.draw(head);
if (entities != null) {
int row = 0; int col = 0;
if (this.getDataExtractOrder() == TableOrder.BY_ROW) {
row = series;
col = cat;
}
else {
row = cat;
col = series;
}
String tip = null;
if (this.getToolTipGenerator() != null) {
tip = this.getToolTipGenerator().generateToolTip(
this.getDataset(), row, col);
}
String url = null;
if (this.getURLGenerator() != null) {
url = this.getURLGenerator().generateURL(this.getDataset(),
row, col);
}
Shape area = new Rectangle(
(int) (point.getX() - headW),
(int) (point.getY() - headH),
(int) (headW * 2), (int) (headH * 2));
CategoryItemEntity entity = new CategoryItemEntity(
area, tip, url, this.getDataset(),
this.getDataset().getRowKey(row),
this.getDataset().getColumnKey(col));
entities.add(entity);
}
}
}
}
// Plot the polygon
// Lastly, fill the web polygon if this is required
Rectangle2D rec = polygon.getBounds2D();
//Paint paint = getSeriesPaint(series);
// create linear vertical gradient based upon the bounds of the polygon.
Paint paint = new GradientPaint(new Point2D.Double(rec.getCenterX(),rec.getMinY()), startColor,
new Point2D.Double(rec.getCenterX(),rec.getMaxY()), endColor);
g2.setPaint(paint);
g2.setStroke(getSeriesOutlineStroke(series));
g2.draw(polygon);
if (this.isWebFilled()) {
// made this the variable alpha instead of the fixed .1f
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
alpha));
g2.fill(polygon);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
getForegroundAlpha()));
}
}
}

Categories

Resources