/** * 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.model; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import javax.swing.event.EventListenerList; import com.rapidminer.Process; import com.rapidminer.ProcessLocation; import com.rapidminer.ProcessStorageListener; import com.rapidminer.RapidMiner; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.flow.NewProcessUndoManager; import com.rapidminer.gui.flow.processrendering.annotations.model.OperatorAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.ProcessAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotation; import com.rapidminer.gui.flow.processrendering.annotations.model.WorkflowAnnotations; import com.rapidminer.gui.flow.processrendering.background.ProcessBackgroundImage; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawUtils; import com.rapidminer.gui.flow.processrendering.draw.ProcessDrawer; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererAnnotationEvent; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererAnnotationEvent.AnnotationEvent; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererEventListener; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererModelEvent; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererModelEvent.ModelEvent; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererOperatorEvent; import com.rapidminer.gui.flow.processrendering.event.ProcessRendererOperatorEvent.OperatorEvent; import com.rapidminer.gui.processeditor.ExtendedProcessEditor; import com.rapidminer.gui.processeditor.ProcessEditor; import com.rapidminer.io.process.AnnotationProcessXMLFilter; import com.rapidminer.io.process.BackgroundImageProcessXMLFilter; import com.rapidminer.io.process.GUIProcessXMLFilter; import com.rapidminer.io.process.ProcessLayoutXMLFilter; import com.rapidminer.io.process.ProcessXMLFilterRegistry; import com.rapidminer.operator.ExecutionUnit; import com.rapidminer.operator.FlagUserData; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.operator.ports.OutputPort; import com.rapidminer.operator.ports.Port; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.parameter.ParameterChangeListener; /** * The model backing the process renderer view. It contains all the data necessary to draw the * current process. The minimal configuration which is required to draw a process via the * {@link ProcessDrawer} can be achieved by calling the following setters: * <ul> * <li>{@link #setDisplayedChain(OperatorChain)}</li> * <li>{@link #setProcesses(ExecutionUnit[])}</li> * <li>{@link #setProcessSize(ExecutionUnit, Dimension)} (see * {@link ProcessDrawUtils#calculatePreferredSize(ProcessRendererModel, ExecutionUnit, int, int)} to * automatically create the correct dimensions for each {@link ExecutionUnit} of a process)</li> * </ul> * <p> * Note that the model itself does not fire any events. To trigger events, call any of the fireXYZ * methods. This is done for performance reasons and to support batch updates and only trigger * events when really needed. * </p> * * @author Marco Boeck, Jan Czogalla * @since 6.4.0 * */ public final class ProcessRendererModel { /** available process zoom factors */ private static final double[] ZOOM_FACTORS = new double[] { 0.25, 0.33, 0.5, 0.67, 0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0 }; /** the starting zoom index equaling no zoom */ private static final int ORIGINAL_ZOOM_INDEX = 6; /** the font for the operator name */ public static final Font OPERATOR_FONT = new Font(Font.DIALOG, Font.BOLD, 11); /** the height of the operator name header */ public static final int HEADER_HEIGHT = OPERATOR_FONT.getSize() + 7; /** the width of each operator */ public static final int OPERATOR_WIDTH = 5 * 16 + 2 * 5; // 5 mini icons + padding /** the minimum height of an operator */ public static final int MIN_OPERATOR_HEIGHT = 50 + HEADER_HEIGHT; /** the size of each operator/process port */ public static final int PORT_SIZE = 14; /** event listener for this model */ private final EventListenerList eventListener; /** * list of {@link ProcessEditor ProcessEditors}; can also contain {@link ExtendedProcessEditor} * instances */ private final EventListenerList processEditors = new EventListenerList(); /** list of {@link ProcessStorageListener ProcessStorageListeners} */ private final LinkedList<ProcessStorageListener> storageListeners = new LinkedList<>(); /** underlying process for this model */ private Process process; /** {@link NewProcessUndoManager} associated with the underlying process */ private NewProcessUndoManager undoManager = new NewProcessUndoManager(); /** the current position in the undo stack */ private int undoIndex = 0; /** indicator whether the process has changed */ private boolean hasChanged = false; /** a list of the currently selected operators */ private List<Operator> selectedOperators; /** a list of the currently dragged operators */ private List<Operator> draggedOperators; /** the displayed processes */ private List<ExecutionUnit> processes; /** the currently displayed operator chain */ private OperatorChain displayedChain; /** whether snap to grid is enabled */ private boolean snapToGrid; /** source port of the current connection */ private OutputPort selectedConnectionSource; /** source port of the connection currently being created */ private Port connectingPortSource; /** index of the process under the mouse */ private int hoveringProcessIndex; /** port under the mouse cursor */ private Port hoveringPort; /** operator under the mouse cursor */ private Operator hoveringOperator; /** source port of the currently hovered connector */ private OutputPort hoveringConnectionSource; /** the currently selected area */ private Rectangle2D selectionRectangle; /** the size of the individual subprocesses */ private final Map<ExecutionUnit, Dimension> processSizes; /** the number of ports for each operator */ private final Map<Operator, Integer> portNumbers; /** if an operator is dragged from the operator tree or if a repository entry is dragged. */ private boolean dragStarted; /** indicates if the droptarget could be set */ private boolean dropTargetSet; /** indicates if an operator source (tree, WoC, ...) is hovered */ private boolean operatorSourceHovered; /** the position the mouse is currently at */ private Point currentMousePosition; /** the current zoom index */ private int zoomIndex = ORIGINAL_ZOOM_INDEX; /** * if canImport of transfer handler has returned <code>true</code>. Will be set to false if * mouse has exited the process renderer */ private boolean importDragged; /** the current mouse position relative to the process the mouse is over */ private Point mousePositionRelativeToProcess; // initialize the filter responsible for reading/writing operator coordinates from/to XML static { if (!RapidMiner.getExecutionMode().isHeadless()) { ProcessXMLFilterRegistry.registerFilter(new GUIProcessXMLFilter()); } } public ProcessRendererModel() { this.eventListener = new EventListenerList(); this.processes = Collections.unmodifiableList(Collections.<ExecutionUnit> emptyList()); this.selectedOperators = Collections.unmodifiableList(Collections.<Operator> emptyList()); this.draggedOperators = Collections.unmodifiableList(Collections.<Operator> emptyList()); this.processSizes = new WeakHashMap<>(); this.portNumbers = new WeakHashMap<>(); this.snapToGrid = Boolean .parseBoolean(ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_SNAP_TO_GRID)); this.hoveringProcessIndex = -1; // listen for snapToGrid changes ParameterService.registerParameterChangeListener(new ParameterChangeListener() { @Override public void informParameterSaved() { // ignore } @Override public void informParameterChanged(String key, String value) { if (RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_SNAP_TO_GRID.equals(key)) { setSnapToGrid(Boolean.parseBoolean(value)); } } }); // listen for selection changes in the ProcessRendererView and notify all registered process // editors registerEventListener(new ProcessRendererEventListener() { @Override public void modelChanged(ProcessRendererModelEvent e) { // ignore } @Override public void operatorsChanged(ProcessRendererOperatorEvent e, Collection<Operator> operators) { if (e.getEventType() == OperatorEvent.SELECTED_OPERATORS_CHANGED) { for (ProcessEditor editor : processEditors.getListeners(ProcessEditor.class)) { editor.setSelection(new LinkedList<Operator>(operators)); } for (ExtendedProcessEditor editor : processEditors.getListeners(ExtendedProcessEditor.class)) { editor.setSelection(new LinkedList<Operator>(operators)); } } } @Override public void annotationsChanged(ProcessRendererAnnotationEvent e, Collection<WorkflowAnnotation> annotations) { // ignore } }); // listen for process change and set the GUI flag addProcessEditor(new ProcessEditor() { @Override public void setSelection(List<Operator> selection) { // don't care } @Override public void processUpdated(Process process) { // don't care } @Override public void processChanged(Process process) { if (process != null) { if (!RapidMiner.getExecutionMode().isHeadless()) { process.getRootOperator().setUserData(RapidMinerGUI.IS_GUI_PROCESS, new FlagUserData()); } } } }); } /** * Returns the underlying {@link Process} or {@code null} if none is set. * * @since 7.5 */ public Process getProcess() { return process; } /** * Sets the underlying process and informs listeners that the displayed chain and the process * have changed. If the given process should be handled as a new one, the undo list will be * reset. Will also inform listeners that the process was loaded if so indicated. * * @param process * the process to be set * @param isNew * indicates if the process should be handled as a new process * @param open * whether the process was newly opened e.g. from a file * * @since 7.5 */ public void setProcess(Process process, boolean isNew, boolean open) { this.process = process; if (isNew) { // prevent addToUndoList from process editor undoManager.clearSnapshot(); } fireProcessChanged(); displayedChain = process.getRootOperator(); fireDisplayedChainChanged(); List<Operator> newList = new ArrayList<>(1); newList.add(displayedChain); this.selectedOperators = Collections.unmodifiableList(newList); fireOperatorSelectionChanged(getSelectedOperators()); if (isNew) { // reset undo manager because this is a new process resetUndo(); } if (open) { fireProcessLoaded(); } } /** * Informs listeners that the process was saved. * * @since 7.5 */ public void processHasBeenSaved() { hasChanged = false; fireProcessStored(); } /** * Returns whether the process has been altered since the last save. * * @since 7.5 */ public boolean hasChanged() { return hasChanged; } /** * Restores the previous state of the process if there are previous steps. Returns an Exception * if a problem occurred, {@code null} otherwise. * * @return an exception if a problem occurred * @see #setToStep(int) * * @since 7.5 */ public Exception undo() { if (!hasUndoSteps()) { return null; } if (!hasRedoSteps()) { // add redo step from current state takeSnapshot(); undoManager.add(true); } undoIndex--; return setToStep(undoIndex); } /** * Returns whether there are undo steps available. * * @since 7.5 */ public boolean hasUndoSteps() { return undoIndex > 0; } /** * Restores the next state of the process if there are next steps. Returns an Exception if a * problem occurred, {@code null} otherwise. * * @return an exception if a problem occurred * @see #setToStep(int) * @since 7.5 */ public Exception redo() { if (!hasRedoSteps()) { return null; } undoIndex++; Exception result = setToStep(undoIndex); if (!hasRedoSteps()) { // remove last step since we are at the end of stack // undo will create a redo step that represents the state at the time of undo undoManager.removeLast(); } return result; } /** * Returns whether there are redo steps available. * * @since 7.5 */ public boolean hasRedoSteps() { return undoIndex < undoManager.getNumberOfUndos() - 1; } /** * Checks whether there was a change in the process and if so, adds a new undo step. Will return * true if a new undo step was created. * * @since 7.5 */ public boolean checkForNewUndoStep() { takeSnapshot(); if (undoManager.snapshotDiffers()) { addToUndoList(false); return true; } return false; } /** * Returns the currently displayed processes. * * @return the immutable list of currently displayed processes. Never returns {@code null}. */ public List<ExecutionUnit> getProcesses() { return processes; } /** * Returns the process at the specified index. * * @param index * the index of the process to return. If index is invalid, throws * @return the currently displayed process at the specified index * @throws IndexOutOfBoundsException * if index < 0 or index >= length */ public ExecutionUnit getProcess(int index) throws ArrayIndexOutOfBoundsException { return getProcesses().get(index); } /** * Returns the index of the given process. * * @param process * the process for which the index should be retrieved * @return the index of the process starting with {@code 0} or {@code -1} if the process is not * part of {@link #getProcesses()} */ public int getProcessIndex(ExecutionUnit process) { return getProcesses().indexOf(process); } /** * Sets the currently displayed processes. * * @param processes * the new processes to display */ public void setProcesses(List<ExecutionUnit> processes) { if (processes == null) { throw new IllegalArgumentException("processes must not be null!"); } this.processes = Collections.unmodifiableList(processes); } /** * Returns the currently displayed {@link OperatorChain}. * * @return the operator chain, never {@code null} */ public OperatorChain getDisplayedChain() { return displayedChain; } /** * Sets the currently displayed {@link OperatorChain}. Call {@link #fireDisplayedChainChanged()} * to trigger the event. * * @param displayedChain * the new operator chain to display */ public void setDisplayedChain(OperatorChain displayedChain) { if (displayedChain == null) { throw new IllegalArgumentException("displayedChain must not be null!"); } addViewSwitchToUndo(displayedChain); this.displayedChain = displayedChain; fireProcessViewChanged(); } /** * Sets the displayed operator chain to the specified one and triggers updates before and after * the change. Will do nothing if the operator chain is already displayed. Convenience method. * * @param displayedChain * the new chain to display * @since 7.5 */ public void setDisplayedChainAndFire(OperatorChain displayedChain) { if (getDisplayedChain() == displayedChain) { return; } fireDisplayedChainWillChange(); setDisplayedChain(displayedChain); fireDisplayedChainChanged(); } /** * Returns the currently in the process view selected {@link Operator}s. * * @return the immutable list of currently selected operators */ public List<Operator> getSelectedOperators() { return selectedOperators; } /** * Clears the operator selection. */ public void clearOperatorSelection() { this.selectedOperators = Collections.unmodifiableList(Collections.<Operator> emptyList()); } /** * Adds the given operator to the currently selected operators. * * @param selectedOperator * this operator is added to the list of currently selected operators */ public void addOperatorToSelection(Operator selectedOperator) { List<Operator> newList = new ArrayList<>(getSelectedOperators().size() + 1); newList.addAll(getSelectedOperators()); newList.add(selectedOperator); this.selectedOperators = Collections.unmodifiableList(newList); } /** * Removes the given operator from the currently selected operators. If the given operator is * not selected, does nothing. * * @param selectedOperator * this operator is removed from the list of currently selected operators */ public void removeOperatorFromSelection(Operator selectedOperator) { List<Operator> newList = new ArrayList<>(getSelectedOperators()); newList.remove(selectedOperator); this.selectedOperators = Collections.unmodifiableList(newList); } /** * Adds the given operators to the currently selected operators. * * @param selectedOperators * these operators are added to the list of currently selected operators */ public void addOperatorsToSelection(List<Operator> selectedOperators) { List<Operator> newList = new ArrayList<>(getSelectedOperators().size() + selectedOperators.size()); newList.addAll(getSelectedOperators()); newList.addAll(selectedOperators); this.selectedOperators = Collections.unmodifiableList(newList); } /** * Returns the currently in the process view dragged {@link Operator}s. * * @return the immutable list of currently dragged operators */ public List<Operator> getDraggedOperators() { return draggedOperators; } /** * Sets the given operators as the currently dragged operators. * * @param draggedOperators * these operators are set as the currently dragged operators */ public void setDraggedOperators(Collection<Operator> draggedOperators) { List<Operator> newList = new ArrayList<>(draggedOperators.size()); newList.addAll(draggedOperators); this.draggedOperators = Collections.unmodifiableList(newList); } /** * Clears the dragged operators. */ public void clearDraggedOperators() { this.draggedOperators = Collections.unmodifiableList(Collections.<Operator> emptyList()); } /** * Whether operators snap to a grid or not. * * @return {@code true} if they do; {@code false} otherwise */ public boolean isSnapToGrid() { return snapToGrid; } /** * Sets whether operators snap to a grid or not. * * @param snapToGrid * whether operators should snap to a grid or not */ public void setSnapToGrid(boolean snapToGrid) { this.snapToGrid = snapToGrid; } /** * Whether a drag operation (operator or repository entry) is in progress. * * @return {@code true} if dragging is in progress; {@code false} otherwise */ public boolean isDragStarted() { return dragStarted; } /** * Sets whether a drag operation (operator or repository entry) is in progress. * * @param dragStarted * {@code true} if dragging is in progress; {@code false} otherwise */ public void setDragStarted(boolean dragStarted) { this.dragStarted = dragStarted; } /** * Whether a drop target was set or not. If this is not set to {@code true}, drag&drop is not * supported. * * @return {@code true} if a valid drop target was set; {@code false} otherwise */ public boolean isDropTargetSet() { return dropTargetSet; } /** * Sets whether a drop target was set or not. If this is not set to {@code true}, drag&drop is * not supported. * * @param dropTargetSet * {@code true} if a valid drop target was set; {@code false} otherwise */ public void setDropTargetSet(boolean dropTargetSet) { this.dropTargetSet = dropTargetSet; } /** * Whether an an operator source (tree, WoC, ...) is hovered or not. * * @return {@code true} if an operator source (tree, WoC, ...); {@code false} otherwise */ public boolean isOperatorSourceHovered() { return operatorSourceHovered; } /** * Sets whether an an operator source (tree, WoC, ...) is hovered or not. * * @param operatorSourceHovered * {@code true} if a an operator source (tree, WoC, ...) is hovered; {@code false} * otherwise */ public void setOperatorSourceHovered(boolean operatorSourceHovered) { this.operatorSourceHovered = operatorSourceHovered; } /** * Sets the current mouse position over the process renderer. Can be {@code null}. * * @param currentMousePosition * the position or {@code null} if it is not over the renderer */ public void setCurrentMousePosition(Point currentMousePosition) { this.currentMousePosition = currentMousePosition; } /** * The current mouse position over the process renderer. Can be {@code null}. * * @return the mouse position or {@code null} */ public Point getCurrentMousePosition() { return currentMousePosition; } /** * Whether the currently dragged import was accepted by the transfer handler, i.e. if the import * is accepted. * * @return {@code true} if the import would be accepted; {@code false} otherwise */ public boolean isImportDragged() { return importDragged; } /** * Sets whether the currently dragged import was accepted by the transfer handler, i.e. if the * import is accepted. * * @param importDragged * {@code true} if the import would be accepted; {@code false} otherwise */ public void setImportDragged(boolean importDragged) { this.importDragged = importDragged; } /** * The selected (via click) connection source port. * * @return the port or {@code null} */ public OutputPort getSelectedConnectionSource() { return selectedConnectionSource; } /** * Sets the selected connection source port. * * @param selectedConnectionSource * the connection source port or {@code null} */ public void setSelectedConnectionSource(OutputPort selectedConnectionSource) { this.selectedConnectionSource = selectedConnectionSource; } /** * The connection source port of the connection currently being created. * * @return the port or {@code null} */ public Port getConnectingPortSource() { return connectingPortSource; } /** * Sets the connection source port of the connection currently being created. * * @param connectingPortSource * the source port of the connection currently being created or {@code null} */ public void setConnectingPortSource(Port connectingPortSource) { this.connectingPortSource = connectingPortSource; } /** * Returns the index of the process over which the mouse currently hovers. * * @return the hovered process index or -1 if not hovering over any */ public int getHoveringProcessIndex() { return hoveringProcessIndex; } /** * Sets the index of the process over which the mosue currently hovers. * * @param hoveringProcessIndex * the hovered process index */ public void setHoveringProcessIndex(int hoveringProcessIndex) { this.hoveringProcessIndex = hoveringProcessIndex; } /** * Gets the port over which the mouse hovers. * * @return the port or {@code null} */ public Port getHoveringPort() { return hoveringPort; } /** * Sets the operator over which the mouse hovers. * * @param hoveringOperator * the operator under the mouse or {@code null} */ public void setHoveringOperator(Operator hoveringOperator) { this.hoveringOperator = hoveringOperator; } /** * Gets the {@link Operator} over which the mouse hovers. * * @return the operator or {@code null} */ public Operator getHoveringOperator() { return hoveringOperator; } /** * Sets the {@link OutputPort} of the connection over which the mouse hovers. * * @param hoveringConnectionSource * the output port of the connection under the mouse or {@code null} */ public void setHoveringConnectionSource(OutputPort hoveringConnectionSource) { this.hoveringConnectionSource = hoveringConnectionSource; } /** * Gets the {@link OutputPort} of the connection over which the mouse hovers. * * @return the output port or {@code null} */ public OutputPort getHoveringConnectionSource() { return hoveringConnectionSource; } /** * Sets the port over which the mouse hovers. * * @param hoveringPort * the port under the mouse or {@code null} */ public void setHoveringPort(Port hoveringPort) { this.hoveringPort = hoveringPort; } /** * Gets the {@link Rectangle2D} which represents the current selection box of the user. * * @return the rectangle or {@code null} */ public Rectangle2D getSelectionRectangle() { return selectionRectangle; } /** * Sets the rectangle which represents the current selection box of the user. * * @param selectionRectangle * the selection rectangle or {@code null} */ public void setSelectionRectangle(Rectangle2D selectionRectangle) { this.selectionRectangle = selectionRectangle; } /** * Returns the size of the given process. * * @param process * the size of this process is returned * @return the size of the specified process or {@code null} */ public Dimension getProcessSize(ExecutionUnit process) { Dimension dim = processSizes.get(process); if (dim == null) { return null; } // copy dim to not allow altering of dim in map dim = new Dimension(dim); if (getZoomFactor() > 1.0) { dim.width *= getZoomFactor(); dim.height *= getZoomFactor(); } return dim; } /** * Returns the width of the given process. Convenience method which simply returns the width of * {@link #getProcessSize(ExecutionUnit)}. * * @param process * the process for which the width should be returned * @return the width or -1 if no process size has been stored */ public double getProcessWidth(ExecutionUnit process) { Dimension dim = processSizes.get(process); if (dim == null) { return -1; } if (getZoomFactor() > 1.0) { return dim.getWidth() * getZoomFactor(); } return dim.getWidth(); } /** * Returns the zoom factor of the process where {@code 1.0} means no zoom, values smaller equal * zooming out and values greater than {@code 1.0} equal zooming in. * * @return */ public double getZoomFactor() { return ZOOM_FACTORS[zoomIndex]; } /** * Sets the zoom factor. If not a valid zoom factor or identical to the current factor, does * nothing. * * @param zoomFactor * factor in {@link #ZOOM_FACTORS} */ public void setZoomFactor(double zoomFactor) { if (getZoomFactor() == zoomFactor) { return; } int index = 0; for (double d : ZOOM_FACTORS) { if (d == zoomFactor) { zoomIndex = index; break; } index++; } } /** * * @return {@code true} if it is still possible to zoom in */ public boolean canZoomIn() { return zoomIndex < ZOOM_FACTORS.length - 1; } /** * * @return {@code true} if it is still possible to zoom out */ public boolean canZoomOut() { return zoomIndex > 0; } /** * * @return {@code true} if it is possible to reset the zoom (aka the process is currently zoomed * in/out) */ public boolean canZoomReset() { return zoomIndex != ORIGINAL_ZOOM_INDEX; } /** * Tries to zoom into the process. If the largest zoom factor has already been reached, does * nothing. */ public void zoomIn() { if (canZoomIn()) { this.zoomIndex += 1; } } /** * Tries to zoom out of the process. If the smallest zoom factor has already been reached, does * nothing. */ public void zoomOut() { if (canZoomOut()) { this.zoomIndex -= 1; } } public void resetZoom() { if (canZoomReset()) { this.zoomIndex = ORIGINAL_ZOOM_INDEX; } } /** * Sets the width for the given process. If {@link #getProcessSize(ExecutionUnit)} returns * {@code null} for the specified process, does nothing. * * @param process * the process for which the height should be set * @param width * the new width */ public void setProcessWidth(ExecutionUnit process, double width) { if (process == null) { throw new IllegalArgumentException("process must not be null!"); } Dimension dim = processSizes.get(process); if (dim == null) { return; } // execution unit dimensions should not use sub-pixels dim.setSize(Math.round(width), dim.getHeight()); } /** * Returns the height of the given process. Convenience method which simply returns the height * of {@link #getProcessSize(ExecutionUnit)}. * * @param process * the process for which the height should be returned * @return the height or -1 if no process size has been stored */ public double getProcessHeight(ExecutionUnit process) { Dimension dim = processSizes.get(process); if (dim == null) { return -1; } if (getZoomFactor() > 1.0) { return dim.getHeight() * getZoomFactor(); } return dim.getHeight(); } /** * Sets the height for the given process. If {@link #getProcessSize(ExecutionUnit)} returns * {@code null} for the specified process, does nothing. * * @param process * the process for which the height should be set * @param height * the new height */ public void setProcessHeight(ExecutionUnit process, double height) { if (process == null) { throw new IllegalArgumentException("process must not be null!"); } Dimension dim = processSizes.get(process); if (dim == null) { return; } // execution unit dimensions should not use subpixels dim.setSize(dim.getWidth(), Math.round(height)); } /** * Sets the size of the given process. * * @param process * the size of this process is stored * @param size * the size of the specified process */ public void setProcessSize(ExecutionUnit process, Dimension size) { if (process == null) { throw new IllegalArgumentException("process must not be null!"); } if (size == null) { throw new IllegalArgumentException("size must not be null!"); } this.processSizes.put(process, size); } /** * Returns a {@link Rectangle2D} representing the given {@link Operator}. * * @param op * the operator in question * @return the rectangle. Can return {@code null} but only if the operator has not been added to * the {@link com.rapidminer.gui.flow.processrendering.view.ProcessRendererView * ProcessRendererView}. */ public Rectangle2D getOperatorRect(Operator op) { return ProcessLayoutXMLFilter.lookupOperatorRectangle(op); } /** * Returns the {@link WorkflowAnnotations} container for the given {@link Operator}. * * @param op * the operator in question * @return the container. Can be {@code null} if no annotations exist for this operator */ public WorkflowAnnotations getOperatorAnnotations(Operator op) { return AnnotationProcessXMLFilter.lookupOperatorAnnotations(op); } /** * Removes the given {@link OperatorAnnotation}. * * @param annotation * the annotation to remove */ public void removeOperatorAnnotation(OperatorAnnotation anno) { AnnotationProcessXMLFilter.removeOperatorAnnotation(anno); } /** * Adds the given {@link OperatorAnnotation}. * * @param annotation * the annotation to add */ public void addOperatorAnnotation(OperatorAnnotation anno) { AnnotationProcessXMLFilter.addOperatorAnnotation(anno); } /** * Returns the {@link WorkflowAnnotations} container for the given {@link ExecutionUnit}. * * @param process * the process in question * @return the container. Can be {@code null} if no annotations exist for this process */ public WorkflowAnnotations getProcessAnnotations(ExecutionUnit process) { return AnnotationProcessXMLFilter.lookupProcessAnnotations(process); } /** * Removes the given {@link ProcessAnnotation}. * * @param annotation * the annotation to remove */ public void removeProcessAnnotation(ProcessAnnotation anno) { AnnotationProcessXMLFilter.removeProcessAnnotation(anno); } /** * Adds the given {@link ProcessAnnotation}. * * @param annotation * the annotation to add */ public void addProcessAnnotation(ProcessAnnotation anno) { AnnotationProcessXMLFilter.addProcessAnnotation(anno); } /** * Returns the {@link ProcessBackgroundImage} for the given {@link ExecutionUnit}. * * @param process * the process in question * @return the background image. Can be {@code null} if none is set for this process */ public ProcessBackgroundImage getBackgroundImage(ExecutionUnit process) { return BackgroundImageProcessXMLFilter.lookupBackgroundImage(process); } /** * Removes the given {@link ProcessBackgroundImage}. * * @param process * the process for which to remove the background image */ public void removeBackgroundImage(ExecutionUnit process) { BackgroundImageProcessXMLFilter.removeBackgroundImage(process); } /** * Sets the given {@link ProcessBackgroundImage}. * * @param image * the image to add */ public void setBackgroundImage(ProcessBackgroundImage image) { BackgroundImageProcessXMLFilter.setBackgroundImage(image); } /** * Returns the number of ports for the given {@link Operator}. * * @param op * the operator in question * @return the number of ports or {@code null} if they have not yet been stored */ public Integer getNumberOfPorts(Operator op) { return portNumbers.get(op); } /** * Sets the number of ports for the given {@link Operator}. * * @param op * the operator in question * @param number * the number of ports or {@code null} */ public Integer setNumberOfPorts(Operator op, Integer number) { return portNumbers.put(op, number); } /** * Sets the {@link Operator} {@link Rectangle2D}. Calculates and sets the height of the operator * to match the existing ports! * * @param op * the operator for which the rectangle should be set * @param rect * the rectangle representing position and size of operator */ public void setOperatorRect(Operator op, Rectangle2D rect) { if (op == null) { throw new IllegalArgumentException("op must not be null!"); } if (rect == null) { throw new IllegalArgumentException("rect must not be null!"); } // make sure operator is neither too tall nor too short double height = ProcessDrawUtils.calcHeighForOperator(op); if (rect.getHeight() != height) { rect.setRect(rect.getX(), rect.getY(), rect.getWidth(), height); } ProcessLayoutXMLFilter.setOperatorRectangle(op, rect); } /** * Returns the spacing of the specified {@link Port}. * * @param port * the port in question * @return the additional spacing before this port */ public int getPortSpacing(Port port) { return ProcessLayoutXMLFilter.lookupPortSpacing(port); } /** * Sets the spacing of the specified {@link Port}. * * @param port * the port in question * @param spacing * the additional spacing before the port */ public void setPortSpacing(Port port, int spacing) { if (port == null) { throw new IllegalArgumentException("port must not be null!"); } ProcessLayoutXMLFilter.setPortSpacing(port, spacing); } /** * Resets the spacing of the specified {@link Port} to the default value. * * @param port * the port in question */ public void resetPortSpacing(Port port) { if (port == null) { throw new IllegalArgumentException("port must not be null!"); } ProcessLayoutXMLFilter.resetPortSpacing(port); } /** * Looks up the view position of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @return The position or null. * @since 7.5 */ public Point getOperatorChainPosition(OperatorChain chain) { return ProcessLayoutXMLFilter.lookupOperatorChainPosition(chain); } /** * Sets the view position of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @param position * The center position. * @since 7.5 */ public void setOperatorChainPosition(OperatorChain chain, Point position) { if (chain == null) { throw new IllegalArgumentException("operator chain must not be null!"); } ProcessLayoutXMLFilter.setOperatorChainPosition(chain, position); } /** * Resets the view position of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @since 7.5 */ public void resetOperatorChainPosition(OperatorChain chain) { if (chain == null) { throw new IllegalArgumentException("operator chain must not be null!"); } ProcessLayoutXMLFilter.resetOperatorChainPosition(chain); } /** * Looks up the zoom of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @return The position or null. * @since 7.5 */ public Double getOperatorChainZoom(OperatorChain chain) { return ProcessLayoutXMLFilter.lookupOperatorChainZoom(chain); } /** * Sets the zoom of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @param position * The zoom. * @since 7.5 */ public void setOperatorChainZoom(OperatorChain chain, Double zoom) { if (chain == null) { throw new IllegalArgumentException("operator chain must not be null!"); } ProcessLayoutXMLFilter.setOperatorChainZoom(chain, zoom); } /** * Resets the zoom of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @since 7.5 */ public void resetOperatorChainZoom(OperatorChain chain) { if (chain == null) { throw new IllegalArgumentException("operator chain must not be null!"); } ProcessLayoutXMLFilter.resetOperatorChainZoom(chain); } /** * Looks up the scroll position of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @return The scroll position or null * @since 7.5 */ public Point getScrollPosition(OperatorChain operatorChain) { return ProcessLayoutXMLFilter.lookupScrollPosition(operatorChain); } /** * Sets the scroll position of the specified {@link OperatorChain}. * * @param operatorChain * The operator. * @param scrollPos * The scroll position. * @since 7.5 */ public void setScrollPosition(OperatorChain operatorChain, Point scrollPos) { ProcessLayoutXMLFilter.setScrollPosition(operatorChain, scrollPos); } /** * Resets the scroll position of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @since 7.5 */ public void resetScrollPosition(OperatorChain operatorChain) { ProcessLayoutXMLFilter.resetScrollPosition(operatorChain); } /** * Looks up the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @return The index or null * @since 7.5 */ public Double getScrollIndex(OperatorChain operatorChain) { return ProcessLayoutXMLFilter.lookupScrollIndex(operatorChain); } /** * Sets the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain * The operator. * @param index * The process index. * @since 7.5 */ public void setScrollIndex(OperatorChain operatorChain, Double index) { ProcessLayoutXMLFilter.setScrollIndex(operatorChain, index); } /** * Resets the scroll process index of the specified {@link OperatorChain}. * * @param operatorChain * The operator chain. * @since 7.5 */ public void resetScrollIndex(OperatorChain operatorChain) { ProcessLayoutXMLFilter.resetScrollIndex(operatorChain); } /** * Looks up the restore flag of the specified {@link Operator}. Indicates if this operator was * restored via undo/redo and should not be scrolled to. * * @param operator * the operator * @return if the flag was set * @since 7.5 */ public boolean getRestore(Operator operator) { return ProcessLayoutXMLFilter.lookupRestore(operator); } /** * Sets the restore flag of the specified {@link Operator}. Indicates that this operator was * restored via undo/redo and should not be scrolled to. * * @param operator * the operator * @param restore * The restore flag * @since 7.5 */ public void setRestore(Operator operator) { ProcessLayoutXMLFilter.setRestore(operator); } /** * Resets the restore flag of the specified {@link OperatorChain}. Indicates that this operator * should be scrolled to when selected. * * @param operator * The operator * @since 7.5 */ public void resetRestore(Operator operator) { ProcessLayoutXMLFilter.resetRestore(operator); } /** * Returns the {@link Point} the mouse is at relative to the process it currently is over. * * @return the location or {@code null} */ public Point getMousePositionRelativeToProcess() { return mousePositionRelativeToProcess; } /** * Sets the {@link Point} the mouse is at relative to the process it currently is over. * * @param mousePositionRelativeToProcess * the point or {@code null} */ public void setMousePositionRelativeToProcess(Point mousePositionRelativeToProcess) { this.mousePositionRelativeToProcess = mousePositionRelativeToProcess; } /** * Adds a {@link ProcessRendererEventListener} which will be informed of all changes to this * model. * * @param listener * the listener instance to add */ public void registerEventListener(final ProcessRendererEventListener listener) { if (listener == null) { throw new IllegalArgumentException("listener must not be null!"); } eventListener.add(ProcessRendererEventListener.class, listener); } /** * Removes the {@link ProcessRendererEventListener} from this model. * * @param listener * the listener instance to remove */ public void removeEventListener(final ProcessRendererEventListener listener) { if (listener == null) { throw new IllegalArgumentException("listener must not be null!"); } eventListener.remove(ProcessRendererEventListener.class, listener); } /** * Adds the given {@link ProcessEditor} listener. Automatically detects * {@link ExtendedProcessEditor}. */ public void addProcessEditor(final ProcessEditor p) { if (p instanceof ExtendedProcessEditor) { processEditors.add(ExtendedProcessEditor.class, (ExtendedProcessEditor) p); } else { processEditors.add(ProcessEditor.class, p); } } /** * Removes the given {@link ProcessEditor} listener. Automatically detects * {@link ExtendedProcessEditor}. */ public void removeProcessEditor(final ProcessEditor p) { if (p instanceof ExtendedProcessEditor) { processEditors.remove(ExtendedProcessEditor.class, (ExtendedProcessEditor) p); } else { processEditors.remove(ProcessEditor.class, p); } } /** * Adds the given {@link ProcessStorageListener}. * * @since 7.5 */ public void addProcessStorageListener(final ProcessStorageListener listener) { synchronized (storageListeners) { storageListeners.add(listener); } } /** * Removes the given {@link ProcessStorageListener}. * * @since 7.5 */ public void removeProcessStorageListener(final ProcessStorageListener listener) { synchronized (storageListeners) { storageListeners.remove(listener); } } /** * Informs the {@link ProcessEditor ProcessEditors} that the process was updated. Usually fired * on validation or when the undo stack was altered or accessed. * * @since 7.5 */ public void fireProcessUpdated() { Process process = getProcess(); for (ProcessEditor editor : processEditors.getListeners(ProcessEditor.class)) { editor.processUpdated(process); } for (ExtendedProcessEditor editor : processEditors.getListeners(ExtendedProcessEditor.class)) { editor.processUpdated(process); } } /** * Fire before displayed operator chain will changed. * * @since 7.5 */ public void fireDisplayedChainWillChange() { fireModelChanged(ModelEvent.DISPLAYED_CHAIN_WILL_CHANGE); } /** * Fire when the displayed operator chain has changed. */ public void fireDisplayedChainChanged() { fireModelChanged(ModelEvent.DISPLAYED_CHAIN_CHANGED); } /** * Fire when the displayed processes have changed. */ public void fireProcessesChanged() { fireModelChanged(ModelEvent.DISPLAYED_PROCESSES_CHANGED); } /** * Fire when a process size has changed. */ public void fireProcessSizeChanged() { fireModelChanged(ModelEvent.PROCESS_SIZE_CHANGED); } /** * Called before the process zoom level will change. The given point and index indicate which * process position should be the (new) center position. * * @param center * the (new) center point * @param index * the (new) process index * @since 7.5 */ public void prepareProcessZoomWillChange(Point center, int index) { setScrollIndex(displayedChain, (double) index); setScrollPosition(displayedChain, center); } /** * Fire when the process zoom level has changed. */ public void fireProcessZoomChanged() { fireModelChanged(ModelEvent.PROCESS_ZOOM_CHANGED); } /** * Fire when the something minor has changed which only requires a repaint. */ public void fireMiscChanged() { fireModelChanged(ModelEvent.MISC_CHANGED); } /** * Fire when an operator has been moved. * * @param operator * the moved operator */ public void fireOperatorMoved(Operator operator) { List<Operator> list = new LinkedList<>(); list.add(operator); fireOperatorsMoved(list); } /** * Fire when operators have been moved. * * @param operators * a collection of moved operators */ public void fireOperatorsMoved(Collection<Operator> operators) { fireOperatorsChanged(OperatorEvent.OPERATORS_MOVED, operators); } /** * Fire when the operator selection has changed. * * @param operators * a collection of selected operators */ public void fireOperatorSelectionChanged(Collection<Operator> operators) { fireOperatorsChanged(OperatorEvent.SELECTED_OPERATORS_CHANGED, operators); } /** * Fire when the number of ports for operators has changed. * * @param operators * a collection of operators which had their ports changed */ public void firePortsChanged(Collection<Operator> operators) { fireOperatorsChanged(OperatorEvent.PORTS_CHANGED, operators); } /** * Fire when an annotation has been moved. * * @param anno * the moved annotation */ public void fireAnnotationMoved(WorkflowAnnotation anno) { List<WorkflowAnnotation> list = new LinkedList<>(); list.add(anno); fireAnnotationsMoved(list); } /** * Fire when annotations have been moved. * * @param annotations * the moved annotations */ public void fireAnnotationsMoved(Collection<WorkflowAnnotation> annotations) { fireAnnotationsChanged(AnnotationEvent.ANNOTATIONS_MOVED, annotations); } /** * Fire when an annotation has been selected. * * @param anno * the selected annotation */ public void fireAnnotationSelected(WorkflowAnnotation anno) { List<WorkflowAnnotation> list = new LinkedList<>(); list.add(anno); fireAnnotationsChanged(AnnotationEvent.SELECTED_ANNOTATION_CHANGED, list); } /** * Fire when the something minor with workflow annotations has changed which only requires a * repaint. * * @param anno * the changed annotation, can be {@code null} */ public void fireAnnotationMiscChanged(WorkflowAnnotation anno) { List<WorkflowAnnotation> list = new LinkedList<>(); list.add(anno); fireAnnotationsChanged(AnnotationEvent.MISC_CHANGED, list); } /** * Adds the last snapshot state of the process to the undo list. Sets the model to changed if it * was not a simple view switch. */ private void addToUndoList(final boolean viewSwitch) { while (undoIndex < undoManager.getNumberOfUndos()) { undoManager.removeLast(); } if (!undoManager.add(viewSwitch)) { return; } String maxSizeProperty = ParameterService.getParameterValue(RapidMinerGUI.PROPERTY_RAPIDMINER_GUI_UNDOLIST_SIZE); int maxSize = 20; try { if (maxSizeProperty != null) { maxSize = Integer.parseInt(maxSizeProperty); } } catch (NumberFormatException e) { LogService.getRoot().warning("com.rapidminer.gui.main_frame_warning"); } while (undoManager.getNumberOfUndos() > maxSize) { undoManager.removeFirst(); } undoIndex = undoManager.getNumberOfUndos(); // mark as changed only if the XML has changed if (!viewSwitch) { hasChanged = true; } fireProcessUpdated(); } /** * Adds the current view to the undo stack. Called before the actual displyed chain will change. */ private void addViewSwitchToUndo(OperatorChain newChain) { takeSnapshot(); addToUndoList(true); } /** * Takes a snapshot of the current process. */ private void takeSnapshot() { Process process = getProcess(); if (process == null) { return; } fireDisplayedChainWillChange(); undoManager.takeSnapshot(process.getRootOperator().getXML(true), getDisplayedChain(), getSelectedOperators(), process.getAllOperators()); } /** * Restores the state of the process according to the given index. Will return a thrown * exception if a problem occurs. */ private Exception setToStep(int index) { String stateXML = undoManager.getXML(index); synchronized (process) { undoManager.clearSnapshot(); try { String currentXML = process.getRootOperator().getXML(true); ProcessLocation procLoc = process.getProcessLocation(); if (!stateXML.equals(currentXML)) { process = undoManager.restoreProcess(index); process.setProcessLocation(procLoc); hasChanged = true; fireProcessChanged(); } // restore displayed chain OperatorChain restoredOperatorChain = undoManager.restoreDisplayedChain(process, index); if (restoredOperatorChain != null) { displayedChain = restoredOperatorChain; fireDisplayedChainChanged(); } // restore selected operator List<Operator> restoredOperators = undoManager.restoreSelectedOperators(process, index); if (restoredOperators != null) { setRestore(restoredOperators.get(0)); selectedOperators = Collections.unmodifiableList(restoredOperators); fireOperatorSelectionChanged(selectedOperators); } fireProcessUpdated(); } catch (Exception e) { return e; } } return null; } /** * Resets the undo stack. */ private void resetUndo() { undoManager.reset(); takeSnapshot(); undoIndex = 0; hasChanged = false; } /** * Fires the given {@link ModelEvent}. * * @param type * the event type */ private void fireModelChanged(final ModelEvent type) { Object[] listeners = eventListener.getListenerList(); // Process the listeners last to first for (int i = 0; i < listeners.length - 1; i += 2) { if (listeners[i] == ProcessRendererEventListener.class) { ProcessRendererModelEvent e = new ProcessRendererModelEvent(type); ((ProcessRendererEventListener) listeners[i + 1]).modelChanged(e); } } } /** * Fires the given {@link OperatorEvent} with the affected {@link Operator}s. * * @param type * the event type * @param operators * the affected operators */ private void fireOperatorsChanged(final OperatorEvent type, Collection<Operator> operators) { Object[] listeners = eventListener.getListenerList(); // Process the listeners last to first for (int i = 0; i < listeners.length - 1; i += 2) { if (listeners[i] == ProcessRendererEventListener.class) { ProcessRendererOperatorEvent e = new ProcessRendererOperatorEvent(type); ((ProcessRendererEventListener) listeners[i + 1]).operatorsChanged(e, operators); } } } /** * Fires the given {@link AnnotationEvent} with the affected {@link WorkflowAnnotation}s. * * @param type * the event type * @param annotations * the affected annotations */ private void fireAnnotationsChanged(final AnnotationEvent type, Collection<WorkflowAnnotation> annotations) { Object[] listeners = eventListener.getListenerList(); // Process the listeners last to first for (int i = 0; i < listeners.length - 1; i += 2) { if (listeners[i] == ProcessRendererEventListener.class) { ProcessRendererAnnotationEvent e = new ProcessRendererAnnotationEvent(type); ((ProcessRendererEventListener) listeners[i + 1]).annotationsChanged(e, annotations); } } } /** * Informs the {@link ProcessEditor ProcessEditors} that the process was updated. Fired when the * process view has changed, e.g. when the user enters/leaves a subprocess in the process design * panel. * * @see #setDisplayedChain(OperatorChain) */ private void fireProcessViewChanged() { Process process = getProcess(); for (ExtendedProcessEditor editor : processEditors.getListeners(ExtendedProcessEditor.class)) { editor.processViewChanged(process); } } /** * Informs the {@link ProcessEditor ProcessEditors} that the process was changed. Fired when the * process was replaced. * * @see #setProcess(Process) */ private void fireProcessChanged() { Process process = getProcess(); for (ProcessEditor editor : processEditors.getListeners(ProcessEditor.class)) { editor.processChanged(process); } for (ExtendedProcessEditor editor : processEditors.getListeners(ExtendedProcessEditor.class)) { editor.processChanged(process); } } /** * Informs the {@link ProcessStorageListener ProcessStorageListeners} that the process was * loaded. * * @see #openProcess(Process) */ private void fireProcessLoaded() { Process process = getProcess(); LinkedList<ProcessStorageListener> list; synchronized (storageListeners) { list = new LinkedList<>(storageListeners); } for (ProcessStorageListener l : list) { l.opened(process); } } /** * Informs the {@link ProcessStorageListener ProcessStorageListeners} that the process was * saved. * * @see #processHasBeenSaved() */ private void fireProcessStored() { Process process = getProcess(); LinkedList<ProcessStorageListener> list; synchronized (storageListeners) { list = new LinkedList<>(storageListeners); } for (ProcessStorageListener l : list) { l.stored(process); } } }