Big update to custom expression feature.
[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.simulation.customexpression.CustomExpression;
17 import net.sf.openrocket.startup.Application;
18 import net.sf.openrocket.util.ArrayList;
19
20 /**
21  * Class describing an entire OpenRocket document, including a rocket and
22  * simulations.  The document contains:
23  * <p>
24  * - the rocket definition
25  * - a default Configuration
26  * - Simulation instances
27  * - the stored file and file save information
28  * - undo/redo information
29  * 
30  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
31  */
32 public class OpenRocketDocument implements ComponentChangeListener {
33         private static final LogHelper log = Application.getLogger();
34         
35         /**
36          * The minimum number of undo levels that are stored.
37          */
38         public static final int UNDO_LEVELS = 50;
39         /**
40          * The margin of the undo levels.  After the number of undo levels exceeds 
41          * UNDO_LEVELS by this amount the undo is purged to that length.
42          */
43         public static final int UNDO_MARGIN = 10;
44         
45         public static final String SIMULATION_NAME_PREFIX = "Simulation ";
46         
47         /** Whether an undo error has already been reported to the user */
48         private static boolean undoErrorReported = false;
49         
50         private final Rocket rocket;
51         private final Configuration configuration;
52         
53         private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
54         private ArrayList<CustomExpression> customExpressions = new ArrayList<CustomExpression>();
55
56
57         /*
58          * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
59          */
60
61         /** 
62          * The undo history of the rocket.   Whenever a new undo position is created while the
63          * rocket is in "dirty" state, the rocket is copied here.
64          */
65         private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
66         private LinkedList<String> undoDescription = new LinkedList<String>();
67
68         /**
69          * The position in the undoHistory we are currently at.  If modifications have been
70          * made to the rocket, the rocket is in "dirty" state and this points to the previous
71          * "clean" state.
72          */
73         private int undoPosition = -1; // Illegal position, init in constructor
74         
75         /**
76          * The description of the next action that modifies this rocket.
77          */
78         private String nextDescription = null;
79         private String storedDescription = null;
80         
81
82         private ArrayList<UndoRedoListener> undoRedoListeners = new ArrayList<UndoRedoListener>(2);
83         
84         private File file = null;
85         private int savedID = -1;
86         
87         private final StorageOptions storageOptions = new StorageOptions();
88         
89
90         private final List<DocumentChangeListener> listeners =
91                         new ArrayList<DocumentChangeListener>();
92         
93         public OpenRocketDocument(Rocket rocket) {
94                 this(rocket.getDefaultConfiguration());
95         }
96         
97         
98         private OpenRocketDocument(Configuration configuration) {
99                 this.configuration = configuration;
100                 this.rocket = configuration.getRocket();
101                 
102                 clearUndo();
103                 
104                 rocket.addComponentChangeListener(this);
105         }
106         
107         
108         public void addCustomExpression(CustomExpression expression){
109                 if (customExpressions.contains(expression)){
110                         log.user("Could not add custom expression "+expression.getName()+" to document as document alerady has a matching expression.");
111                 } else {
112                         customExpressions.add(expression);
113                 }
114         }
115         
116         public void removeCustomExpression(CustomExpression expression){
117                 customExpressions.remove(expression);
118         }
119         
120         public ArrayList<CustomExpression> getCustomExpressions(){
121                 return customExpressions;
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         /**
327          * Clear the undo history.
328          */
329         public void clearUndo() {
330                 log.info("Clearing undo history of " + this);
331                 undoHistory.clear();
332                 undoDescription.clear();
333                 
334                 undoHistory.add(rocket.copyWithOriginalID());
335                 undoDescription.add(null);
336                 undoPosition = 0;
337
338                 fireUndoRedoChangeEvent();
339         }
340         
341         
342         @Override
343         public void componentChanged(ComponentChangeEvent e) {
344                 
345                 if (!e.isUndoChange()) {
346                         if (undoPosition < undoHistory.size() - 1) {
347                                 log.info("Rocket changed while in undo history, removing redo information for " + this +
348                                                 " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() +
349                                                 " isClean=" + isCleanState());
350                         }
351                         // Remove any redo information if available
352                         while (undoPosition < undoHistory.size() - 1) {
353                                 undoHistory.removeLast();
354                                 undoDescription.removeLast();
355                         }
356                         
357                         // Set the latest description
358                         undoDescription.set(undoPosition, nextDescription);
359                 }
360                 
361                 fireUndoRedoChangeEvent();
362         }
363         
364         
365         /**
366          * Return whether undo action is available.
367          * @return      <code>true</code> if undo can be performed
368          */
369         public boolean isUndoAvailable() {
370                 if (undoPosition > 0)
371                         return true;
372                 
373                 return !isCleanState();
374         }
375         
376         /**
377          * Return the description of what action would be undone if undo is called.
378          * @return      the description what would be undone, or <code>null</code> if description unavailable.
379          */
380         public String getUndoDescription() {
381                 if (!isUndoAvailable())
382                         return null;
383                 
384                 if (isCleanState()) {
385                         return undoDescription.get(undoPosition - 1);
386                 } else {
387                         return undoDescription.get(undoPosition);
388                 }
389         }
390         
391         
392         /**
393          * Return whether redo action is available.
394          * @return      <code>true</code> if redo can be performed
395          */
396         public boolean isRedoAvailable() {
397                 return undoPosition < undoHistory.size() - 1;
398         }
399         
400         /**
401          * Return the description of what action would be redone if redo is called.
402          * @return      the description of what would be redone, or <code>null</code> if description unavailable.
403          */
404         public String getRedoDescription() {
405                 if (!isRedoAvailable())
406                         return null;
407                 
408                 return undoDescription.get(undoPosition);
409         }
410         
411         
412         /**
413          * Perform undo operation on the rocket.
414          */
415         public void undo() {
416                 log.info("Performing undo for " + this + " undoPosition=" + undoPosition +
417                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
418                 if (!isUndoAvailable()) {
419                         logUndoError("Undo not available");
420                         fireUndoRedoChangeEvent();
421                         return;
422                 }
423                 if (storedDescription != null) {
424                         logUndoError("undo() called with storedDescription=" + storedDescription);
425                 }
426                 
427                 // Update history position
428                 
429                 if (isCleanState()) {
430                         // We are in a clean state, simply move backwards in history
431                         undoPosition--;
432                 } else {
433                         if (undoPosition != undoHistory.size() - 1) {
434                                 logUndoError("undo position inconsistency");
435                         }
436                         // Modifications have been made, save the state and restore previous state
437                         undoHistory.add(rocket.copyWithOriginalID());
438                         undoDescription.add(null);
439                 }
440                 
441                 rocket.checkComponentStructure();
442                 undoHistory.get(undoPosition).checkComponentStructure();
443                 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
444                 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
445                 rocket.checkComponentStructure();
446         }
447         
448         
449         /**
450          * Perform redo operation on the rocket.
451          */
452         public void redo() {
453                 log.info("Performing redo for " + this + " undoPosition=" + undoPosition +
454                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState());
455                 if (!isRedoAvailable()) {
456                         logUndoError("Redo not available");
457                         fireUndoRedoChangeEvent();
458                         return;
459                 }
460                 if (storedDescription != null) {
461                         logUndoError("redo() called with storedDescription=" + storedDescription);
462                 }
463                 
464                 undoPosition++;
465                 
466                 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
467         }
468         
469         
470         private boolean isCleanState() {
471                 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
472         }
473         
474         
475         /**
476          * Log a non-fatal undo/redo error or inconsistency.  Reports it to the user the first 
477          * time it occurs, but not on subsequent times.  Logs automatically the undo system state.
478          */
479         private void logUndoError(String error) {
480                 log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition +
481                                 " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() +
482                                 " nextDescription=" + nextDescription + " storedDescription=" + storedDescription,
483                                 new TraceException());
484                 
485                 if (!undoErrorReported) {
486                         undoErrorReported = true;
487                         Application.getExceptionHandler().handleErrorCondition("Undo/Redo error: " + error);
488                 }
489         }
490         
491         
492
493         /**
494          * Return a copy of this document.  The rocket is copied with original ID's, the default
495          * motor configuration ID is maintained and the simulations are copied to the new rocket.
496          * No undo/redo information or file storage information is maintained.
497          * 
498          * @return      a copy of this document.
499          */
500         public OpenRocketDocument copy() {
501                 Rocket rocketCopy = rocket.copyWithOriginalID();
502                 OpenRocketDocument documentCopy = new OpenRocketDocument(rocketCopy);
503                 documentCopy.getDefaultConfiguration().setMotorConfigurationID(configuration.getMotorConfigurationID());
504                 for (Simulation s : simulations) {
505                         documentCopy.addSimulation(s.duplicateSimulation(rocketCopy));
506                 }
507                 return documentCopy;
508         }
509         
510         
511
512         ///////  Listeners
513         
514         public void addUndoRedoListener( UndoRedoListener listener ) {
515                 undoRedoListeners.add(listener);
516         }
517         
518         public void removeUndoRedoListener( UndoRedoListener listener ) {
519                 undoRedoListeners.remove(listener);
520         }
521         
522         private void fireUndoRedoChangeEvent() {
523                 UndoRedoListener[] array = undoRedoListeners.toArray(new UndoRedoListener[0]);
524                 for (UndoRedoListener l : array) {
525                         l.setAllValues();
526                 }
527                 
528         }
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 }