/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.flow; import java.awt.Point; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; import com.rapidminer.Process; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.flow.processrendering.model.ProcessRendererModel; import com.rapidminer.io.process.ProcessLayoutXMLFilter; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorChain; import com.rapidminer.tools.XMLException; /** * Handles the list of undo states for the current {@link Process} in the {@link MainFrame}. * Operations are <i>not</i> synchronized. Is a repurposed class of an older implementation * ({@link ProcessUndoManager}). * * @author Jan Czogalla * @since 7.5 */ public class NewProcessUndoManager { /** * Simple storage construct for a {@link Process} state. * * @author Jan Czogalla * @since 7.5 */ private static class ProcessUndoState { String processXML; String displayedChain; List<String> selectedOperators; List<String> viewUserData; } private final List<ProcessUndoState> undoList = new ArrayList<>(); private ProcessUndoState lastSnapshot; private ProcessUndoState snapshot; /** Resets the list of undo states, removes all stored states. */ public void reset() { undoList.clear(); clearSnapshot(); } /** Returns the number of currently stored states. */ public int getNumberOfUndos() { return undoList.size(); } /** * Returns the XML string representing the {@link Process} at the given undo index. * * @param index * @return the XML or {@code null} if no undo step existed for the given index */ public String getXML(int index) { try { return undoList.get(index).processXML; } catch (IndexOutOfBoundsException e) { return null; } } /** Removes the last undo step. If there is none, does nothing. */ public void removeLast() { if (undoList.isEmpty()) { return; } undoList.remove(undoList.size() - 1); } /** Removes the first undo step. If there is none, does nothing. */ public void removeFirst() { if (undoList.isEmpty()) { return; } undoList.remove(0); } /** Returns whether the process has changed between snapshots. */ public boolean snapshotDiffers() { ProcessUndoState last = lastSnapshot; ProcessUndoState current = snapshot; return last != null && last != current && !last.processXML.equals(current.processXML); } /** * Adds an undo step that was previously created by * {@link #takeSnapshot(String, OperatorChain, Collection, Collection)}. Will add the current * snapshot if so indicated; usually, the current snapshot will be used when a view change * occurred,and the last snapshot if the XML of the process changed in some way * * @param useCurrent * indicates whether the current or last snapshot should be used * @return if the state was actually added * @see ProcessRendererModel#addToUndoList(boolean) */ public boolean add(boolean useCurrent) { ProcessUndoState state = useCurrent ? snapshot : lastSnapshot; // ProcessUndoState state = lastSnapshot; if (state != null && state.processXML != null) { undoList.add(state); return true; } return false; } /** * Takes a snapshot, consisting of the (non-null) {@link Process} XML, the currently displayed * {@link OperatorChain}, the list of currently selected {@link Operator Operators} and a list * of all {@link Operator Operators} present in the process. */ public void takeSnapshot(String processXML, OperatorChain displayedChain, Collection<Operator> selectedOperators, Collection<Operator> allOperators) { if (processXML == null) { throw new IllegalArgumentException("processXML must not be null!"); } ProcessUndoState state = new ProcessUndoState(); state.processXML = processXML; state.displayedChain = displayedChain == null ? null : displayedChain.getName(); state.selectedOperators = selectedOperators.stream().map(op -> op.getName()).collect(Collectors.toList()); state.viewUserData = extractUserData(allOperators); lastSnapshot = snapshot; snapshot = state; } /** Clears both tiers of snapshots. */ public void clearSnapshot() { snapshot = lastSnapshot = null; } /** * Restores a {@link Process} from the given undo index if possible, setting the user data from * the stored state. May skip unreadable user data. Throws an Exception if an error occurs while * parsing the XML string ({@link Process#Process(String)}). * * @return the restored process or null if the index is invalid * */ public Process restoreProcess(int index) throws IOException, XMLException { ProcessUndoState state; try { state = undoList.get(index); } catch (IndexOutOfBoundsException e) { return null; } Process p = new Process(state.processXML); if (state.viewUserData == null) { return p; } restoreUserData(state, p); return p; } /** * Restores the viewed {@link OperatorChain} from the given undo index and {@link Process}. Will * return null if the index is invalid or the process does not contain the stored chain. */ public OperatorChain restoreDisplayedChain(Process p, int index) { ProcessUndoState state; try { state = undoList.get(index); } catch (IndexOutOfBoundsException e) { return null; } restoreUserData(state, p); return (OperatorChain) p.getOperator(state.displayedChain); } /** * Restores the list of {@link Operator Operators} from the given undo index and * {@link Process}. Will return null if the index is invalid or no operator from the stored * state is present in the process. */ public List<Operator> restoreSelectedOperators(Process p, int index) { ProcessUndoState state; try { state = undoList.get(index); } catch (IndexOutOfBoundsException e) { return null; } List<Operator> selected = state.selectedOperators.stream().map(p::getOperator).filter(op -> op != null) .collect(Collectors.toList()); return selected.isEmpty() ? null : selected; } /** Restores the user data from the given state in the given {@link Process}. */ private void restoreUserData(ProcessUndoState state, Process p) { if (state.viewUserData == null) { return; } state.viewUserData.forEach(vud -> setUserData(p, vud)); } /** Set the user data encoded by the given string. */ private void setUserData(Process p, String vud) { try { String[] vudSplit = vud.split(" ", 3); OperatorChain opChain = (OperatorChain) p.getOperator(vudSplit[2]); if (opChain == null) { return; } Object ud = userDataFrom(vudSplit[0]); if (vudSplit[1].equals(ProcessLayoutXMLFilter.KEY_OPERATOR_CHAIN_POSITION)) { ProcessLayoutXMLFilter.setOperatorChainPosition(opChain, (Point) ud); } else { ProcessLayoutXMLFilter.setOperatorChainZoom(opChain, (Double) ud); } } catch (Exception e) { // ignore } } /** * Creates the object represented by the given String. Will be either a {@link Point} or a * {@link Double} */ private Object userDataFrom(String string) { if (string.contains(",")) { String[] point = string.split(","); return new Point(Integer.parseInt(point[0]), Integer.parseInt(point[1])); } return Double.parseDouble(string); } /** * Extracts relevant view user data from the {@link OperatorChain OperatorChains} from the given * list. Returns a list of the form "value key name". */ private List<String> extractUserData(Collection<Operator> allOperators) { List<String> userData = new ArrayList<>(); for (Operator op : allOperators) { if (!(op instanceof OperatorChain)) { continue; } OperatorChain opChain = (OperatorChain) op; String name = opChain.getName(); Point position = ProcessLayoutXMLFilter.lookupOperatorChainPosition(opChain); if (position != null) { userData.add(position.x + "," + position.y + " " + ProcessLayoutXMLFilter.KEY_OPERATOR_CHAIN_POSITION + " " + name); } Double zoom = ProcessLayoutXMLFilter.lookupOperatorChainZoom(opChain); if (zoom != null) { userData.add(zoom + " " + ProcessLayoutXMLFilter.KEY_OPERATOR_CHAIN_ZOOM + " " + name); } } return userData.isEmpty() ? null : userData; } }