/*******************************************************************************
* Copyright (c) 2005, 2016 The Chisel Group 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: Ian Bull (The Chisel Group) - initial API and implementation
* Mateusz Matela - "Tree Views for Zest" contribution, Google Summer of Code 2009
* Matthias Wienand (itemis AG) - refactorings
******************************************************************************/
package org.eclipse.gef.layout.algorithms;
import org.eclipse.gef.geometry.planar.Dimension;
import org.eclipse.gef.geometry.planar.Point;
import org.eclipse.gef.geometry.planar.Rectangle;
import org.eclipse.gef.graph.Node;
import org.eclipse.gef.layout.LayoutProperties;
/**
* The {@link AlgorithmHelper} class contains utility methods for the laying out
* of entities within bounds.
*
* @author Ian Bull
* @author Mateusz Matela
* @author mwienand
*/
class AlgorithmHelper {
/**
* The minimum size of a node.
*/
private static int MIN_NODE_SIZE = 8;
/**
* This percent of a node's maximum size is reserved for the node, the rest
* is used as padding, i.e.
* <code>node_size = max_size * PADDING_PERCENT</code>.
*/
private static double PADDING_PERCENT = 0.8;
/**
* Fits given entities within given bounds, preserving their relative
* locations. If an entity is resizable and the <i>resize</i> flag is set to
* <code>true</code>, then the entity will be scaled according to the bounds
* change, i.e. <code>scale_factor = dst_bounds / start_bounds</code>.
*
* @param entities
* The {@link Node}s to fit.
* @param destinationBounds
* The {@link Rectangle} representing the layout bounds.
* @param resize
* <code>true</code> to indicate that the entities can be
* resized, otherwise <code>false</code>.
*/
public static void fitWithinBounds(Node[] entities,
Rectangle destinationBounds, boolean resize) {
if (entities.length == 1) {
fitSingleEntity(entities[0], destinationBounds, resize);
return;
}
Rectangle startingBounds = getLayoutBounds(entities, false);
double sizeScale = Math.min(
destinationBounds.getWidth() / startingBounds.getWidth(),
destinationBounds.getHeight() / startingBounds.getHeight());
for (int i = 0; i < entities.length; i++) {
Node entity = entities[i];
Dimension size = LayoutProperties.getSize(entity);
if (LayoutProperties.isMovable(entity)) {
Point location = LayoutProperties.getLocation(entity);
double percentX = startingBounds.getWidth() == 0 ? 0
: (location.x - startingBounds.getX())
/ (startingBounds.getWidth());
double percentY = startingBounds.getHeight() == 0 ? 0
: (location.y - startingBounds.getY())
/ (startingBounds.getHeight());
if (resize && LayoutProperties.isResizable(entity)) {
size.width *= sizeScale;
size.height *= sizeScale;
LayoutProperties.setSize(entity, new Dimension(size));
}
location.x = destinationBounds.getX() + size.width / 2
+ percentX
* (destinationBounds.getWidth() - size.width);
location.y = destinationBounds.getY() + size.height / 2
+ percentY
* (destinationBounds.getHeight() - size.height);
} else if (resize && LayoutProperties.isResizable(entity)) {
LayoutProperties.setSize(entity, size.getScaled(sizeScale));
}
}
}
private static void fitSingleEntity(Node entity,
Rectangle destinationBounds, boolean resize) {
if (LayoutProperties.isMovable(entity)) {
LayoutProperties.setLocation(entity,
new Point(
destinationBounds.getX()
+ destinationBounds.getWidth() / 2,
destinationBounds.getY()
+ destinationBounds.getHeight() / 2));
}
if (resize && LayoutProperties.isResizable(entity)) {
double width = destinationBounds.getWidth();
double height = destinationBounds.getHeight();
double preferredAspectRatio = LayoutProperties
.getPreferredAspectRatio(entity);
if (preferredAspectRatio > 0) {
LayoutProperties.setSize(entity,
fixAspectRatio(width, height, preferredAspectRatio));
} else {
LayoutProperties.setSize(entity, new Dimension(width, height));
}
}
}
/**
* Resizes the nodes so that they have a maximal area without overlapping
* each other, with additional empty space of 20% of node's width (or
* height, if bigger). It does nothing if there's less than two nodes.
*
* @param entities
* The {@link Node}s of which the sizes are maximized.
*/
public static void maximizeSizes(Node[] entities) {
if (entities.length > 1) {
Dimension minDistance = getMinimumDistance(entities);
double nodeSize = Math.max(minDistance.width, minDistance.height)
* PADDING_PERCENT;
double width = nodeSize;
double height = nodeSize;
for (int i = 0; i < entities.length; i++) {
Node entity = entities[i];
if (LayoutProperties.isResizable(entity)) {
double preferredRatio = LayoutProperties
.getPreferredAspectRatio(entity);
if (preferredRatio > 0) {
LayoutProperties.setSize(entity,
fixAspectRatio(width, height, preferredRatio));
} else {
LayoutProperties.setSize(entity,
new Dimension(width, height));
}
}
}
}
}
private static Dimension fixAspectRatio(double width, double height,
double preferredRatio) {
double actualRatio = width / height;
if (actualRatio > preferredRatio) {
width = height * preferredRatio;
if (width < MIN_NODE_SIZE) {
width = MIN_NODE_SIZE;
height = width / preferredRatio;
}
}
if (actualRatio < preferredRatio) {
height = width / preferredRatio;
if (height < MIN_NODE_SIZE) {
height = MIN_NODE_SIZE;
width = height * preferredRatio;
}
}
return new Dimension(width, height);
}
/**
* Find the bounds in which the nodes are located. Using the bounds against
* the real bounds of the screen, the nodes can proportionally be placed
* within the real bounds. The bounds can be determined either including the
* size of the nodes or not. If the size is not included, the bounds will
* only be guaranteed to include the center of each node.
*
* @param entities
* The {@link Node}s for which the layout bounds are computed.
* @param includeNodeSize
* <code>true</code> to indicate that the entities' sizes should
* be taken into consideration, otherwise <code>false</code>.
* @return A {@link Rectangle} representing the layout bounds of the given
* {@link Node}s.
*/
public static Rectangle getLayoutBounds(Node[] entities,
boolean includeNodeSize) {
double rightSide = Double.NEGATIVE_INFINITY;
double bottomSide = Double.NEGATIVE_INFINITY;
double leftSide = Double.POSITIVE_INFINITY;
double topSide = Double.POSITIVE_INFINITY;
for (int i = 0; i < entities.length; i++) {
Node entity = entities[i];
Point location = LayoutProperties.getLocation(entity);
Dimension size = LayoutProperties.getSize(entity);
if (includeNodeSize) {
leftSide = Math.min(location.x - size.width / 2, leftSide);
topSide = Math.min(location.y - size.height / 2, topSide);
rightSide = Math.max(location.x + size.width / 2, rightSide);
bottomSide = Math.max(location.y + size.height / 2, bottomSide);
} else {
leftSide = Math.min(location.x, leftSide);
topSide = Math.min(location.y, topSide);
rightSide = Math.max(location.x, rightSide);
bottomSide = Math.max(location.y, bottomSide);
}
}
return new Rectangle(leftSide, topSide, rightSide - leftSide,
bottomSide - topSide);
}
/**
* minDistance is the closest that any two points are together. These two
* points become the center points for the two closest nodes, which we wish
* to make them as big as possible without overlapping. This will be the
* maximum of minDistanceX and minDistanceY minus a bit, lets say 20%
*
* We make the recommended node size a square for convenience.
*
* <pre>
* _______
* | |
* | |
* | + |
* | |\ |
* |___|_\_|_____
* | | \ |
* | | \ |
* +-|---+ |
* | |
* |_______|
* </pre>
*
* @param entities
* The {@link Node}s for which the minimum distance is computed.
* @return A {@link Dimension} representing the minimum distance (in x- and
* y-direction).
*/
public static Dimension getMinimumDistance(Node[] entities) {
Dimension horAndVertdistance = new Dimension(Double.MAX_VALUE,
Double.MAX_VALUE);
double minDistance = Double.MAX_VALUE;
// TODO: Very Slow!
for (int i = 0; i < entities.length; i++) {
Point location1 = LayoutProperties.getLocation(entities[i]);
for (int j = i + 1; j < entities.length; j++) {
Point location2 = LayoutProperties.getLocation(entities[j]);
double distanceX = location1.x - location2.x;
double distanceY = location1.y - location2.y;
double distance = distanceX * distanceX + distanceY * distanceY;
if (distance < minDistance) {
minDistance = distance;
horAndVertdistance.width = Math.abs(distanceX);
horAndVertdistance.height = Math.abs(distanceY);
}
}
}
return horAndVertdistance;
}
}