ScalaFX/JavaFX 8 获取最近的节点

ScalaFX/JavaFX 8 get nearest Nodes

目前正在尝试使用 ScalaFX。

想象一下:

我有一些节点,它们由一些边连接。

现在,当我单击鼠标按钮时,我想 select 单击鼠标旁边的按钮,例如如果我在 1 和 2 之间点击,我希望这两个被 selected,如果我在 0 之前点击,只有那个(因为它是第一个)等等

目前(并且只是作为概念证明)我通过添加一些辅助结构来做到这一点。我有一个 [Index, Node] 和 select 类型的 HashMap,它们是这样的:

wrapper.onMouseClicked = (mouseEvent: MouseEvent) =>
{
    val lowerIndex: Int = (mouseEvent.sceneX).toString.charAt(0).asDigit
    val left = nodes.get(lowerIndex)
    val right = nodes.get(lowerIndex+1)

    left.get.look.setStyle("-fx-background-color: orange;")
    right.get.look.setStyle("-fx-background-color: orange;")
}

这只是,但我需要一个额外的数据结构 它在 2D 中会变得非常乏味,比如当我也有 Y 坐标时。

我更喜欢

中提到的一些方法

How to detect Node at specific point in JavaFX?

JavaFX 2.2 get node at coordinates (visual tree hit testing)

这些问题基于旧版本的 JavaFX 并使用已弃用的方法。

到目前为止,我在 ScalaFX 8 中找不到任何替代品或解决方案。有什么好的方法可以让所有节点都在一定半径内吗?

所以“Nearest neighbor search”是您要解决的一般问题。

你的问题描述不够详细。例如,节点之间的距离是否相等?节点是以网格模式排列还是随机排列?节点距离是基于节点中心的点、周围的框、任意形状节点上的实际最近点建模的吗?等等

我假设随机放置可能重叠的形状,并且选择不是基于绘画顺序,而是基于形状边界框的最近角。更准确的选择器可能会通过将单击点与实际形状周围的椭圆区域而不是形状边界框进行比较来工作(因为当前选择器对于重叠对角线之类的东西使用起来有点挑剔)。

可以使用 k-d tree algorithm or an R-tree,但一般来说,线性暴力搜索可能对大多数应用程序都适用。

暴力求解算法示例

private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
    Point2D pClick = new Point2D(x, y);
    Node nearestNode = null;
    double closestDistance = Double.POSITIVE_INFINITY;

    for (Node node : nodes) {
        Bounds bounds = node.getBoundsInParent();
        Point2D[] corners = new Point2D[] {
                new Point2D(bounds.getMinX(), bounds.getMinY()),
                new Point2D(bounds.getMaxX(), bounds.getMinY()),
                new Point2D(bounds.getMaxX(), bounds.getMaxY()),
                new Point2D(bounds.getMinX(), bounds.getMaxY()),
        };

        for (Point2D pCompare: corners) {
            double nextDist = pClick.distance(pCompare);
            if (nextDist < closestDistance) {
                closestDistance = nextDist;
                nearestNode = node;
            }
        }
    }

    return nearestNode;
}

可执行解决方案

import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.*;
import javafx.scene.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;

import java.io.IOException;
import java.net.*;
import java.util.Random;

public class FindNearest extends Application {
    private static final int N_SHAPES = 10;
    private static final double W = 600, H = 400;

    private ShapeMachine machine;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() throws MalformedURLException, URISyntaxException {
        double maxShapeSize = W / 8;
        double minShapeSize = maxShapeSize / 2;
        machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
    }

    @Override
    public void start(final Stage stage) throws IOException, URISyntaxException {
        Pane pane = new Pane();
        pane.setPrefSize(W, H);
        for (int i = 0; i < N_SHAPES; i++) {
            pane.getChildren().add(machine.randomShape());
        }

        pane.setOnMouseClicked(event -> {
            Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
            highlightSelected(node, pane.getChildren());
        });

        Scene scene = new Scene(pane);
        configureExitOnAnyKey(stage, scene);

        stage.setScene(scene);
        stage.setResizable(false);
        stage.show();
    }

    private void highlightSelected(Node selected, ObservableList<Node> children) {
        for (Node node: children) {
           node.setEffect(null);
        }

        if (selected != null) {
            selected.setEffect(new DropShadow(10, Color.YELLOW));
        }
    }

    private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
        Point2D pClick = new Point2D(x, y);
        Node nearestNode = null;
        double closestDistance = Double.POSITIVE_INFINITY;

        for (Node node : nodes) {
            Bounds bounds = node.getBoundsInParent();
            Point2D[] corners = new Point2D[] {
                    new Point2D(bounds.getMinX(), bounds.getMinY()),
                    new Point2D(bounds.getMaxX(), bounds.getMinY()),
                    new Point2D(bounds.getMaxX(), bounds.getMaxY()),
                    new Point2D(bounds.getMinX(), bounds.getMaxY()),
            };

            for (Point2D pCompare: corners) {
                double nextDist = pClick.distance(pCompare);
                if (nextDist < closestDistance) {
                    closestDistance = nextDist;
                    nearestNode = node;
                }
            }
        }

        return nearestNode;
    }

    private void configureExitOnAnyKey(final Stage stage, Scene scene) {
        scene.setOnKeyPressed(keyEvent -> stage.hide());
    }
}

辅助随机形状生成class

这个class不是解决方案的关键,它只是生成一些用于测试的形状。

class ShapeMachine {

    private static final Random random = new Random();
    private final double canvasWidth, canvasHeight, maxShapeSize, minShapeSize;

    ShapeMachine(double canvasWidth, double canvasHeight, double maxShapeSize, double minShapeSize) {
        this.canvasWidth = canvasWidth;
        this.canvasHeight = canvasHeight;
        this.maxShapeSize = maxShapeSize;
        this.minShapeSize = minShapeSize;
    }

    private Color randomColor() {
        return Color.rgb(random.nextInt(256), random.nextInt(256), random.nextInt(256), 0.1 + random.nextDouble() * 0.9);
    }

    enum Shapes {Circle, Rectangle, Line}

    public Shape randomShape() {
        Shape shape = null;

        switch (Shapes.values()[random.nextInt(Shapes.values().length)]) {
            case Circle:
                shape = randomCircle();
                break;
            case Rectangle:
                shape = randomRectangle();
                break;
            case Line:
                shape = randomLine();
                break;
            default:
                System.out.println("Unknown Shape");
                System.exit(1);
        }

        Color fill = randomColor();
        shape.setFill(fill);
        shape.setStroke(deriveStroke(fill));
        shape.setStrokeWidth(deriveStrokeWidth(shape));
        shape.setStrokeLineCap(StrokeLineCap.ROUND);
        shape.relocate(randomShapeX(), randomShapeY());

        return shape;
    }

    private double deriveStrokeWidth(Shape shape) {
        return Math.max(shape.getLayoutBounds().getWidth() / 10, shape.getLayoutBounds().getHeight() / 10);
    }

    private Color deriveStroke(Color fill) {
        return fill.desaturate();
    }

    private double randomShapeSize() {
        double range = maxShapeSize - minShapeSize;
        return random.nextDouble() * range + minShapeSize;
    }

    private double randomShapeX() {
        return random.nextDouble() * (canvasWidth + maxShapeSize) - maxShapeSize / 2;
    }

    private double randomShapeY() {
        return random.nextDouble() * (canvasHeight + maxShapeSize) - maxShapeSize / 2;
    }

    private Shape randomLine() {
        int xZero = random.nextBoolean() ? 1 : 0;
        int yZero = random.nextBoolean() || xZero == 0 ? 1 : 0;

        int xSign = random.nextBoolean() ? 1 : -1;
        int ySign = random.nextBoolean() ? 1 : -1;

        return new Line(0, 0, xZero * xSign * randomShapeSize(), yZero * ySign * randomShapeSize());
    }

    private Shape randomRectangle() {
        return new Rectangle(0, 0, randomShapeSize(), randomShapeSize());
    }

    private Shape randomCircle() {
        double radius = randomShapeSize() / 2;
        return new Circle(radius, radius, radius);
    }

}

在 zoomable/scrollable 区域中放置对象的进一步示例

此解决方案使用上面最近的节点解决方案代码,并将其与来自 JavaFX correct scaling. The purpose is to demonstrate that the choosing algorithm works even on nodes which have had a scaling transform applied to them (because it is based upon boundsInParent) 的 ScrollPane 代码中的缩放节点组合。该代码只是作为概念证明,而不是作为如何将功能构建到 class 域模型中的风格示例:-)

import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableList;
import javafx.event.*;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;

import java.net.MalformedURLException;
import java.net.URISyntaxException;

public class GraphicsScalingApp extends Application {
    private static final int N_SHAPES = 10;
    private static final double W = 600, H = 400;

    private ShapeMachine machine;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() throws MalformedURLException, URISyntaxException {
        double maxShapeSize = W / 8;
        double minShapeSize = maxShapeSize / 2;
        machine = new ShapeMachine(W, H, maxShapeSize, minShapeSize);
    }

    @Override
    public void start(final Stage stage) {
        Pane pane = new Pane();
        pane.setPrefSize(W, H);
        for (int i = 0; i < N_SHAPES; i++) {
            pane.getChildren().add(machine.randomShape());
        }

        pane.setOnMouseClicked(event -> {
            Node node = findNearestNode(pane.getChildren(), event.getX(), event.getY());
            System.out.println("Found: " + node + " at " + event.getX() + "," + event.getY());
            highlightSelected(node, pane.getChildren());
        });

        final Group group = new Group(
                pane
        );

        Parent zoomPane = createZoomPane(group);

        VBox layout = new VBox();
        layout.getChildren().setAll(createMenuBar(stage, group), zoomPane);

        VBox.setVgrow(zoomPane, Priority.ALWAYS);

        Scene scene = new Scene(layout);

        stage.setTitle("Zoomy");
        stage.getIcons().setAll(new Image(APP_ICON));
        stage.setScene(scene);
        stage.show();
    }

    private Parent createZoomPane(final Group group) {
        final double SCALE_DELTA = 1.1;
        final StackPane zoomPane = new StackPane();

        zoomPane.getChildren().add(group);

        final ScrollPane scroller = new ScrollPane();
        final Group scrollContent = new Group(zoomPane);
        scroller.setContent(scrollContent);

        scroller.viewportBoundsProperty().addListener(new ChangeListener<Bounds>() {
            @Override
            public void changed(ObservableValue<? extends Bounds> observable,
                                Bounds oldValue, Bounds newValue) {
                zoomPane.setMinSize(newValue.getWidth(), newValue.getHeight());
            }
        });

        scroller.setPrefViewportWidth(256);
        scroller.setPrefViewportHeight(256);

        zoomPane.setOnScroll(new EventHandler<ScrollEvent>() {
            @Override
            public void handle(ScrollEvent event) {
                event.consume();

                if (event.getDeltaY() == 0) {
                    return;
                }

                double scaleFactor = (event.getDeltaY() > 0) ? SCALE_DELTA
                        : 1 / SCALE_DELTA;

                // amount of scrolling in each direction in scrollContent coordinate
                // units
                Point2D scrollOffset = figureScrollOffset(scrollContent, scroller);

                group.setScaleX(group.getScaleX() * scaleFactor);
                group.setScaleY(group.getScaleY() * scaleFactor);

                // move viewport so that old center remains in the center after the
                // scaling
                repositionScroller(scrollContent, scroller, scaleFactor, scrollOffset);

            }
        });

        // Panning via drag....
        final ObjectProperty<Point2D> lastMouseCoordinates = new SimpleObjectProperty<Point2D>();
        scrollContent.setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                lastMouseCoordinates.set(new Point2D(event.getX(), event.getY()));
            }
        });

        scrollContent.setOnMouseDragged(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {
                double deltaX = event.getX() - lastMouseCoordinates.get().getX();
                double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
                double deltaH = deltaX * (scroller.getHmax() - scroller.getHmin()) / extraWidth;
                double desiredH = scroller.getHvalue() - deltaH;
                scroller.setHvalue(Math.max(0, Math.min(scroller.getHmax(), desiredH)));

                double deltaY = event.getY() - lastMouseCoordinates.get().getY();
                double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
                double deltaV = deltaY * (scroller.getHmax() - scroller.getHmin()) / extraHeight;
                double desiredV = scroller.getVvalue() - deltaV;
                scroller.setVvalue(Math.max(0, Math.min(scroller.getVmax(), desiredV)));
            }
        });

        return scroller;
    }

    private Point2D figureScrollOffset(Node scrollContent, ScrollPane scroller) {
        double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
        double hScrollProportion = (scroller.getHvalue() - scroller.getHmin()) / (scroller.getHmax() - scroller.getHmin());
        double scrollXOffset = hScrollProportion * Math.max(0, extraWidth);
        double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
        double vScrollProportion = (scroller.getVvalue() - scroller.getVmin()) / (scroller.getVmax() - scroller.getVmin());
        double scrollYOffset = vScrollProportion * Math.max(0, extraHeight);
        return new Point2D(scrollXOffset, scrollYOffset);
    }

    private void repositionScroller(Node scrollContent, ScrollPane scroller, double scaleFactor, Point2D scrollOffset) {
        double scrollXOffset = scrollOffset.getX();
        double scrollYOffset = scrollOffset.getY();
        double extraWidth = scrollContent.getLayoutBounds().getWidth() - scroller.getViewportBounds().getWidth();
        if (extraWidth > 0) {
            double halfWidth = scroller.getViewportBounds().getWidth() / 2;
            double newScrollXOffset = (scaleFactor - 1) * halfWidth + scaleFactor * scrollXOffset;
            scroller.setHvalue(scroller.getHmin() + newScrollXOffset * (scroller.getHmax() - scroller.getHmin()) / extraWidth);
        } else {
            scroller.setHvalue(scroller.getHmin());
        }
        double extraHeight = scrollContent.getLayoutBounds().getHeight() - scroller.getViewportBounds().getHeight();
        if (extraHeight > 0) {
            double halfHeight = scroller.getViewportBounds().getHeight() / 2;
            double newScrollYOffset = (scaleFactor - 1) * halfHeight + scaleFactor * scrollYOffset;
            scroller.setVvalue(scroller.getVmin() + newScrollYOffset * (scroller.getVmax() - scroller.getVmin()) / extraHeight);
        } else {
            scroller.setHvalue(scroller.getHmin());
        }
    }

    private SVGPath createCurve() {
        SVGPath ellipticalArc = new SVGPath();
        ellipticalArc.setContent("M10,150 A15 15 180 0 1 70 140 A15 25 180 0 0 130 130 A15 55 180 0 1 190 120");
        ellipticalArc.setStroke(Color.LIGHTGREEN);
        ellipticalArc.setStrokeWidth(4);
        ellipticalArc.setFill(null);
        return ellipticalArc;
    }

    private SVGPath createStar() {
        SVGPath star = new SVGPath();
        star.setContent("M100,10 L100,10 40,180 190,60 10,60 160,180 z");
        star.setStrokeLineJoin(StrokeLineJoin.ROUND);
        star.setStroke(Color.BLUE);
        star.setFill(Color.DARKBLUE);
        star.setStrokeWidth(4);
        return star;
    }

    private MenuBar createMenuBar(final Stage stage, final Group group) {
        Menu fileMenu = new Menu("_File");
        MenuItem exitMenuItem = new MenuItem("E_xit");
        exitMenuItem.setGraphic(new ImageView(new Image(CLOSE_ICON)));
        exitMenuItem.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                stage.close();
            }
        });
        fileMenu.getItems().setAll(exitMenuItem);
        Menu zoomMenu = new Menu("_Zoom");
        MenuItem zoomResetMenuItem = new MenuItem("Zoom _Reset");
        zoomResetMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.ESCAPE));
        zoomResetMenuItem.setGraphic(new ImageView(new Image(ZOOM_RESET_ICON)));
        zoomResetMenuItem.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                group.setScaleX(1);
                group.setScaleY(1);
            }
        });
        MenuItem zoomInMenuItem = new MenuItem("Zoom _In");
        zoomInMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.I));
        zoomInMenuItem.setGraphic(new ImageView(new Image(ZOOM_IN_ICON)));
        zoomInMenuItem.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                group.setScaleX(group.getScaleX() * 1.5);
                group.setScaleY(group.getScaleY() * 1.5);
            }
        });
        MenuItem zoomOutMenuItem = new MenuItem("Zoom _Out");
        zoomOutMenuItem.setAccelerator(new KeyCodeCombination(KeyCode.O));
        zoomOutMenuItem.setGraphic(new ImageView(new Image(ZOOM_OUT_ICON)));
        zoomOutMenuItem.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                group.setScaleX(group.getScaleX() * 1 / 1.5);
                group.setScaleY(group.getScaleY() * 1 / 1.5);
            }
        });
        zoomMenu.getItems().setAll(zoomResetMenuItem, zoomInMenuItem,
                zoomOutMenuItem);
        MenuBar menuBar = new MenuBar();
        menuBar.getMenus().setAll(fileMenu, zoomMenu);
        return menuBar;
    }


    private void highlightSelected(Node selected, ObservableList<Node> children) {
        for (Node node : children) {
            node.setEffect(null);
        }

        if (selected != null) {
            selected.setEffect(new DropShadow(10, Color.YELLOW));
        }
    }

    private Node findNearestNode(ObservableList<Node> nodes, double x, double y) {
        Point2D pClick = new Point2D(x, y);
        Node nearestNode = null;
        double closestDistance = Double.POSITIVE_INFINITY;

        for (Node node : nodes) {
            Bounds bounds = node.getBoundsInParent();
            Point2D[] corners = new Point2D[]{
                    new Point2D(bounds.getMinX(), bounds.getMinY()),
                    new Point2D(bounds.getMaxX(), bounds.getMinY()),
                    new Point2D(bounds.getMaxX(), bounds.getMaxY()),
                    new Point2D(bounds.getMinX(), bounds.getMaxY()),
            };

            for (Point2D pCompare : corners) {
                double nextDist = pClick.distance(pCompare);
                if (nextDist < closestDistance) {
                    closestDistance = nextDist;
                    nearestNode = node;
                }
            }
        }

        return nearestNode;
    }


    // icons source from:
    // http://www.iconarchive.com/show/soft-scraps-icons-by-deleket.html
    // icon license: CC Attribution-Noncommercial-No Derivate 3.0 =?
    // http://creativecommons.org/licenses/by-nc-nd/3.0/
    // icon Commercial usage: Allowed (Author Approval required -> Visit artist
    // website for details).

    public static final String APP_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/128/Zoom-icon.png";
    public static final String ZOOM_RESET_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-icon.png";
    public static final String ZOOM_OUT_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-Out-icon.png";
    public static final String ZOOM_IN_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Zoom-In-icon.png";
    public static final String CLOSE_ICON = "http://icons.iconarchive.com/icons/deleket/soft-scraps/24/Button-Close-icon.png";
}