Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX How do I scale the coordinates of a Path without altering the line width?

I am currently working on a step line chart with a zoom and panning functionality. Since the amount of data I need to process is pretty large, i can not afford to recreate the whole path of the step line, every time layoutPlotChildren() is called. So my idea was to create the path element once and just transform it on zoom an pan events.

So far everything worked out as planned, but i am having a big problem when I'm trying to scale the path. Since the Path class is inherited from Shape, scaling on the Graph not only transforms the coordinates of the path, but also alters its thickness.

I created a minimal working example to present the problem:

public class Sandbox extends Application
{

    @Override public void start(Stage primaryStage)
    {
        Path path = new Path();
        path.getElements().add(new MoveTo(0, 0));
        path.getElements().add(new LineTo(100, 0));
        path.getElements().add(new LineTo(100, 100));
        path.getElements().add(new LineTo(200, 100));

        path.setScaleY(3);

        Pane pane = new StackPane(path);

        Scene scene = new Scene(pane, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

As you can see, the coordinates are scaled as intended, but all horizontal lines are three times as thick as before. I completely understand, why this is happening, but I can't think of any possibilities to prevent the thickening of the lines. Do you have any suggestions on how I could solve this problem while still assuring acceptable performance?

like image 592
Markus K Avatar asked Jan 29 '18 15:01

Markus K


2 Answers

Note that this is not a problem of the Shape class, but a problem of what scaling actually does: A in Node scaled with a factor x every distance is rendered x times as long as in the unscaled Node. This includes the thickness of any lines.

You could however modify the coordinates of your path elements by multiplying the coordinates with the scale factor however:

// class for holing a pair of (x, y) properties with method for modifying the values based on scale
public static class PointScaler {

    private final DoubleProperty xProperty;
    private final DoubleProperty yProperty;
    private final double x0;
    private final double y0;

    public PointScaler(DoubleProperty xProperty, DoubleProperty yProperty) {
        this.xProperty = xProperty;
        this.yProperty = yProperty;

        // save untransformed values
        this.x0 = xProperty.get();
        this.y0 = yProperty.get();
    }

    public void setScale(double factor) {
        xProperty.set(factor * x0);
        yProperty.set(factor * y0);
    }

}

private double scaleFactor = 1;

@Override
public void start(Stage primaryStage) {
    Path path = new Path();
    MoveTo m0 = new MoveTo(0, 0);
    LineTo l0 = new LineTo(100, 0);
    LineTo l1 = new LineTo(100, 100);
    LineTo l2 = new LineTo(200, 100);
    path.getElements().addAll(m0, l0, l1, l2);

    // create list with scaler for each (x,y) pair in the path elements
    List<PointScaler> scalers = new ArrayList<>();
    scalers.add(new PointScaler(m0.xProperty(), m0.yProperty()));
    scalers.add(new PointScaler(l0.xProperty(), l0.yProperty()));
    scalers.add(new PointScaler(l1.xProperty(), l1.yProperty()));
    scalers.add(new PointScaler(l2.xProperty(), l2.yProperty()));

    Pane pane = new StackPane(path);

    Scene scene = new Scene(pane, 800, 600);

    // change scale on click of mouse buttons
    scene.setOnMouseClicked(evt -> {
        if (evt.getButton() == MouseButton.PRIMARY) {
            scaleFactor *= 1.2;
        } else if (evt.getButton() == MouseButton.SECONDARY) {
            scaleFactor /= 1.2;
        } else {
            return;
        }
        // apply scale to path points
        for (PointScaler scaler : scalers) {
            scaler.setScale(scaleFactor);
        }
    });

    primaryStage.setScene(scene);
    primaryStage.show();
}

Note that the setScale method keeps x and y scale the same but it shouldn't be too hard modify the method to work for different transforms. Note that some PathElements like CubicCurveTo and QuadCurveTo require more than a single PointScaler instance.

like image 53
fabian Avatar answered Sep 30 '22 01:09

fabian


Based on fabians great response, I created my own solution, which is a simplified version for step lines (my initial problem), but not nearly as flexible as the aforementioned response. I will post my answer here for completeness, but I still think fabians answers has to be the accepted one.

public class Sandbox extends Application
{

    private DoubleProperty scaleX = new SimpleDoubleProperty(1d);
    private DoubleProperty scaleY = new SimpleDoubleProperty(1d);

    @Override public void start(Stage primaryStage)
    {
        Path path = new Path();
        path.getElements().add(new MoveTo(0, 0));

        path.getElements().add(createLineTo(100, 0));
        path.getElements().add(createLineTo(100, 100));
        path.getElements().add(createLineTo(200, 100));

        scaleY.setValue(3);

        Pane pane = new StackPane(path);
        Scene scene = new Scene(pane, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private LineTo createLineTo(double x, double y)
    {
        LineTo lineTo = new LineTo();
        lineTo.xProperty().bind(scaleX.multiply(x));
        lineTo.yProperty().bind(scaleY.multiply(y));
        return lineTo;
    }
}

This mainly prevents the foreach loop and the need to create a separate class, but as mentioned before, this is not as versatile as fabians answer.

like image 36
Markus K Avatar answered Sep 30 '22 01:09

Markus K