I’m writting an JavaFX application, where i use UTM32 coordinates for my geometries, i have a dynamic stroke width dependent on my zoom level. The issue is that i can’t trigger events when my stroke width is decreased at a certain factor. If you use Intellij you can start a new fx-project and copy following code to reproduce this behaviour:
hello-view.fxml
<?xml version="1.0" encoding="UTF-8"?> <?import javafx.geometry.Insets?> <?import javafx.scene.layout.Pane?> <?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.VBox?> <VBox alignment="CENTER" prefHeight="248.0" prefWidth="335.0" spacing="20.0" xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demo.HelloController"> <padding> <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/> </padding> <Pane fx:id="mapPane" prefHeight="200.0" prefWidth="200.0"> <children> <StackPane fx:id="stackPane" prefHeight="150.0" prefWidth="200.0"> </StackPane> </children> </Pane> </VBox>
HelloApplication.java
package com.example.demo; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; import java.io.IOException; public class HelloApplication extends Application { @Override public void start(Stage stage) throws IOException { FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml")); Scene scene = new Scene(fxmlLoader.load(), 320, 240); stage.setTitle("Hello!"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(); } }
HelloController.java
package com.example.demo; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Line; import javafx.scene.shape.Shape; import java.net.URL; import java.util.Random; import java.util.ResourceBundle; public class HelloController implements Initializable { private final Group mapGroup = new Group(); @FXML private StackPane stackPane = new StackPane(); @FXML public Pane mapPane; double mouseAnchorX, mouseAnchorY, translateAnchorX, translateAnchorY; @Override public void initialize(URL location, ResourceBundle resources) { addLines(); stackPane.getChildren().add(mapGroup); mapPane.setOnScroll(onMouseScrolledEventHandler()); mapPane.setOnMousePressed(onMousePressedEventHandler()); } private void addLines() { for (int i = 0; i < 10; ++i) { Line line = new Line(getRandomDouble(5000000, 5000002), getRandomDouble(5000000, 5000002), getRandomDouble(5000000, 5000002), getRandomDouble(5000000, 5000002)); line.addEventHandler(MouseEvent.MOUSE_PRESSED, onShapeSelected()); (line).setSmooth(true); mapGroup.getChildren().add(line); } } private double getRandomDouble(double rangeMin, double rangeMax) { Random rand = new Random(); return rangeMin + (rangeMax - rangeMin) * rand.nextDouble(); } private EventHandler<MouseEvent> onShapeSelected() { return mouseEvent -> { if (!mouseEvent.isPrimaryButtonDown()) { return; } System.out.println("Event triggered"); ((Shape) mouseEvent.getSource()).setStroke(Color.RED); }; } public void decreaseStrokeWidth(double val) { for (Node shape : mapGroup.getChildren()) { ((Shape) shape).setStrokeWidth(val); } } private EventHandler<MouseEvent> onMousePressedEventHandler() { return event -> { if (event.isPrimaryButtonDown()) { if (!event.getPickResult().getIntersectedNode().getParent().equals(mapGroup)) { for (Node shape : mapGroup.getChildren()) { ((Shape) shape).setStroke(Color.BLACK); } } } if (event.isSecondaryButtonDown()) { mouseAnchorX = event.getSceneX(); mouseAnchorY = event.getSceneY(); translateAnchorX = mapGroup.getTranslateX(); translateAnchorY = mapGroup.getTranslateY(); } }; } private EventHandler<ScrollEvent> onMouseScrolledEventHandler() { return event -> { double scale = mapGroup.getScaleY(); double oldScale = scale; scale = event.getDeltaY() < 0 ? (scale / 1.1) : (scale * 1.1); double dx = (event.getSceneX() - (mapGroup.getBoundsInParent().getWidth() / 2 + mapGroup.getBoundsInParent().getMinX())); double dy = (event.getSceneY() - (mapGroup.getBoundsInParent().getHeight() / 2 + mapGroup.getBoundsInParent().getMinY())); double f = (scale / oldScale) - 1; mapGroup.setScaleX(scale); mapGroup.setScaleY(scale); mapGroup.setTranslateX(mapGroup.getTranslateX() - f * dx); mapGroup.setTranslateY(mapGroup.getTranslateY() - f * dy); decreaseStrokeWidth(Math.max(1 / scale, 0.2)); // Change this parameter event.consume(); }; } }
You can change the coordinate range of the lines and the strokewidth. Now can anyone explain this beaviour to me and can give me some suggestions how to deal with this issue. I really wan’t to find a workaround for this, but my guess is that it has something to do with internal interpolation.
Advertisement
Answer
That’s why I often don’t use event handlers directly on geometries. Instead I add a single event handler to the pane and when a event happens I iterate over my geometries manually. Especially for very thin lines, which are always difficult to hit exactly with the mouse, I test whether the geometry intersects a little rectangle defined around the event location. That way I could even select invisible lines without problem 🙂 This procedure is also helpful for more complex selection schemes, e.g., if you want to have a list of all possible selection candidates etc.