/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.flow.processrendering.draw;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.logging.Level;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.UIManager;
import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel;
import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.operator.ExecutionUnit;
import com.rapidminer.operator.IOObject;
import com.rapidminer.operator.IOObjectCollection;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.ports.IncompatibleMDClassException;
import com.rapidminer.operator.ports.InputPort;
import com.rapidminer.operator.ports.OutputPort;
import com.rapidminer.operator.ports.Port;
import com.rapidminer.operator.ports.metadata.CollectionMetaData;
import com.rapidminer.operator.ports.metadata.CompatibilityLevel;
import com.rapidminer.operator.ports.metadata.MetaData;
import com.rapidminer.tools.ClassColorMap;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.ParentResolvingMap;
import com.rapidminer.tools.plugin.Plugin;
/**
* Contains utility methods which are helpful for drawing a process via Java2D.
*
* @author Marco Boeck
* @since 6.4.0
*
*/
public final class ProcessDrawUtils {
/** dummy label used to create disabled icons */
private static final JLabel DUMMY_LABEL = new JLabel();
/** shadow color */
private static final Color TRANSPARENT_GRAY = new Color(Color.GRAY.getRed(), Color.GRAY.getGreen(), Color.GRAY.getBlue(),
0);
/** map containing IOObject colors */
private static ParentResolvingMap<Class<?>, Color> IO_CLASS_TO_COLOR_MAP = new ClassColorMap();
static {
try {
IO_CLASS_TO_COLOR_MAP.parseProperties("com/rapidminer/resources/groups.properties", "io.", ".color",
OperatorDescription.class.getClassLoader(), null);
} catch (IOException e) {
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.gui.flow.ProcessRenderer.loading_operator_group_colors_error");
}
}
/**
* Private constructor which throws if called.
*/
private ProcessDrawUtils() {
throw new UnsupportedOperationException("Static utility class");
}
/**
* This method adds the colors of the given property file to the global group colors.
*
* @param groupProperties
* the properties group name
* @param pluginName
* the name of the plugin
* @param classLoader
* the classloader responsible who registered the colors
* @param provider
* the extension for the registered group color
*/
public static void registerAdditionalGroupColors(final String groupProperties, final String pluginName,
final ClassLoader classLoader, final Plugin provider) {
SwingTools.registerAdditionalGroupColors(groupProperties, pluginName, classLoader, provider);
}
/**
* This method adds the colors of the given property file to the io object colors.
*
* @param groupProperties
* the properties group name
* @param pluginName
* the name of the plugin
* @param classLoader
* the classloader responsible who registered the colors
* @param provider
* the extension to registered IOObjects for
*/
public static void registerAdditionalObjectColors(final String groupProperties, final String pluginName,
final ClassLoader classLoader, final Plugin provider) {
try {
IO_CLASS_TO_COLOR_MAP.parseProperties(groupProperties, "io.", ".color", classLoader, provider);
} catch (IOException e) {
LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.flow.ProcessRenderer.loading_io_object_colors_error",
pluginName);
}
}
/**
* Returns the color specified for the given type of metadata.
*
* @param md
* the metadata or {@code null}
* @return a color, never {@code null}
*/
public static Color getColorFor(final MetaData md) {
if (md == null) {
return Color.WHITE;
}
if (md instanceof CollectionMetaData) {
MetaData elementMetaDataRecursive = ((CollectionMetaData) md).getElementMetaDataRecursive();
if (elementMetaDataRecursive != null) {
return IO_CLASS_TO_COLOR_MAP.get(elementMetaDataRecursive.getObjectClass());
} else {
return IO_CLASS_TO_COLOR_MAP.get(IOObject.class);
}
} else {
return IO_CLASS_TO_COLOR_MAP.get(md.getObjectClass());
}
}
/**
* Returns a color for the given {@link Port} depending on the metadata at the port.
*
* @param port
* the port for which a color should be retrieved
* @param defaultColor
* the default color if the port has no data
* @param enabled
* if the port is enabled
* @return a color, never {@code null}
*/
public static Color getColorFor(final Port port, final Color defaultColor, final boolean enabled) {
if (port == null) {
throw new IllegalArgumentException("port must not be null!");
}
if (defaultColor == null) {
throw new IllegalArgumentException("defaultColor must not be null!");
}
if (!enabled) {
return Color.LIGHT_GRAY;
}
IOObject data = port.getAnyDataOrNull();
MetaData md = null;
try {
md = port.getMetaData(MetaData.class);
} catch (IncompatibleMDClassException e) {
// should not happen
return defaultColor;
}
if (data != null) {
if (data instanceof IOObjectCollection) {
return IO_CLASS_TO_COLOR_MAP.get(((IOObjectCollection<?>) data).getElementClass(true));
} else {
return IO_CLASS_TO_COLOR_MAP.get(data.getClass());
}
} else if (md != null) {
return ProcessDrawUtils.getColorFor(md);
} else {
return defaultColor;
}
}
/**
* Snaps the given point to the nearest snapping point for the {@link ProcessRendererView}. Does
* not check if snapping is enabled!
*
* @param point
* the point which should snap to the nearest possible grid position
* @return the nearest position on the grid, never {@code null}
*/
public static Point snap(final Point2D point) {
if (point == null) {
throw new IllegalArgumentException("point must not be null!");
}
int snappedX = (int) point.getX() - ProcessDrawer.GRID_X_OFFSET;
int factor = (snappedX + ProcessDrawer.GRID_WIDTH / 2) / ProcessDrawer.GRID_WIDTH;
snappedX /= ProcessDrawer.GRID_WIDTH;
snappedX = factor * ProcessDrawer.GRID_WIDTH + ProcessDrawer.GRID_X_OFFSET;
int snappedY = (int) point.getY() - ProcessDrawer.GRID_Y_OFFSET;
factor = (snappedY + ProcessDrawer.GRID_HEIGHT / 2) / ProcessDrawer.GRID_HEIGHT;
snappedY /= ProcessDrawer.GRID_HEIGHT;
snappedY = factor * ProcessDrawer.GRID_HEIGHT + ProcessDrawer.GRID_Y_OFFSET;
return new Point(snappedX, snappedY);
}
/**
* Calculates a {@link Rectangle2D} from the specified start and end point.
*
* @param start
* the starting point
* @param end
* the end point
* @return the rectangle or {@code null} if the start/end point was {@code null}
*/
public static Rectangle2D createRectangle(final Point start, final Point end) {
if (start == null || end == null) {
return null;
}
return new Rectangle2D.Double(Math.min(start.getX(), end.getX()), Math.min(start.getY(), end.getY()),
Math.abs(start.getX() - end.getX()), Math.abs(start.getY() - end.getY()));
}
/**
* Draws a shadow around the given rectangle.
*
* @param rect
* the rectangle which should get a shadow
* @param g2
* the graphics context to draw the shadow on
*/
public static void drawShadow(final Rectangle2D rect, final Graphics2D g2) {
Graphics2D g2S = (Graphics2D) g2.create();
Rectangle2D shadow = new Rectangle2D.Double(rect.getX() + 5, rect.getY() + ProcessDrawer.HEADER_HEIGHT + 5,
rect.getWidth(), rect.getHeight() - ProcessDrawer.HEADER_HEIGHT);
GeneralPath bottom = new GeneralPath();
bottom.moveTo(shadow.getX(), rect.getMaxY());
bottom.lineTo(rect.getMaxX(), rect.getMaxY());
bottom.lineTo(shadow.getMaxX(), shadow.getMaxY());
bottom.lineTo(shadow.getMinX(), shadow.getMaxY());
bottom.closePath();
g2S.setPaint(new GradientPaint((float) rect.getX(), (float) rect.getMaxY(), Color.gray, (float) rect.getX(),
(float) shadow.getMaxY(), TRANSPARENT_GRAY));
g2S.fill(bottom);
GeneralPath right = new GeneralPath();
right.moveTo(rect.getMaxX(), shadow.getMinY());
right.lineTo(shadow.getMaxX(), shadow.getMinY());
right.lineTo(shadow.getMaxX(), shadow.getMaxY());
right.lineTo(rect.getMaxX(), rect.getMaxY());
right.closePath();
g2S.setPaint(new GradientPaint((float) rect.getMaxX(), (float) shadow.getY(), Color.gray, (float) shadow.getMaxX(),
(float) shadow.getY(), TRANSPARENT_GRAY));
g2S.fill(right);
g2S.dispose();
}
/**
* Creates a spline connector shape from one {@link Port} to another.
*
* @param fromPort
* the origin port
* @param toPort
* the end port
* @param model
* the model required to create port locations
* @return the shape representing the connection or {@code null} if the ports have no location
* yet
*/
public static Shape createConnector(final OutputPort fromPort, final Port toPort, final ProcessRendererModel model) {
Point2D from = ProcessDrawUtils.createPortLocation(fromPort, model);
Point2D to = ProcessDrawUtils.createPortLocation(toPort, model);
if (from == null || to == null) {
return null;
}
from = new Point2D.Double(from.getX() + ProcessDrawer.PORT_SIZE / 2, from.getY());
to = new Point2D.Double(to.getX() - ProcessDrawer.PORT_SIZE / 2, to.getY());
return ProcessDrawUtils.createConnectionSpline(from, to);
}
/**
* Creates a spline connector shape from one {@link Point2D} to another.
*
* @param from
* the starting point
* @param to
* the end point
* @return the shape representing the connection, never {@code null}
*/
public static Shape createConnectionSpline(final Point2D from, final Point2D to) {
if (from == null || to == null) {
throw new IllegalArgumentException("from and to must not be null!");
}
int delta = 10;
GeneralPath connector = new GeneralPath();
connector.moveTo(from.getX() + 1, from.getY());
double cx = (from.getX() + to.getX()) / 2;
double cy = (from.getY() + to.getY()) / 2;
if (to.getX() >= from.getX() + 2 * delta) {
connector.curveTo(cx, from.getY(), cx, from.getY(), cx, cy);
connector.curveTo(cx, to.getY(), cx, to.getY(), to.getX() - 1, to.getY());
} else {
connector.curveTo(from.getX() + delta, from.getY(), from.getX() + delta, cy, cx, cy);
connector.curveTo(to.getX() - delta, cy, to.getX() - delta, to.getY(), to.getX() - 1, to.getY());
}
return connector;
}
/**
* Returns the location of the given {@link Port}.
*
* @param port
* the location for this port will be returned
* @param model
* the model required to create port locations
* @return the point or {@code null} if the port has no location yet
*/
public static Point createPortLocation(final Port port, final ProcessRendererModel model) {
if (port.getPorts() == null) {
return new Point(0, 0);
}
Operator op = port.getPorts().getOwner().getOperator();
int index = port.getPorts().getAllPorts().indexOf(port);
int addOffset = 0;
int xOffset = 0;
for (int i = 0; i <= index; i++) {
addOffset += model.getPortSpacing(port.getPorts().getPortByIndex(i));
}
ExecutionUnit process;
Point point;
if (op == model.getDisplayedChain()) {
// this is an inner port
process = port.getPorts().getOwner().getConnectionContext();
if (port instanceof OutputPort) {
point = new Point(0 + xOffset, ProcessDrawer.OPERATOR_MIN_HEIGHT / 2 + ProcessDrawer.PORT_OFFSET
+ index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset);
} else {
point = new Point((int) Math.ceil(model.getProcessWidth(process) * (1 / model.getZoomFactor()) - xOffset),
ProcessDrawer.OPERATOR_MIN_HEIGHT / 2 + ProcessDrawer.PORT_OFFSET
+ index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset);
}
} else {
// this is an outer port of a nested operator
process = op.getExecutionUnit();
Rectangle2D opRect = model.getOperatorRect(op);
// called before notifaction of added operator was received, no location set yet
if (opRect == null) {
return null;
}
if (port instanceof InputPort) {
point = new Point((int) Math.ceil(opRect.getX()), (int) Math.ceil(
opRect.getY() + ProcessDrawer.PORT_OFFSET + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset));
} else {
point = new Point((int) Math.ceil(opRect.getMaxX()), (int) Math.ceil(
opRect.getY() + ProcessDrawer.PORT_OFFSET + index * ProcessDrawer.PORT_SIZE * 3 / 2 + addOffset));
}
}
return point;
}
/**
* Returns the absolute point for a given point in a process.
*
* @param p
* the relative point which is relative to the displayed process
* @param processIndex
* the index of the process in question
* @param model
* the model required to calculate the absolute location
* @return the absolute point (relative to the process renderer view) or {@code null} if the
* process index is invalid
*/
public static Point convertToAbsoluteProcessPoint(final Point p, final int processIndex,
final ProcessRendererModel model) {
double xOffset = 0;
for (int i = 0; i < model.getProcesses().size(); i++) {
if (i == processIndex) {
return new Point((int) (p.getX() + xOffset), (int) p.getY());
}
xOffset += ProcessDrawer.WALL_WIDTH * 2 + model.getProcessWidth(model.getProcess(i));
}
return null;
}
/**
* Returns the relative point for a given absolute point.
*
* @param p
* the relative point which is relative to the displayed process
* @param processIndex
* the index of the process in question
* @param model
* the model required to calculate the relative location
* @return the relative point which is relative to the displayed process or {@code null} if the
* process index is invalid
*/
public static Point convertToRelativePoint(final Point p, final int processIndex, final ProcessRendererModel model) {
double xOffset = 0;
for (int i = 0; i < model.getProcesses().size(); i++) {
if (i == processIndex) {
return new Point((int) (p.getX() - xOffset), (int) p.getY());
}
xOffset += ProcessDrawer.WALL_WIDTH * 2 + model.getProcessWidth(model.getProcess(i));
}
return null;
}
/**
* Abbreviates the string using {@code ...} if necessary.
*
* @param string
* the string to shorten
* @param g2
* the graphics context
* @param maxWidth
* the max width in px the string is allowed to use
* @return the shorted string, never {@code null}
*/
public static String fitString(String string, final Graphics2D g2, final int maxWidth) {
if (string == null) {
throw new IllegalArgumentException("string must not be null!");
}
if (g2 == null) {
throw new IllegalArgumentException("g2 must not be null!");
}
Rectangle2D bounds = g2.getFont().getStringBounds(string, g2.getFontRenderContext());
if (bounds.getWidth() <= maxWidth) {
return string;
}
while (g2.getFont().getStringBounds(string + "...", g2.getFontRenderContext()).getWidth() > maxWidth) {
if (string.length() == 0) {
return "...";
}
string = string.substring(0, string.length() - 1);
}
return string + "...";
}
/**
* Returns whether the specified {@link Operator} has at least one free input and output port.
*
* @param operator
* the operator in question
* @return {@code true} if the operator has one free input and output port ; {@code false}
* otherwise
*/
public static boolean hasOperatorFreePorts(final Operator operator) {
if (operator == null) {
return false;
}
boolean hasFreeInput = false;
for (InputPort port : operator.getInputPorts().getAllPorts()) {
if (!port.isConnected()) {
hasFreeInput = true;
break;
}
}
if (!hasFreeInput) {
return false;
}
for (OutputPort port : operator.getOutputPorts().getAllPorts()) {
if (!port.isConnected()) {
return true;
}
}
return false;
}
/**
* Returns whether the specified {@link Operator} can be inserted into the currently hovered
* connection. If no connection is being hovered, only checks if the operator has at least one
* free input and output port.
*
* @param operator
* the operator in question
* @return {@code true} if the operator has one free input and output port ; {@code false}
* otherwise
*/
public static boolean canOperatorBeInsertedIntoConnection(final ProcessRendererModel model, final Operator operator) {
if (operator == null) {
return false;
}
OutputPort hoveringConnectionSource = model.getHoveringConnectionSource();
MetaData md = null;
try {
md = hoveringConnectionSource != null ? hoveringConnectionSource.getMetaData(MetaData.class) : null;
} catch (IncompatibleMDClassException e) {
// should not happen
return false;
}
boolean hasFreeInput = false;
for (InputPort port : operator.getInputPorts().getAllPorts()) {
if (!port.isConnected()) {
if (md != null) {
if (port.isInputCompatible(md, CompatibilityLevel.PRE_VERSION_5)) {
hasFreeInput = true;
break;
}
} else {
hasFreeInput = true;
break;
}
}
}
if (!hasFreeInput) {
return false;
}
for (OutputPort port : operator.getOutputPorts().getAllPorts()) {
if (!port.isConnected()) {
return true;
}
}
return false;
}
/**
* Returns the height for the specified {@link Operator}.
*
* @param operator
* the operator in question
* @return
*/
public static double calcHeighForOperator(Operator operator) {
double calcHeight = 40 + ProcessRendererModel.PORT_SIZE * 3 / 2
* Math.max(operator.getInputPorts().getNumberOfPorts(), operator.getOutputPorts().getNumberOfPorts());
double height = Math.max(ProcessRendererModel.MIN_OPERATOR_HEIGHT, calcHeight);
return height;
}
/**
* Returns the given icon in an appropriate enabled/disabled state.
*
* @param operator
* if the operator is enabled, returns the passed icon directly, otherwise it is
* displayed as disabled
* @param icon
* the icon
* @return the original icon (if the given operator is enabled) or the icon in a disabled state
*/
public static ImageIcon getIcon(final Operator operator, final ImageIcon icon) {
if (operator.isEnabled()) {
return icon;
} else {
return (ImageIcon) UIManager.getLookAndFeel().getDisabledIcon(DUMMY_LABEL, icon);
}
}
/**
* Calculates the preferred size for the given {@link ExecutionUnit} based on the total
* available target renderer size.
* <p>
* Note that if the currently displayed chain (see
* {@link ProcessRendererModel#getDisplayedChain()}) has more than one {@link ExecutionUnit},
* the returned size will accommodate the simultaneous display of all execution units in the
* given size.
* </p>
*
* @param model
* the model required to calculate the size
* @param unit
* the process for which the initial size should be created
* @param width
* the total width which is available
* @param height
* the total height which is available
* @return the size, never {@code null}
* @since 7.5
*/
public static Dimension calculatePreferredSize(final ProcessRendererModel model, final ExecutionUnit unit, int width,
int height) {
int processes = model.getProcesses().size();
int wallSpace = (processes - 1) * 2 * ProcessDrawer.WALL_WIDTH;
return new Dimension((width - wallSpace) / processes, height);
}
}