/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.flow.processrendering.view; import java.awt.BasicStroke; import java.awt.Dimension; import java.awt.Point; import java.awt.Shape; import java.awt.Stroke; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import javax.swing.JScrollPane; import javax.swing.JViewport; import javax.swing.SwingConstants; import javax.swing.Timer; import com.mxgraph.layout.hierarchical.mxHierarchicalLayout; import com.mxgraph.model.mxGraphModel; import com.mxgraph.util.mxRectangle; import com.mxgraph.view.mxGraph; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.dnd.TransferableOperator; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererModelEvent; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.ProcessRootOperator; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.Ports; import com.rapidminer.operator.ports.metadata.CompatibilityLevel; import com.rapidminer.operator.ports.metadata.MetaData; import com.rapidminer.repository.Entry; import com.rapidminer.repository.Folder; import com.rapidminer.repository.RepositoryException; import com.rapidminer.repository.RepositoryLocation; import com.rapidminer.tools.I18N; /** * The controller for the {@link ProcessRendererView} which is responsible for intercepting and * reacting to user interaction with the view. * * @author Marco Boeck * @since 6.4.0 * */ public class ProcessRendererController { /** used to detect connection hovering */ private static final Stroke CONNECTION_HOVER_DETECTION_STROKE = new BasicStroke(30); /** the view this controller manipulates */ private ProcessRendererView view; /** the model behind the process renderer */ private ProcessRendererModel model; /** * Creates a new process renderer controller which is responsible to intercept and react to user * interaction with the view and manipulating the model. * * @param view * @param model */ public ProcessRendererController(ProcessRendererView view, ProcessRendererModel model) { this.view = view; this.model = model; } /** * Automatically arranges the operators in the specified process according to a layouting * algorithm. * * @param process * the operators of this process will be arranged */ public void autoArrange(final ExecutionUnit process) { List<ExecutionUnit> list = new ArrayList<>(1); list.add(0, process); autoArrange(list); } /** * Automatically arranges the operators in the specified process according to a layouting * algorithm. * * @param process * the operators of this process will be arranged */ public void autoArrange(final List<ExecutionUnit> processes) { int unitNumber = processes.size(); List<Map<Operator, Rectangle2D>> newPositions = new ArrayList<>(unitNumber); for (int i = 0; i < unitNumber; i++) { if (processes.get(i) == null) { throw new IllegalArgumentException("process must not be null!"); } Collection<Operator> sorted = processes.get(i).getOperators(); mxGraphModel graphModel = new mxGraphModel(); mxGraph graph = new mxGraph(graphModel); Map<Operator, Object> vertexMap = new HashMap<>(); List<Operator> unconnectedOps = new LinkedList<>(); List<Operator> connectedOps = new LinkedList<>(); // insert vertices for (Operator op : sorted) { // skip unconnected operators if (!isOperatorConnected(op)) { unconnectedOps.add(op); continue; } connectedOps.add(op); Rectangle2D operatorRect = model.getOperatorRect(op); Object opVert = graph.insertVertex(null, null, op.getName(), operatorRect.getX(), operatorRect.getY(), operatorRect.getWidth(), operatorRect.getHeight(), null); vertexMap.put(op, opVert); } // connect vertices for (Operator source : sorted) { for (OutputPort out : source.getOutputPorts().getAllPorts()) { if (out.isConnected()) { Operator dest = out.getDestination().getPorts().getOwner().getOperator(); if (!(dest instanceof ProcessRootOperator)) { String value = source.getName() + " to " + dest.getName(); graph.insertEdge(null, null, value, vertexMap.get(source), vertexMap.get(dest), null); } } } } // calculate new layout mxHierarchicalLayout layout = new mxHierarchicalLayout(graph, SwingConstants.WEST); layout.setInterRankCellSpacing(ProcessDrawer.GRID_X_OFFSET); layout.execute(graph.getDefaultParent()); newPositions.add(i, new HashMap<Operator, Rectangle2D>()); for (Operator op : connectedOps) { mxRectangle cellBounds = graph.getCellBounds(vertexMap.get(op)); double x = cellBounds.getX() + ProcessDrawer.GRID_X_OFFSET; double y = cellBounds.getY() + ProcessDrawer.GRID_Y_OFFSET; if (!unconnectedOps.isEmpty()) { y += ProcessDrawer.OPERATOR_MIN_HEIGHT + ProcessDrawer.GRID_Y_OFFSET; } if (model.isSnapToGrid()) { Point snappedPoint = ProcessDrawUtils.snap(new Point2D.Double(x, y)); newPositions.get(i).put(op, new Rectangle2D.Double(snappedPoint.getX(), snappedPoint.getY(), cellBounds.getWidth(), cellBounds.getHeight())); } else { newPositions.get(i).put(op, new Rectangle2D.Double(x, y, cellBounds.getWidth(), cellBounds.getHeight())); } } int index = 0; for (Operator op : unconnectedOps) { newPositions.get(i).put(op, autoPosition(op, index, false)); ++index; } } moveOperators(processes, newPositions, 10, 100); } /** * Automatically adapts the size of all processes to fit the available space. */ public synchronized void autoFit() { for (ExecutionUnit unit : model.getProcesses()) { ensureOperatorsHaveLocation(unit); autoFit(unit, false); } balance(); view.updateExtensionButtons(); } /** * Calculates the position of an operator. * * @param op * @param index * @param setPosition * If {@code true}, will actually change the position of the operator, otherwise will * not move the operator * @return the position and size of the operator, never {@code null} */ public Rectangle2D autoPosition(Operator op, int index, boolean setPosition) { int maxPerRow = (int) Math.max(1, Math.floor(model.getProcessWidth(op.getExecutionUnit()) / (ProcessDrawer.GRID_AUTOARRANGE_WIDTH + 10))); int col = index % maxPerRow; int row = index / maxPerRow; Rectangle2D old = model.getOperatorRect(op); double x = col * ProcessDrawer.GRID_AUTOARRANGE_WIDTH + ProcessDrawer.GRID_X_OFFSET; double y = ProcessDrawer.GRID_AUTOARRANGE_HEIGHT * row + ProcessDrawer.GRID_Y_OFFSET; double width = Math.floor(old != null ? old.getWidth() : ProcessDrawer.OPERATOR_WIDTH); double height = Math.floor(old != null ? old.getHeight() : ProcessDrawer.OPERATOR_MIN_HEIGHT); Rectangle2D rect; if (model.isSnapToGrid()) { Point snappedPoint = ProcessDrawUtils.snap(new Point2D.Double(x, y)); rect = new Rectangle2D.Double(snappedPoint.getX(), snappedPoint.getY(), width, height); } else { rect = new Rectangle2D.Double(x, y, width, height); } if (setPosition) { model.setOperatorRect(op, rect); model.fireOperatorMoved(op); } return rect; } /** * Opens a rename textfield at the location of the specified operator. * * @param op * the operator to be renamed */ public void rename(final Operator op) { view.rename(op); } /** * Select the given operator. * * @param op * the operator to select * @param clear * if {@code true}, an existing selection will be cleared */ void selectOperator(final Operator op, final boolean clear) { selectOperator(op, clear, false); } /** * Makes sure the current processes are up to date when the displayed chain changes. Also auto * fits in case the view is actually being shown right now. */ void processDisplayedChainChanged() { List<ExecutionUnit> processes; OperatorChain op = model.getDisplayedChain(); if (op == null) { processes = Collections.<ExecutionUnit> emptyList(); } else { processes = new LinkedList<>(op.getSubprocesses()); } model.setProcesses(processes); model.fireProcessesChanged(); // only auto fit when the view when is actually displayed if (view.isShowing()) { autoFit(); } } /** * Makes sure the process still fits the new height of the operator whose ports have changed. * Also triggers height recalculation of the operator itself. * * @param op * the operator which had his ports changed */ void processPortsChanged(Operator op) { if (model.getOperatorRect(op) != null) { model.setOperatorRect(op, model.getOperatorRect(op)); // make sure that process size fits new size of operators ensureProcessSizeFits(op.getExecutionUnit(), model.getOperatorRect(op)); } } /** * Select the given operator. * * @param op * the operator to select * @param clear * if {@code true}, an existing selection will be cleared * @param range * if true, select interval from last already selected operator to now selected * operator */ void selectOperator(final Operator op, final boolean clear, final boolean range) { boolean changed = false; LinkedList<Operator> selectedOperators = new LinkedList<>(model.getSelectedOperators()); 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 && last != model.getDisplayedChain()) { selectedOperators.add(last); } } } } else { if (selectedOperators.contains(model.getDisplayedChain())) { selectedOperators.remove(model.getDisplayedChain()); } } if (range) { int lastIndex = -1; boolean sameUnit = true; if (!selectedOperators.isEmpty()) { Operator lastSelected = selectedOperators.getLast(); if (lastSelected.getExecutionUnit() == null) { // happens if 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) { RapidMinerGUI.getMainFrame().selectOperators(selectedOperators); } } /** * Starting from the current selection, selects the first operator in the given direction. * * @param e * the key event which triggered the selection */ void selectInDirection(final KeyEvent e) { int keyCode = e.getKeyCode(); if (model.getSelectedOperators().isEmpty()) { for (ExecutionUnit unit : model.getProcesses()) { if (unit.getNumberOfOperators() > 0) { selectOperator(unit.getOperators().get(0), true); } } } else { Operator current = model.getSelectedOperators().get(0); if (current.getParent() != model.getDisplayedChain()) { return; } Rectangle2D pos = model.getOperatorRect(current); ExecutionUnit unit = current.getExecutionUnit(); if (unit == null) { return; } double smallestDistance = Double.POSITIVE_INFINITY; Operator closest = null; for (Operator other : unit.getOperators()) { Rectangle2D otherPos = model.getOperatorRect(other); 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()); } } } /** * Returns whether an operator has is connected to either output or input ports. * * @param op * the operator in question * @return {@code true} if the operator has a connection; {@code false} otherwise */ boolean hasConnections(final 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; } /** * Returns whether the given {@link Transferable} can be accepted as a drop or not. * * @param t * the transferable * @return {@code true} if the process renderer can handle the drop; {@code false} otherwise */ boolean canImportTransferable(final Transferable t) { for (DataFlavor flavor : t.getTransferDataFlavors()) { // check if folder is being dragged. Folders cannot be dropped on the process panel if (flavor == TransferableOperator.LOCAL_TRANSFERRED_REPOSITORY_LOCATION_FLAVOR) { RepositoryLocation location; try { // get repository location location = (RepositoryLocation) t.getTransferData(flavor); // locate entry Entry locateEntry = location.locateEntry(); // if entry is folder, return false if (locateEntry instanceof Folder) { return false; } } catch (UnsupportedFlavorException e) { } catch (IOException e) { } catch (RepositoryException e) { } } } return true; } /** * Connects the operators specified by the output and input port and enables them. * * @param out * the output port * @param in * the input port */ void connect(final OutputPort out, final InputPort 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); } out.connectTo(in); } /** * Ensures that the process is at least width wide. * * @param executionUnit * the process to ensure the minimum width * @param width * the mininum width */ void ensureWidth(final ExecutionUnit executionUnit, final int width) { Dimension old = new Dimension((int) model.getProcessWidth(executionUnit), (int) model.getProcessHeight(executionUnit)); if (width > old.getWidth()) { model.setProcessWidth(executionUnit, width); balance(); model.fireProcessSizeChanged(); } } /** * Ensures that the process is at least height heigh. * * @param executionUnit * the process to ensure the minimum height * @param width * the mininum height */ void ensureHeight(final ExecutionUnit executionUnit, final int height) { Dimension old = new Dimension((int) model.getProcessWidth(executionUnit), (int) model.getProcessHeight(executionUnit)); if (height > old.getHeight()) { model.setProcessHeight(executionUnit, height); balance(); model.fireProcessSizeChanged(); } } /** * Increases the process size if necessary for the given operator. * * @param process * the process for which the size should be checked * @param location * the location which must fit inside the process * @return {@code true} if process was resized; {@code false} otherwise */ boolean ensureProcessSizeFits(final ExecutionUnit process, final Rectangle2D rect) { Dimension processSize = model.getProcessSize(process); if (processSize == null) { return false; } if (rect == null) { return false; } boolean needsResize = false; double processWidth = processSize.getWidth() * (1 / model.getZoomFactor()); double processHeight = processSize.getHeight() * (1 / model.getZoomFactor()); double width = processWidth; double height = processHeight; if (processSize != null) { if (processWidth < rect.getMaxX() + ProcessDrawer.GRID_X_OFFSET) { double diff = rect.getMaxX() + ProcessDrawer.GRID_X_OFFSET - processWidth; if (diff > ProcessDrawer.GRID_X_OFFSET) { width += diff; } else { width += ProcessDrawer.GRID_X_OFFSET; } needsResize = true; } if (processHeight < rect.getMaxY() + ProcessDrawer.GRID_Y_OFFSET) { double diff = rect.getMaxY() + ProcessDrawer.GRID_Y_OFFSET - processHeight; if (diff > ProcessDrawer.GRID_Y_OFFSET) { height += diff; } else { height += ProcessDrawer.GRID_Y_OFFSET; } needsResize = true; } if (needsResize) { model.setProcessWidth(process, width); model.setProcessHeight(process, height); balance(); model.fireProcessSizeChanged(); return true; } } return false; } /** * Checks whether we have a port under the given point (in process space) and, as a side effect, * remembers the hovering port and potentially resets the hovering operator. * * @param ports * the ports to be checked * @param x * the x coordinate * @param y * the y coordinate * @return {@code true} if a port of the given list lies under the coordinates; {@code false} * otherwise */ boolean checkPortUnder(final Ports<? extends Port> ports, final int x, final int y) { for (Port port : ports.getAllPorts()) { Point2D location = ProcessDrawUtils.createPortLocation(port, model); if (location == null) { continue; } int dx = (int) location.getX() - x; int dy = (int) location.getY() - y; if (dx * dx + dy * dy < 3 * ProcessDrawer.PORT_SIZE * ProcessDrawer.PORT_SIZE / 2) { if (model.getHoveringPort() != port) { model.setHoveringPort(port); if (model.getHoveringPort().getPorts().getOwner().getOperator() == model.getDisplayedChain()) { showStatus(I18N.getGUILabel("processRenderer.displayChain.port.hover")); } else { showStatus(I18N.getGUILabel("processRenderer.operator.port.hover")); } view.setHoveringOperator(null); model.fireMiscChanged(); } return true; } } return false; } /** * Returns the index of the specified process in the current list of processes. * * @param executionUnit * the process we want to get the index of * @return the index of the process or -1 if it is not currently displayed */ int getIndex(final ExecutionUnit executionUnit) { for (int i = 0; i < model.getProcesses().size(); i++) { if (model.getProcess(i) == executionUnit) { return i; } } return -1; } /** * Returns the total height the process renderer occupies. * * @return the height including the padding to the top and bottom */ double getTotalHeight() { double height = 0; for (ExecutionUnit u : model.getProcesses()) { double h = model.getProcessHeight(u); if (h > height) { height = h; } } return height; } /** * Returns the total width the process renderer occupies. * * @return the width including the walls to the left and right */ double getTotalWidth() { double width = 0; int count = 0; for (ExecutionUnit u : model.getProcesses()) { if (count > 0) { width += 2 * ProcessDrawer.WALL_WIDTH; } double w = model.getProcessWidth(u); width += w; count++; } return width; } /** * Sets the initial sizes of all processes if the model does not already contain them. * */ void setInitialSizes() { List<ExecutionUnit> units = model.getProcesses(); for (ExecutionUnit unit : units) { Dimension size = model.getProcessSize(unit); if (size == null) { size = createInitialSize(unit); model.setProcessSize(unit, size); } } } /** * Returns the first connected {@link OutputPort} for which the specified point lies inside the * connector shape. * * @param p * the point in question * @param unit * the process for which to check * @return an output port for which the point lies inside the connector or {@code null} */ OutputPort getPortForConnectorNear(final Point p, final ExecutionUnit unit) { List<OutputPort> candidates = new LinkedList<>(); candidates.addAll(unit.getInnerSources().getAllPorts()); for (Operator op : unit.getOperators()) { candidates.addAll(op.getOutputPorts().getAllPorts()); } for (OutputPort port : candidates) { if (port.isConnected()) { Shape connector = ProcessDrawUtils.createConnector(port, port.getDestination(), model); if (connector == null) { return null; } Shape thick = CONNECTION_HOVER_DETECTION_STROKE.createStrokedShape(connector); if (thick.contains(p)) { return port; } } } return null; } /** * Returns the closest operator to the left of the given point and process. * * @param p * looks for an operator to the left of this location * @param unit * the process for the location * @return the closest operator or {@code null} */ Operator getClosestLeftNeighbour(final Point2D p, final ExecutionUnit unit) { Operator closest = null; double minDist = Double.POSITIVE_INFINITY; for (Operator op : unit.getOperators()) { Rectangle2D rect = model.getOperatorRect(op); 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; } /** * Insert the specified operator into the currently hovered connection. * * @param operator * the operator to be inserted */ @SuppressWarnings("deprecation") void insertIntoHoveringConnection(final Operator operator) { OutputPort hoveringConnectionSource = model.getHoveringConnectionSource(); 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 (RapidMinerGUI.getMainFrame().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(); model.setHoveringConnectionSource(null); } } /** * Set spacing and reduce spacing for successor if possible. * * @param port * the port to be moved * @param delta * by how much the port spacing should be changed * @return how much the port was moved */ double shiftPortSpacing(final Port port, final double delta) { // remember old spacing final Ports<? extends Port> ports = port.getPorts(); final int myIndex = ports.getAllPorts().indexOf(port); final Double old = (double) model.getPortSpacing(port); double newY = old + delta; if (model.isSnapToGrid()) { newY = Math.floor(newY / (ProcessDrawer.PORT_SIZE * 3d / 2d)) * (ProcessDrawer.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 = model.getPortSpacing(other); if (otherSpacing < diff) { model.resetPortSpacing(other); } else { model.setPortSpacing(other, (int) (otherSpacing - diff)); break; } } // see if it still fits into process frame model.setPortSpacing(port, (int) (old + diff)); Point bottomPortPos = ProcessDrawUtils.createPortLocation(ports.getPortByIndex(ports.getNumberOfPorts() - 1), model); // if it doesn't, revert double height = model.getProcessHeight(ports.getOwner().getConnectionContext()); if (bottomPortPos != null && bottomPortPos.getY() > height) { double tooMuch = bottomPortPos.getY() - height; diff -= tooMuch; model.setPortSpacing(port, (int) (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 = model.getPortSpacing(other); if (otherSpacing < -diff) { actuallyRemoved += model.getPortSpacing(other); model.resetPortSpacing(other); } else { model.setPortSpacing(other, (int) (otherSpacing + diff)); actuallyRemoved = -diff; break; } } if (ports.getNumberOfPorts() > myIndex + 1) { Port other = ports.getPortByIndex(myIndex + 1); model.setPortSpacing(other, (int) (model.getPortSpacing(other) + actuallyRemoved)); } return -actuallyRemoved; } else { // cannot happen return 0; } } /** * Returns a {@link Rectangle2D} representing an {@link Operator}. First tries to look up the * data from the model, if that fails determines a position it automatically. * * @param op * the operator for which a rectangle should be created * @return the rectangle representing the operator, never {@code null} */ Rectangle2D createOperatorPosition(Operator op) { Rectangle2D rect = model.getOperatorRect(op); if (rect != null) { return rect; } // 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 == model.getDisplayedChain() || model.getOperatorRect(sourceOp) != null; dependenciesOk &= destOp == model.getDisplayedChain() || model.getOperatorRect(destOp) != null; if (dependenciesOk) { Point2D sourcePos = ProcessDrawUtils.createPortLocation(op.getInputPorts().getPortByIndex(0).getSource(), model); Point2D destPos = ProcessDrawUtils.createPortLocation(op.getOutputPorts().getPortByIndex(0).getDestination(), model); double x = Math.floor((sourcePos.getX() + destPos.getX()) / 2 - ProcessDrawer.OPERATOR_WIDTH / 2); double y = Math.floor((sourcePos.getY() + destPos.getY()) / 2 - ProcessDrawer.PORT_OFFSET); rect = new Rectangle2D.Double(x, y, ProcessDrawer.OPERATOR_WIDTH, ProcessDrawer.OPERATOR_MIN_HEIGHT); } } 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 = autoPosition(op, index, false); } return rect; } /** * Ensures that each operator in the given {@link ExecutionUnit} has a location. * * @param unit * the process in question * @return the list of operators that did not have a location and now have one */ List<Operator> ensureOperatorsHaveLocation(ExecutionUnit unit) { List<Operator> movedOperators = new LinkedList<>(); for (Operator op : unit.getOperators()) { // check if all operators have positions, if not, set them now Rectangle2D rect = model.getOperatorRect(op); if (rect == null) { rect = createOperatorPosition(op); model.setOperatorRect(op, rect); movedOperators.add(op); } } return movedOperators; } /** * Shows the given message in the main GUI status bar. * * @param msg * the message */ void showStatus(final String msg) { RapidMinerGUI.getMainFrame().getStatusBar().setSpecialText(msg); } /** * Clears the main GUI status bar message. */ void clearStatus() { RapidMinerGUI.getMainFrame().getStatusBar().clearSpecialText(); } /** * Creates the initial size for a process. * * @param unit * the process for which the initial size should be created * @return the size, never {@code null} */ private Dimension createInitialSize(final ExecutionUnit unit) { Dimension frameSize; if (view.getParent() instanceof JViewport) { frameSize = view.getParent().getSize(); } else { frameSize = view.getSize(); } return ProcessDrawUtils.calculatePreferredSize(model, unit, frameSize.width, frameSize.height); } /** * Moves the operators of the specified process to their new positions with an animation. * * @param process * the process of the operators to be moved * @param newPositions * the new position for each operator * @param steps * the number of movement steps to reach their new position * @param time * the time in ms for the animation */ private void moveOperators(final List<ExecutionUnit> processes, final List<Map<Operator, Rectangle2D>> newPositions, final int steps, final int time) { // store current position, dx, and dy for each operator final int listSize = processes.size(); final List<Map<Operator, Rectangle2D>> current = new ArrayList<>(listSize); final List<Map<Operator, Double>> dxs = new ArrayList<>(listSize); final List<Map<Operator, Double>> dys = new ArrayList<>(listSize); for (int i = 0; i < listSize; i++) { current.add(i, new HashMap<Operator, Rectangle2D>()); dxs.add(i, new HashMap<Operator, Double>()); dys.add(i, new HashMap<Operator, Double>()); for (Operator op : newPositions.get(i).keySet()) { Rectangle2D currentPos = model.getOperatorRect(op); Rectangle2D endPos = newPositions.get(i).get(op); current.get(i).put(op, currentPos); dxs.get(i).put(op, Math.floor((endPos.getX() - currentPos.getX()) / steps)); dys.get(i).put(op, Math.floor((endPos.getY() - currentPos.getY()) / steps)); } } final Timer operatorMoverTimer = new Timer((int) (time / (double) steps), null); ActionListener operatorMover = new ActionListener() { private int count = 0; @Override public void actionPerformed(ActionEvent e) { // iterate over all ExecutionUnits for (int i = 0; i < listSize; i++) { // iterate over all moving operators Iterator<Operator> iterator = current.get(i).keySet().iterator(); while (iterator.hasNext()) { Operator op = iterator.next(); // check if display rect equals current stored rect Rectangle2D displayRect = model.getOperatorRect(op); Rectangle2D currentRect = current.get(i).get(op); if (currentRect.getX() != displayRect.getX() || currentRect.getY() != displayRect.getY()) { // remove operator as it has been moved by the user iterator.remove(); } // calculate new display position for operator double dx = dxs.get(i).get(op); double dy = dys.get(i).get(op); double x, y; // during animation, we don't really care about exact positioning if (count < steps - 1) { x = currentRect.getX() + dx; y = currentRect.getY() + dy; } else { // this is the final position, it has to be exact x = newPositions.get(i).get(op).getX(); y = newPositions.get(i).get(op).getY(); } currentRect = new Rectangle2D.Double(x, y, currentRect.getWidth(), currentRect.getHeight()); // update current rect in map and also set position as display position current.get(i).put(op, currentRect); model.setOperatorRect(op, currentRect); model.fireOperatorMoved(op); } } // after moving all operators, update UI, increase counter, and reset timer // use {@link ProcessRendererView#doRepaint()} instead of {@link // ProcessRendererView#repaint()} since repaint must happen instantly view.doRepaint(); ++count; if (count == steps) { operatorMoverTimer.stop(); autoFit(); processes.get(0).getEnclosingOperator().getProcess().updateNotify(); } } }; operatorMoverTimer.addActionListener(operatorMover); operatorMoverTimer.start(); } /** * Returns whether the specified operator is connected or not. An operator is considered * connected if any of his input or output ports are connected. * * @param op * @return {@code true} if it is connected; {@code false} otherwise */ private boolean isOperatorConnected(Operator op) { for (InputPort port : op.getInputPorts().getAllPorts()) { if (port.isConnected()) { return true; } } for (OutputPort port : op.getOutputPorts().getAllPorts()) { if (port.isConnected()) { return true; } } return false; } /** * Balance the display of all processes so they are of equal size. */ private void balance() { balanceHeights(); } /** * Balance the display height of all processes so they are of equal size. */ private void balanceHeights() { double height = 0; for (ExecutionUnit p : model.getProcesses()) { double h = model.getProcessHeight(p); if (h > height) { height = h; } } for (ExecutionUnit p : model.getProcesses()) { setHeight(p, height * model.getZoomFactor()); } } /** * Set the height of the given process. Fires a * {@link ProcessRendererModelEvent.ModelEvent#PROCESS_SIZE_CHANGED} event if the height has * changed. * * @param executionUnit * the process for which to set the height * @param width * the new height */ private void setHeight(final ExecutionUnit executionUnit, final double height) { if (model.getProcessHeight(executionUnit) != height) { model.setProcessHeight(executionUnit, height * (1 / model.getZoomFactor())); model.fireProcessSizeChanged(); } } /** * Automatically adapts the size of the given process to fit the available space. * * @param process * the size of this process will be adapted * @param balance * if {@code true}, will balance the size of all processes */ private void autoFit(ExecutionUnit process, boolean balance) { double w = 0; double h = 0; for (Operator op : process.getOperators()) { // operator location Rectangle2D bounds = model.getOperatorRect(op); if (bounds.getMaxX() > w) { w = bounds.getMaxX(); } if (bounds.getMaxY() > h) { h = bounds.getMaxY(); } // operator annotations WorkflowAnnotations annotations = model.getOperatorAnnotations(op); if (annotations != null) { for (WorkflowAnnotation anno : annotations.getAnnotationsDrawOrder()) { bounds = anno.getLocation(); if (bounds.getMaxX() > w) { w = bounds.getMaxX(); } if (bounds.getMaxY() > h) { h = bounds.getMaxY(); } } } } // process annotations WorkflowAnnotations annotations = model.getProcessAnnotations(process); if (annotations != null) { for (WorkflowAnnotation anno : annotations.getAnnotationsDrawOrder()) { Rectangle2D bounds = anno.getLocation(); if (bounds.getMaxX() > w) { w = bounds.getMaxX(); } if (bounds.getMaxY() > h) { h = bounds.getMaxY(); } } } for (Port port : process.getInnerSources().getAllPorts()) { Point pLoc = ProcessDrawUtils.createPortLocation(port, model); if (pLoc != null) { h = Math.max(h, pLoc.getY()); } } for (Port port : process.getInnerSinks().getAllPorts()) { Point pLoc = ProcessDrawUtils.createPortLocation(port, model); if (pLoc != null) { h = Math.max(h, pLoc.getY()); } } double minWidth = ProcessDrawer.OPERATOR_WIDTH * 2; double subprocessWidth = w + ProcessDrawer.GRID_X_OFFSET; double subprocessHeight = h + ProcessDrawer.GRID_Y_OFFSET; // consider zoom factor if (model.getZoomFactor() < 1) { subprocessWidth *= model.getZoomFactor(); subprocessHeight *= model.getZoomFactor(); } double height = subprocessHeight; double width = subprocessWidth > minWidth ? subprocessWidth : minWidth; // if less than original size, set to original size Dimension initialSize = createInitialSize(process); if (width < initialSize.getWidth()) { width = initialSize.getWidth(); } if (height < initialSize.getHeight()) { height = initialSize.getHeight(); } // at this point we have might have scrollbars. Check if we a) have them and b) need them. // If not, add scrollbar size / number of processes to width/height // if this code is removed, process size is smaller than it could be after first autofit if (JScrollPane.class.isAssignableFrom(view.getParent().getParent().getClass())) { JScrollPane sp = (JScrollPane) view.getParent().getParent(); if (sp.getHorizontalScrollBar().isVisible() && sp.getVerticalScrollBar().isVisible()) { double targetWidth = sp.getSize().getWidth() / model.getProcesses().size(); double targetHeight = sp.getSize().getHeight(); double sbWidth = sp.getVerticalScrollBar().getSize().getWidth() / model.getProcesses().size(); double sbHeight = sp.getHorizontalScrollBar().getSize().getHeight(); if (width < targetWidth && subprocessWidth < targetWidth) { width += sbWidth; } if (height < targetHeight && subprocessHeight < targetHeight) { height += sbHeight; } } } Dimension newDim = new Dimension(); newDim.setSize(width, height); model.setProcessSize(process, newDim); if (balance) { balance(); } model.fireProcessSizeChanged(); } }