/*******************************************************************************
* Copyright (c) 2017 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Matthias Wienand (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.handlers;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import org.eclipse.gef.common.adapt.IAdaptable;
import org.eclipse.gef.fx.nodes.Connection;
import org.eclipse.gef.fx.nodes.InfiniteCanvas;
import org.eclipse.gef.fx.nodes.OrthogonalRouter;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.mvc.fx.models.SnappingModel;
import org.eclipse.gef.mvc.fx.models.SnappingModel.SnappingLocation;
import org.eclipse.gef.mvc.fx.parts.IContentPart;
import org.eclipse.gef.mvc.fx.parts.ISnappablePart;
import org.eclipse.gef.mvc.fx.parts.IVisualPart;
import org.eclipse.gef.mvc.fx.parts.PartUtils;
import javafx.geometry.Bounds;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
/**
* The {@link SnapToGeometrySupport} can be used by {@link IHandler}s in order
* to perform snapping.
*/
public class SnapToGeometrySupport
extends IAdaptable.Bound.Impl<IVisualPart<? extends Node>> {
private IContentPart<? extends Node> snappedPart;
private List<SnappingLocation> xLocations = new ArrayList<>();
private List<SnappingLocation> yLocations = new ArrayList<>();
/**
* The IS_NOT_SNAPPED {@link Predicate} tests if the given
* {@link IVisualPart} is not the currently snapped part.
*/
protected Predicate<IVisualPart<? extends Node>> IS_NOT_SNAPPED = (p) -> {
return p != snappedPart;
};
/**
* The IS_SNAPPABLE {@link Predicate} tests if the given {@link IVisualPart}
* is implementing the {@link ISnappablePart} interface.
*/
protected Predicate<IVisualPart<? extends Node>> IS_SNAPPABLE = (p) -> {
return p instanceof ISnappablePart;
};
/**
* The IS_VISIBLE {@link Predicate} tests if the given {@link IVisualPart}
* is fully visible within the viewport.
*/
protected Predicate<IVisualPart<? extends Node>> IS_VISIBLE = (p) -> {
// get viewport
InfiniteCanvas canvas = (InfiniteCanvas) getAdaptable().getRoot()
.getViewer().getCanvas();
// no snapping feedback for parts outside the viewport
Bounds boundsInCanvas = canvas.sceneToLocal(
p.getVisual().localToScene(p.getVisual().getLayoutBounds()));
if (boundsInCanvas.getMinX() > canvas.getWidth()
|| boundsInCanvas.getMinY() > canvas.getHeight()
|| boundsInCanvas.getMaxX() < 0
|| boundsInCanvas.getMaxY() < 0) {
return false;
}
return true;
};
/**
* The IS_VISUAL_CONNECTION {@link Predicate} tests if the visual of the
* given {@link IVisualPart} is of type {@link Connection}.
*/
protected Predicate<IVisualPart<? extends Node>> IS_ORTHO_CONNECTION = (
p) -> {
return p.getVisual() instanceof Connection
&& ((Connection) p.getVisual())
.getRouter() instanceof OrthogonalRouter;
};
/**
* The IS_LEAF {@link Predicate} tests if the given {@link IVisualPart} has
* no children.
*/
protected Predicate<IVisualPart<? extends Node>> IS_LEAF = (p) -> {
return p.getChildrenUnmodifiable().isEmpty();
};
/**
*
*/
protected void findConnectionLeafSnappingLocations() {
List<ISnappablePart<? extends Node>> connectionLeafParts = PartUtils
.filterParts(
getAdaptable().getRoot().getViewer().getContentPartMap()
.values(),
IS_NOT_SNAPPED.and(IS_SNAPPABLE).and(IS_LEAF)
.and(IS_ORTHO_CONNECTION).and(IS_VISIBLE));
// compute snapping locations
// TODO: keep lists sorted by location
for (ISnappablePart<? extends Node> contextPart : connectionLeafParts) {
// filter out edges which the snappedPart is connected to
if (contextPart.getAnchoragesUnmodifiable()
.containsKey(snappedPart)) {
continue;
}
xLocations.addAll(contextPart.getVerticalSnappingLocations());
yLocations.addAll(contextPart.getHorizontalSnappingLocations());
}
}
/**
*
*/
protected void findNonConnectionLeafSnappingLocations() {
// determine currently rendered NodeParts
List<ISnappablePart<? extends Node>> nodeParts = PartUtils.filterParts(
getAdaptable().getRoot().getViewer().getContentPartMap()
.values(),
IS_NOT_SNAPPED.and(IS_SNAPPABLE).and(IS_LEAF)
.and(IS_ORTHO_CONNECTION.negate()).and(IS_VISIBLE));
// compute snapping locations
// TODO: keep lists sorted by location
for (ISnappablePart<? extends Node> contextPart : nodeParts) {
xLocations.addAll(contextPart.getVerticalSnappingLocations());
yLocations.addAll(contextPart.getHorizontalSnappingLocations());
}
}
/**
* Returns the distance at which snapping is performed, i.e. the
* to-be-snapped location needs to be no further away from a snapping
* location than this distance in order to be snapped to the snapping
* location.
*
* @return The distance at which snapping is performed.
*/
protected double getSnapDistance() {
return 10d;
}
/**
* Returns a {@link Dimension} specifying the translation that needs to be
* applied to the given location so that it is snapped according to the
* rules of this {@link SnapToGeometrySupport}.
*
* @param x
* The horizontal coordinate of the location that is snapped.
* @param y
* The vertical coordinate of the location that is snapped.
* @return The {@link Dimension} specifying the snap translation, or
* <code>null</code> if not snapped.
*/
public Dimension snap(double x, double y) {
// find nearest snapping location
// TODO: binary search
double minDistance = 0d;
SnappingLocation snappingLocation = null;
for (SnappingLocation xl : xLocations) {
double locationX = xl.getPositionInScene();
double distance = x - locationX;
if (Math.abs(distance) < getSnapDistance()
&& (snappingLocation == null
|| Math.abs(distance) < Math.abs(minDistance))) {
minDistance = distance;
snappingLocation = xl;
}
}
// compute snapping offset
Dimension snappingOffset = null;
if (snappingLocation != null) {
snappingOffset = new Dimension(minDistance, 0);
}
// find nearest snapping location
// TODO: binary search
minDistance = 0d;
snappingLocation = null;
for (SnappingLocation xl : xLocations) {
double locationX = xl.getPositionInScene();
double distance = x - locationX;
if (Math.abs(distance) < getSnapDistance()
&& (snappingLocation == null
|| Math.abs(distance) < Math.abs(minDistance))) {
minDistance = distance;
snappingLocation = xl;
}
}
// compute snapping offset
if (snappingLocation != null) {
if (snappingOffset != null) {
snappingOffset.setHeight(minDistance);
} else {
snappingOffset = new Dimension(0, minDistance);
}
}
return snappingOffset;
}
/**
* Returns a {@link Dimension} containing the snap-to-location offset for
* the currently snapped part. The given {@link Bounds} (in scene
* coordinates) is tested for min, center, and max snapping.
*
* @param boundsInScene
* The {@link Bounds} of the snapped part within the scene
* coordinate system.
* @return A {@link Dimension} containing the snap-to-location offset.
*/
public Dimension snapToLocation(Bounds boundsInScene) {
Dimension minSnapOffset = snap(boundsInScene.getMinX(),
boundsInScene.getMinY());
Dimension maxSnapOffset = snap(boundsInScene.getMaxX(),
boundsInScene.getMaxY());
Dimension centerSnapOffset = snap(
boundsInScene.getMinX() + 0.5 * boundsInScene.getWidth(),
boundsInScene.getMinY() + 0.5 * boundsInScene.getHeight());
// compute source locations (min, center, max)
double minX = boundsInScene.getMinX();
double minY = boundsInScene.getMinY();
double centerX = minX + boundsInScene.getWidth() / 2;
double centerY = minY + boundsInScene.getHeight() / 2;
double maxX = boundsInScene.getMaxX();
double maxY = boundsInScene.getMaxY();
// find nearest snapping location
// TODO: binary search
double minDistance = 0d;
SnappingLocation snappingLocation = null;
for (SnappingLocation xl : xLocations) {
double locationX = xl.getPositionInScene();
double deltaMin = minX - locationX;
double deltaCenter = centerX - locationX;
double deltaMax = maxX - locationX;
double distance = 0d;
if (Math.abs(deltaMin) < Math.abs(deltaCenter)
&& Math.abs(deltaMin) < Math.abs(deltaMax)) {
distance = deltaMin;
} else if (Math.abs(deltaCenter) < Math.abs(deltaMin)
&& Math.abs(deltaCenter) < Math.abs(deltaMax)) {
distance = deltaCenter;
} else if (Math.abs(deltaMax) < Math.abs(deltaMin)
&& Math.abs(deltaMax) < Math.abs(deltaCenter)) {
distance = deltaMax;
}
if (Math.abs(distance) < 10 && (snappingLocation == null
|| Math.abs(distance) < Math.abs(minDistance))) {
// snaps
minDistance = distance;
snappingLocation = xl;
}
}
// compute snapping offset
Dimension snappingOffset = snappingLocation == null
? new Dimension(0, 0) : new Dimension(minDistance, 0);
// TODO: binary search
minDistance = 0d;
snappingLocation = null;
for (SnappingLocation yl : yLocations) {
double locationY = yl.getPositionInScene();
double deltaMin = minY - locationY;
double deltaCenter = centerY - locationY;
double deltaMax = maxY - locationY;
double distance = 0d;
if (Math.abs(deltaMin) < Math.abs(deltaCenter)
&& Math.abs(deltaMin) < Math.abs(deltaMax)) {
distance = deltaMin;
} else if (Math.abs(deltaCenter) < Math.abs(deltaMin)
&& Math.abs(deltaCenter) < Math.abs(deltaMax)) {
distance = deltaCenter;
} else if (Math.abs(deltaMax) < Math.abs(deltaMin)
&& Math.abs(deltaMax) < Math.abs(deltaCenter)) {
distance = deltaMax;
}
if (Math.abs(distance) < 10 && (snappingLocation == null
|| Math.abs(distance) < Math.abs(minDistance))) {
// snaps
minDistance = distance;
snappingLocation = yl;
}
}
// expand snapping offset
if (snappingLocation != null) {
snappingOffset.setHeight(minDistance);
}
// compute new min, center, and max positions
double newMinX = boundsInScene.getMinX() - snappingOffset.width;
double newMinY = boundsInScene.getMinY() - snappingOffset.height;
double newCenterX = newMinX + boundsInScene.getWidth() / 2;
double newCenterY = newMinY + boundsInScene.getHeight() / 2;
double newMaxX = boundsInScene.getMaxX() - snappingOffset.width;
double newMaxY = boundsInScene.getMaxY() - snappingOffset.height;
// copy currently established snapping locations to model
final List<SnappingLocation> snappingLocations = new ArrayList<>();
// TODO: binary search
for (SnappingLocation xl : xLocations) {
double locationX = xl.getPositionInScene();
double distance = Math.min(Math.abs(newMinX - locationX),
Math.abs(newCenterX - locationX));
distance = Math.min(distance, Math.abs(newMaxX - locationX));
if (distance < 1) {
snappingLocations.add(xl.getCopy());
}
}
// TODO: binary search
for (SnappingLocation yl : yLocations) {
double locationY = yl.getPositionInScene();
double distance = Math.min(Math.abs(newMinY - locationY),
Math.abs(newCenterY - locationY));
distance = Math.min(distance, Math.abs(newMaxY - locationY));
if (distance < 1) {
snappingLocations.add(yl.getCopy());
}
}
// update SnapToLocationModel
getAdaptable().getRoot().getViewer().getAdapter(SnappingModel.class)
.setSnappingLocations(snappingLocations);
// transform snapping offset to snappedPart's parent
Point2D startInParent = snappedPart.getVisual().getParent()
.sceneToLocal(minX, minY);
Point2D endInParent = snappedPart.getVisual().getParent()
.sceneToLocal(newMinX, newMinY);
return new Dimension(endInParent.getX() - startInParent.getX(),
endInParent.getY() - startInParent.getY());
}
/**
* Returns a {@link Dimension} containing the snap-to-location offset for
* the currently snapped part. The given {@link Bounds} (in scene
* coordinates) is tested for min, center, and max snapping.
*
* @param orientation
* The {@link Orientation} of the {@link SnappingLocation}s to
* snap to.
* @param positionInScene
* The position coordinate of the snapped part.
* @return A {@link Dimension} containing the snap-to-location offset.
*/
public Dimension snapToLocation(Orientation orientation,
double positionInScene) {
// find nearest snapping location
double minDistance = 0d;
SnappingLocation snappingLocation = null;
Dimension snappingOffset = new Dimension();
// TODO: binary search
if (orientation == Orientation.VERTICAL) {
for (SnappingLocation xl : xLocations) {
double locationX = xl.getPositionInScene();
double distance = positionInScene - locationX;
if (Math.abs(distance) < 10 && (snappingLocation == null
|| Math.abs(distance) < Math.abs(minDistance))) {
// snaps
minDistance = distance;
snappingLocation = xl;
}
}
// compute snapping offset
if (snappingLocation != null) {
snappingOffset.setWidth(minDistance);
}
} else {
for (SnappingLocation yl : yLocations) {
double locationY = yl.getPositionInScene();
double distance = positionInScene - locationY;
if (Math.abs(distance) < 10 && (snappingLocation == null
|| Math.abs(distance) < Math.abs(minDistance))) {
// snaps
minDistance = distance;
snappingLocation = yl;
}
}
// expand snapping offset
if (snappingLocation != null) {
snappingOffset.setHeight(minDistance);
}
}
// nothing to do when not snapping
if (snappingLocation == null) {
getAdaptable().getRoot().getViewer().getAdapter(SnappingModel.class)
.setSnappingLocations(
Collections.<SnappingLocation> emptyList());
return new Dimension();
}
// copy currently established snapping locations to model
final List<SnappingLocation> snappingLocations = new ArrayList<>();
// TODO: binary search
if (orientation == Orientation.VERTICAL) {
for (SnappingLocation xl : xLocations) {
double locationX = xl.getPositionInScene();
double distance = Math
.abs(snappingLocation.getPositionInScene() - locationX);
if (distance < 1) {
snappingLocations.add(xl.getCopy());
}
}
} else {
for (SnappingLocation yl : yLocations) {
double locationY = yl.getPositionInScene();
double distance = Math
.abs(snappingLocation.getPositionInScene() - locationY);
if (distance < 1) {
snappingLocations.add(yl.getCopy());
}
}
}
// update SnapToLocationModel
getAdaptable().getRoot().getViewer().getAdapter(SnappingModel.class)
.setSnappingLocations(snappingLocations);
// transform snapping offset to snappedPart's parent
Point2D startPoint = Orientation.VERTICAL == orientation
? new Point2D(positionInScene, 0)
: new Point2D(0, positionInScene);
Point2D endPoint = Orientation.VERTICAL == orientation
? new Point2D(snappingLocation.getPositionInScene(), 0)
: new Point2D(0, snappingLocation.getPositionInScene());
Point2D startInParent = snappedPart.getVisual().getParent()
.sceneToLocal(startPoint);
Point2D endInParent = snappedPart.getVisual().getParent()
.sceneToLocal(endPoint);
if (Orientation.VERTICAL == orientation) {
return new Dimension(endInParent.getX() - startInParent.getX(), 0);
} else {
return new Dimension(0, endInParent.getY() - startInParent.getY());
}
}
/**
* Identifies and stores all possible snapping locations for the given
* target part.
*
* @param snappedPart
* The {@link IContentPart} that might be snapped.
*/
public void startSnapping(IContentPart<? extends Node> snappedPart) {
// save the snapped part
this.snappedPart = snappedPart;
findNonConnectionLeafSnappingLocations();
findConnectionLeafSnappingLocations();
}
/**
* Clears the snapping locations and the SnapToLocationModel.
*/
public void stopSnapping() {
SnappingModel snappingModel = getAdaptable().getRoot().getViewer()
.getAdapter(SnappingModel.class);
snappingModel.setSnappingLocations(
Collections.<SnappingLocation> emptyList());
xLocations.clear();
yLocations.clear();
}
}