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