1 package net.sf.openrocket.document;
4 import java.util.LinkedList;
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;
20 * Class describing an entire OpenRocket document, including a rocket and
21 * simulations. The document contains:
23 * - the rocket definition
24 * - a default Configuration
25 * - Simulation instances
26 * - the stored file and file save information
27 * - undo/redo information
29 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
31 public class OpenRocketDocument implements ComponentChangeListener {
32 private static final LogHelper log = Application.getLogger();
35 * The minimum number of undo levels that are stored.
37 public static final int UNDO_LEVELS = 50;
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.
42 public static final int UNDO_MARGIN = 10;
44 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
46 /** Whether an undo error has already been reported to the user */
47 private static boolean undoErrorReported = false;
49 private final Rocket rocket;
50 private final Configuration configuration;
52 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
56 * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
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.
63 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
64 private LinkedList<String> undoDescription = new LinkedList<String>();
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
71 private int undoPosition = -1; // Illegal position, init in constructor
74 * The description of the next action that modifies this rocket.
76 private String nextDescription = null;
77 private String storedDescription = null;
80 private ArrayList<UndoRedoListener> undoRedoListeners = new ArrayList<UndoRedoListener>(2);
82 private File file = null;
83 private int savedID = -1;
85 private final StorageOptions storageOptions = new StorageOptions();
88 private final List<DocumentChangeListener> listeners =
89 new ArrayList<DocumentChangeListener>();
91 public OpenRocketDocument(Rocket rocket) {
92 this(rocket.getDefaultConfiguration());
96 private OpenRocketDocument(Configuration configuration) {
97 this.configuration = configuration;
98 this.rocket = configuration.getRocket();
102 rocket.addComponentChangeListener(this);
108 public Rocket getRocket() {
113 public Configuration getDefaultConfiguration() {
114 return configuration;
118 public File getFile() {
122 public void setFile(File file) {
127 public boolean isSaved() {
128 return rocket.getModID() == savedID;
131 public void setSaved(boolean saved) {
135 this.savedID = rocket.getModID();
139 * Retrieve the default storage options for this document.
141 * @return the storage options.
143 public StorageOptions getDefaultStorageOptions() {
144 return storageOptions;
151 public List<Simulation> getSimulations() {
152 return simulations.clone();
155 public int getSimulationCount() {
156 return simulations.size();
159 public Simulation getSimulation(int n) {
160 return simulations.get(n);
163 public int getSimulationIndex(Simulation simulation) {
164 return simulations.indexOf(simulation);
167 public void addSimulation(Simulation simulation) {
168 simulations.add(simulation);
169 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
172 public void addSimulation(Simulation simulation, int n) {
173 simulations.add(n, simulation);
174 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
177 public void removeSimulation(Simulation simulation) {
178 simulations.remove(simulation);
179 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
182 public Simulation removeSimulation(int n) {
183 Simulation simulation = simulations.remove(n);
184 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
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.
193 * @return the new name.
195 public String getNextSimulationName() {
196 // Generate unique name for the simulation
198 for (Simulation s : simulations) {
199 String name = s.getName();
200 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
202 maxValue = Math.max(maxValue,
203 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
204 } catch (NumberFormatException ignore) {
208 return SIMULATION_NAME_PREFIX + (maxValue + 1);
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.
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.
222 * If this method is called successively without any change events occurring between the
223 * calls, only the last call will have any effect.
225 * @param description A short description of the following actions.
227 public void addUndoPosition(String description) {
229 if (storedDescription != null) {
230 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
231 " description=" + description);
234 // Check whether modifications have been done since last call
235 if (isCleanState()) {
237 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
238 nextDescription = description;
242 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
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.
248 if (undoPosition != undoHistory.size() - 1) {
249 logUndoError("undo position inconsistency");
251 while (undoPosition < undoHistory.size() - 1) {
252 undoHistory.removeLast();
253 undoDescription.removeLast();
257 // Add the current state to the undo history
258 undoHistory.add(rocket.copyWithOriginalID());
259 undoDescription.add(null);
260 nextDescription = description;
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();
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.
282 * @param description Description of the following undoable operations.
284 public void startUndo(String description) {
285 if (storedDescription != null) {
286 logUndoError("startUndo called while storedDescription=" + storedDescription +
287 " description=" + description);
289 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
290 String store = nextDescription;
291 addUndoPosition(description);
292 storedDescription = store;
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
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);
310 * Clear the undo history.
312 public void clearUndo() {
313 log.info("Clearing undo history of " + this);
315 undoDescription.clear();
317 undoHistory.add(rocket.copyWithOriginalID());
318 undoDescription.add(null);
321 fireUndoRedoChangeEvent();
326 public void componentChanged(ComponentChangeEvent e) {
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());
334 // Remove any redo information if available
335 while (undoPosition < undoHistory.size() - 1) {
336 undoHistory.removeLast();
337 undoDescription.removeLast();
340 // Set the latest description
341 undoDescription.set(undoPosition, nextDescription);
344 fireUndoRedoChangeEvent();
349 * Return whether undo action is available.
350 * @return <code>true</code> if undo can be performed
352 public boolean isUndoAvailable() {
353 if (undoPosition > 0)
356 return !isCleanState();
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.
363 public String getUndoDescription() {
364 if (!isUndoAvailable())
367 if (isCleanState()) {
368 return undoDescription.get(undoPosition - 1);
370 return undoDescription.get(undoPosition);
376 * Return whether redo action is available.
377 * @return <code>true</code> if redo can be performed
379 public boolean isRedoAvailable() {
380 return undoPosition < undoHistory.size() - 1;
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.
387 public String getRedoDescription() {
388 if (!isRedoAvailable())
391 return undoDescription.get(undoPosition);
396 * Perform undo operation on the rocket.
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();
406 if (storedDescription != null) {
407 logUndoError("undo() called with storedDescription=" + storedDescription);
410 // Update history position
412 if (isCleanState()) {
413 // We are in a clean state, simply move backwards in history
416 if (undoPosition != undoHistory.size() - 1) {
417 logUndoError("undo position inconsistency");
419 // Modifications have been made, save the state and restore previous state
420 undoHistory.add(rocket.copyWithOriginalID());
421 undoDescription.add(null);
424 rocket.checkComponentStructure();
425 undoHistory.get(undoPosition).checkComponentStructure();
426 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
427 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
428 rocket.checkComponentStructure();
433 * Perform redo operation on the rocket.
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();
443 if (storedDescription != null) {
444 logUndoError("redo() called with storedDescription=" + storedDescription);
449 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
453 private boolean isCleanState() {
454 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
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.
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());
468 if (!undoErrorReported) {
469 undoErrorReported = true;
470 Application.getExceptionHandler().handleErrorCondition("Undo/Redo error: " + error);
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.
481 * @return a copy of this document.
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));
497 public void addUndoRedoListener( UndoRedoListener listener ) {
498 undoRedoListeners.add(listener);
501 public void removeUndoRedoListener( UndoRedoListener listener ) {
502 undoRedoListeners.remove(listener);
505 private void fireUndoRedoChangeEvent() {
506 UndoRedoListener[] array = undoRedoListeners.toArray(new UndoRedoListener[0]);
507 for (UndoRedoListener l : array) {
513 public void addDocumentChangeListener(DocumentChangeListener listener) {
514 listeners.add(listener);
517 public void removeDocumentChangeListener(DocumentChangeListener listener) {
518 listeners.remove(listener);
521 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
522 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
523 for (DocumentChangeListener l : array) {
524 l.documentChanged(event);