/**
* 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;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.swing.Action;
import javax.swing.JPopupMenu;
import javax.swing.JToggleButton;
import com.rapidminer.gui.RapidMinerGUI;
import com.rapidminer.gui.actions.ToggleAction;
import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawDecorator;
import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer;
import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel;
import com.rapidminer.gui.flow.processrendering.view.ProcessEventDecorator;
import com.rapidminer.gui.flow.processrendering.view.ProcessRendererView;
import com.rapidminer.gui.flow.processrendering.view.RenderPhase;
import com.rapidminer.gui.tools.ResourceAction;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.operator.ExecutionUnit;
import com.rapidminer.operator.Operator;
import com.rapidminer.operator.ports.InputPort;
/**
* This class lets the user view and edit the execution order of a process.
*
* @author Simon Fischer
*
*/
public class FlowVisualizer {
private static final Stroke FLOW_STROKE = new BasicStroke(10f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
private static final Font FLOW_FONT = new Font("Dialog", Font.BOLD, 18);
private static final Stroke LINE_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
private static final Stroke HIGHLIGHT_STROKE = new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
private static final Color PASSIVE_COLOR = new Color(0, 0, 0, 50);
private static final Color FLOW_COLOR = new Color(SwingTools.RAPIDMINER_LIGHT_ORANGE.getRed(),
SwingTools.RAPIDMINER_LIGHT_ORANGE.getGreen(), SwingTools.RAPIDMINER_LIGHT_ORANGE.getBlue(), 125);
private static final Color GRAY_OUT = new Color(255, 255, 255, 100);
public final ToggleAction ALTER_EXECUTION_ORDER = new ToggleAction(true, "render_execution_order") {
private static final long serialVersionUID = -8333670355512143502L;
@Override
public void actionToggled(ActionEvent e) {
if (isSelected()) {
// update execution order before visualizing it
RapidMinerGUI.getMainFrame().getProcess().getRootOperator().updateExecutionOrder();
}
setActive(isSelected());
view.requestFocusInWindow();
}
};
protected JToggleButton SHOW_ORDER_TOGGLEBUTTON = ALTER_EXECUTION_ORDER.createToggleButton();
private final Action BRING_TO_FRONT = new ResourceAction("bring_operator_to_front") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
if (hoveringOperator != null) {
hoveringOperator.getExecutionUnit().moveToIndex(hoveringOperator, 0);
}
}
};
private boolean active = false;
private final ProcessRendererView view;
private Operator startOperator;
private Operator endOperator;
private Operator hoveringOperator;
private Collection<Operator> dependentOps;
/** the decorator which draws the flow visualization */
private final ProcessEventDecorator eventDecorator = new ProcessEventDecorator() {
@Override
public void processMouseEvent(ExecutionUnit process, MouseEventType type, MouseEvent e) {
// ignore if not active
if (!isActive()) {
return;
}
switch (type) {
case MOUSE_CLICKED:
showPopupMenu(e);
break;
case MOUSE_MOVED:
hoveringOperator = findOperator(e.getPoint());
if (startOperator != null) {
if (hoveringOperator != startOperator) {
endOperator = hoveringOperator;
recomputeDependentOperators();
view.repaint();
}
}
break;
case MOUSE_PRESSED:
if (showPopupMenu(e)) {
return;
}
Operator op = findOperator(e.getPoint());
switch (e.getButton()) {
case MouseEvent.BUTTON1:
if (startOperator == null) {
if (op != startOperator) {
startOperator = op;
dependentOps = null;
recomputeDependentOperators();
view.repaint();
}
} else if (dependentOps != null) {
startOperator.getExecutionUnit().bringToFront(dependentOps, startOperator);
startOperator = endOperator = null;
dependentOps = null;
view.repaint();
}
break;
case MouseEvent.BUTTON3:
startOperator = endOperator = null;
dependentOps = null;
view.repaint();
break;
}
break;
case MOUSE_RELEASED:
showPopupMenu(e);
break;
// $CASES-OMITTED$
default:
break;
}
// no matter what, while flow visualizer is active we consume all events
e.consume();
}
@Override
public void processKeyEvent(ExecutionUnit process, KeyEventType type, KeyEvent e) {
// ignore if not active
if (!isActive()) {
return;
}
if (type == KeyEventType.KEY_PRESSED) {
if (KeyEvent.VK_ESCAPE == e.getKeyCode()) {
SHOW_ORDER_TOGGLEBUTTON.doClick();
}
}
// no matter what, while flow visualizer is active we consume all events
e.consume();
}
};
/** the decorator which draws the flow visualization */
private final ProcessDrawDecorator drawDecorator = new ProcessDrawDecorator() {
@Override
public void draw(ExecutionUnit process, Graphics2D g2, ProcessRendererModel model) {
if (active) {
// Re-Arrange operators
List<Operator> operators = new LinkedList<Operator>(process.getOperators());
if (dependentOps != null) {
operators.removeAll(dependentOps);
int insertionIndex = operators.indexOf(startOperator) + 1;
for (Operator depOp : dependentOps) {
operators.add(insertionIndex++, depOp);
}
}
// they should be sorted already.
Point2D lastPoint = null;
g2.setStroke(FLOW_STROKE);
for (Operator op : operators) {
if (!op.isEnabled()) {
continue;
}
Rectangle2D r = view.getModel().getOperatorRect(op);
if (startOperator == null || dependentOps != null && dependentOps.contains(op)) {
g2.setColor(FLOW_COLOR);
} else {
g2.setColor(PASSIVE_COLOR);
}
if (lastPoint != null) {
g2.draw(new Line2D.Double(lastPoint.getX(), lastPoint.getY(), r.getCenterX(), r.getCenterY()
+ ProcessDrawer.HEADER_HEIGHT / 2 - 2));
}
lastPoint = new Point2D.Double(r.getCenterX(), r.getCenterY() + ProcessDrawer.HEADER_HEIGHT / 2 - 2);
}
int i = 0;
g2.setStroke(LINE_STROKE);
g2.setFont(FLOW_FONT);
boolean illegalStart = operators.indexOf(endOperator) < operators.indexOf(startOperator);
for (Operator op : operators) {
if (!op.isEnabled()) {
continue;
}
i++;
Rectangle2D r = view.getModel().getOperatorRect(op);
int size = 30;
double y = r.getCenterY() + ProcessDrawer.HEADER_HEIGHT / 2 - 2;
// gray out operator rect
Color oldC = g2.getColor();
g2.setColor(GRAY_OUT);
g2.fill(r);
g2.setColor(oldC);
Ellipse2D circle = new Ellipse2D.Double(r.getCenterX() - size / 2, y - size / 2, size, size);
// Fill circle
if (illegalStart && op == endOperator) {
g2.setColor(Color.red);
} else if (op == startOperator || op == endOperator) {
g2.setColor(SwingTools.LIGHT_BLUE);
} else if (dependentOps != null && dependentOps.contains(op)) {
g2.setColor(SwingTools.LIGHT_BLUE);
} else {
g2.setColor(Color.WHITE);
}
g2.fill(circle);
// Draw circle
if (op == hoveringOperator || startOperator == null || startOperator == op || dependentOps != null
&& dependentOps.contains(op)) {
g2.setColor(Color.BLACK);
} else {
g2.setColor(Color.LIGHT_GRAY);
}
if (op == hoveringOperator) {
g2.setStroke(HIGHLIGHT_STROKE);
} else {
g2.setStroke(LINE_STROKE);
}
g2.draw(circle);
String label = "" + i;
Rectangle2D bounds = FLOW_FONT.getStringBounds(label, g2.getFontRenderContext());
g2.drawString(label, (float) (r.getCenterX() - bounds.getWidth() / 2), (float) (y - bounds.getHeight()
/ 2 - bounds.getY()));
}
}
}
@Override
public void print(ExecutionUnit process, Graphics2D g2, ProcessRendererModel model) {
draw(process, g2, model);
}
};
public FlowVisualizer(ProcessRendererView processRenderer) {
this.view = processRenderer;
processRenderer.addEventDecorator(eventDecorator, RenderPhase.OVERLAY);
processRenderer.addDrawDecorator(drawDecorator, RenderPhase.OVERLAY);
SHOW_ORDER_TOGGLEBUTTON.setText(null);
}
/**
* Activates/Deactivates the flow visualizer.
*
* @param active
*/
private void setActive(boolean active) {
if (this.active != active) {
this.active = active;
if (!active) {
startOperator = null;
startOperator = endOperator = null;
dependentOps = null;
}
view.repaint();
}
}
/**
* Returns whether the flow visualizer is active.
*
* @return
*/
public boolean isActive() {
return active;
}
/**
* Return the operators the specified one depends on.
*
* @param enclosingOperator
* @param startIndex
* @param endIndex
* @param topologicallySortedCandidates
* @return
*/
private Collection<Operator> getDependingOperators(Operator enclosingOperator, int startIndex, int endIndex,
List<Operator> topologicallySortedCandidates) {
if (endIndex <= startIndex) {
return Collections.emptyList();
}
Set<Operator> foundDependingOperators = new HashSet<Operator>();
Set<Operator> completedOperators = new HashSet<Operator>();
Operator stopWhenReaching = topologicallySortedCandidates.get(startIndex);
foundDependingOperators.add(topologicallySortedCandidates.get(endIndex));
for (int opIndex = endIndex; opIndex > startIndex; opIndex--) {
Operator op = topologicallySortedCandidates.get(opIndex);
// remember that we are already working on this one
completedOperators.add(op);
// Do we depend on that one? Otherwise, we can continue with the next.
// (The startIndex-th operator is always in this set, so we actually start doing
// something.)
if (!foundDependingOperators.contains(op)) {
continue;
}
for (InputPort in : op.getInputPorts().getAllPorts()) {
if (in.isConnected()) {
Operator predecessor = in.getSource().getPorts().getOwner().getOperator();
// Skip if connected to inner sink
if (predecessor == enclosingOperator) {
continue;
} else {
// Skip if working on it already
if (completedOperators.contains(predecessor)) {
continue;
// Skip when reaching end of the range
} else if (predecessor == stopWhenReaching) { // did we reach the end?
continue;
} else {
// Skip when beyond bounds
int predecessorIndex = topologicallySortedCandidates.indexOf(predecessor);
if (predecessorIndex <= startIndex) {
continue;
} else {
// Otherwise, add to set of depending operators
foundDependingOperators.add(predecessor);
}
}
}
}
}
}
List<Operator> orderedResult = new LinkedList<Operator>();
for (Operator op : topologicallySortedCandidates) {
if (foundDependingOperators.contains(op)) {
orderedResult.add(op);
}
}
return orderedResult;
}
/**
* Finds the operator under the given point.
*
* @param point
* the point in question
* @return the operator or {@code null}
*/
private Operator findOperator(Point point) {
int processIndex = view.getProcessIndexUnder(point);
if (processIndex != -1) {
Point mousePositionRelativeToProcess = view.toProcessSpace(point, processIndex);
if (mousePositionRelativeToProcess == null) {
return null;
}
for (Operator op : view.getModel().getDisplayedChain().getSubprocess(processIndex).getOperators()) {
Rectangle2D rect = view.getModel().getOperatorRect(op);
if (rect.contains(new Point2D.Double(mousePositionRelativeToProcess.x, mousePositionRelativeToProcess.y))) {
return op;
}
}
}
return null;
}
/**
* Calculate operator dependencies.
*/
private void recomputeDependentOperators() {
if (startOperator == null || endOperator == null) {
dependentOps = null;
} else {
ExecutionUnit unit = startOperator.getExecutionUnit();
if (endOperator.getExecutionUnit() != unit) {
dependentOps = null;
return;
} else {
List<Operator> operators = unit.getOperators();
dependentOps = getDependingOperators(view.getModel().getDisplayedChain(), operators.indexOf(startOperator),
operators.indexOf(endOperator), operators);
}
}
}
/**
* Determine whether to show the popup menu.
*
* @param e
* @return
*/
private boolean showPopupMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
JPopupMenu menu = new JPopupMenu();
if (hoveringOperator != null) {
menu.add(BRING_TO_FRONT);
}
// Add action to leave FlowVisualiser and add seperator if it is not the only action.
if (menu.getSubElements().length > 0) {
menu.addSeparator();
}
menu.add(new ResourceAction("render_execution_order_apply") {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
ALTER_EXECUTION_ORDER.actionPerformed(e);
}
});
menu.show(view, e.getX(), e.getY());
e.consume();
return true;
} else {
return false;
}
}
}