/*
* RapidMiner
*
* Copyright (C) 2001-2011 by Rapid-I and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapid-i.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.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
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.actions.ToggleAction;
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.RAPID_I_ORANGE.getRed(),
SwingTools.RAPID_I_ORANGE.getGreen(),
SwingTools.RAPID_I_ORANGE.getBlue(), 150);
// protected JToggleButton SHOW_ORDER_TOGGLEBUTTON = new JToggleButton(new ResourceAction(true, "render_execution_order") {
// private static final long serialVersionUID = 1L;
// @Override
// public void actionPerformed(ActionEvent e) {
// setActive(SHOW_ORDER_TOGGLEBUTTON.isSelected());
// processRenderer.repaint();
// }
// });
public final ToggleAction ALTER_EXECUTION_ORDER = new ToggleAction(true, "render_execution_order") {
private static final long serialVersionUID = -8333670355512143502L;
@Override
public void actionToggled(ActionEvent e) {
setActive(isSelected());
processRenderer.repaint();
}
};
protected JToggleButton SHOW_ORDER_TOGGLEBUTTON = ALTER_EXECUTION_ORDER.createToggleButton();
public final Action SHOW_EXECUTION_ORDER = new ResourceAction("show_execution_order") {
private static final long serialVersionUID = 3932329413268066576L;
@Override
public void actionPerformed(ActionEvent e) {
setActive(true);
processRenderer.repaint();
StringBuilder b = new StringBuilder();
for (ExecutionUnit unit : processRenderer.getDisplayedChain().getSubprocesses()) {
b.append("<strong>").append(unit.getName()).append("</strong><br/><ol>");
for (Operator op : unit.topologicalSort()) {
b.append("<li>").append(op.getName()).append("</li>");
}
b.append("</ol>");
}
SwingTools.showLongMessage("execution_order_info", b.toString());
setActive(ALTER_EXECUTION_ORDER.isSelected());
processRenderer.repaint();
}
};
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 ProcessRenderer processRenderer;
private Operator startOperator;
private Operator endOperator;
private Operator hoveringOperator;
private Collection<Operator> dependentOps;
public FlowVisualizer(ProcessRenderer processRenderer2) {
this.processRenderer = processRenderer2;
installListeners();
SHOW_ORDER_TOGGLEBUTTON.setText(null);
}
private void installListeners() {
processRenderer.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (isActive()) {
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();
processRenderer.repaint();
}
} else if (dependentOps != null) {
startOperator.getExecutionUnit().bringToFront(dependentOps, startOperator);
startOperator = endOperator = null;
dependentOps = null;
processRenderer.repaint();
}
break;
case MouseEvent.BUTTON3:
startOperator = endOperator = null;
dependentOps = null;
processRenderer.repaint();
break;
}
//e.consume();
}
}
@Override
public void mouseClicked(MouseEvent e) {
if (isActive()) {
if (showPopupMenu(e)) {
return;
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (isActive()) {
if (showPopupMenu(e)) {
return;
}
}
}
});
processRenderer.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
if (isActive()) {
hoveringOperator = findOperator(e.getPoint());
if (startOperator != null) {
if (hoveringOperator != startOperator) {
endOperator = hoveringOperator;
recomputeDependentOperators();
processRenderer.repaint();
}
}
//e.consume();
}
}
});
}
public void setActive(boolean active) {
this.active = active;
}
public boolean isActive() {
return active;
}
public void render(Graphics2D g, ExecutionUnit process) {
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.
//GeneralPath p = new GeneralPath();
Point2D lastPoint = null;
g.setStroke(FLOW_STROKE);
for (Operator op : operators) {
if (!op.isEnabled()) continue;
Rectangle2D r = processRenderer.getOperatorRect(op, true);
if ((startOperator == null) || ((dependentOps != null) && dependentOps.contains(op))) {
g.setColor(FLOW_COLOR);
} else {
g.setColor(PASSIVE_COLOR);
}
if (lastPoint != null) {
g.draw(new Line2D.Double(lastPoint.getX(), lastPoint.getY(), r.getCenterX(), r.getCenterY()));
}
lastPoint = new Point2D.Double(r.getCenterX(), r.getCenterY());
}
int i = 0;
g.setStroke(LINE_STROKE);
g.setFont(FLOW_FONT);
boolean illegalStart = operators.indexOf(endOperator) < operators.indexOf(startOperator);
for (Operator op : operators) {
if (!op.isEnabled()) continue;
i++;
Rectangle2D r = processRenderer.getOperatorRect(op, true);
int size = 30;
double y = (r.getMinY() + ProcessRenderer.HEADER_HEIGHT) + (r.getHeight() - ProcessRenderer.HEADER_HEIGHT - 10)/2;
Ellipse2D circle = new Ellipse2D.Double(r.getCenterX() - size/2, y - size/2, size, size);
// Fill circle
if (illegalStart && (op == endOperator)) {
g.setColor(Color.red);
} else if ((op == startOperator) || (op == endOperator)) {
g.setColor(SwingTools.LIGHT_BLUE);
} else if ((dependentOps != null) && dependentOps.contains(op)) {
g.setColor(SwingTools.LIGHT_BLUE);
} else {
g.setColor(Color.WHITE);
}
g.fill(circle);
// Draw circle
if ((op == hoveringOperator) || (startOperator == null) || (startOperator == op) || ((dependentOps != null) && dependentOps.contains(op))) {
g.setColor(Color.BLACK);
} else {
g.setColor(Color.LIGHT_GRAY);
}
if (op == hoveringOperator) {
g.setStroke(HIGHLIGHT_STROKE);
} else {
g.setStroke(LINE_STROKE);
}
g.draw(circle);
String label = ""+i;
Rectangle2D bounds = FLOW_FONT.getStringBounds(label, g.getFontRenderContext());
g.drawString(label, (float)(r.getCenterX()-bounds.getWidth()/2), (float)(y - (bounds.getHeight())/2 - bounds.getY()));
}
}
}
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 contine 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;
}
public Operator findOperator(Point point) {
int processIndex = processRenderer.getProcessIndexUnder(point);
if (processIndex != -1) {
Point mousePositionRelativeToProcess = processRenderer.toProcessSpace(point, processIndex);
for (Operator op : processRenderer.getDisplayedChain().getSubprocess(processIndex).getOperators()) {
Rectangle2D rect = processRenderer.getOperatorRect(op, true);
if (rect.contains(new Point2D.Double(mousePositionRelativeToProcess.x, mousePositionRelativeToProcess.y))) {
return op;
}
}
}
return null;
}
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(processRenderer.getDisplayedChain(),
operators.indexOf(startOperator),
operators.indexOf(endOperator),
operators);
}
}
}
private boolean showPopupMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
if (hoveringOperator != null) {
JPopupMenu menu = new JPopupMenu();
menu.add(BRING_TO_FRONT);
menu.show(processRenderer, e.getX(), e.getY());
return true;
} else {
return false;
}
} else {
return false;
}
}
}