/*
* This file is part of LaTeXDraw.
* Copyright (c) 2005-2017 Arnaud BLOUIN
* LaTeXDraw is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later version.
* LaTeXDraw is distributed without any warranty; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*/
package net.sf.latexdraw.view.jfx;
import java.awt.geom.Point2D;
import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Bounds;
import javafx.scene.Group;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.ImagePattern;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Paint;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.shape.StrokeType;
import net.sf.latexdraw.models.MathUtils;
import net.sf.latexdraw.models.ShapeFactory;
import net.sf.latexdraw.models.interfaces.shape.Color;
import net.sf.latexdraw.models.interfaces.shape.FillingStyle;
import net.sf.latexdraw.models.interfaces.shape.ILine;
import net.sf.latexdraw.models.interfaces.shape.IPoint;
import net.sf.latexdraw.models.interfaces.shape.ISingleShape;
import net.sf.latexdraw.models.interfaces.shape.LineStyle;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
/**
* The base class of a JFX single shape view.
* @param <S> The type of the model.
* @param <T> The type of the JFX shape used to draw the view.
* @author Arnaud Blouin
*/
public abstract class ViewSingleShape<S extends ISingleShape, T extends Shape> extends ViewShape<S> {
protected final @NonNull T border;
protected final @Nullable T dblBorder;
protected final @Nullable T shadow;
private final @NonNull ChangeListener<?> strokesUpdateCall = (obj, oldVal, newVal) -> updateStrokes();
private final @Nullable ChangeListener<?> fillUpdateCall;
private final @Nullable ChangeListener<Boolean> shadowSetCall;
private final @Nullable ChangeListener<Number> shadowUpdateCall = (obs, oldVal, newVal) -> updateShadowPosition();
/**
* Creates the view.
* @param sh The model.
*/
ViewSingleShape(final @NonNull S sh) {
super(sh);
border = createJFXShape();
border.setStrokeLineJoin(StrokeLineJoin.MITER);
if(model.isShadowable()) {
shadow = createJFXShape();
getChildren().add(shadow);
shadowSetCall = (obs, oldVal, newVal) -> {
if(shadow != null) {
shadow.setDisable(!newVal);
if(newVal && model.isFillable() && model.shadowFillsShape()) {
border.setFill(getFillingPaint(model.getFillingStyle()));
}
}
};
model.shadowProperty().addListener(shadowSetCall);
shadow.strokeProperty().bind(Bindings.createObjectBinding(() -> model.getShadowCol().toJFX(), model.shadowColProperty()));
shadow.fillProperty().bind(Bindings.createObjectBinding(
() -> model.isFillable() && (model.isFilled() || model.shadowFillsShape()) ? model.getShadowCol().toJFX() : null, model.shadowColProperty()));
model.shadowAngleProperty().addListener(shadowUpdateCall);
model.shadowSizeProperty().addListener(shadowUpdateCall);
shadow.strokeTypeProperty().bind(border.strokeTypeProperty());
shadow.visibleProperty().bind(Bindings.createBooleanBinding(() -> !shadow.isDisable(), shadow.disableProperty()));
shadow.setDisable(!model.hasShadow());
}else {
shadow = null;
shadowSetCall = null;
}
getChildren().add(border);
if(model.isDbleBorderable()) {
dblBorder = createJFXShape();
getChildren().add(dblBorder);
dblBorder.setFill(null);
dblBorder.layoutXProperty().bind(border.layoutXProperty());
dblBorder.layoutYProperty().bind(border.layoutYProperty());
model.dbleBordProperty().addListener((ChangeListener<? super Boolean>) strokesUpdateCall);
model.dbleBordSepProperty().addListener((ChangeListener<? super Number>) strokesUpdateCall);
model.dbleBordColProperty().addListener((ChangeListener<? super Color>) strokesUpdateCall);
dblBorder.visibleProperty().bind(Bindings.createBooleanBinding(() -> !dblBorder.isDisable(), dblBorder.disableProperty()));
} else {
dblBorder = null;
}
if(model.isThicknessable()) {
model.thicknessProperty().addListener((ChangeListener<? super Number>) strokesUpdateCall);
}
if(model.isLineStylable()) {
model.linestyleProperty().addListener((ChangeListener<? super LineStyle>) strokesUpdateCall);
model.dashSepBlackProperty().addListener((ChangeListener<? super Number>) strokesUpdateCall);
model.dashSepWhiteProperty().addListener((ChangeListener<? super Number>) strokesUpdateCall);
model.dotSepProperty().addListener((ChangeListener<? super Number>) strokesUpdateCall);
}
if(model.isFillable()) {
fillUpdateCall = (obs, oldVal, newVal) -> border.setFill(getFillingPaint(model.getFillingStyle()));
model.fillingProperty().addListener((ChangeListener<? super FillingStyle>) fillUpdateCall);
model.gradColStartProperty().addListener((ChangeListener<? super Color>) fillUpdateCall);
model.gradColEndProperty().addListener((ChangeListener<? super Color>) fillUpdateCall);
model.gradMidPtProperty().addListener((ChangeListener<? super Number>) fillUpdateCall);
model.gradAngleProperty().addListener((ChangeListener<? super Number>) fillUpdateCall);
model.fillingColProperty().addListener((ChangeListener<? super Color>) fillUpdateCall);
model.hatchingsAngleProperty().addListener((ChangeListener<? super Number>) fillUpdateCall);
model.hatchingsSepProperty().addListener((ChangeListener<? super Number>) fillUpdateCall);
model.hatchingsWidthProperty().addListener((ChangeListener<? super Number>) fillUpdateCall);
model.hatchingsColProperty().addListener((ChangeListener<? super Color>) fillUpdateCall);
border.setFill(getFillingPaint(model.getFillingStyle()));
// The filling must be updated on resize and co.
border.boundsInLocalProperty().addListener((ChangeListener<? super Bounds>) fillUpdateCall);
}else {
fillUpdateCall = null;
}
border.strokeProperty().bind(Bindings.createObjectBinding(() -> model.getLineColour().toJFX(), model.lineColourProperty()));
bindBorderMovable();
updateStrokes();
updateShadowPosition();
}
protected void bindRotationAngle() {
rotateProperty().bind(Bindings.createDoubleBinding(() -> Math.toDegrees(model.getRotationAngle()), model.rotationAngleProperty()));
}
protected void updateShadowPosition() {
if(shadow != null) {
final IPoint gc = model.getGravityCentre();
final IPoint shadowgc = ShapeFactory.INST.createPoint(gc.getX() + model.getShadowSize(), gc.getY());
shadowgc.setPoint(shadowgc.rotatePoint(gc, model.getShadowAngle()));
shadow.setTranslateX(shadowgc.getX() - gc.getX());
shadow.setTranslateY(gc.getY() - shadowgc.getY());
}
}
protected abstract @NonNull T createJFXShape();
private Paint getFillingPaint(final FillingStyle style) {
switch(style) {
case NONE: return model.hasShadow() && model.shadowFillsShape() ? model.getFillingCol().toJFX() : null;
case GRAD: return computeGradient();
case PLAIN: return model.getFillingCol().toJFX();
case CLINES_PLAIN:
case HLINES_PLAIN:
case VLINES_PLAIN:
case CLINES:
case VLINES:
case HLINES: return gethatchingsFillingPaint(style);
default: return null;
}
}
private Paint gethatchingsFillingPaint(final FillingStyle style) {
final Bounds bounds = border.getBoundsInParent();
if(bounds.getWidth() > 0d && bounds.getHeight() > 0d) {
final Group hatchings = new Group();
final double hAngle = model.getHatchingsAngle();
hatchings.getChildren().add(new Rectangle(bounds.getWidth(), bounds.getHeight(), style.isFilled() ? model.getFillingCol().toJFX() : null));
if(style == FillingStyle.VLINES || style == FillingStyle.VLINES_PLAIN) {
computeHatchings(hatchings, hAngle, bounds.getWidth(), bounds.getHeight());
}else {
if(style == FillingStyle.HLINES || style == FillingStyle.HLINES_PLAIN) {
computeHatchings(hatchings, hAngle > 0d ? hAngle - Math.PI / 2d : hAngle + Math.PI / 2d, bounds.getWidth(), bounds.getHeight());
}else {
if(style == FillingStyle.CLINES || style == FillingStyle.CLINES_PLAIN) {
computeHatchings(hatchings, hAngle, bounds.getWidth(), bounds.getHeight());
computeHatchings(hatchings, hAngle > 0d ? hAngle - Math.PI / 2d : hAngle + Math.PI / 2d, bounds.getWidth(), bounds.getHeight());
}
}
}
final WritableImage image = new WritableImage((int) bounds.getWidth(), (int) bounds.getHeight());
Platform.runLater(() -> hatchings.snapshot(new SnapshotParameters(), image));
return new ImagePattern(image, 0, 0, 1, 1, true);
}
return null;
}
private void computeHatchings(final Group hatchings, final double angle, final double width, final double height) {
double angle2 = angle % (Math.PI * 2d);
final float halfPI = (float) (Math.PI / 2d);
final double val = model.getHatchingsWidth() + model.getHatchingsSep();
if(angle2 > 0d) {
if((float) angle2 > 3f * halfPI) {
angle2 -= Math.PI * 2d;
}else {
if((float) angle2 > halfPI) {
angle2 -= Math.PI;
}
}
}else {
if((float) angle2 < -3f * halfPI) {
angle2 += Math.PI * 2d;
}else {
if((float) angle2 < -halfPI) {
angle2 += Math.PI;
}
}
}
if(MathUtils.INST.equalsDouble(angle2, 0d)) {
for(double x = 0d; x < width; x += val) {
createHatchingLine(hatchings, x, 0d, x, height, width, height);
}
}else if(MathUtils.INST.equalsDouble(angle2, Math.abs(halfPI))) {
for(double y = 0d; y < height; y += val) {
createHatchingLine(hatchings, 0d, y, width, y, width, height);
}
}else {
final double incX = val / Math.cos(angle2);
final double incY = val / Math.sin(angle2);
final double limitX;
double startY;
double endX = 0d;
if(angle2 > 0) {
startY = 0d;
limitX = width + height * Math.tan(angle2);
}else {
startY = height;
limitX = width - height * Math.tan(angle2);
}
final double endY = startY;
while(endX < limitX) {
endX += incX;
startY += incY;
createHatchingLine(hatchings, 0d, startY, endX, endY, width, height);
}
}
}
private void createHatchingLine(final Group group, final double x1, final double y1, final double x2, final double y2,
final double clipWidth, final double clipHeight) {
final Line line = new Line();
line.setStrokeWidth(model.getHatchingsWidth());
line.setStrokeLineJoin(StrokeLineJoin.MITER);
line.setStrokeLineCap(StrokeLineCap.SQUARE);
line.setStroke(model.getHatchingsCol().toJFX());
line.setStartX(x1);
line.setStartY(y1);
line.setEndX(x2);
line.setEndY(y2);
// Required, otherwise the line may not be drawn.
line.setClip(new Rectangle(0, 0, clipWidth, clipHeight));
group.getChildren().add(line);
}
private LinearGradient computeGradient() {
final IPoint tl = model.getTopLeftPoint();
final IPoint br = model.getBottomRightPoint();
IPoint pt1 = ShapeFactory.INST.createPoint((tl.getX() + br.getX()) / 2d, tl.getY());
IPoint pt2 = ShapeFactory.INST.createPoint((tl.getX() + br.getX()) / 2d, br.getY());
double angle = model.getGradAngle() % (2d * Math.PI);
double gradMidPt = model.getGradMidPt();
if(angle < 0d) angle = 2d * Math.PI + angle;
if(angle >= Math.PI) {
gradMidPt = 1d - gradMidPt;
angle -= Math.PI;
}
if(MathUtils.INST.equalsDouble(angle, 0d)) {
if(gradMidPt < 0.5) pt1.setY(pt2.getY() - Point2D.distance(pt2.getX(), pt2.getY(), (tl.getX() + br.getX()) / 2d, br.getY()));
pt2.setY(tl.getY() + (br.getY() - tl.getY()) * gradMidPt);
}else {
if(MathUtils.INST.equalsDouble(angle % (Math.PI / 2d), 0d)) {
pt1 = ShapeFactory.INST.createPoint(tl.getX(), (tl.getY() + br.getY()) / 2d);
pt2 = ShapeFactory.INST.createPoint(br.getX(), (tl.getY() + br.getY()) / 2d);
if(gradMidPt < 0.5)
pt1.setX(pt2.getX() - Point2D.distance(pt2.getX(), pt2.getY(), br.getX(), (tl.getY() + br.getY()) / 2d));
pt2.setX(tl.getX() + (br.getX() - tl.getX()) * gradMidPt);
}else {
final IPoint cg = model.getGravityCentre();
final ILine l2;
final ILine l;
pt1 = pt1.rotatePoint(cg, -angle);
pt2 = pt2.rotatePoint(cg, -angle);
l = ShapeFactory.INST.createLine(pt1, pt2);
if(angle >= 0d && angle < Math.PI / 2d) l2 = l.getPerpendicularLine(tl);
else l2 = l.getPerpendicularLine(ShapeFactory.INST.createPoint(tl.getX(), br.getY()));
pt1 = l.getIntersection(l2);
final double distance = Point2D.distance(cg.getX(), cg.getY(), pt1.getX(), pt1.getY());
l.setX1(pt1.getX());
l.setY1(pt1.getY());
final IPoint[] pts = l.findPoints(pt1, 2d * distance * gradMidPt);
pt2 = pts[0];
if(gradMidPt < 0.5) pt1 = pt1.rotatePoint(model.getGravityCentre(), Math.PI);
}
}
return new LinearGradient(pt1.getX(), pt1.getY(), pt2.getX(), pt2.getY(), false, CycleMethod.NO_CYCLE,
new Stop(0d, model.getGradColStart().toJFX()), new Stop(1d, model.getGradColEnd().toJFX()));
}
private void bindBorderMovable() {
if(model.isBordersMovable()) {
border.strokeTypeProperty().bind(Bindings.createObjectBinding(() -> {
switch(model.getBordersPosition()) {
case INTO: return StrokeType.INSIDE;
case MID: return StrokeType.CENTERED;
case OUT: return StrokeType.OUTSIDE;
default: return StrokeType.INSIDE;
}
}, model.borderPosProperty()));
if(dblBorder != null) {
dblBorder.strokeTypeProperty().bind(border.strokeTypeProperty());
}
}
}
protected double getDbleBorderGap() {
if(!model.isDbleBorderable()) {
return 0d;
}
switch(model.getBordersPosition()) {
case MID: return 0d;
case INTO: return model.getThickness();
case OUT: return -model.getThickness();
}
return 0d;
}
private void updateStrokes() {
if(model.isThicknessable()) {
border.setStrokeWidth(model.getFullThickness());
if(shadow != null) {
shadow.setStrokeWidth(model.getFullThickness());
}
}
if(dblBorder != null) {
dblBorder.setDisable(!model.hasDbleBord());
if(model.hasDbleBord()) {
dblBorder.setStroke(model.getDbleBordCol().toJFX());
dblBorder.setStrokeWidth(model.getDbleBordSep());
}
}
if(model.isLineStylable()) {
switch(model.getLineStyle()) {
case DASHED:
border.setStrokeLineCap(StrokeLineCap.BUTT);
border.getStrokeDashArray().clear();
border.getStrokeDashArray().addAll(model.getDashSepBlack(), model.getDashSepWhite());
break;
case DOTTED:// FIXME problem when dotted line + INTO/OUT border position.
border.setStrokeLineCap(StrokeLineCap.ROUND);
border.getStrokeDashArray().clear();
border.getStrokeDashArray().addAll(0d, model.getDotSep() + model.getFullThickness());
break;
case SOLID:
border.setStrokeLineCap(StrokeLineCap.SQUARE);
border.getStrokeDashArray().clear();
break;
}
}
}
public T getBorder() {
return border;
}
public Optional<T> getDbleBorder() {
return Optional.ofNullable(dblBorder);
}
public Optional<T> getShadow() {
return Optional.ofNullable(shadow);
}
@Override
public void flush() {
super.flush();
if(model.isThicknessable()) {
model.thicknessProperty().removeListener((ChangeListener<? super Number>) strokesUpdateCall);
}
if(model.isLineStylable()) {
model.linestyleProperty().removeListener((ChangeListener<? super LineStyle>) strokesUpdateCall);
model.dashSepBlackProperty().removeListener((ChangeListener<? super Number>) strokesUpdateCall);
model.dashSepWhiteProperty().removeListener((ChangeListener<? super Number>) strokesUpdateCall);
model.dotSepProperty().removeListener((ChangeListener<? super Number>) strokesUpdateCall);
}
if(dblBorder != null) {
dblBorder.layoutXProperty().unbind();
dblBorder.layoutYProperty().unbind();
model.dbleBordProperty().removeListener((ChangeListener<? super Boolean>) strokesUpdateCall);
model.dbleBordSepProperty().removeListener((ChangeListener<? super Number>) strokesUpdateCall);
model.dbleBordColProperty().removeListener((ChangeListener<? super Color>) strokesUpdateCall);
dblBorder.strokeTypeProperty().unbind();
dblBorder.visibleProperty().unbind();
}
if(fillUpdateCall != null) {
model.fillingProperty().removeListener((ChangeListener<? super FillingStyle>) fillUpdateCall);
model.gradColStartProperty().removeListener((ChangeListener<? super Color>) fillUpdateCall);
model.gradColEndProperty().removeListener((ChangeListener<? super Color>) fillUpdateCall);
model.gradMidPtProperty().removeListener((ChangeListener<? super Number>) fillUpdateCall);
model.gradAngleProperty().removeListener((ChangeListener<? super Number>) fillUpdateCall);
model.gradColEndProperty().removeListener((ChangeListener<? super Color>) fillUpdateCall);
model.fillingColProperty().removeListener((ChangeListener<? super Color>) fillUpdateCall);
model.hatchingsAngleProperty().removeListener((ChangeListener<? super Number>) fillUpdateCall);
model.hatchingsSepProperty().removeListener((ChangeListener<? super Number>) fillUpdateCall);
model.hatchingsWidthProperty().removeListener((ChangeListener<? super Number>) fillUpdateCall);
model.hatchingsColProperty().removeListener((ChangeListener<? super Color>) fillUpdateCall);
border.boundsInLocalProperty().removeListener((ChangeListener<? super Bounds>) fillUpdateCall);
}
if(shadowSetCall != null) {
model.shadowProperty().removeListener(shadowSetCall);
shadow.strokeProperty().unbind();
shadow.fillProperty().unbind();
model.shadowAngleProperty().removeListener(shadowUpdateCall);
model.shadowSizeProperty().removeListener(shadowUpdateCall);
shadow.strokeTypeProperty().unbind();
shadow.visibleProperty().unbind();
}
border.strokeProperty().unbind();
if(model.isBordersMovable()) {
border.strokeTypeProperty().unbind();
}
}
}