major optimization updates
[debian/openrocket] / src / net / sf / openrocket / document / OpenRocketDocument.java
index 83c4b0b7ebeba0c15f5860bafde4c75a161e07a8..c935bd294e0d5ca12e113a480390924ef43614bc 100644 (file)
@@ -1,9 +1,7 @@
 package net.sf.openrocket.document;
-//TODO: LOW: move class somewhere else?
 
 import java.awt.event.ActionEvent;
 import java.io.File;
-import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -13,15 +11,35 @@ import javax.swing.Action;
 import net.sf.openrocket.document.events.DocumentChangeEvent;
 import net.sf.openrocket.document.events.DocumentChangeListener;
 import net.sf.openrocket.document.events.SimulationChangeEvent;
+import net.sf.openrocket.gui.main.ExceptionHandler;
+import net.sf.openrocket.l10n.Translator;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.logging.TraceException;
 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
 import net.sf.openrocket.rocketcomponent.Configuration;
 import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.ArrayList;
 import net.sf.openrocket.util.BugException;
 import net.sf.openrocket.util.Icons;
 
-
+/**
+ * Class describing an entire OpenRocket document, including a rocket and
+ * simulations.  The document contains:
+ * <p>
+ * - the rocket definition
+ * - a default Configuration
+ * - Simulation instances
+ * - the stored file and file save information
+ * - undo/redo information
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
 public class OpenRocketDocument implements ComponentChangeListener {
+       private static final LogHelper log = Application.getLogger();
+       private static final Translator trans = Application.getTranslator();
+       
        /**
         * The minimum number of undo levels that are stored.
         */
@@ -31,43 +49,64 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * UNDO_LEVELS by this amount the undo is purged to that length.
         */
        public static final int UNDO_MARGIN = 10;
-
        
        public static final String SIMULATION_NAME_PREFIX = "Simulation ";
        
+       /** Whether an undo error has already been reported to the user */
+       private static boolean undoErrorReported = false;
+       
+
+
        private final Rocket rocket;
        private final Configuration configuration;
-
-       private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
        
+       private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
        
-       private int undoPosition = -1;  // Illegal position, init in constructor
+
+       /*
+        * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
+        */
+
+       /** 
+        * The undo history of the rocket.   Whenever a new undo position is created while the
+        * rocket is in "dirty" state, the rocket is copied here.
+        */
        private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
        private LinkedList<String> undoDescription = new LinkedList<String>();
        
+       /**
+        * The position in the undoHistory we are currently at.  If modifications have been
+        * made to the rocket, the rocket is in "dirty" state and this points to the previous
+        * "clean" state.
+        */
+       private int undoPosition = -1; // Illegal position, init in constructor
+       
+       /**
+        * The description of the next action that modifies this rocket.
+        */
        private String nextDescription = null;
        private String storedDescription = null;
        
-       
+
        private File file = null;
        private int savedID = -1;
        
        private final StorageOptions storageOptions = new StorageOptions();
        
-       
-       private final List<DocumentChangeListener> listeners = 
-               new ArrayList<DocumentChangeListener>();
+
+       private final List<DocumentChangeListener> listeners =
+                       new ArrayList<DocumentChangeListener>();
        
        /* These must be initialized after undo history is set up. */
        private final UndoRedoAction undoAction;
        private final UndoRedoAction redoAction;
-               
+       
        
        public OpenRocketDocument(Rocket rocket) {
                this(rocket.getDefaultConfiguration());
        }
        
-
+       
        private OpenRocketDocument(Configuration configuration) {
                this.configuration = configuration;
                this.rocket = configuration.getRocket();
@@ -81,31 +120,31 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
-       
+
+
        public Rocket getRocket() {
                return rocket;
        }
-
+       
        
        public Configuration getDefaultConfiguration() {
                return configuration;
        }
-
-
+       
+       
        public File getFile() {
                return file;
        }
-
+       
        public void setFile(File file) {
                this.file = file;
        }
        
-
+       
        public boolean isSaved() {
                return rocket.getModID() == savedID;
        }
-
+       
        public void setSaved(boolean saved) {
                if (saved == false)
                        this.savedID = -1;
@@ -123,34 +162,40 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
-       
-       
-       @SuppressWarnings("unchecked")
+
+
+
        public List<Simulation> getSimulations() {
-               return (ArrayList<Simulation>)simulations.clone();
+               return simulations.clone();
        }
+       
        public int getSimulationCount() {
                return simulations.size();
        }
+       
        public Simulation getSimulation(int n) {
                return simulations.get(n);
        }
+       
        public int getSimulationIndex(Simulation simulation) {
                return simulations.indexOf(simulation);
        }
+       
        public void addSimulation(Simulation simulation) {
                simulations.add(simulation);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
        }
+       
        public void addSimulation(Simulation simulation, int n) {
                simulations.add(n, simulation);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
        }
+       
        public void removeSimulation(Simulation simulation) {
                simulations.remove(simulation);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
        }
+       
        public Simulation removeSimulation(int n) {
                Simulation simulation = simulations.remove(n);
                fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
@@ -167,16 +212,17 @@ public class OpenRocketDocument implements ComponentChangeListener {
        public String getNextSimulationName() {
                // Generate unique name for the simulation
                int maxValue = 0;
-               for (Simulation s: simulations) {
+               for (Simulation s : simulations) {
                        String name = s.getName();
                        if (name.startsWith(SIMULATION_NAME_PREFIX)) {
                                try {
-                                       maxValue = Math.max(maxValue, 
+                                       maxValue = Math.max(maxValue,
                                                        Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
-                               } catch (NumberFormatException ignore) { }
+                               } catch (NumberFormatException ignore) {
+                               }
                        }
                }
-               return SIMULATION_NAME_PREFIX + (maxValue+1);
+               return SIMULATION_NAME_PREFIX + (maxValue + 1);
        }
        
        
@@ -196,43 +242,52 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * @param description A short description of the following actions.
         */
        public void addUndoPosition(String description) {
-
+               
+               if (storedDescription != null) {
+                       logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
+                                       " description=" + description);
+               }
+               
                // Check whether modifications have been done since last call
                if (isCleanState()) {
                        // No modifications
+                       log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
                        nextDescription = description;
                        return;
                }
-
+               
+               log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
                
                /*
                 * Modifications have been made to the rocket.  We should be at the end of the
-                * undo history, but check for consistency.
+                * undo history, but check for consistency and try to recover.
                 */
-               assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
-               while (undoPosition < undoHistory.size()-1) {
+               if (undoPosition != undoHistory.size() - 1) {
+                       logUndoError("undo position inconsistency");
+               }
+               while (undoPosition < undoHistory.size() - 1) {
                        undoHistory.removeLast();
                        undoDescription.removeLast();
                }
                
-               
+
                // Add the current state to the undo history
-               undoHistory.add(rocket.copy());
-               undoDescription.add(description);
+               undoHistory.add(rocket.copyWithOriginalID());
+               undoDescription.add(null);
                nextDescription = description;
                undoPosition++;
                
-               
+
                // Maintain maximum undo size
-               if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) {
-                       for (int i=0; i < UNDO_MARGIN+1; i++) {
+               if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
+                       for (int i = 0; i < UNDO_MARGIN; i++) {
                                undoHistory.removeFirst();
                                undoDescription.removeFirst();
                                undoPosition--;
                        }
                }
        }
-
+       
        
        /**
         * Start a time-limited undoable operation.  After the operation {@link #stopUndo()}
@@ -244,8 +299,14 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * @param description   Description of the following undoable operations.
         */
        public void startUndo(String description) {
-               storedDescription = nextDescription;
+               if (storedDescription != null) {
+                       logUndoError("startUndo called while storedDescription=" + storedDescription +
+                                       " description=" + description);
+               }
+               log.info("Starting time-limited undoable operation '" + description + "' for " + this);
+               String store = nextDescription;
                addUndoPosition(description);
+               storedDescription = store;
        }
        
        /**
@@ -254,8 +315,11 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * performed.
         */
        public void stopUndo() {
-               addUndoPosition(storedDescription);
+               log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
+                               nextDescription + "     storedDescription=" + storedDescription);
+               String stored = storedDescription;
                storedDescription = null;
+               addUndoPosition(stored);
        }
        
        
@@ -273,10 +337,11 @@ public class OpenRocketDocument implements ComponentChangeListener {
         * Clear the undo history.
         */
        public void clearUndo() {
+               log.info("Clearing undo history of " + this);
                undoHistory.clear();
                undoDescription.clear();
                
-               undoHistory.add(rocket.copy());
+               undoHistory.add(rocket.copyWithOriginalID());
                undoDescription.add(null);
                undoPosition = 0;
                
@@ -291,8 +356,13 @@ public class OpenRocketDocument implements ComponentChangeListener {
        public void componentChanged(ComponentChangeEvent e) {
                
                if (!e.isUndoChange()) {
+                       if (undoPosition < undoHistory.size() - 1) {
+                               log.info("Rocket changed while in undo history, removing redo information for " + this +
+                                               " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
+                                               " isClean=" + isCleanState());
+                       }
                        // Remove any redo information if available
-                       while (undoPosition < undoHistory.size()-1) {
+                       while (undoPosition < undoHistory.size() - 1) {
                                undoHistory.removeLast();
                                undoDescription.removeLast();
                        }
@@ -304,8 +374,12 @@ public class OpenRocketDocument implements ComponentChangeListener {
                undoAction.setAllValues();
                redoAction.setAllValues();
        }
-
        
+       
+       /**
+        * Return whether undo action is available.
+        * @return      <code>true</code> if undo can be performed
+        */
        public boolean isUndoAvailable() {
                if (undoPosition > 0)
                        return true;
@@ -313,22 +387,34 @@ public class OpenRocketDocument implements ComponentChangeListener {
                return !isCleanState();
        }
        
+       /**
+        * Return the description of what action would be undone if undo is called.
+        * @return      the description what would be undone, or <code>null</code> if description unavailable.
+        */
        public String getUndoDescription() {
                if (!isUndoAvailable())
                        return null;
                
                if (isCleanState()) {
-                       return undoDescription.get(undoPosition-1);
+                       return undoDescription.get(undoPosition - 1);
                } else {
                        return undoDescription.get(undoPosition);
                }
        }
-
        
+       
+       /**
+        * Return whether redo action is available.
+        * @return      <code>true</code> if redo can be performed
+        */
        public boolean isRedoAvailable() {
-               return undoPosition < undoHistory.size()-1;
+               return undoPosition < undoHistory.size() - 1;
        }
        
+       /**
+        * Return the description of what action would be redone if redo is called.
+        * @return      the description of what would be redone, or <code>null</code> if description unavailable.
+        */
        public String getRedoDescription() {
                if (!isRedoAvailable())
                        return null;
@@ -337,35 +423,63 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
-       
+       /**
+        * Perform undo operation on the rocket.
+        */
        public void undo() {
+               log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
+                               " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
                if (!isUndoAvailable()) {
-                       throw new IllegalStateException("Undo not available.");
+                       logUndoError("Undo not available");
+                       undoAction.setAllValues();
+                       redoAction.setAllValues();
+                       return;
                }
-
+               if (storedDescription != null) {
+                       logUndoError("undo() called with storedDescription=" + storedDescription);
+               }
+               
                // Update history position
                
                if (isCleanState()) {
                        // We are in a clean state, simply move backwards in history
                        undoPosition--;
                } else {
+                       if (undoPosition != undoHistory.size() - 1) {
+                               logUndoError("undo position inconsistency");
+                       }
                        // Modifications have been made, save the state and restore previous state
-                       undoHistory.add(rocket.copy());
+                       undoHistory.add(rocket.copyWithOriginalID());
                        undoDescription.add(null);
                }
                
-               rocket.loadFrom(undoHistory.get(undoPosition).copy());
+               rocket.checkComponentStructure();
+               undoHistory.get(undoPosition).checkComponentStructure();
+               undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
+               rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
+               rocket.checkComponentStructure();
        }
        
        
+       /**
+        * Perform redo operation on the rocket.
+        */
        public void redo() {
+               log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
+                               " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
                if (!isRedoAvailable()) {
-                       throw new IllegalStateException("Redo not available.");
+                       logUndoError("Redo not available");
+                       undoAction.setAllValues();
+                       redoAction.setAllValues();
+                       return;
+               }
+               if (storedDescription != null) {
+                       logUndoError("redo() called with storedDescription=" + storedDescription);
                }
                
                undoPosition++;
                
-               rocket.loadFrom(undoHistory.get(undoPosition).copy());
+               rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
        }
        
        
@@ -374,8 +488,43 @@ public class OpenRocketDocument implements ComponentChangeListener {
        }
        
        
+       /**
+        * Log a non-fatal undo/redo error or inconsistency.  Reports it to the user the first 
+        * time it occurs, but not on subsequent times.  Logs automatically the undo system state.
+        */
+       private void logUndoError(String error) {
+               log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
+                               " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
+                               " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
+                               new TraceException());
+               
+               if (!undoErrorReported) {
+                       undoErrorReported = true;
+                       ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
+               }
+       }
        
        
+
+       /**
+        * Return a copy of this document.  The rocket is copied with original ID's, the default
+        * motor configuration ID is maintained and the simulations are copied to the new rocket.
+        * No undo/redo information or file storage information is maintained.
+        * 
+        * @return      a copy of this document.
+        */
+       public OpenRocketDocument copy() {
+               Rocket rocketCopy = rocket.copyWithOriginalID();
+               OpenRocketDocument documentCopy = new OpenRocketDocument(rocketCopy);
+               documentCopy.getDefaultConfiguration().setMotorConfigurationID(configuration.getMotorConfigurationID());
+               for (Simulation s : simulations) {
+                       documentCopy.addSimulation(s.duplicateSimulation(rocketCopy));
+               }
+               return documentCopy;
+       }
+       
+       
+
        ///////  Listeners
        
        public void addDocumentChangeListener(DocumentChangeListener listener) {
@@ -388,14 +537,14 @@ public class OpenRocketDocument implements ComponentChangeListener {
        
        protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
                DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
-               for (DocumentChangeListener l: array) {
+               for (DocumentChangeListener l : array) {
                        l.documentChanged(event);
                }
        }
        
        
-       
-       
+
+
        /**
         * Inner class to implement undo/redo actions.
         */
@@ -408,56 +557,61 @@ public class OpenRocketDocument implements ComponentChangeListener {
                // Sole constructor
                public UndoRedoAction(int type) {
                        if (type != UNDO && type != REDO) {
-                               throw new IllegalArgumentException("Unknown type = "+type);
+                               throw new IllegalArgumentException("Unknown type = " + type);
                        }
                        this.type = type;
                        setAllValues();
                }
-
+               
                
                // Actual action to make
+               @Override
                public void actionPerformed(ActionEvent e) {
                        switch (type) {
                        case UNDO:
+                               log.user("Performing undo, event=" + e);
                                undo();
                                break;
-                               
+                       
                        case REDO:
+                               log.user("Performing redo, event=" + e);
                                redo();
                                break;
                        }
                }
-
+               
                
                // Set all the values correctly (name and enabled/disabled status)
                public void setAllValues() {
-                       String name,desc;
-                       boolean enabled;
+                       String name, desc;
+                       boolean actionEnabled;
                        
                        switch (type) {
                        case UNDO:
-                               name = "Undo";
+                               //// Undo
+                               name = trans.get("OpenRocketDocument.Undo");
                                desc = getUndoDescription();
-                               enabled = isUndoAvailable();
+                               actionEnabled = isUndoAvailable();
                                this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
                                break;
-                               
+                       
                        case REDO:
-                               name = "Redo";
+                               ////Redo
+                               name = trans.get("OpenRocketDocument.Redo");
                                desc = getRedoDescription();
-                               enabled = isRedoAvailable();
+                               actionEnabled = isRedoAvailable();
                                this.putValue(SMALL_ICON, Icons.EDIT_REDO);
                                break;
-                               
+                       
                        default:
-                               throw new BugException("illegal type="+type);
+                               throw new BugException("illegal type=" + type);
                        }
                        
                        if (desc != null)
-                               name = name + " ("+desc+")";
+                               name = name + " (" + desc + ")";
                        
                        putValue(NAME, name);
-                       setEnabled(enabled);
+                       setEnabled(actionEnabled);
                }
        }
 }