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