所以“Nearest neighbor search”是你正在试图解决的普遍问题。
您的问题声明是在细节上有点短。例如,节点彼此等距吗?节点排列成网格模式还是随机排列?是根据节点中心的一个点建模的节点距离,一个环绕的盒子,是任意形状节点上的实际最近点?等
我假设随机放置,可能重叠的形状,并且采摘不是基于绘画次序,但形状的边框最接近的角落。通过将点击点与实际形状周围的椭圆形区域进行比较,而不是使用边界框形状(因为当前选择器对于像重叠对角线之类的东西会有点挑剔),可能会更加准确。
A k-d tree algorithm或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());
}
}
辅助随机形状生成类
此类不关键解决方案,它只是生成s ome形状进行测试。
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);
}
}
此外例如将对象放置在可缩放的/可滚动区域
将该溶液从上方使用最近的节点溶液代码,并与从一个ScrollPane代码缩放节点结合它:JavaFX correct scaling。目的是为了证明选择算法即使在应用了缩放变换的节点上也能正常工作(因为它基于boundsInParent)。下面的代码只是意味着作为一个概念证明而不是如何构建功能集成到一类领域模型:-)
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";
}
所以没有内置溶液中的文体样本? *叹息*我希望绝对位置可以保存在一个易于访问的地方:( – Sorona
那么节点位置与节点一起保存,我使用'getBoundsInParent'从节点获取它们。还有其他各种内置函数可以转换节点协调本地或场景坐标(在[node javadoc](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/Node.html)中搜索“localTo” )还有内置的拾取函数,但是这些拾取函数会根据命中次数选择节点,而不是在未命中时选择最近的节点。因此,要根据未命中找到最近的节点,您需要自己提供该逻辑。这是一个非常简单的搜索算法。 – jewelsea
雅,算法很简单,但它不适用于我。可能是因为我的ZoomableScrollPane,但它根本没有选择正确的:( – Sorona