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.simulation.customexpression.CustomExpression;
17 import net.sf.openrocket.startup.Application;
18 import net.sf.openrocket.util.ArrayList;
21 * Class describing an entire OpenRocket document, including a rocket and
22 * simulations. The document contains:
24 * - the rocket definition
25 * - a default Configuration
26 * - Simulation instances
27 * - the stored file and file save information
28 * - undo/redo information
30 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
32 public class OpenRocketDocument implements ComponentChangeListener {
33 private static final LogHelper log = Application.getLogger();
36 * The minimum number of undo levels that are stored.
38 public static final int UNDO_LEVELS = 50;
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.
43 public static final int UNDO_MARGIN = 10;
45 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
47 /** Whether an undo error has already been reported to the user */
48 private static boolean undoErrorReported = false;
50 private final Rocket rocket;
51 private final Configuration configuration;
53 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
54 private ArrayList<CustomExpression> customExpressions = new ArrayList<CustomExpression>();
58 * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
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.
65 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
66 private LinkedList<String> undoDescription = new LinkedList<String>();
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
73 private int undoPosition = -1; // Illegal position, init in constructor
76 * The description of the next action that modifies this rocket.
78 private String nextDescription = null;
79 private String storedDescription = null;
82 private ArrayList<UndoRedoListener> undoRedoListeners = new ArrayList<UndoRedoListener>(2);
84 private File file = null;
85 private int savedID = -1;
87 private final StorageOptions storageOptions = new StorageOptions();
90 private final List<DocumentChangeListener> listeners =
91 new ArrayList<DocumentChangeListener>();
93 public OpenRocketDocument(Rocket rocket) {
94 this(rocket.getDefaultConfiguration());
98 private OpenRocketDocument(Configuration configuration) {
99 this.configuration = configuration;
100 this.rocket = configuration.getRocket();
104 rocket.addComponentChangeListener(this);
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.");
112 customExpressions.add(expression);
116 public void removeCustomExpression(CustomExpression expression){
117 customExpressions.remove(expression);
120 public List<CustomExpression> getCustomExpressions(){
121 return customExpressions;
125 public Rocket getRocket() {
130 public Configuration getDefaultConfiguration() {
131 return configuration;
135 public File getFile() {
139 public void setFile(File file) {
144 public boolean isSaved() {
145 return rocket.getModID() == savedID;
148 public void setSaved(boolean saved) {
152 this.savedID = rocket.getModID();
156 * Retrieve the default storage options for this document.
158 * @return the storage options.
160 public StorageOptions getDefaultStorageOptions() {
161 return storageOptions;
168 public List<Simulation> getSimulations() {
169 return simulations.clone();
172 public int getSimulationCount() {
173 return simulations.size();
176 public Simulation getSimulation(int n) {
177 return simulations.get(n);
180 public int getSimulationIndex(Simulation simulation) {
181 return simulations.indexOf(simulation);
184 public void addSimulation(Simulation simulation) {
185 simulations.add(simulation);
186 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
189 public void addSimulation(Simulation simulation, int n) {
190 simulations.add(n, simulation);
191 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
194 public void removeSimulation(Simulation simulation) {
195 simulations.remove(simulation);
196 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
199 public Simulation removeSimulation(int n) {
200 Simulation simulation = simulations.remove(n);
201 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
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.
210 * @return the new name.
212 public String getNextSimulationName() {
213 // Generate unique name for the simulation
215 for (Simulation s : simulations) {
216 String name = s.getName();
217 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
219 maxValue = Math.max(maxValue,
220 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
221 } catch (NumberFormatException ignore) {
225 return SIMULATION_NAME_PREFIX + (maxValue + 1);
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.
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.
239 * If this method is called successively without any change events occurring between the
240 * calls, only the last call will have any effect.
242 * @param description A short description of the following actions.
244 public void addUndoPosition(String description) {
246 if (storedDescription != null) {
247 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
248 " description=" + description);
251 // Check whether modifications have been done since last call
252 if (isCleanState()) {
254 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
255 nextDescription = description;
259 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
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.
265 if (undoPosition != undoHistory.size() - 1) {
266 logUndoError("undo position inconsistency");
268 while (undoPosition < undoHistory.size() - 1) {
269 undoHistory.removeLast();
270 undoDescription.removeLast();
274 // Add the current state to the undo history
275 undoHistory.add(rocket.copyWithOriginalID());
276 undoDescription.add(null);
277 nextDescription = description;
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();
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.
299 * @param description Description of the following undoable operations.
301 public void startUndo(String description) {
302 if (storedDescription != null) {
303 logUndoError("startUndo called while storedDescription=" + storedDescription +
304 " description=" + description);
306 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
307 String store = nextDescription;
308 addUndoPosition(description);
309 storedDescription = store;
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
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);
327 * Clear the undo history.
329 public void clearUndo() {
330 log.info("Clearing undo history of " + this);
332 undoDescription.clear();
334 undoHistory.add(rocket.copyWithOriginalID());
335 undoDescription.add(null);
338 fireUndoRedoChangeEvent();
343 public void componentChanged(ComponentChangeEvent e) {
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());
351 // Remove any redo information if available
352 while (undoPosition < undoHistory.size() - 1) {
353 undoHistory.removeLast();
354 undoDescription.removeLast();
357 // Set the latest description
358 undoDescription.set(undoPosition, nextDescription);
361 fireUndoRedoChangeEvent();
366 * Return whether undo action is available.
367 * @return <code>true</code> if undo can be performed
369 public boolean isUndoAvailable() {
370 if (undoPosition > 0)
373 return !isCleanState();
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.
380 public String getUndoDescription() {
381 if (!isUndoAvailable())
384 if (isCleanState()) {
385 return undoDescription.get(undoPosition - 1);
387 return undoDescription.get(undoPosition);
393 * Return whether redo action is available.
394 * @return <code>true</code> if redo can be performed
396 public boolean isRedoAvailable() {
397 return undoPosition < undoHistory.size() - 1;
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.
404 public String getRedoDescription() {
405 if (!isRedoAvailable())
408 return undoDescription.get(undoPosition);
413 * Perform undo operation on the rocket.
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();
423 if (storedDescription != null) {
424 logUndoError("undo() called with storedDescription=" + storedDescription);
427 // Update history position
429 if (isCleanState()) {
430 // We are in a clean state, simply move backwards in history
433 if (undoPosition != undoHistory.size() - 1) {
434 logUndoError("undo position inconsistency");
436 // Modifications have been made, save the state and restore previous state
437 undoHistory.add(rocket.copyWithOriginalID());
438 undoDescription.add(null);
441 rocket.checkComponentStructure();
442 undoHistory.get(undoPosition).checkComponentStructure();
443 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
444 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
445 rocket.checkComponentStructure();
450 * Perform redo operation on the rocket.
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();
460 if (storedDescription != null) {
461 logUndoError("redo() called with storedDescription=" + storedDescription);
466 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
470 private boolean isCleanState() {
471 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
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.
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());
485 if (!undoErrorReported) {
486 undoErrorReported = true;
487 Application.getExceptionHandler().handleErrorCondition("Undo/Redo error: " + error);
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.
498 * @return a copy of this document.
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));
514 public void addUndoRedoListener( UndoRedoListener listener ) {
515 undoRedoListeners.add(listener);
518 public void removeUndoRedoListener( UndoRedoListener listener ) {
519 undoRedoListeners.remove(listener);
522 private void fireUndoRedoChangeEvent() {
523 UndoRedoListener[] array = undoRedoListeners.toArray(new UndoRedoListener[0]);
524 for (UndoRedoListener l : array) {
530 public void addDocumentChangeListener(DocumentChangeListener listener) {
531 listeners.add(listener);
534 public void removeDocumentChangeListener(DocumentChangeListener listener) {
535 listeners.remove(listener);
538 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
539 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
540 for (DocumentChangeListener l : array) {
541 l.documentChanged(event);