/* * 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.Component; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Stroke; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Arc2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.RectangularShape; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.WeakHashMap; import java.util.logging.Level; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JTextField; import javax.swing.JViewport; import javax.swing.UIManager; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.rapidminer.BreakpointListener; import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.actions.ConnectPortToRepositoryAction; import com.rapidminer.gui.actions.StoreInRepositoryAction; import com.rapidminer.gui.dnd.OperatorTransferHandler; import com.rapidminer.gui.dnd.ReceivingOperatorTransferHandler; import com.rapidminer.gui.tools.PrintingTools; import com.rapidminer.gui.tools.ResourceAction; import com.rapidminer.gui.tools.ResourceMenu; import com.rapidminer.gui.tools.SwingTools; import com.rapidminer.gui.tools.components.ToolTipWindow; import com.rapidminer.gui.tools.components.ToolTipWindow.TipProvider; import com.rapidminer.io.process.ProcessXMLFilter; import com.rapidminer.io.process.ProcessXMLFilterRegistry; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.IOObject; import com.rapidminer.operator.IOObjectCollection; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.ProcessRootOperator; import com.rapidminer.operator.ProcessSetupError.Severity; import com.rapidminer.operator.ResultObject; import com.rapidminer.operator.io.RepositorySource; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.InputPorts; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.OutputPorts; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.PortException; import com.rapidminer.operator.ports.Ports; import com.rapidminer.operator.ports.metadata.CollectionMetaData; import com.rapidminer.operator.ports.metadata.CompatibilityLevel; import com.rapidminer.operator.ports.metadata.ExampleSetMetaData; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.operator.ports.metadata.MetaDataError; import com.rapidminer.operator.ports.metadata.Precondition; import com.rapidminer.operator.ports.quickfix.QuickFix; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.repository.gui.RepositoryLocationChooser; import com.rapidminer.tools.ClassColorMap; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.ParentResolvingMap; /** * This class renders a process graph. It also stores all data about visualization * like location and size of the operators. This data is stored in a weak hash map * so it should be garbage collected when the operators are removed. * * @author Simon Fischer */ public class ProcessRenderer extends JPanel { private enum Orientation { X_AXIS, Y_AXIS } private static Orientation ORIENTATION = Orientation.X_AXIS; 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()); } catch (IOException e) { LogService.getRoot().warning("Cannot load operator group colors."); } } /** * This method adds the colors of the given property file to the global group colors */ public static void registerAdditionalGroupColors(String groupProperties, String pluginName, ClassLoader classLoader) { SwingTools.registerAdditionalGroupColors(groupProperties, pluginName, classLoader); } /** * This method adds the colors of the given property file to the io object colors */ public static void registerAdditionalObjectColors(String groupProperties, String pluginName, ClassLoader classLoader) { try { IO_CLASS_TO_COLOR_MAP.parseProperties(groupProperties, "io.", ".color", classLoader); } catch (IOException e) { LogService.getRoot().warning("Cannot load io object colors for plugin " + pluginName + "."); } } private final transient TipProvider tipProvider = new TipProvider() { @Override public Object getIdUnder(Point point) { if (connectingPortSource == null) { return hoveringPort; } else { return null; } } @Override public String getTip(Object o) { Port port = (Port) o; StringBuilder tip = new StringBuilder(); if (displayedChain instanceof ProcessRootOperator) { if (port.getPorts() == displayedChain.getSubprocess(0).getInnerSources()) { int index = displayedChain.getSubprocess(0).getInnerSources().getAllPorts().indexOf(port); List<String> locations = displayedChain.getProcess().getContext().getInputRepositoryLocations(); if (index >= 0 && index < locations.size()) { String dest = locations.get(index); tip.append("Loaded from: ").append(dest).append("<br/>"); } } else if (port.getPorts() == displayedChain.getSubprocess(0).getInnerSinks()) { int index = displayedChain.getSubprocess(0).getInnerSinks().getAllPorts().indexOf(port); List<String> locations = displayedChain.getProcess().getContext().getOutputRepositoryLocations(); if (index >= 0 && index < locations.size()) { String dest = locations.get(index); tip.append("Stored at: ").append(dest).append("<br/>"); } } } tip.append("<strong>"); tip.append(port.getSpec()); tip.append("</strong> ("); tip.append(port.getName()); tip.append(")<br/>"); tip.append("<em>Meta data:</em> "); MetaData metaData = port.getMetaData(); if (metaData != null) { if (metaData instanceof ExampleSetMetaData) { tip.append(((ExampleSetMetaData) metaData).getShortDescription()); } else { tip.append(metaData.getDescription()); } tip.append("<br/><em>Generated by:</em> "); tip.append(metaData.getGenerationHistoryAsHTML()); tip.append("<br>"); } else { tip.append("-<br/>"); } IOObject data = port.getAnyDataOrNull(); if (data != null) { tip.append("</br><em>Data:</em> "); tip.append(data.toString()); tip.append("<br/>"); } tip.append(port.getDescription()); if (!port.getErrors().isEmpty()) { boolean hasErrors = false; boolean hasWarnings = false; for (MetaDataError error : port.getErrors()) { if (error.getSeverity() == Severity.ERROR) hasErrors = true; if (error.getSeverity() == Severity.WARNING) hasWarnings = true; } if (hasErrors) { tip.append("<br/><strong style=\"color:red\">"); tip.append(port.getErrors().size()); tip.append(" error(s):</strong>"); for (MetaDataError error : port.getErrors()) { if (error.getSeverity() == Severity.ERROR) { tip.append("<br/> "); tip.append(error.getMessage()); } } } if (hasWarnings) { tip.append("<br/><strong style=\"color:#FFA500\">"); tip.append(port.getErrors().size()); tip.append(" warnings(s):</strong>"); for (MetaDataError error : port.getErrors()) { if (error.getSeverity() == Severity.WARNING) { tip.append("<br/> "); tip.append(error.getMessage()); } } } } return tip.toString(); } @Override public Component getCustomComponent(Object o) { Port hoveringPort = (Port) o; MetaData metaData = hoveringPort.getMetaData(); if (metaData != null && metaData instanceof ExampleSetMetaData) { return ExampleSetMetaDataTableModel.makeTableForToolTip((ExampleSetMetaData) metaData); } else { return null; } } }; public ResourceAction RENAME_ACTION = new ResourceAction("rename_in_processrenderer") { { setCondition(OPERATOR_SELECTED, MANDATORY); } private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (!getSelection().isEmpty()) { rename(getSelection().get(0)); } } }; public ResourceAction SELECT_ALL_ACTION = new ResourceAction("select_all") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { selectedOperators.clear(); for (ExecutionUnit unit : processes) { selectedOperators.addAll(unit.getOperators()); } mainFrame.selectOperators(selectedOperators); repaint(); } }; public Action ARRANGE_OPERATORS_ACTION = new ResourceAction(true, "arrange_operators") { private static final long serialVersionUID = 4636292007315749350L; @Override public void actionPerformed(ActionEvent e) { for (ExecutionUnit u : processes) { autoArrange(u); } } }; public Action AUTO_FIT_ACTION = new ResourceAction(true, "auto_fit") { private static final long serialVersionUID = 3932329413268066576L; @Override public void actionPerformed(ActionEvent ae) { autoFit(); } }; private final ResourceAction DELETE_SELECTED_CONNECTION = new ResourceAction("delete_selected_connection") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (selectedConnectionSource != null) { if (selectedConnectionSource.isConnected()) { selectedConnectionSource.disconnect(); repaint(); } } else { mainFrame.getActions().DELETE_OPERATOR_ACTION.actionPerformed(e); } } }; private class ChangeSizeAction extends ResourceAction { private static final long serialVersionUID = 1L; private final int dx, dy; private ChangeSizeAction(String key, int dx, int dy) { super("processrenderer." + key); this.dx = dx; this.dy = dy; } @Override public void actionPerformed(ActionEvent e) { if (hoveringProcessIndex == -1) { return; } ExecutionUnit unit = processes[hoveringProcessIndex]; changeProcessSize(unit, dx, dy); } } public final Action INCREASE_PROCESS_LAYOUT_WIDTH_ACTION = new ChangeSizeAction("increase_width", +GRID_WIDTH, 0); public final Action DECREASE_PROCESS_LAYOUT_WIDTH_ACTION = new ChangeSizeAction("decrease_width", -GRID_WIDTH, 0); public final Action INCREASE_PROCESS_LAYOUT_HEIGHT_ACTION = new ChangeSizeAction("increase_height", 0, +GRID_HEIGHT); public final Action DECREASE_PROCESS_LAYOUT_HEIGHT_ACTION = new ChangeSizeAction("decrease_height", 0, -GRID_HEIGHT); private final InterpolationMap nameRolloutInterpolationMap = new InterpolationMap(this); private static JLabel DUMMY_LABEL = new JLabel(); private static int OPERATOR_WIDTH = 5 * 16 + 2 * 5; // 5 mini icons + padding private static int MIN_OPERATOR_HEIGHT = 60; private static int PORT_SIZE = 12; private static int PADDING = 10; private static int WALL_WIDTH = 25; // 25 private static int GRID_WIDTH = OPERATOR_WIDTH * 3 / 4; private static int GRID_HEIGHT = MIN_OPERATOR_HEIGHT * 3 / 4; private static int GRID_X_OFFSET = OPERATOR_WIDTH / 2; private static int GRID_Y_OFFSET = MIN_OPERATOR_HEIGHT / 2; private static int GRID_AUTOARRANGE_WIDTH = OPERATOR_WIDTH * 3 / 2; private static int GRID_AUTOARRANGE_HEIGHT = MIN_OPERATOR_HEIGHT * 3 / 2; private static RenderingHints HI_QUALITY_HINTS = new RenderingHints(null); private static RenderingHints LOW_QUALITY_HINTS = new RenderingHints(null); static { HI_QUALITY_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); HI_QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); LOW_QUALITY_HINTS.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); LOW_QUALITY_HINTS.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } private static ImageIcon IMAGE_WARNING = SwingTools.createIcon("16/sign_warning.png"); private static ImageIcon IMAGE_BREAKPOINT_WITHIN = SwingTools.createIcon("16/breakpoint.png"); private static ImageIcon IMAGE_BREAKPOINTS = SwingTools.createIcon("16/breakpoints.png"); private static ImageIcon IMAGE_BREAKPOINT_BEFORE = SwingTools.createIcon("16/breakpoint_up.png"); private static ImageIcon IMAGE_BREAKPOINT_AFTER = SwingTools.createIcon("16/breakpoint_down.png"); private static ImageIcon IMAGE_BRANCH = SwingTools.createIcon("16/elements_selection.png"); private static ImageIcon IMAGE_COMMENT = SwingTools.createIcon("16/document_text.png"); private static ImageIcon OPERATOR_RUNNING = SwingTools.createIcon("16/bullet_triangle_glass_green.png"); private static ImageIcon OPERATOR_READY = SwingTools.createIcon("16/bullet_ball_glass_green.png"); private static ImageIcon OPERATOR_DIRTY = SwingTools.createIcon("16/bullet_ball_glass_yellow.png"); private static ImageIcon OPERATOR_ERROR_ICON = SwingTools.createIcon("16/bullet_ball_glass_red.png"); private static final long serialVersionUID = 1L; private static Color INNER_COLOR = Color.WHITE; private static Color SHADOW_COLOR = Color.LIGHT_GRAY; private static Color LINE_COLOR = Color.DARK_GRAY; private static Stroke LINE_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static Stroke HIGHLIGHT_STROKE = new BasicStroke(2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static Stroke SELECTION_RECT_STROKE = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 5f, new float[] { 2f, 2f }, 0f); private static Paint SELECTION_RECT_PAINT = Color.GRAY; private static Color PROCESS_TITLE_COLOR = SHADOW_COLOR; private static Paint SHADOW_TOP_GRADIENT = new GradientPaint(0, 0, SHADOW_COLOR, PADDING, 0, Color.WHITE); private static Paint SHADOW_LEFT_GRADIENT = new GradientPaint(0, 0, SHADOW_COLOR, 0, PADDING, Color.WHITE); private static Stroke CONNECTION_LINE_STROKE = new BasicStroke(1.3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static Stroke CONNECTION_HIGHLIGHT_STROKE = new BasicStroke(2.2f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static Stroke CONNECTION_COLLECTION_LINE_STROKE = new BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static Stroke CONNECTION_COLLECTION_HIGHLIGHT_STROKE = new BasicStroke(4f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static Font OPERATOR_FONT = new Font("Dialog", Font.BOLD, 11); protected static final int HEADER_HEIGHT = OPERATOR_FONT.getSize() + 7; private static final Font PROCESS_FONT = new Font("Dialog", Font.BOLD, 12); private static final Font PORT_FONT = new Font("Dialog", Font.PLAIN, 9); private static final Color PORT_NAME_COLOR = Color.DARK_GRAY; private static final Color PORT_NAME_SELECTION_COLOR = Color.GRAY; private static final Color ACTIVE_EDGE_COLOR = SwingTools.RAPID_I_ORANGE; private static final Stroke FRAME_STROKE_SELECTED = new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Stroke FRAME_STROKE_NORMAL = new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); private static final Color FRAME_COLOR_SELECTED = SwingTools.RAPID_I_ORANGE; private static final Color FRAME_COLOR_NORMAL = LINE_COLOR; // private static int PROCESS_HEIGHT = 400; /** The widths of the individual subprocesses. */ private transient final Map<ExecutionUnit, Dimension> processSizes = new WeakHashMap<ExecutionUnit, Dimension>(); /** Distances between ports. */ private transient final Map<Port, Double> portSpacings = new WeakHashMap<Port, Double>(); /** The displayed processes. */ private ExecutionUnit[] processes = new ExecutionUnit[0]; /** Maps operators to their positions (in process coordinate space). */ private transient final Map<Operator, Rectangle2D> operatorRects = new WeakHashMap<Operator, Rectangle2D>(); private static int PORT_OFFSET = OPERATOR_FONT.getSize() + 6 + PORT_SIZE; private Point currentMousePosition = null; private Point mousePositionAtDragStart = null; private Point mousePositionAtLastEvaluation = null; private boolean hasDragged = false; private Rectangle2D selectionRectangle = null; private Map<Operator, Rectangle2D> draggedOperatorsOrigins; /** Port currently being dragged (within this component only!) */ private Port draggedPort = null; /** Operator selected by a click. */ private final LinkedList<Operator> selectedOperators = new LinkedList<Operator>(); /** Operator under the mouse cursor. */ private Operator hoveringOperator = null; /** Port under the mouse cursor. */ private Port hoveringPort = null; /** Source port of the connection currently being created. */ private OutputPort connectingPortSource = null; /** Index of the process under the mouse. */ private int hoveringProcessIndex = -1; /** Operator after which the dropped operator will be added. */ private Operator dropInsertionPredecessor; /** Source port of the connector hit by the drop cursor. */ private OutputPort hoveringConnectionSource; /** * Source port of the connection selected by clicking on it. * * @see #hoveringConnectionSource */ private OutputPort selectedConnectionSource; private final ProcessPanel processPanel; private final MainFrame mainFrame; private Point mousePositionRelativeToProcess = null; private final List<ExtensionButton> subprocessExtensionButtons = new LinkedList<ExtensionButton>(); private OperatorChain displayedChain; private final ReceivingOperatorTransferHandler transferHandler; private final FlowVisualizer flowVisualizer = new FlowVisualizer(this); public ProcessRenderer(ProcessPanel processPanel, MainFrame mainFrame) { new PanningManager(this); ProcessXMLFilterRegistry.registerFilter(new GUIProcessXMLFilter()); this.mainFrame = mainFrame; this.processPanel = processPanel; setLayout(null); // for absolute positioning of tipPane transferHandler = new ReceivingOperatorTransferHandler() { private static final long serialVersionUID = 7526109471182298215L; @Override public boolean dropNow(final List<Operator> newOperators, Point loc) { ProcessRenderer.this.mainFrame.getStatusBar().clearSpecialText(); if (newOperators.isEmpty()) { return true; } List<Operator> selection = getSelection(); // if we don't have a loc, we can use the mouse cursor if (loc == null && (selection == null || selection.isEmpty() || selection.size() == 1 && selection.get(0) == displayedChain)) { loc = currentMousePosition; } // determine process to drop to int processIndex; if (loc != null) { processIndex = getProcessIndexUnder(loc.getLocation()); } else { if (selection != null && !selection.isEmpty()) { processIndex = Arrays.asList(processes).indexOf(selection.get(0).getExecutionUnit()); if (processIndex == -1) { processIndex = 0; } } else { processIndex = 0; } } try { if (processIndex != -1) { if (loc != null) { // this is a drop Operator firstOperator = newOperators.get(0); Point dest = toProcessSpace(loc, processIndex); // if we drop a single Retrieve operator on an inner source of the root op, // we immediately attach the repository location to the port. boolean isRoot = displayedChain instanceof ProcessRootOperator; boolean dropsSource = firstOperator instanceof RepositorySource; if (isRoot && dropsSource && newOperators.size() == 1) { if (checkPortUnder(processes[processIndex].getInnerSources(), (int) dest.getX(), (int) dest.getY())) { String location = firstOperator.getParameters().getParameterOrNull(RepositorySource.PARAMETER_REPOSITORY_ENTRY); int index = hoveringPort.getPorts().getAllPorts().indexOf(hoveringPort); displayedChain.getProcess().getContext().setInputRepositoryLocation(index, location); return true; } } operatorRects.put(firstOperator, new Rectangle2D.Double(dest.getX() - OPERATOR_WIDTH / 2, dest.getY() - MIN_OPERATOR_HEIGHT / 2, OPERATOR_WIDTH, MIN_OPERATOR_HEIGHT)); // index at which the first operator is inserted int firstInsertionIndex; final boolean firstMustBeWired; // insert first operator. Possibly insert into connection if (hoveringConnectionSource != null && canBeInsertedIntoConnection(firstOperator)) { int predecessorIndex = processes[processIndex].getOperators().indexOf(hoveringConnectionSource.getPorts().getOwner().getOperator()); if (predecessorIndex != -1) { firstInsertionIndex = predecessorIndex + 1; } else { // can happen if dropIntersectsOutputPort is an inner source firstInsertionIndex = getDropInsertionIndex(processIndex); } processes[processIndex].addOperator(firstOperator, firstInsertionIndex); insertIntoHoveringConnection(firstOperator); firstMustBeWired = false; } else { firstInsertionIndex = getDropInsertionIndex(processIndex); processes[processIndex].addOperator(firstOperator, firstInsertionIndex); firstMustBeWired = true; } // insert the rest (1..n). First, insert, then wire for (int i = 1; i < newOperators.size(); i++) { Operator newOp = newOperators.get(i); processes[processIndex].addOperator(newOp, firstInsertionIndex + i); // TODO: inserting operators relative to first operator } AutoWireThread.autoWireInBackground(newOperators, firstMustBeWired); } else { // this is a paste. Insert all, then wire all for (Operator newOp : newOperators) { processes[processIndex].addOperator(newOp); } AutoWireThread.autoWireInBackground(newOperators, true); for (Operator newOp : newOperators) { // auto position as a side effect getOperatorRect(newOp, true); } } boolean first = true; for (Operator op : newOperators) { ProcessRenderer.this.selectOperator(op, first); first = false; } dropInsertionPredecessor = null; return true; } else { dropInsertionPredecessor = null; return false; } } catch (RuntimeException e) { LogService.getRoot().log(Level.WARNING, "During drop: " + e, e); throw e; } } @Override protected boolean isDropLocationOk(List<Operator> newOperators, Point loc) { if (!isEnabled()) { return false; } if (getProcessIndexUnder(loc) == -1) { return false; } else { for (Operator newOperator : newOperators) { if (newOperator instanceof OperatorChain) { if (displayedChain == newOperator || ((OperatorChain) newOperator).getAllInnerOperators().contains(displayedChain)) { return false; } } } return true; } } @Override protected void markDropOver(Point dropPoint) { int pid = ProcessRenderer.this.getProcessIndexUnder(dropPoint); if (pid != -1) { Point processSpace = toProcessSpace(dropPoint, pid); hoveringConnectionSource = getPortForConnectorNear(processSpace, processes[pid]); setDropInsertionPredecessor(getClosestLeftNeighbour(processSpace, processes[pid])); } repaint(); } @Override protected List<Operator> getDraggedOperators() { return getSelection(); } /** * Returns the index at which an operator should be inserted. The operator * is inserted after {@link #dropInsertionPredecessor} or as the last * operator if {@link #dropInsertionPredecessor} is null. */ private int getDropInsertionIndex(int processIndex) { if (dropInsertionPredecessor == null) { return processes[processIndex].getOperators().size(); } else { return dropInsertionPredecessor.getExecutionUnit().getOperators().indexOf(dropInsertionPredecessor) + 1; } } @Override protected void dropEnds() { } @Override protected Process getProcess() { return displayedChain.getProcess(); } }; setTransferHandler(transferHandler); addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_BACK_SPACE: if (displayedChain != null && displayedChain.getParent() != null) { ProcessRenderer.this.mainFrame.selectOperator(displayedChain.getParent()); } e.consume(); break; case KeyEvent.VK_LEFT: case KeyEvent.VK_RIGHT: case KeyEvent.VK_UP: case KeyEvent.VK_DOWN: if (e.isControlDown()) { changeProcessSize(e, displayedChain.getSubprocess(0)); } else { selectInDirection(e); } e.consume(); break; case KeyEvent.VK_ENTER: if (!getSelection().isEmpty()) { Operator selected = getSelection().get(0); if (selected instanceof OperatorChain) { ProcessRenderer.this.processPanel.showOperatorChain((OperatorChain) selected); } } e.consume(); break; } } }); ((ResourceAction) mainFrame.getActions().TOGGLE_BREAKPOINT[BreakpointListener.BREAKPOINT_AFTER]).addToActionMap(this, WHEN_FOCUSED); ((ResourceAction) mainFrame.getActions().TOGGLE_ACTIVATION_ITEM).addToActionMap(this, WHEN_FOCUSED); RENAME_ACTION.addToActionMap(this, WHEN_FOCUSED); SELECT_ALL_ACTION.addToActionMap(this, WHEN_FOCUSED); DELETE_SELECTED_CONNECTION.addToActionMap(this, WHEN_FOCUSED); OperatorTransferHandler.addToActionMap(this); new ToolTipWindow(tipProvider, this); init(); } protected int getIndex(ExecutionUnit executionUnit) { for (int i = 0; i < processes.length; i++) { if (processes[i] == executionUnit) { return i; } } return -1; } private void init() { addMouseMotionListener(MOUSE_HANDLER); addMouseListener(MOUSE_HANDLER); setPreferredSize(new Dimension(1000, 440)); setMinimumSize(new Dimension(100, 100)); setMaximumSize(new Dimension(2000, 2000)); // flowVisualizer.installListeners(); } /** Adapts the process size according to the key event. */ private void changeProcessSize(KeyEvent e, ExecutionUnit unit) { // Dimension size = processSizes.get(unit); switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: changeProcessSize(unit, -GRID_WIDTH, 0); break; case KeyEvent.VK_RIGHT: changeProcessSize(unit, +GRID_WIDTH, 0); break; case KeyEvent.VK_UP: changeProcessSize(unit, 0, -GRID_HEIGHT); break; case KeyEvent.VK_DOWN: changeProcessSize(unit, 0, +GRID_HEIGHT); break; } } private void changeProcessSize(ExecutionUnit unit, int dx, int dy) { if (unit == null) { return; } Dimension size = processSizes.get(unit); if (dx == 0) { size = new Dimension((int) size.getWidth(), (int) getHeight(unit, true) + dy); } else { size = new Dimension((int) getWidth(unit, true) + dx, (int) size.getHeight()); } processSizes.put(unit, size); balance(); updateComponentSize(); } /** Starting from the current selection, selects the first operator in the given direction. */ private void selectInDirection(KeyEvent e) { int keyCode = e.getKeyCode(); if (getSelection().isEmpty()) { for (ExecutionUnit unit : processes) { if (unit.getNumberOfOperators() > 0) { selectOperator(unit.getOperators().get(0), true); } } } else { Operator current = getSelection().get(0); if (current.getParent() != displayedChain) { return; } Rectangle2D pos = getOperatorRect(current, true); ExecutionUnit unit = current.getExecutionUnit(); if (unit == null) { return; } double smallestDistance = Double.POSITIVE_INFINITY; Operator closest = null; for (Operator other : unit.getOperators()) { Rectangle2D otherPos = getOperatorRect(other, true); boolean ok = false; switch (keyCode) { case KeyEvent.VK_LEFT: ok = otherPos.getMinX() < pos.getMinX(); break; case KeyEvent.VK_RIGHT: ok = otherPos.getMaxX() > pos.getMaxX(); break; case KeyEvent.VK_UP: ok = otherPos.getMinY() < pos.getMinY(); break; case KeyEvent.VK_DOWN: ok = otherPos.getMaxY() > pos.getMaxY(); break; } if (ok) { double dx = otherPos.getCenterX() - pos.getCenterX(); double dy = otherPos.getCenterY() - pos.getCenterY(); double dist = dx * dx + dy * dy; if (dist < smallestDistance) { smallestDistance = dist; closest = other; } } } if (closest != null) { selectOperator(closest, !e.isShiftDown()); } } } protected void showOperatorChain(OperatorChain op) { displayedChain = op; ExecutionUnit processes[]; if (op == null) { processes = new ExecutionUnit[0]; } else { processes = new ExecutionUnit[op.getNumberOfSubprocesses()]; for (int i = 0; i < processes.length; i++) { processes[i] = op.getSubprocess(i); } } showProcesses(processes); } private void showProcesses(ExecutionUnit[] processes) { this.processes = processes; setInitialSizes(processes); setupExtensionButtons(); updateComponentSize(); repaint(); } private void setupExtensionButtons() { for (ExtensionButton button : subprocessExtensionButtons) { remove(button); } subprocessExtensionButtons.clear(); if (displayedChain.areSubprocessesExtendable()) { for (int index = 0; index < processes.length; index++) { double width = getWidth(processes[index]) + 1; Point loc = fromProcessSpace(new Point(0, 0), index); if (index == 0) { ExtensionButton addButton2 = new ExtensionButton(displayedChain, -1, true); addButton2.setBounds((int) (loc.getX() - addButton2.getPreferredSize().getWidth() + 1), (int) (loc.getY() - 1), (int) addButton2.getPreferredSize().getWidth(), (int) addButton2.getPreferredSize().getHeight()); subprocessExtensionButtons.add(addButton2); add(addButton2); } ExtensionButton addButton = new ExtensionButton(displayedChain, index, true); addButton.setBounds((int) (loc.getX() + width), (int) (loc.getY() - 1), (int) addButton.getPreferredSize().getWidth(), (int) addButton.getPreferredSize().getHeight()); subprocessExtensionButtons.add(addButton); add(addButton); if (processes.length > 1) { ExtensionButton deleteButton = new ExtensionButton(displayedChain, index, false); deleteButton.setBounds((int) (loc.getX() + width), (int) (loc.getY() + addButton.getHeight() - 1), (int) deleteButton.getPreferredSize().getWidth(), (int) deleteButton.getPreferredSize().getHeight()); subprocessExtensionButtons.add(deleteButton); add(deleteButton); } } } } private void updateExtensionButtons() { for (ExtensionButton button : subprocessExtensionButtons) { int subprocessIndex = button.getSubprocessIndex(); if (subprocessIndex >= 0) { Point loc = fromProcessSpace(new Point(0, 0), subprocessIndex); double width = getWidth(processes[subprocessIndex]) + 1; button.setBounds((int) (loc.getX() + width), (int) loc.getY() + (button.isAdd() ? 0 : button.getHeight()) - 1, (int) button.getPreferredSize().getWidth(), (int) button.getPreferredSize().getHeight()); } else { Point loc = fromProcessSpace(new Point(0, 0), 0); button.setBounds((int) loc.getX() - button.getWidth(), (int) loc.getY() + (button.isAdd() ? 0 : button.getHeight()) - 1, (int) button.getPreferredSize().getWidth(), (int) button.getPreferredSize().getHeight()); } } } @Override public void paintComponent(Graphics graphics) { super.paintComponent(graphics); snapToGrid = !"false".equals(ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_SNAP_TO_GRID)); if (draggedOperatorsOrigins != null || connectingPortSource != null) { ((Graphics2D) graphics).setRenderingHints(LOW_QUALITY_HINTS); } else { ((Graphics2D) graphics).setRenderingHints(HI_QUALITY_HINTS); } render((Graphics2D) graphics); } /** * Returns the operators rectangle. * * @param autoPositionIfMissing * If true and no position has been defined yet, a position will be * created, either based on the position of adjacent operators or based on the index. */ Rectangle2D getOperatorRect(Operator op, boolean autoPositionIfMissing) { Rectangle2D rect = operatorRects.get(op); if (rect == null && autoPositionIfMissing) { // if connected (e.g. because inserted by quick fix), place in the middle if (op.getInputPorts().getNumberOfPorts() > 0 && op.getOutputPorts().getNumberOfPorts() > 0 && op.getInputPorts().getPortByIndex(0).isConnected() && op.getOutputPorts().getPortByIndex(0).isConnected()) { // to avoid that this method is called again from getPortLocation() we check whether all children know where they are. boolean dependenciesOk = true; Operator sourceOp = op.getInputPorts().getPortByIndex(0).getSource().getPorts().getOwner().getOperator(); Operator destOp = op.getOutputPorts().getPortByIndex(0).getDestination().getPorts().getOwner().getOperator(); dependenciesOk &= sourceOp == displayedChain || operatorRects.containsKey(sourceOp); dependenciesOk &= destOp == displayedChain || operatorRects.containsKey(destOp); if (dependenciesOk) { Point2D sourcePos = getPortLocation(op.getInputPorts().getPortByIndex(0).getSource()); Point2D destPos = getPortLocation(op.getOutputPorts().getPortByIndex(0).getDestination()); rect = new Rectangle2D.Double((sourcePos.getX() + destPos.getX()) / 2 - OPERATOR_WIDTH / 2, (sourcePos.getY() + destPos.getY()) / 2 - PORT_OFFSET, OPERATOR_WIDTH, MIN_OPERATOR_HEIGHT); setOperatorRect(op, rect); } } if (rect == null) { // otherwise, or, if positions were not known in previous approach, position according to index int index = 0; ExecutionUnit unit = op.getExecutionUnit(); if (unit != null) { index = unit.getOperators().indexOf(op); } // rect is added to operatorRects as a side effect rect = autoPosition(op, index); } } return rect; } /** Sets the operators position and increases the process size if necessary. */ private void setOperatorRect(Operator operator, Rectangle2D rect) { if (rect == null) { throw new NullPointerException("rect is null"); } operatorRects.put(operator, rect); Dimension processSize = processSizes.get(operator.getExecutionUnit()); if (processSize != null) { boolean needsResize = false; if (processSize.getWidth() < rect.getMaxX() + PADDING) { processSize.setSize(rect.getMaxX() + PADDING, processSize.getHeight()); needsResize = true; } if (processSize.getHeight() < rect.getMaxY() + PADDING) { processSize.setSize(processSize.getWidth(), rect.getMaxY() + PADDING); needsResize = true; } if (needsResize) { updateComponentSize(); } } } /** * Returns the distance to the next port below this port. Note that * this spacing is additional to the 3/2*PORT_SIZE raster used by {@link #getPortLocation(Port)}. */ private double getPortSpacing(Port port) { Double d = portSpacings.get(port); if (d != null) { return d; } else { return 0; } } /** Set spacing and reduce spacing for successor if possible. */ private double shiftPortSpacing(Port port, double delta) { // remember old spacing final Ports ports = port.getPorts(); final int myIndex = ports.getAllPorts().indexOf(port); final Double oldD = portSpacings.get(port); final double old = oldD == null ? 0 : oldD; double newY = old + delta; if (isSnapToGrid()) { newY = Math.floor(newY / (PORT_SIZE * 3d / 2d)) * (PORT_SIZE * 3 / 2); } double diff = newY - old; if (diff == 0) { return 0; } else if (diff > 0) { // find ports which this port will "push" down for (int i = myIndex + 1; i < ports.getNumberOfPorts(); i++) { Port other = ports.getPortByIndex(i); double otherSpacing = getPortSpacing(other); if (otherSpacing < diff) { portSpacings.remove(other); } else { portSpacings.put(other, otherSpacing - diff); break; } } // see if it still fits into process frame portSpacings.put(port, old + diff); Point bottomPortPos = getPortLocation(ports.getPortByIndex(ports.getNumberOfPorts() - 1)); // if it doesn't, revert double height = getHeight(ports.getOwner().getConnectionContext()) - PADDING; if (bottomPortPos.getY() > height) { double tooMuch = bottomPortPos.getY() - height; diff -= tooMuch; portSpacings.put(port, old + diff); } return diff; } else if (diff < 0) { // find ports which this port will "push" up double actuallyRemoved = 0; for (int i = myIndex; i >= 0; i--) { Port other = ports.getPortByIndex(i); double otherSpacing = getPortSpacing(other); if (otherSpacing < -diff) { actuallyRemoved += getPortSpacing(other); portSpacings.remove(other); } else { portSpacings.put(other, otherSpacing + diff); actuallyRemoved = -diff; break; } } if (ports.getNumberOfPorts() > myIndex + 1) { Port other = ports.getPortByIndex(myIndex + 1); portSpacings.put(other, getPortSpacing(other) + actuallyRemoved); } return -actuallyRemoved; } else { // cannot happen return 0; } } private Point getPortLocation(Port port) { 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; for (int i = 0; i <= index; i++) { addOffset += getPortSpacing(port.getPorts().getPortByIndex(i)); } ExecutionUnit process; Point point; if (op == displayedChain) { // this is an inner port process = port.getPorts().getOwner().getConnectionContext(); if (port instanceof OutputPort) { point = new Point(0, MIN_OPERATOR_HEIGHT / 2 + PORT_OFFSET + index * PORT_SIZE * 3 / 2 + addOffset); } else { point = new Point((int) getWidth(process), MIN_OPERATOR_HEIGHT / 2 + PORT_OFFSET + index * PORT_SIZE * 3 / 2 + addOffset); } } else { // this is an outer port of a nested operator process = op.getExecutionUnit(); Rectangle2D opRect = getOperatorRect(op, true); if (port instanceof InputPort) { point = new Point((int) opRect.getX(), (int) opRect.getY() + PORT_OFFSET + index * PORT_SIZE * 3 / 2 + addOffset); } else { point = new Point((int) opRect.getMaxX(), (int) opRect.getY() + PORT_OFFSET + index * PORT_SIZE * 3 / 2 + addOffset); } } return point; } public void renderSubprocess(int index, Graphics2D g) { double width = getWidth(processes[index]); double height = getHeight(processes[index]); Shape frame = new Rectangle2D.Double(0, 0, width, height); g.setPaint(INNER_COLOR); g.fill(frame); g.setColor(PROCESS_TITLE_COLOR); g.setFont(PROCESS_FONT); g.drawString(processes[index].getName(), PADDING + 2, PROCESS_FONT.getSize() + PADDING); g.setPaint(SHADOW_TOP_GRADIENT); g.fill(new Rectangle2D.Double(0, 0, PADDING, height)); GeneralPath top = new GeneralPath(); int shadowWidth = PADDING; top.moveTo(0, 0); top.lineTo(width, 0); top.lineTo(width, shadowWidth); top.lineTo(shadowWidth, shadowWidth); top.closePath(); g.setPaint(SHADOW_LEFT_GRADIENT); g.fill(top); g.setPaint(LINE_COLOR); g.setStroke(LINE_STROKE); g.draw(frame); // render operators: as a side effect the port locations are stored for (Operator operator : processes[index].getOperators()) { if (!selectedOperators.contains(operator)) { renderOperator(operator, g); } } Iterator<Operator> selectionIterator = selectedOperators.descendingIterator(); while (selectionIterator.hasNext()) { Operator op = selectionIterator.next(); if (processes[index].getOperators().contains(op)) { renderOperator(op, g); } } // render ports renderPorts(processes[index].getInnerSources(), null, g, true); renderPorts(processes[index].getInnerSinks(), null, g, true); renderConnections(processes[index].getInnerSources(), g); flowVisualizer.render(g, processes[index]); } private void renderPorts(Ports<? extends Port> ports, Color baseColor, Graphics2D g, boolean enabled) { boolean input = ports instanceof InputPorts; g.setStroke(LINE_STROKE); for (Port port : ports.getAllPorts()) { boolean hasError = !port.getErrors().isEmpty(); Point location = getPortLocation(port); double x = location.getX(); double y = location.getY(); Shape ellipseTop, ellipseBottom, ellipseBoth; int startAngle; if (input) { startAngle = 90; ellipseTop = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE / 2, y - PORT_SIZE / 2, PORT_SIZE, PORT_SIZE), startAngle, 90, Arc2D.PIE); ellipseBottom = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE / 2, y - PORT_SIZE / 2, PORT_SIZE, PORT_SIZE), startAngle + 90, 90, Arc2D.PIE); ellipseBoth = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE / 2, y - PORT_SIZE / 2, PORT_SIZE, PORT_SIZE), startAngle, 180, Arc2D.PIE); } else { startAngle = 270; ellipseBottom = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE / 2, y - PORT_SIZE / 2, PORT_SIZE, PORT_SIZE), startAngle, 90, Arc2D.PIE); ellipseTop = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE / 2, y - PORT_SIZE / 2, PORT_SIZE, PORT_SIZE), startAngle + 90, 90, Arc2D.PIE); ellipseBoth = new Arc2D.Double(new Rectangle2D.Double(x - PORT_SIZE / 2, y - PORT_SIZE / 2, PORT_SIZE, PORT_SIZE), startAngle, 180, Arc2D.PIE); } Color line = Color.DARK_GRAY; Color fill = Color.WHITE; if (enabled) { // What we have if (!hasError) { fill = getColorFor(port, false, Color.WHITE); } else { fill = Color.RED; } g.setColor(fill); if (port instanceof OutputPort) { g.fill(ellipseBoth); } // What we want if (port instanceof InputPort) { g.fill(ellipseTop); InputPort inPort = (InputPort) port; for (Precondition precondition : inPort.getAllPreconditions()) { g.setColor(getColorFor(precondition.getExpectedMetaData())); break; } g.fill(ellipseBottom); } g.setColor(line); if (hoveringPort == port) { g.setStroke(HIGHLIGHT_STROKE); } else { g.setStroke(LINE_STROKE); } g.draw(ellipseBoth); } else { g.setColor(fill); g.fill(ellipseBoth); g.setColor(line); g.draw(ellipseBoth); } g.setFont(PORT_FONT); int xt; Rectangle2D strBounds = PORT_FONT.getStringBounds(port.getShortName(), g.getFontRenderContext()); if (input) { xt = PORT_SIZE / 2; } else { xt = -(int) strBounds.getWidth() - 3; } if (hasError) { g.setColor(Color.RED); g.setFont(PORT_FONT.deriveFont(Font.BOLD)); } else { if (baseColor == null) { if (port == hoveringPort) { g.setColor(PORT_NAME_SELECTION_COLOR); } else { g.setColor(PORT_NAME_COLOR); } } else { if (port == hoveringPort) { g.setPaint(baseColor.darker()); } else { g.setPaint(baseColor.darker().darker()); } } } g.drawString(port.getShortName(), (int) x + xt, (int) (y + strBounds.getHeight() / 2 - 2)); } } /** Returns the given icon in an appropriate enabled/disabled state. */ private ImageIcon getIcon(Operator operator, ImageIcon icon) { if (operator.isEnabled() && this.isEnabled()) { return icon; } else { return (ImageIcon) UIManager.getLookAndFeel().getDisabledIcon(DUMMY_LABEL, icon); } } private void renderOperator(Operator operator, Graphics2D g) { Rectangle2D frame = getOperatorRect(operator, true); double height = Math.max(MIN_OPERATOR_HEIGHT, 40 + PORT_SIZE * 3 / 2 * Math.max(operator.getInputPorts().getNumberOfPorts(), operator.getOutputPorts().getNumberOfPorts())); if (frame.getHeight() != height) { frame.setRect(frame.getX(), frame.getY(), frame.getWidth(), height); } final double headerHeight = HEADER_HEIGHT; double headerWidth; Shape bodyShape; double nameRollout = nameRolloutInterpolationMap.getValue(operator); if (nameRollout > 0) { // if (operator == getHoveringOperator()) { Rectangle2D nameBounds = OPERATOR_FONT.getStringBounds(operator.getName(), g.getFontRenderContext()); headerWidth = nameBounds.getWidth() + 6; if (headerWidth > frame.getWidth()) { double dif = headerWidth - frame.getWidth(); headerWidth = frame.getWidth() + nameRollout * dif; GeneralPath path = new GeneralPath(); path.moveTo(frame.getMinX(), frame.getMinY()); path.lineTo(frame.getMinX() + headerWidth, frame.getMinY()); path.lineTo(frame.getMinX() + headerWidth, frame.getMinY() + headerHeight); path.lineTo(frame.getMaxX(), frame.getMinY() + headerHeight); path.lineTo(frame.getMaxX(), frame.getMaxY()); path.lineTo(frame.getMinX(), frame.getMaxY()); path.closePath(); bodyShape = path; } else { headerWidth = frame.getWidth(); bodyShape = frame; } } else { headerWidth = frame.getWidth(); bodyShape = frame; } // Shadow if (!selectedOperators.isEmpty() && operator == selectedOperators.get(0) || dropInsertionPredecessor == operator) { Rectangle2D shadow = new Rectangle2D.Double(frame.getX() + 5, frame.getY() + 5, frame.getWidth(), frame.getHeight()); GeneralPath bottom = new GeneralPath(); bottom.moveTo(shadow.getX(), frame.getMaxY()); bottom.lineTo(frame.getMaxX(), frame.getMaxY()); bottom.lineTo(shadow.getMaxX(), shadow.getMaxY()); bottom.lineTo(shadow.getMinX(), shadow.getMaxY()); bottom.closePath(); g.setPaint(new GradientPaint((float) frame.getX(), (float) frame.getMaxY(), Color.gray, (float) frame.getX(), (float) shadow.getMaxY(), INNER_COLOR)); g.fill(bottom); GeneralPath right = new GeneralPath(); right.moveTo(frame.getMaxX(), shadow.getMinY()); right.lineTo(shadow.getMaxX(), shadow.getMinY()); right.lineTo(shadow.getMaxX(), shadow.getMaxY()); right.lineTo(frame.getMaxX(), frame.getMaxY()); right.closePath(); g.setPaint(new GradientPaint((float) frame.getMaxX(), (float) shadow.getY(), Color.gray, (float) shadow.getMaxX(), (float) shadow.getY(), INNER_COLOR)); g.fill(right); } // Frame head Color baseColor = SwingTools.getOperatorColor(operator); if (!operator.isEnabled() || !this.isEnabled()) { baseColor = Color.LIGHT_GRAY; } g.setPaint(baseColor); g.fill(frame); // head gradient Rectangle2D bar = new Rectangle2D.Double(frame.getX(), frame.getY(), headerWidth, headerHeight); Color c0 = new Color(Math.max(baseColor.getRed() - 25, 0), Math.max(baseColor.getGreen() - 25, 0), Math.max(baseColor.getBlue() - 25, 0)); Color c1 = baseColor; Rectangle2D[] regions = splitHorizontalBar(bar, 0.0, 0.2, 0.6); GradientPaint gp = new GradientPaint(0.0f, (float) regions[0].getMinY(), c0, 0.0f, (float) regions[0].getMaxX(), c1); g.setPaint(gp); g.fill(regions[0]); gp = new GradientPaint(0.0f, (float) regions[1].getMinY(), c1, 0.0f, (float) regions[1].getMaxY(), Color.WHITE); g.setPaint(gp); g.fill(regions[1]); gp = new GradientPaint(0.0f, (float) regions[2].getMinY(), Color.WHITE, 0.0f, (float) regions[2].getMaxY(), c1); g.setPaint(gp); g.fill(regions[2]); gp = new GradientPaint(0.0f, (float) regions[3].getMinY(), c1, 0.0f, (float) regions[3].getMaxY(), c0.darker()); g.setPaint(gp); g.fill(regions[3]); // Frame Body g.setPaint(LINE_COLOR); g.setStroke(LINE_STROKE); if (selectedOperators.contains(operator) || operator == dropInsertionPredecessor) { g.setPaint(FRAME_COLOR_SELECTED); g.setStroke(FRAME_STROKE_SELECTED); } else { g.setPaint(FRAME_COLOR_NORMAL); g.setStroke(FRAME_STROKE_NORMAL); } g.draw(bodyShape); // Label: Name g.setFont(OPERATOR_FONT); if (operator.isEnabled()) { if (operator == getHoveringOperator()) { g.setPaint(baseColor.darker()); } else if (selectedOperators.contains(operator)) { g.setPaint(baseColor.darker()); } else { g.setPaint(baseColor.darker().darker()); } } else { g.setPaint(baseColor.darker().darker()); } // if (operator != getHoveringOperator()) { g.drawString(fitString(operator.getName(), g, (int) headerWidth - 3), (int) frame.getX() + 4, (int) (frame.getY() + OPERATOR_FONT.getSize() + 1)); // } else { // g.drawString(operator.getName(), (int)frame.getX() + 4, (int)(frame.getY() + OPERATOR_FONT.getSize() + 1)); // } // Icon ImageIcon icon = operator.getOperatorDescription().getIcon(); if (icon != null) { if (!operator.isEnabled()) { icon = getIcon(operator, icon); } icon.paintIcon(this, g, (int) (frame.getX() + frame.getWidth() / 2 - icon.getIconWidth() / 2), (int) (frame.getY() + headerHeight + (height - headerHeight - 10) / 2 - icon.getIconHeight() / 2)); } // Ports renderConnections(operator.getOutputPorts(), g); renderPorts(operator.getInputPorts(), baseColor, g, operator.isEnabled()); renderPorts(operator.getOutputPorts(), baseColor, g, operator.isEnabled()); // Small icons int iconX = (int) frame.getX() + 3; // Dirtyness ImageIcon opIcon; if (operator.isRunning()) { opIcon = OPERATOR_RUNNING; } else if (!operator.isDirty()) { opIcon = OPERATOR_READY; } else if (!operator.getErrorList().isEmpty()) { opIcon = OPERATOR_ERROR_ICON; } else { opIcon = OPERATOR_DIRTY; } getIcon(operator, opIcon).paintIcon(this, g, iconX, (int) (frame.getY() + frame.getHeight() - opIcon.getIconHeight() - 1)); iconX += opIcon.getIconWidth() + 1; // Errors if (!operator.getErrorList().isEmpty()) { getIcon(operator, IMAGE_WARNING).paintIcon(this, g, iconX, (int) (frame.getY() + frame.getHeight() - IMAGE_WARNING.getIconHeight() - 1)); } iconX += IMAGE_WARNING.getIconWidth() + 1; // Breakpoint if (operator.hasBreakpoint()) { ImageIcon breakpointIcon; if (operator.getNumberOfBreakpoints() == 1) { if (operator.hasBreakpoint(BreakpointListener.BREAKPOINT_BEFORE)) { breakpointIcon = IMAGE_BREAKPOINT_BEFORE; } else if (operator.hasBreakpoint(BreakpointListener.BREAKPOINT_AFTER)) { breakpointIcon = IMAGE_BREAKPOINT_AFTER; } else { breakpointIcon = IMAGE_BREAKPOINT_WITHIN; } } else { breakpointIcon = IMAGE_BREAKPOINTS; } getIcon(operator, breakpointIcon).paintIcon(this, g, iconX, (int) (frame.getY() + frame.getHeight() - breakpointIcon.getIconHeight() - 1)); } iconX += IMAGE_BREAKPOINTS.getIconWidth() + 1; // Comment if (operator.getUserDescription() != null && operator.getUserDescription().length() > 0) { getIcon(operator, IMAGE_COMMENT).paintIcon(this, g, iconX, (int) (frame.getY() + frame.getHeight() - IMAGE_COMMENT.getIconHeight() - 1)); } iconX += IMAGE_COMMENT.getIconWidth() + 1; if (operator instanceof OperatorChain) { getIcon(operator, IMAGE_BRANCH).paintIcon(this, g, iconX, (int) (frame.getY() + frame.getHeight() - IMAGE_BRANCH.getIconHeight() - 1)); } iconX += IMAGE_BRANCH.getIconWidth() + 1; } private Rectangle2D[] splitHorizontalBar(RectangularShape bar, double a, double b, double c) { Rectangle2D[] result = new Rectangle2D[4]; double y0 = bar.getMinY(); double y1 = Math.rint(y0 + bar.getHeight() * a); double y2 = Math.rint(y0 + bar.getHeight() * b); double y3 = Math.rint(y0 + bar.getHeight() * c); result[0] = new Rectangle2D.Double(bar.getMinX(), bar.getMinY(), bar.getWidth(), y1 - y0); result[1] = new Rectangle2D.Double(bar.getMinX(), y1, bar.getWidth(), y2 - y1); result[2] = new Rectangle2D.Double(bar.getMinX(), y2, bar.getWidth(), y3 - y2); result[3] = new Rectangle2D.Double(bar.getMinX(), y3, bar.getWidth(), bar.getMaxY() - y3); return result; } private Color getColorFor(Port port, boolean alwaysDark, Color defaultColor) { if (!isEnabled()) { return Color.LIGHT_GRAY; } IOObject data = port.getAnyDataOrNull(); 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 (port.getMetaData() != null) { MetaData md = port.getMetaData(); return getColorFor(md); } else { return defaultColor; } } public static Color getColorFor(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()); } } private void renderConnections(OutputPorts ports, Graphics2D g) { for (int i = 0; i < ports.getNumberOfPorts(); i++) { OutputPort from = ports.getPortByIndex(i); Port to = from.getDestination(); g.setColor(getColorFor(from, true, Color.LIGHT_GRAY)); if (to != null) { if (from.getMetaData() instanceof CollectionMetaData) { if (from == hoveringPort || to == hoveringPort || from == hoveringConnectionSource || from == selectedConnectionSource) { g.setStroke(CONNECTION_COLLECTION_HIGHLIGHT_STROKE); } else { g.setStroke(CONNECTION_COLLECTION_LINE_STROKE); } g.draw(createConnector(from, to)); g.setColor(Color.white); g.setStroke(LINE_STROKE); g.draw(createConnector(from, to)); } else { if (from == hoveringPort || to == hoveringPort || from == hoveringConnectionSource || from == selectedConnectionSource) { g.setStroke(CONNECTION_HIGHLIGHT_STROKE); } else { g.setStroke(CONNECTION_LINE_STROKE); } // g.draw(new Line2D.Double(portLocations.get(from), portLocations.get(to))); g.draw(createConnector(from, to)); } } if (connectingPortSource == from && mousePositionRelativeToProcess != null) { g.setColor(ACTIVE_EDGE_COLOR); Point2D fromLoc = getPortLocation(connectingPortSource); g.draw(new Line2D.Double(fromLoc.getX() + PORT_SIZE / 2, fromLoc.getY(), mousePositionRelativeToProcess.getX(), mousePositionRelativeToProcess.getY())); } } } public void render(Graphics2D graphics) { if (processes == null || processes.length == 0) { return; } Graphics2D g = (Graphics2D) graphics.create(); g.translate(0, -1); g.translate(0, PADDING); for (int i = 0; i < processes.length; i++) { switch (ORIENTATION) { case X_AXIS: g.translate(WALL_WIDTH, 0); break; case Y_AXIS: g.translate(0, PADDING); break; } renderSubprocess(i, g); switch (ORIENTATION) { case X_AXIS: g.translate(getWidth(processes[i]) + WALL_WIDTH, 0); break; case Y_AXIS: g.translate(0, getHeight(processes[i]) + PADDING); break; } } g.translate(0, PADDING); if (selectionRectangle != null) { Graphics2D selG = (Graphics2D) graphics.create(); selG.setPaint(SELECTION_RECT_PAINT); selG.setStroke(SELECTION_RECT_STROKE); selG.draw(selectionRectangle); selG.dispose(); } g.dispose(); } private final transient MouseAdapter MOUSE_HANDLER = new MouseAdapter() { private boolean pressHasSelected = false; @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseMoved(MouseEvent e) { currentMousePosition = e.getPoint(); if (flowVisualizer.isActive()) { return; } if (connectingPortSource != null) { repaint(); } updateHoveringState(e); } private void updateHoveringState(MouseEvent e) { hoveringProcessIndex = getProcessIndexUnder(e.getPoint()); if (hoveringProcessIndex != -1) { mousePositionRelativeToProcess = toProcessSpace(e.getPoint(), hoveringProcessIndex); int relativeX = (int) mousePositionRelativeToProcess.getX(); int relativeY = (int) mousePositionRelativeToProcess.getY(); OutputPort connectionSourceUnderMouse = getPortForConnectorNear(mousePositionRelativeToProcess, processes[hoveringProcessIndex]); if (connectionSourceUnderMouse != hoveringConnectionSource) { hoveringConnectionSource = connectionSourceUnderMouse; repaint(); } // find inner sinks/sources under mouse if (checkPortUnder(processes[hoveringProcessIndex].getInnerSinks(), relativeX, relativeY) || checkPortUnder(processes[hoveringProcessIndex].getInnerSources(), relativeX, relativeY)) { return; } // find operator under mouse List<Operator> operators = processes[hoveringProcessIndex].getOperators(); ListIterator<Operator> iterator = operators.listIterator(operators.size()); while (iterator.hasPrevious()) { Operator op = iterator.previous(); // first, check whether we are over a port if (checkPortUnder(op.getInputPorts(), relativeX, relativeY) || checkPortUnder(op.getOutputPorts(), relativeX, relativeY)) { return; } // If not, check operator. Rectangle2D rect = getOperatorRect(op, true); if (rect.contains(new Point2D.Double(relativeX, relativeY))) { if (getHoveringOperator() != op) { hoveringPort = null; setHoveringOperator(op); updateCursor(); if (getHoveringOperator() instanceof OperatorChain) { showStatus("Double-click to zoom, drag to move."); } else { showStatus("Drag to move."); } repaint(); } return; } } } if (getHoveringOperator() != null) { setHoveringOperator(null); updateCursor(); repaint(); } if (hoveringPort != null) { hoveringPort = null; updateCursor(); repaint(); } clearStatus(); } @Override public void mousePressed(MouseEvent e) { if (flowVisualizer.isActive()) { return; } if (renameField != null) { remove(renameField); } requestFocus(); pressHasSelected = false; mousePositionAtDragStart = e.getPoint(); mousePositionAtLastEvaluation = e.getPoint(); hasDragged = false; if (e.isPopupTrigger()) { if (showPopupMenu(e)) { return; } } if (e.getButton() == MouseEvent.BUTTON1) { if (selectedConnectionSource != hoveringConnectionSource) { selectedConnectionSource = hoveringConnectionSource; repaint(); } } if (e.getButton() == MouseEvent.BUTTON2) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); return; } if (e.getButton() == MouseEvent.BUTTON1 && hoveringPort != null) { if (e.isAltDown()) { if (hoveringPort instanceof OutputPort) { if (((OutputPort) hoveringPort).isConnected()) { ((OutputPort) hoveringPort).disconnect(); } } else if (hoveringPort instanceof InputPort) { if (((InputPort) hoveringPort).isConnected()) { ((InputPort) hoveringPort).getSource().disconnect(); } } repaint(); } else { if (hoveringPort instanceof OutputPort) { if (connectingPortSource == null) { connectingPortSource = (OutputPort) hoveringPort; } else { connectingPortSource = null; } } else if (hoveringPort instanceof InputPort) { if (connectingPortSource != null) { connectConnectingPortSourceWithHoveringPort(); } } } } else if (getHoveringOperator() == null) { // deselect unless shift is pressed if (!e.isShiftDown() && !e.isControlDown()) { selectOperator(displayedChain, true); } } if (hoveringPort != null) { selectOperator(hoveringPort.getPorts().getOwner().getOperator(), true); pressHasSelected = true; } else { if (getHoveringOperator() == null) { if (!e.isShiftDown() && !e.isControlDown()) { selectOperator(displayedChain, true); pressHasSelected = true; } } } if (getHoveringOperator() != null) { // control down and reducing selection from {A,B,C} to {A} is delayed to mouseReleased if (!e.isControlDown() && !selectedOperators.contains(getHoveringOperator())) { selectOperator(getHoveringOperator(), true, e.isShiftDown()); pressHasSelected = true; } // start dragging draggedOperatorsOrigins = new HashMap<Operator, Rectangle2D>(); for (Operator op : selectedOperators) { if (op.getExecutionUnit() == getHoveringOperator().getExecutionUnit()) { draggedOperatorsOrigins.put(op, (Rectangle2D) getOperatorRect(op, false).clone()); } } // no rollout during drag nameRolloutInterpolationMap.clear(); } else if (hoveringPort != null) { draggedPort = hoveringPort; } else if (e.getButton() == MouseEvent.BUTTON1) { // start selection selectionRectangle = getSelectionRectangle(mousePositionAtDragStart, e.getPoint()); } } private void connectConnectingPortSourceWithHoveringPort() { try { Operator destOp = hoveringPort.getPorts().getOwner().getOperator(); boolean hasConnections = hasConnections(destOp); connect(connectingPortSource, (InputPort) hoveringPort); // move directly after source if first connection if (!hasConnections) { Operator sourceOp = connectingPortSource.getPorts().getOwner().getOperator(); if (destOp != displayedChain && sourceOp != displayedChain) { destOp.getExecutionUnit().moveToIndex(destOp, destOp.getExecutionUnit().getOperators().indexOf(sourceOp) + 1); } } } catch (PortException e1) { if (e1.hasRepairOptions()) { e1.showRepairDialog(ProcessRenderer.this); } else { JOptionPane.showMessageDialog(null, e1.getMessage(), "Cannot connect", JOptionPane.ERROR_MESSAGE); } repaint(); } finally { repaint(); connectingPortSource = null; } } @Override public void mouseReleased(MouseEvent e) { if (flowVisualizer.isActive()) { return; } if ((e.getModifiers() & MouseEvent.BUTTON2_MASK) != 0) { setCursor(Cursor.getDefaultCursor()); return; } if (e.isConsumed()) { return; } if (e.isPopupTrigger()) { if (showPopupMenu(e)) { return; } } if (connectingPortSource != null) { if (e.getButton() == MouseEvent.BUTTON3) { cancelConnectionDragging(); return; } else if (e.getButton() == MouseEvent.BUTTON1 && hoveringPort != null && hoveringPort instanceof InputPort && !e.isAltDown()) { connectConnectingPortSourceWithHoveringPort(); } } try { if (selectionRectangle != null) { if (selectionRectangle.getWidth() > 3 && selectionRectangle.getHeight() > 3) { int processIndex = getProcessIndexUnder(mousePositionAtDragStart); if (processIndex == -1) { processIndex = getProcessIndexUnder(e.getPoint()); } if (processIndex == -1) { processIndex = getProcessIndexUnder(new Point((int) selectionRectangle.getCenterX(), (int) selectionRectangle.getCenterY())); } Point offset = toProcessSpace(new Point(0, 0), processIndex); if (offset != null) { selectionRectangle.setFrame(selectionRectangle.getX() + offset.getX(), selectionRectangle.getY() + offset.getY(), selectionRectangle.getWidth(), selectionRectangle.getHeight()); if (!e.isShiftDown() && !e.isControlDown() || selectedOperators.size() == 1 && selectedOperators.get(0) == displayedChain) { // if we have only // selected the // parent, we // ignore SHIFT and // CTRL selectedOperators.clear(); } for (Operator op : processes[processIndex].getOperators()) { Rectangle2D opRect = getOperatorRect(op, true); if (selectionRectangle.contains(opRect)) { selectOperator(op, false); } } } } selectionRectangle = null; } else { if (hasDragged && draggedOperatorsOrigins != null && draggedOperatorsOrigins.size() == 1) { insertIntoHoveringConnection(getHoveringOperator()); } else if (!hasDragged && getHoveringOperator() != null && !e.isPopupTrigger() && (e.isControlDown() || selectedOperators.contains(getHoveringOperator()) && !pressHasSelected)) { // control and deselection was delayed to mouseReleased selectOperator(getHoveringOperator(), !e.isControlDown(), e.isShiftDown()); } } if (draggedOperatorsOrigins != null || draggedPort != null) { // mainFrame.addToUndoList(); displayedChain.getProcess().updateNotify(); } } finally { mousePositionAtDragStart = null; draggedPort = null; draggedOperatorsOrigins = null; hasDragged = false; } repaint(); } @Override public void mouseDragged(MouseEvent e) { currentMousePosition = e.getPoint(); if (flowVisualizer.isActive()) { return; } // Pan viewport if ((e.getModifiers() & MouseEvent.BUTTON2_MASK) != 0) { if (getParent() instanceof JViewport) { JViewport jv = (JViewport) getParent(); Point p = jv.getViewPosition(); int newX = p.x - (e.getX() - mousePositionAtDragStart.x); int newY = p.y - (e.getY() - mousePositionAtDragStart.y); int maxX = getWidth() - jv.getWidth(); int maxY = getHeight() - jv.getHeight(); if (newX < 0) newX = 0; if (newX > maxX) newX = maxX; if (newY < 0) newY = 0; if (newY > maxY) newY = maxY; jv.setViewPosition(new Point(newX, newY)); return; } } // drag ports if (connectingPortSource != null) { repaint(); // We cannot drag when it is an inner sink: dragging means moving the port. if (connectingPortSource.getPorts().getOwner().getOperator() == displayedChain) { connectingPortSource = null; } } // find process in which we are dragging hoveringProcessIndex = getProcessIndexUnder(e.getPoint()); if (hoveringProcessIndex != -1) { mousePositionRelativeToProcess = toProcessSpace(e.getPoint(), hoveringProcessIndex); } hasDragged = true; if (draggedOperatorsOrigins != null && !draggedOperatorsOrigins.isEmpty()) { ExecutionUnit draggingInSubprocess = draggedOperatorsOrigins.keySet().iterator().next().getExecutionUnit(); Operator hoveringOperator = getHoveringOperator(); if (hoveringOperator != null) { if (draggedOperatorsOrigins.size() == 1) { if (canBeInsertedIntoConnection(hoveringOperator)) { int pid = getIndex(draggingInSubprocess); Point processSpace = toProcessSpace(e.getPoint(), pid); hoveringConnectionSource = getPortForConnectorNear(processSpace, draggingInSubprocess); } } double difX = e.getX() - mousePositionAtDragStart.getX(); double difY = e.getY() - mousePositionAtDragStart.getY(); // hoveringOperator is always included in draggedOperators if (!draggedOperatorsOrigins.containsKey(hoveringOperator)) { draggedOperatorsOrigins.put(hoveringOperator, getOperatorRect(hoveringOperator, false)); } double targetX = draggedOperatorsOrigins.get(hoveringOperator).getX() + difX; double targetY = draggedOperatorsOrigins.get(hoveringOperator).getY() + difY; if (targetX < 0) { targetX = 0; } if (targetY < 0) { targetY = 0; } // use only hovering operator for snapping if (isSnapToGrid()) { Point snapped = snap(new Point2D.Double(targetX, targetY)); targetX = snapped.getX(); targetY = snapped.getY(); } // now, set difX and difY to shift /after/ snapped and clipped difX = targetX - draggedOperatorsOrigins.get(hoveringOperator).getX(); difY = targetY - draggedOperatorsOrigins.get(hoveringOperator).getY(); // bound to subprocess double unitWidth = getWidth(draggingInSubprocess); double unitHeight = getHeight(draggingInSubprocess); for (Operator op : draggedOperatorsOrigins.keySet()) { Rectangle2D origin = draggedOperatorsOrigins.get(op); if (origin.getMaxX() + difX >= unitWidth) { difX -= origin.getMaxX() + difX - unitWidth; } if (origin.getMaxY() + difY >= unitHeight) { difY -= origin.getMaxY() + difY - unitHeight; } if (origin.getMinX() + difX < 0) { difX -= origin.getMinX() + difX; } if (origin.getMinY() + difY < 0) { difY -= origin.getMinY() + difY; } } double maxX = 0; double maxY = 0; // shift for (Operator op : draggedOperatorsOrigins.keySet()) { Rectangle2D origin = draggedOperatorsOrigins.get(op); Rectangle2D opPos = new Rectangle2D.Double(origin.getX() + difX, origin.getY() + difY, origin.getWidth(), origin.getHeight()); setOperatorRect(op, opPos); } // scrollRectToVisible(new Rectangle(e.getX(), e.getY(), (int)opPosAtDragStart.getWidth(), // (int)opPosAtDragStart.getHeight())); ensureWidth(draggingInSubprocess, (int) maxX); ensureHeight(draggingInSubprocess, (int) maxY); repaint(); } } else if (draggedPort != null && draggedPort.getPorts().getOwner().getOperator() == displayedChain) { // ports are draggeable only if they belong to the displayedChain <-> they are inner sinks our sources double diff = e.getY() - mousePositionAtLastEvaluation.getY(); // setPortSpacing(draggedPort, portSpacingAtDragStart + diff); double shifted = shiftPortSpacing(draggedPort, diff); mousePositionAtLastEvaluation.setLocation(mousePositionAtLastEvaluation.getX(), mousePositionAtLastEvaluation.getY() + shifted); repaint(); } else if (selectionRectangle != null) { selectionRectangle = getSelectionRectangle(mousePositionAtDragStart, e.getPoint()); repaint(); } else if (connectingPortSource != null) { updateHoveringState(e); } } @Override public void mouseClicked(MouseEvent e) { if (flowVisualizer.isActive()) { return; } requestFocus(); // selectOperator(hoveringOperator, !e.isControlDown()); if (e.isPopupTrigger()) { if (showPopupMenu(e)) { return; } } switch (e.getButton()) { case MouseEvent.BUTTON1: if (e.getClickCount() == 2) { if (hoveringPort != null && hoveringPort.getPorts().getOwner().getOperator() instanceof ProcessRootOperator) { RepositoryLocation procLoc = displayedChain.getProcess().getRepositoryLocation(); RepositoryLocation resolveRelativeTo = null; if (procLoc != null) { resolveRelativeTo = procLoc.parent(); } String loc = RepositoryLocationChooser.selectLocation(resolveRelativeTo, ProcessRenderer.this); if (loc != null) { int index = hoveringPort.getPorts().getAllPorts().indexOf(hoveringPort); // This looks weird, but it is correct: The input ports are OutputPorts! if (hoveringPort instanceof OutputPort) { displayedChain.getProcess().getContext().setInputRepositoryLocation(index, loc); } else { displayedChain.getProcess().getContext().setOutputRepositoryLocation(index, loc); } } } else if (getHoveringOperator() != null) { if (getHoveringOperator() instanceof OperatorChain) { processPanel.showOperatorChain((OperatorChain) getHoveringOperator()); } } } repaint(); break; case MouseEvent.BUTTON3: if (connectingPortSource != null) { cancelConnectionDragging(); break; } } repaint(); } private void cancelConnectionDragging() { connectingPortSource = null; repaint(); } @Override public void mouseExited(MouseEvent e) { mainFrame.getStatusBar().clearSpecialText(); }; }; private void selectOperator(Operator op, boolean clear) { selectOperator(op, clear, false); } /** * * @param op * The operator to add. * @param clear * If true, clear before adding * @param range * If true, select interval from last selected operator to op. */ private void selectOperator(Operator op, boolean clear, boolean range) { boolean changed = false; if (clear || op == null) { if (!selectedOperators.isEmpty()) { changed = true; if (!range) { selectedOperators.clear(); } else { Operator last = null; if (!selectedOperators.isEmpty()) { last = selectedOperators.getLast(); } selectedOperators.clear(); if (last != null) { selectedOperators.add(last); } } } } if (range) { int lastIndex = -1; boolean sameUnit = true; if (!selectedOperators.isEmpty()) { Operator lastSelected = selectedOperators.getLast(); if (lastSelected.getExecutionUnit() == null) { // happns is last == Root sameUnit = false; } else { lastIndex = lastSelected.getExecutionUnit().getOperators().indexOf(lastSelected); if (lastSelected.getExecutionUnit() != op.getExecutionUnit()) { sameUnit = false; } } } if (sameUnit) { int index = op.getExecutionUnit().getOperators().indexOf(op); if (lastIndex < index) { for (int i = lastIndex + 1; i <= index; i++) { selectedOperators.add(op.getExecutionUnit().getOperators().get(i)); } } else if (lastIndex > index) { for (int i = lastIndex - 1; i >= index; i--) { selectedOperators.add(op.getExecutionUnit().getOperators().get(i)); } } } } else { boolean contains = selectedOperators.contains(op); if (op != null) { if (!contains) { selectedOperators.add(op); changed = true; } else if (!clear) { selectedOperators.remove(op); changed = true; } } } if (changed) { mainFrame.selectOperators(selectedOperators); } repaint(); } public List<Operator> getSelection() { return selectedOperators; } public void setSelection(List<Operator> selectedOperators) { if (!this.selectedOperators.equals(selectedOperators)) { this.selectedOperators.clear(); this.selectedOperators.addAll(selectedOperators); repaint(); } } private boolean showPopupMenu(final MouseEvent e) { if (connectingPortSource != null) { return false; } JPopupMenu menu = new JPopupMenu(); // port or not port, that is the question if (hoveringPort != null) { // add port actions final IOObject data = hoveringPort.getAnyDataOrNull(); if (data != null && data instanceof ResultObject) { JMenuItem showResult = new JMenuItem(new ResourceAction(true, "show_port_data", ((ResultObject) data).getName()) { private static final long serialVersionUID = -6557085878445788274L; @Override public void actionPerformed(ActionEvent e) { data.setSource(hoveringPort.getPorts().getOwner().getOperator().getName()); mainFrame.getResultDisplay().showResult(((ResultObject) data)); } }); menu.add(showResult); menu.add(new StoreInRepositoryAction(data)); menu.addSeparator(); } List<QuickFix> fixes = hoveringPort.collectQuickFixes(); if (!fixes.isEmpty()) { JMenu fixMenu = new ResourceMenu("quick_fixes"); for (QuickFix fix : fixes) { fixMenu.add(fix.getAction()); } menu.add(fixMenu); } if (hoveringPort.isConnected()) { menu.add(new ResourceAction(true, "disconnect") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent e) { if (hoveringPort != null) { if (hoveringPort.isConnected()) { if (hoveringPort instanceof OutputPort) { ((OutputPort) hoveringPort).disconnect(); } else { ((InputPort) hoveringPort).getSource().disconnect(); } } } } }); } if (displayedChain instanceof ProcessRootOperator) { if (hoveringPort.getPorts() == displayedChain.getSubprocess(0).getInnerSources() || hoveringPort.getPorts() == displayedChain.getSubprocess(0).getInnerSinks()) { menu.add(new ConnectPortToRepositoryAction(hoveringPort)); } } } else { // add operator actions mainFrame.getActions().addToOperatorPopupMenu(menu, RENAME_ACTION); // if not hovering on operator, add process panel actions if (getHoveringOperator() == null) { menu.addSeparator(); JMenu orderMenu = new ResourceMenu("execution_order"); orderMenu.add(flowVisualizer.ALTER_EXECUTION_ORDER.createMenuItem()); orderMenu.add(flowVisualizer.SHOW_EXECUTION_ORDER); menu.add(orderMenu); JMenu layoutMenu = new ResourceMenu("process_layout"); layoutMenu.add(new ResourceAction("arrange_operators") { private static final long serialVersionUID = 1L; @Override public void actionPerformed(ActionEvent ae) { int index = getProcessIndexUnder(e.getPoint()); if (index == -1) { for (ExecutionUnit u : processes) { autoArrange(u); } } else { autoArrange(processes[index]); } } }); layoutMenu.add(AUTO_FIT_ACTION); if (hoveringProcessIndex != -1) { layoutMenu.add(INCREASE_PROCESS_LAYOUT_WIDTH_ACTION); layoutMenu.add(DECREASE_PROCESS_LAYOUT_WIDTH_ACTION); layoutMenu.add(INCREASE_PROCESS_LAYOUT_HEIGHT_ACTION); layoutMenu.add(DECREASE_PROCESS_LAYOUT_HEIGHT_ACTION); } menu.add(layoutMenu); menu.addSeparator(); String name = "Process"; if (displayedChain.getProcess().getProcessLocation() != null) { name = displayedChain.getProcess().getProcessLocation().getShortName(); } menu.add(PrintingTools.makeExportPrintMenu(this, name)); } if (getHoveringOperator() != null) { boolean first = true; for (OutputPort port : getHoveringOperator().getOutputPorts().getAllPorts()) { final IOObject data = port.getAnyDataOrNull(); if (data != null && data instanceof ResultObject) { if (first) { menu.addSeparator(); first = false; } JMenuItem showResult = new JMenuItem(new ResourceAction(true, "show_port_data", ((ResultObject) data).getName()) { private static final long serialVersionUID = -6557085878445788274L; @Override public void actionPerformed(ActionEvent e) { data.setSource(getHoveringOperator().getName()); mainFrame.getResultDisplay().showResult(((ResultObject) data)); } }); menu.add(showResult); } } } } // show popup if (menu.getSubElements().length > 0) { menu.show(this, e.getX(), e.getY()); } return true; } private static enum ConnectorShape { LINEAR, ORTHOGONAL, SPLINES; }; private final ConnectorShape connectorShape = ConnectorShape.SPLINES; private boolean snapToGrid; private final OverviewPanel overviewPanel = new OverviewPanel(this); private JTextField renameField = null; private Shape createConnector(Port fromPort, Port toPort) { Point2D from = getPortLocation(fromPort); Point2D to = getPortLocation(toPort); from = new Point2D.Double(from.getX() + PORT_SIZE / 2, from.getY()); to = new Point2D.Double(to.getX() - PORT_SIZE / 2, to.getY()); int delta = 10; switch (connectorShape) { case LINEAR: return new Line2D.Double(from, to); case ORTHOGONAL: { GeneralPath connector = new GeneralPath(); double sourceIndex = fromPort.getPorts().getAllPorts().indexOf(fromPort); double destIndex = -toPort.getPorts().getAllPorts().indexOf(toPort); int totalNumPorts = Math.max(fromPort.getPorts().getNumberOfPorts(), toPort.getPorts().getNumberOfPorts()); double freeSpace = to.getX() - from.getX() - 2 * delta; double centerX = (from.getX() + to.getX()) / 2 + 0.5 * freeSpace * ((sourceIndex + destIndex + 1d) / totalNumPorts - 0.5); connector.moveTo(from.getX() + 1, from.getY()); if (to.getX() >= from.getX() + 2 * delta) { if (to.getY() != from.getY()) { connector.lineTo(centerX, from.getY()); connector.lineTo(centerX, to.getY()); } } else { connector.lineTo(from.getX() + delta, from.getY()); connector.lineTo(from.getX() + delta, (from.getY() + to.getY()) / 2); connector.lineTo(to.getX() - delta, (from.getY() + to.getY()) / 2); connector.lineTo(to.getX() - delta, to.getY()); } connector.lineTo(to.getX() - 1, to.getY()); return connector; } case SPLINES: { 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; } // cannot happen default: return null; } } private OutputPort getPortForConnectorNear(Point p, ExecutionUnit unit) { List<OutputPort> candidates = new LinkedList<OutputPort>(); candidates.addAll(unit.getInnerSources().getAllPorts()); for (Operator op : unit.getOperators()) { candidates.addAll(op.getOutputPorts().getAllPorts()); } Stroke thickStroke = new BasicStroke(5); for (OutputPort port : candidates) { if (port.isConnected()) { Shape connector = createConnector(port, port.getDestination()); Shape thick = thickStroke.createStrokedShape(connector); if (thick.contains(p)) { return port; } } } return null; } // private int getDefaultProcessHeight() { // if (ORIENTATION == Orientation.Y_AXIS) { // return PROCESS_HEIGHT * 2/3; // } else { // return PROCESS_HEIGHT; // } // } private double getWidth(ExecutionUnit executionUnit) { return getWidth(executionUnit, true); } private void setInitialSizes(ExecutionUnit[] units) { Dimension frameSize; if (getParent() instanceof JViewport) { frameSize = getParent().getSize(); } else { frameSize = getSize(); } for (ExecutionUnit unit : units) { Dimension size = processSizes.get(unit); if (size == null) { size = new Dimension((int) (frameSize.getWidth() / processes.length - WALL_WIDTH * 2), (int) (frameSize.getHeight() - 2 * PADDING)); processSizes.put(unit, size); } } } /** * @param fill * If true, use all free space in the parent viewport */ private double getWidth(ExecutionUnit executionUnit, boolean fill) { Dimension size = processSizes.get(executionUnit); double width = size.getWidth(); if (fill) { if (getParent() instanceof JViewport) { double viewportWidth = ((JViewport) getParent()).getWidth(); double totalWidth = getTotalWidth(false); double free = viewportWidth - totalWidth; if (free > 0) { switch (ORIENTATION) { case Y_AXIS: width += free; break; case X_AXIS: width += (int) free / processes.length; break; } } } } return width; } private double getHeight(ExecutionUnit executionUnit) { return getHeight(executionUnit, true); } private double getTotalHeight() { return getTotalHeight(true); } private double getHeight(ExecutionUnit executionUnit, boolean fill) { Dimension size = processSizes.get(executionUnit); double height = size.getHeight(); if (fill) { if (getParent() instanceof JViewport) { double viewportHeight = ((JViewport) getParent()).getHeight(); double free = viewportHeight - getTotalHeight(false); if (free > 0) { switch (ORIENTATION) { case X_AXIS: height += free; break; case Y_AXIS: height += free / processes.length; break; } } } } return height; } private double getTotalWidth() { return getTotalWidth(true); } private double getTotalWidth(boolean fill) { double width = 0; for (ExecutionUnit u : processes) { double w = getWidth(u, fill) + 2 * WALL_WIDTH; switch (ORIENTATION) { case X_AXIS: width += w; break; case Y_AXIS: if (w > width) { width = w; } break; } } return width; } private double getTotalHeight(boolean fill) { double height = 0; for (ExecutionUnit u : processes) { double h = getHeight(u, fill) + 2 * PADDING; switch (ORIENTATION) { case X_AXIS: if (h > height) { height = h; } break; case Y_AXIS: height += h; break; } } return height; } private void setWidth(ExecutionUnit executionUnit, double width) { Dimension old = processSizes.get(executionUnit); old.setSize(width, old.getHeight()); updateComponentSize(); } private void setHeight(ExecutionUnit executionUnit, double height) { Dimension old = processSizes.get(executionUnit); old.setSize(old.getWidth(), height); updateComponentSize(); } private void ensureWidth(ExecutionUnit executionUnit, int width) { Dimension old = processSizes.get(executionUnit); if (width > old.getWidth()) { old.setSize(width, old.getHeight()); balance(); updateComponentSize(); } } private void ensureHeight(ExecutionUnit executionUnit, int height) { Dimension old = processSizes.get(executionUnit); if (height > old.getHeight()) { old.setSize(old.getWidth(), height); balance(); updateComponentSize(); } } private Rectangle2D autoPosition(Operator op, int index) { int maxPerRow = (int) Math.max(1, Math.floor(getWidth(op.getExecutionUnit(), true) / GRID_AUTOARRANGE_WIDTH)); int col = index % maxPerRow; int row = index / maxPerRow; Rectangle2D old = operatorRects.get(op); Rectangle2D rect = new Rectangle2D.Double(col * GRID_AUTOARRANGE_WIDTH + GRID_X_OFFSET, GRID_AUTOARRANGE_HEIGHT * row + GRID_Y_OFFSET, old != null ? old.getWidth() : OPERATOR_WIDTH, old != null ? old.getHeight() : MIN_OPERATOR_HEIGHT); setOperatorRect(op, rect); return rect; } private void autoArrange(ExecutionUnit process) { Collection<Operator> sorted = process.getOperators(); int i = 0; for (Operator op : sorted) { autoPosition(op, i++); } autoFit(process, true); process.getEnclosingOperator().getProcess().updateNotify(); } private void autoFit(ExecutionUnit process, boolean balance) { double w = 0; double h = 0; for (Operator op : process.getOperators()) { Rectangle2D bounds = getOperatorRect(op, true); if (bounds.getMaxX() > w) { w = bounds.getMaxX(); } if (bounds.getMaxY() > h) { h = bounds.getMaxY(); } } for (Port port : process.getInnerSources().getAllPorts()) { h = Math.max(h, getPortLocation(port).getY()); } for (Port port : process.getInnerSinks().getAllPorts()) { h = Math.max(h, getPortLocation(port).getY()); } setWidth(process, (int) (w + 3 * PADDING)); setHeight(process, (int) (h + 3 * PADDING)); if (balance) { balance(); repaint(); } updateExtensionButtons(); } private void autoFit() { for (ExecutionUnit unit : processes) { autoFit(unit, false); } balance(); repaint(); updateExtensionButtons(); } private void balance() { switch (ORIENTATION) { case X_AXIS: balanceHeights(); break; case Y_AXIS: balanceWidths(); break; } } private void balanceHeights() { double height = 0; for (ExecutionUnit p : processes) { double h = getHeight(p); if (h > height) { height = h; } } for (ExecutionUnit p : processes) { setHeight(p, height); } } private void balanceWidths() { double width = 0; for (ExecutionUnit p : processes) { double w = getWidth(p); if (w > width) { width = w; } } for (ExecutionUnit p : processes) { setWidth(p, width); } } private void updateComponentSize() { Dimension newSize = new Dimension((int) getTotalWidth(), (int) getTotalHeight()); updateExtensionButtons(); if (!newSize.equals(getPreferredSize())) { setPreferredSize(newSize); revalidate(); } } int getProcessIndexUnder(Point2D p) { switch (ORIENTATION) { case X_AXIS: if (p.getY() < PADDING || p.getY() > getTotalHeight()) { return -1; } int xOffset = 0; for (int i = 0; i < processes.length; i++) { xOffset += WALL_WIDTH; int relativeX = (int) p.getX() - xOffset; if (relativeX >= 0 && relativeX <= getWidth(processes[i])) { return i; } xOffset += WALL_WIDTH + getWidth(processes[i]); } break; case Y_AXIS: if (p.getX() < WALL_WIDTH || p.getX() > WALL_WIDTH + getTotalWidth()) { return -1; } int yOffset = 0; for (int i = 0; i < processes.length; i++) { int relativeY = (int) p.getY() - yOffset; if (relativeY >= 0 && relativeY <= getHeight(processes[i])) { return i; } yOffset += PADDING + getHeight(processes[i]); } break; } return -1; } private Point fromProcessSpace(Point p, int processIndex) { switch (ORIENTATION) { case X_AXIS: double xOffset = 0; for (int i = 0; i < processes.length; i++) { xOffset += WALL_WIDTH; if (i == processIndex) { return new Point((int) (p.getX() + xOffset), (int) (p.getY() + PADDING)); } xOffset += WALL_WIDTH + getWidth(processes[i]); } break; case Y_AXIS: double yOffset = 0; for (int i = 0; i < processes.length; i++) { if (i == processIndex) { return new Point((int) (p.getX() + WALL_WIDTH), (int) (p.getY() + yOffset + PADDING)); } yOffset += PADDING + getHeight(processes[i]); } break; } return null; } Point toProcessSpace(Point p, int processIndex) { switch (ORIENTATION) { case X_AXIS: int xOffset = 0; for (int i = 0; i < processes.length; i++) { xOffset += WALL_WIDTH; if (i == processIndex) { return new Point((int) p.getX() - xOffset, (int) p.getY() - PADDING); } xOffset += WALL_WIDTH + getWidth(processes[i]); } break; case Y_AXIS: int yOffset = 0; for (int i = 0; i < processes.length; i++) { if (i == processIndex) { return new Point((int) p.getX() - WALL_WIDTH, (int) p.getY() - yOffset - PADDING); } yOffset += PADDING + getHeight(processes[i]); } break; } return null; } private Operator getClosestLeftNeighbour(Point2D p, ExecutionUnit unit) { Operator closest = null; double minDist = Double.POSITIVE_INFINITY; for (Operator op : unit.getOperators()) { Rectangle2D rect = getOperatorRect(op, true); if (rect.getMaxX() >= p.getX()) { continue; } double dx = rect.getMaxX() - p.getX(); double dy = rect.getMaxY() - p.getY(); double dist = dx * dx + dy * dy; if (dist < minDist) { minDist = dist; closest = op; } } return closest; } private void showStatus(String msg) { RapidMinerGUI.getMainFrame().getStatusBar().setSpecialText(msg); } private void clearStatus() { RapidMinerGUI.getMainFrame().getStatusBar().clearSpecialText(); } private void updateCursor() { if (getHoveringOperator() != null || hoveringPort != null) { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); } else { setCursor(Cursor.getDefaultCursor()); } } private Rectangle2D getSelectionRectangle(Point dragStart, Point2D e) { if (dragStart == null) { return null; } return new Rectangle2D.Double(Math.min(dragStart.getX(), e.getX()), Math.min(dragStart.getY(), e.getY()), Math.abs(dragStart.getX() - e.getX()), Math.abs(dragStart.getY() - e.getY())); } public void processUpdated() { if (displayedChain.getNumberOfSubprocesses() != processes.length) { showOperatorChain(displayedChain); } repaint(); } private Point snap(Point2D point) { int snappedX = (int) point.getX() - GRID_X_OFFSET; int factor = (snappedX + GRID_WIDTH / 2) / GRID_WIDTH; snappedX /= GRID_WIDTH; snappedX = factor * GRID_WIDTH + GRID_X_OFFSET; int snappedY = (int) point.getY() - GRID_Y_OFFSET; factor = (snappedY + GRID_HEIGHT / 2) / GRID_HEIGHT; snappedY /= GRID_HEIGHT; snappedY = factor * GRID_HEIGHT + GRID_Y_OFFSET; return new Point(snappedX, snappedY); } private boolean isSnapToGrid() { return snapToGrid; } /** Abbreviates the string using ... if necessary. */ private String fitString(String string, Graphics2D g, int maxWidth) { Rectangle2D bounds = g.getFont().getStringBounds(string, g.getFontRenderContext()); if (bounds.getWidth() < maxWidth) { return string; } while (g.getFont().getStringBounds(string + "...", g.getFontRenderContext()).getWidth() > maxWidth) { if (string.length() == 0) { return "..."; } string = string.substring(0, string.length() - 1); } return string + "..."; } private boolean canBeInsertedIntoConnection(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; } private void insertIntoHoveringConnection(Operator operator) { if (hoveringConnectionSource == null) { return; } InputPort oldDest = hoveringConnectionSource.getDestination(); oldDest.lock(); hoveringConnectionSource.lock(); try { // no IndexOutOfBoundsException since checked above InputPort bestInputPort = null; MetaData md = hoveringConnectionSource.getMetaData(); if (md != null) { for (InputPort inCandidate : operator.getInputPorts().getAllPorts()) { if (!inCandidate.isConnected() && inCandidate.isInputCompatible(md, CompatibilityLevel.PRE_VERSION_5)) { bestInputPort = inCandidate; break; } } } else { for (InputPort inCandidate : operator.getInputPorts().getAllPorts()) { if (!inCandidate.isConnected()) { bestInputPort = inCandidate; break; } } } if (bestInputPort != null) { hoveringConnectionSource.disconnect(); connect(hoveringConnectionSource, bestInputPort); if (mainFrame.VALIDATE_AUTOMATICALLY_ACTION.isSelected()) { hoveringConnectionSource.getPorts().getOwner().getOperator().transformMetaData(); operator.transformMetaData(); } OutputPort bestOutput = null; for (OutputPort outCandidate : operator.getOutputPorts().getAllPorts()) { if (!outCandidate.isConnected()) { md = outCandidate.getMetaData(); if (md != null && oldDest.isInputCompatible(md, CompatibilityLevel.PRE_VERSION_5)) { bestOutput = outCandidate; break; } } } if (bestOutput == null) { for (OutputPort outCandidate : operator.getOutputPorts().getAllPorts()) { if (!outCandidate.isConnected()) { bestOutput = outCandidate; break; } } } if (bestOutput != null) { connect(bestOutput, oldDest); } } } finally { oldDest.unlock(); hoveringConnectionSource.unlock(); hoveringConnectionSource = null; } } public OverviewPanel getOverviewPanel() { return overviewPanel; } @Override public void repaint() { super.repaint(); if (overviewPanel != null) { overviewPanel.repaint(); } } private void rename(final Operator op) { int processIndex = getIndex(op.getExecutionUnit()); if (processIndex == -1) { String name = SwingTools.showInputDialog("rename_operator", op.getName()); if (name != null && name.length() > 0) { op.rename(name); } return; } renameField = new JTextField(10); Rectangle2D rect = getOperatorRect(op, false); double rollout = nameRolloutInterpolationMap.getValue(op); int width = 0; if (rollout > 0) { width = (int) OPERATOR_FONT.getStringBounds(op.getName(), ((Graphics2D) getGraphics()).getFontRenderContext()).getWidth(); } width = Math.max(width, OPERATOR_WIDTH); // if (width > rect.getWidth()) { // rect.setFrame(rect.getX(), rect.getY(), width, rect.getHeight()); // } renameField.setText(op.getName()); renameField.selectAll(); Point p = fromProcessSpace(new Point((int) rect.getX(), (int) rect.getY()), processIndex); renameField.setBounds((int) p.getX(), (int) p.getY() - 4, width + 1, 21); // renameField.setFont(renameField.getFont().deriveFont(11f)); renameField.setFont(OPERATOR_FONT); add(renameField); renameField.requestFocus(); // accepting changes on enter and focus lost renameField.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (renameField != null) { String name = renameField.getText().trim(); if (name.length() > 0) { op.rename(name); } remove(renameField); renameField = null; repaint(); } } }); renameField.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { } @Override public void focusLost(FocusEvent e) { if (renameField != null) { String name = renameField.getText().trim(); if (name.length() > 0) { op.rename(name); } remove(renameField); renameField = null; repaint(); } } }); // ignore changes on escape renameField.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { remove(renameField); renameField = null; repaint(); } } @Override public void keyTyped(KeyEvent e) { } }); repaint(); } public void setHoveringOperator(Operator hoveringOperator) { if (hoveringOperator != this.hoveringOperator) { if (this.hoveringOperator != null) { nameRolloutInterpolationMap.rollIn(this.hoveringOperator); } if (hoveringOperator != null) { nameRolloutInterpolationMap.rollOut(hoveringOperator); } } this.hoveringOperator = hoveringOperator; } public Operator getHoveringOperator() { return hoveringOperator; } private void setDropInsertionPredecessor(Operator closestLeftNeighbour) { dropInsertionPredecessor = closestLeftNeighbour; if (dropInsertionPredecessor != null) { mainFrame.getStatusBar().setSpecialText("Operator will be inserted after " + dropInsertionPredecessor); } else { mainFrame.getStatusBar().setSpecialText("Operator will be inserted as the last operator in this process."); } } private boolean hasConnections(Operator op) { for (Port port : op.getInputPorts().getAllPorts()) { if (port.isConnected()) { return true; } } for (Port port : op.getOutputPorts().getAllPorts()) { if (port.isConnected()) { return true; } } return false; } public FlowVisualizer getFlowVisualizer() { return flowVisualizer; } public OperatorChain getDisplayedChain() { return displayedChain; } /** Connects two operators and enables them. */ private void connect(OutputPort out, InputPort in) { out.connectTo(in); Operator inOp = in.getPorts().getOwner().getOperator(); if (!inOp.isEnabled()) { inOp.setEnabled(true); } Operator outOp = out.getPorts().getOwner().getOperator(); if (!outOp.isEnabled()) { outOp.setEnabled(true); } } /** Adds positions of operators etc. */ private class GUIProcessXMLFilter implements ProcessXMLFilter { /** Adds GUI information to the element. */ @Override public void operatorExported(Operator op, Element opElement) { Rectangle2D bounds = getOperatorRect(op, false); if (bounds != null) { opElement.setAttribute("x", "" + (int) bounds.getX()); opElement.setAttribute("y", "" + (int) bounds.getY()); opElement.setAttribute("width", "" + (int) bounds.getWidth()); opElement.setAttribute("height", "" + (int) bounds.getHeight()); } } /** Adds GUI information to the element. */ @Override public void executionUnitExported(ExecutionUnit process, Element element) { Dimension size = processSizes.get(process); if (size != null) { element.setAttribute("width", "" + (int) size.getWidth()); element.setAttribute("height", "" + (int) size.getHeight()); } for (Port port : process.getInnerSources().getAllPorts()) { Element spacingElement = element.getOwnerDocument().createElement("portSpacing"); spacingElement.setAttribute("port", "source_" + port.getName()); spacingElement.setAttribute("spacing", "" + (int) getPortSpacing(port)); element.appendChild(spacingElement); } for (Port port : process.getInnerSinks().getAllPorts()) { Element spacingElement = element.getOwnerDocument().createElement("portSpacing"); spacingElement.setAttribute("port", "sink_" + port.getName()); spacingElement.setAttribute("spacing", "" + (int) getPortSpacing(port)); element.appendChild(spacingElement); } } /** Extracts GUI information from the XML element. */ @Override public void operatorImported(Operator op, Element opElement) { String x = opElement.getAttribute("x"); String y = opElement.getAttribute("y"); String w = opElement.getAttribute("width"); String h = opElement.getAttribute("height"); if (x != null && x.length() > 0) { try { setOperatorRect(op, new Rectangle2D.Double(Double.parseDouble(x), Double.parseDouble(y), Double.parseDouble(w), Double.parseDouble(h))); } catch (Exception e) { // ignore silently } } } /** Extracts GUI information from the XML element. */ @Override public void executionUnitImported(ExecutionUnit process, Element element) { String w = element.getAttribute("width"); String h = element.getAttribute("height"); try { if (w != null && w.length() > 0) { processSizes.put(process, new Dimension((int) Double.parseDouble(w), (int) Double.parseDouble(h))); } } catch (NumberFormatException e) { } NodeList children = element.getChildNodes(); for (Port port : process.getInnerSources().getAllPorts()) { for (int i = 0; i < children.getLength(); i++) { if (children.item(i) instanceof Element && "portSpacing".equals(((Element) children.item(i)).getTagName())) { Element psElement = (Element) children.item(i); if (("source_" + port.getName()).equals(psElement.getAttribute("port"))) { try { portSpacings.put(port, (double) Integer.parseInt(psElement.getAttribute("spacing"))); } catch (NumberFormatException e) { } break; } } } } for (Port port : process.getInnerSinks().getAllPorts()) { for (int i = 0; i < children.getLength(); i++) { if (children.item(i) instanceof Element && "portSpacing".equals(((Element) children.item(i)).getTagName())) { Element psElement = (Element) children.item(i); if (("sink_" + port.getName()).equals(psElement.getAttribute("port"))) { try { portSpacings.put(port, (double) Integer.parseInt(psElement.getAttribute("spacing"))); } catch (NumberFormatException e) { } break; } } } } } } /** * Checks whether we have a port under the given point (in process space) * and, as a side effect, remembers the {@link #hoveringPort}. */ private boolean checkPortUnder(Ports<? extends Port> ports, int x, int y) { for (Port port : ports.getAllPorts()) { Point2D location = getPortLocation(port); if (location == null) { continue; } int dx = (int) location.getX() - x; int dy = (int) location.getY() - y; if (dx * dx + dy * dy < 3 * PORT_SIZE * PORT_SIZE / 2) { if (hoveringPort != port) { hoveringPort = port; if (hoveringPort instanceof OutputPort) { showStatus("Click to connect, ALT to disconnect."); } setHoveringOperator(null); updateCursor(); repaint(); } return true; } } return false; } }