major optimization updates
[debian/openrocket] / src / net / sf / openrocket / document / OpenRocketDocument.java
1 package net.sf.openrocket.document;
2
3 import java.awt.event.ActionEvent;
4 import java.io.File;
5 import java.util.LinkedList;
6 import java.util.List;
7
8 import javax.swing.AbstractAction;
9 import javax.swing.Action;
10
11 import net.sf.openrocket.document.events.DocumentChangeEvent;
12 import net.sf.openrocket.document.events.DocumentChangeListener;
13 import net.sf.openrocket.document.events.SimulationChangeEvent;
14 import net.sf.openrocket.gui.main.ExceptionHandler;
15 import net.sf.openrocket.l10n.Translator;
16 import net.sf.openrocket.logging.LogHelper;
17 import net.sf.openrocket.logging.TraceException;
18 import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
19 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
20 import net.sf.openrocket.rocketcomponent.Configuration;
21 import net.sf.openrocket.rocketcomponent.Rocket;
22 import net.sf.openrocket.startup.Application;
23 import net.sf.openrocket.util.ArrayList;
24 import net.sf.openrocket.util.BugException;
25 import net.sf.openrocket.util.Icons;
26
27 /**
28  * Class describing an entire OpenRocket document, including a rocket and
29  * simulations.  The document contains:
30  * <p>
31  * - the rocket definition
32  * - a default Configuration
33  * - Simulation instances
34  * - the stored file and file save information
35  * - undo/redo information
36  * 
37  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
38  */
39 public class OpenRocketDocument implements ComponentChangeListener {
40         private static final LogHelper log = Application.getLogger();
41         private static final Translator trans = Application.getTranslator();
42         
43         /**
44          * The minimum number of undo levels that are stored.
45          */
46         public static final int UNDO_LEVELS = 50;
47         /**
48          * The margin of the undo levels.  After the number of undo levels exceeds 
49          * UNDO_LEVELS by this amount the undo is purged to that length.
50          */
51         public static final int UNDO_MARGIN = 10;
52         
53         public static final String SIMULATION_NAME_PREFIX = "Simulation ";
54         
55         /** Whether an undo error has already been reported to the user */
56         private static boolean undoErrorReported = false;
57         
58
59
60         private final Rocket rocket;
61         private final Configuration configuration;
62         
63         private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
64         
65
66         /*
67          * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
68          */
69
70         /** 
71          * The undo history of the rocket.   Whenever a new undo position is created while the
72          * rocket is in "dirty" state, the rocket is copied here.
73          */
74         private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
75         private LinkedList<String> undoDescription = new LinkedList<String>();
76         
77         /**
78          * The position in the undoHistory we are currently at.  If modifications have been
79          * made to the rocket, the rocket is in "dirty" state and this points to the previous
80          * "clean" state.
81          */
82         private int undoPosition = -1; // Illegal position, init in constructor
83         
84         /**
85          * The description of the next action that modifies this rocket.
86          */
87         private String nextDescription = null;
88         private String storedDescription = null;
89         
90
91         private File file = null;
92         private int savedID = -1;
93         
94         private final StorageOptions storageOptions = new StorageOptions();
95         
96
97         private final List<DocumentChangeListener> listeners =
98                         new ArrayList<DocumentChangeListener>();
99         
100         /* These must be initialized after undo history is set up. */
101         private final UndoRedoAction undoAction;
102         private final UndoRedoAction redoAction;
103         
104         
105         public OpenRocketDocument(Rocket rocket) {
106                 this(rocket.getDefaultConfiguration());
107         }
108         
109         
110         private OpenRocketDocument(Configuration configuration) {
111                 this.configuration = configuration;
112                 this.rocket = configuration.getRocket();
113                 
114                 clearUndo();
115                 
116                 undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
117                 redoAction = new UndoRedoAction(UndoRedoAction.REDO);
118                 
119                 rocket.addComponentChangeListener(this);
120         }
121         
122         
123
124
125         public Rocket getRocket() {
126                 return rocket;
127         }
128         
129         
130         public Configuration getDefaultConfiguration() {
131                 return configuration;
132         }
133         
134         
135         public File getFile() {
136                 return file;
137         }
138         
139         public void setFile(File file) {
140                 this.file = file;
141         }
142         
143         
144         public boolean isSaved() {
145                 return rocket.getModID() == savedID;
146         }
147         
148         public void setSaved(boolean saved) {
149                 if (saved == false)
150                         this.savedID = -1;
151                 else
152                         this.savedID = rocket.getModID();
153         }
154         
155         /**
156          * Retrieve the default storage options for this document.
157          * 
158          * @return      the storage options.
159          */
160         public StorageOptions getDefaultStorageOptions() {
161                 return storageOptions;
162         }
163         
164         
165
166
167
168         public List<Simulation> getSimulations() {
169                 return simulations.clone();
170         }
171         
172         public int getSimulationCount() {
173                 return simulations.size();
174         }
175         
176         public Simulation getSimulation(int n) {
177                 return simulations.get(n);
178         }
179         
180         public int getSimulationIndex(Simulation simulation) {
181                 return simulations.indexOf(simulation);
182         }
183         
184         public void addSimulation(Simulation simulation) {
185                 simulations.add(simulation);
186                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
187         }
188         
189         public void addSimulation(Simulation simulation, int n) {
190                 simulations.add(n, simulation);
191                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
192         }
193         
194         public void removeSimulation(Simulation simulation) {
195                 simulations.remove(simulation);
196                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
197         }
198         
199         public Simulation removeSimulation(int n) {
200                 Simulation simulation = simulations.remove(n);
201                 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
202                 return simulation;
203         }
204         
205         /**
206          * Return a unique name suitable for the next simulation.  The name begins
207          * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any
208          * previous simulation.
209          * 
210          * @return      the new name.
211          */
212         public String getNextSimulationName() {
213                 // Generate unique name for the simulation
214                 int maxValue = 0;
215                 for (Simulation s : simulations) {
216                         String name = s.getName();
217                         if (name.startsWith(SIMULATION_NAME_PREFIX)) {
218                                 try {
219                                         maxValue = Math.max(maxValue,
220                                                         Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
221                                 } catch (NumberFormatException ignore) {
222                                 }
223                         }
224                 }
225                 return SIMULATION_NAME_PREFIX + (maxValue + 1);
226         }
227         
228         
229         /**
230          * Adds an undo point at this position.  This method should be called *before* any
231          * action that is to be undoable.  All actions after the call will be undone by a 
232          * single "Undo" action.
233          * <p>
234          * The description should be a short, descriptive string of the actions that will 
235          * follow.  This is shown to the user e.g. in the Edit-menu, for example 
236          * "Undo (Modify body tube)".  If the actions are not known (in general should not
237          * be the case!) description may be null.
238          * <p>
239          * If this method is called successively without any change events occurring between the
240          * calls, only the last call will have any effect.
241          * 
242          * @param description A short description of the following actions.
243          */
244         public void addUndoPosition(String description) {
245                 
246                 if (storedDescription != null) {
247                         logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
248                                         " description=" + description);
249                 }
250                 
251                 // Check whether modifications have been done since last call
252                 if (isCleanState()) {
253                         // No modifications
254                         log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
255                         nextDescription = description;
256                         return;
257                 }
258                 
259                 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
260                 
261                 /*
262                  * Modifications have been made to the rocket.  We should be at the end of the
263                  * undo history, but check for consistency and try to recover.
264                  */
265                 if (undoPosition != undoHistory.size() - 1) {
266                         logUndoError("undo position inconsistency");
267                 }
268                 while (undoPosition < undoHistory.size() - 1) {
269                         undoHistory.removeLast();
270                         undoDescription.removeLast();
271                 }
272                 
273
274                 // Add the current state to the undo history
275                 undoHistory.add(rocket.copyWithOriginalID());
276                 undoDescription.add(null);
277                 nextDescription = description;
278                 undoPosition++;
279                 
280
281                 // Maintain maximum undo size
282                 if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) {
283                         for (int i = 0; i < UNDO_MARGIN; i++) {
284                                 undoHistory.removeFirst();
285                                 undoDescription.removeFirst();
286                                 undoPosition--;
287                         }
288                 }
289         }
290         
291         
292         /**
293          * Start a time-limited undoable operation.  After the operation {@link #stopUndo()}
294          * must be called, which will restore the previous undo description into effect.
295          * Only one level of start-stop undo descriptions is supported, i.e. start-stop
296          * undo cannot be nested, and no other undo operations may be called between
297          * the start and stop calls.
298          * 
299          * @param description   Description of the following undoable operations.
300          */
301         public void startUndo(String description) {
302                 if (storedDescription != null) {
303                         logUndoError("startUndo called while storedDescription=" + storedDescription +
304                                         " description=" + description);
305                 }
306                 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
307                 String store = nextDescription;
308                 addUndoPosition(description);
309                 storedDescription = store;
310         }
311         
312         /**
313          * End the previous time-limited undoable operation.  This must be called after
314          * {@link #startUndo(String)} has been called before any other undo operations are
315          * performed.
316          */
317         public void stopUndo() {
318                 log.info("Ending time-limited undoable operation for " + this + " nextDescription=" +
319                                 nextDescription + "     storedDescription=" + storedDescription);
320                 String stored = storedDescription;
321                 storedDescription = null;
322                 addUndoPosition(stored);
323         }
324         
325         
326         public Action getUndoAction() {
327                 return undoAction;
328         }
329         
330         
331         public Action getRedoAction() {
332                 return redoAction;
333         }
334         
335         
336         /**
337          * Clear the undo history.
338          */
339         public void clearUndo() {
340                 log.info("Clearing undo history of " + this);
341                 undoHistory.clear();
342                 undoDescription.clear();
343                 
344                 undoHistory.add(rocket.copyWithOriginalID());
345                 undoDescription.add(null);
346                 undoPosition = 0;
347                 
348                 if (undoAction != null)
349                         undoAction.setAllValues();
350                 if (redoAction != null)
351                         redoAction.setAllValues();
352         }
353         
354         
355         @Override
356         public void componentChanged(ComponentChangeEvent e) {
357                 
358                 if (!e.isUndoChange()) {
359                         if (undoPosition < undoHistory.size() - 1) {
360                                 log.info("Rocket changed while in undo history, removing redo information for " + this +
361                                                 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
362                                                 " isClean=" + isCleanState());
363                         }
364                         // Remove any redo information if available
365                         while (undoPosition < undoHistory.size() - 1) {
366                                 undoHistory.removeLast();
367                                 undoDescription.removeLast();
368                         }
369                         
370                         // Set the latest description
371                         undoDescription.set(undoPosition, nextDescription);
372                 }
373                 
374                 undoAction.setAllValues();
375                 redoAction.setAllValues();
376         }
377         
378         
379         /**
380          * Return whether undo action is available.
381          * @return      <code>true</code> if undo can be performed
382          */
383         public boolean isUndoAvailable() {
384                 if (undoPosition > 0)
385                         return true;
386                 
387                 return !isCleanState();
388         }
389         
390         /**
391          * Return the description of what action would be undone if undo is called.
392          * @return      the description what would be undone, or <code>null</code> if description unavailable.
393          */
394         public String getUndoDescription() {
395                 if (!isUndoAvailable())
396                         return null;
397                 
398                 if (isCleanState()) {
399                         return undoDescription.get(undoPosition - 1);
400                 } else {
401                         return undoDescription.get(undoPosition);
402                 }
403         }
404         
405         
406         /**
407          * Return whether redo action is available.
408          * @return      <code>true</code> if redo can be performed
409          */
410         public boolean isRedoAvailable() {
411                 return undoPosition < undoHistory.size() - 1;
412         }
413         
414         /**
415          * Return the description of what action would be redone if redo is called.
416          * @return      the description of what would be redone, or <code>null</code> if description unavailable.
417          */
418         public String getRedoDescription() {
419                 if (!isRedoAvailable())
420                         return null;
421                 
422                 return undoDescription.get(undoPosition);
423         }
424         
425         
426         /**
427          * Perform undo operation on the rocket.
428          */
429         public void undo() {
430                 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
431                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
432                 if (!isUndoAvailable()) {
433                         logUndoError("Undo not available");
434                         undoAction.setAllValues();
435                         redoAction.setAllValues();
436                         return;
437                 }
438                 if (storedDescription != null) {
439                         logUndoError("undo() called with storedDescription=" + storedDescription);
440                 }
441                 
442                 // Update history position
443                 
444                 if (isCleanState()) {
445                         // We are in a clean state, simply move backwards in history
446                         undoPosition--;
447                 } else {
448                         if (undoPosition != undoHistory.size() - 1) {
449                                 logUndoError("undo position inconsistency");
450                         }
451                         // Modifications have been made, save the state and restore previous state
452                         undoHistory.add(rocket.copyWithOriginalID());
453                         undoDescription.add(null);
454                 }
455                 
456                 rocket.checkComponentStructure();
457                 undoHistory.get(undoPosition).checkComponentStructure();
458                 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
459                 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
460                 rocket.checkComponentStructure();
461         }
462         
463         
464         /**
465          * Perform redo operation on the rocket.
466          */
467         public void redo() {
468                 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
469                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
470                 if (!isRedoAvailable()) {
471                         logUndoError("Redo not available");
472                         undoAction.setAllValues();
473                         redoAction.setAllValues();
474                         return;
475                 }
476                 if (storedDescription != null) {
477                         logUndoError("redo() called with storedDescription=" + storedDescription);
478                 }
479                 
480                 undoPosition++;
481                 
482                 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
483         }
484         
485         
486         private boolean isCleanState() {
487                 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
488         }
489         
490         
491         /**
492          * Log a non-fatal undo/redo error or inconsistency.  Reports it to the user the first 
493          * time it occurs, but not on subsequent times.  Logs automatically the undo system state.
494          */
495         private void logUndoError(String error) {
496                 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
497                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
498                                 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
499                                 new TraceException());
500                 
501                 if (!undoErrorReported) {
502                         undoErrorReported = true;
503                         ExceptionHandler.handleErrorCondition("Undo/Redo error: " + error);
504                 }
505         }
506         
507         
508
509         /**
510          * Return a copy of this document.  The rocket is copied with original ID's, the default
511          * motor configuration ID is maintained and the simulations are copied to the new rocket.
512          * No undo/redo information or file storage information is maintained.
513          * 
514          * @return      a copy of this document.
515          */
516         public OpenRocketDocument copy() {
517                 Rocket rocketCopy = rocket.copyWithOriginalID();
518                 OpenRocketDocument documentCopy = new OpenRocketDocument(rocketCopy);
519                 documentCopy.getDefaultConfiguration().setMotorConfigurationID(configuration.getMotorConfigurationID());
520                 for (Simulation s : simulations) {
521                         documentCopy.addSimulation(s.duplicateSimulation(rocketCopy));
522                 }
523                 return documentCopy;
524         }
525         
526         
527
528         ///////  Listeners
529         
530         public void addDocumentChangeListener(DocumentChangeListener listener) {
531                 listeners.add(listener);
532         }
533         
534         public void removeDocumentChangeListener(DocumentChangeListener listener) {
535                 listeners.remove(listener);
536         }
537         
538         protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
539                 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
540                 for (DocumentChangeListener l : array) {
541                         l.documentChanged(event);
542                 }
543         }
544         
545         
546
547
548         /**
549          * Inner class to implement undo/redo actions.
550          */
551         private class UndoRedoAction extends AbstractAction {
552                 public static final int UNDO = 1;
553                 public static final int REDO = 2;
554                 
555                 private final int type;
556                 
557                 // Sole constructor
558                 public UndoRedoAction(int type) {
559                         if (type != UNDO && type != REDO) {
560                                 throw new IllegalArgumentException("Unknown type = " + type);
561                         }
562                         this.type = type;
563                         setAllValues();
564                 }
565                 
566                 
567                 // Actual action to make
568                 @Override
569                 public void actionPerformed(ActionEvent e) {
570                         switch (type) {
571                         case UNDO:
572                                 log.user("Performing undo, event=" + e);
573                                 undo();
574                                 break;
575                         
576                         case REDO:
577                                 log.user("Performing redo, event=" + e);
578                                 redo();
579                                 break;
580                         }
581                 }
582                 
583                 
584                 // Set all the values correctly (name and enabled/disabled status)
585                 public void setAllValues() {
586                         String name, desc;
587                         boolean actionEnabled;
588                         
589                         switch (type) {
590                         case UNDO:
591                                 //// Undo
592                                 name = trans.get("OpenRocketDocument.Undo");
593                                 desc = getUndoDescription();
594                                 actionEnabled = isUndoAvailable();
595                                 this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
596                                 break;
597                         
598                         case REDO:
599                                 ////Redo
600                                 name = trans.get("OpenRocketDocument.Redo");
601                                 desc = getRedoDescription();
602                                 actionEnabled = isRedoAvailable();
603                                 this.putValue(SMALL_ICON, Icons.EDIT_REDO);
604                                 break;
605                         
606                         default:
607                                 throw new BugException("illegal type=" + type);
608                         }
609                         
610                         if (desc != null)
611                                 name = name + " (" + desc + ")";
612                         
613                         putValue(NAME, name);
614                         setEnabled(actionEnabled);
615                 }
616         }
617 }