1 package net.sf.openrocket.document;
4 import java.util.Collections;
5 import java.util.HashSet;
6 import java.util.LinkedHashSet;
7 import java.util.LinkedList;
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;
28 * Class describing an entire OpenRocket document, including a rocket and
29 * simulations. The document contains:
31 * - the rocket definition
32 * - a default Configuration
33 * - Simulation instances
34 * - the stored file and file save information
35 * - undo/redo information
37 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
39 public class OpenRocketDocument implements ComponentChangeListener {
40 private static final LogHelper log = Application.getLogger();
43 * The minimum number of undo levels that are stored.
45 public static final int UNDO_LEVELS = 50;
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.
50 public static final int UNDO_MARGIN = 10;
52 public static final String SIMULATION_NAME_PREFIX = "Simulation ";
54 /** Whether an undo error has already been reported to the user */
55 private static boolean undoErrorReported = false;
57 private final Rocket rocket;
58 private final Configuration configuration;
60 private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
61 private ArrayList<CustomExpression> customExpressions = new ArrayList<CustomExpression>();
65 * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.*
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.
72 private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
73 private LinkedList<String> undoDescription = new LinkedList<String>();
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
80 private int undoPosition = -1; // Illegal position, init in constructor
83 * The description of the next action that modifies this rocket.
85 private String nextDescription = null;
86 private String storedDescription = null;
89 private ArrayList<UndoRedoListener> undoRedoListeners = new ArrayList<UndoRedoListener>(2);
91 private File file = null;
92 private int savedID = -1;
94 private final StorageOptions storageOptions = new StorageOptions();
97 private final List<DocumentChangeListener> listeners =
98 new ArrayList<DocumentChangeListener>();
100 public OpenRocketDocument(Rocket rocket) {
101 this(rocket.getDefaultConfiguration());
105 private OpenRocketDocument(Configuration configuration) {
106 this.configuration = configuration;
107 this.rocket = configuration.getRocket();
111 rocket.addComponentChangeListener(this);
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.");
119 customExpressions.add(expression);
123 public void removeCustomExpression(CustomExpression expression){
124 customExpressions.remove(expression);
127 public List<CustomExpression> getCustomExpressions(){
128 return customExpressions;
132 * Returns a set of all the flight data types defined or available in any way in the rocket document
134 public Set<FlightDataType> getFlightDataTypes(){
135 Set<FlightDataType> allTypes = new LinkedHashSet<FlightDataType>();
138 Collections.addAll(allTypes, FlightDataType.ALL_TYPES);
140 // custom expressions
141 for (CustomExpression exp : customExpressions){
142 allTypes.add(exp.getType());
145 // simulation listeners
146 for (Simulation sim : simulations){
147 for (String className : sim.getSimulationListeners()) {
148 SimulationListener l = null;
150 Class<?> c = Class.forName(className);
151 l = (SimulationListener) c.newInstance();
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);
162 /// not implemented yet
169 public Rocket getRocket() {
174 public Configuration getDefaultConfiguration() {
175 return configuration;
179 public File getFile() {
183 public void setFile(File file) {
188 public boolean isSaved() {
189 return rocket.getModID() == savedID;
192 public void setSaved(boolean saved) {
196 this.savedID = rocket.getModID();
200 * Retrieve the default storage options for this document.
202 * @return the storage options.
204 public StorageOptions getDefaultStorageOptions() {
205 return storageOptions;
212 public List<Simulation> getSimulations() {
213 return simulations.clone();
216 public int getSimulationCount() {
217 return simulations.size();
220 public Simulation getSimulation(int n) {
221 return simulations.get(n);
224 public int getSimulationIndex(Simulation simulation) {
225 return simulations.indexOf(simulation);
228 public void addSimulation(Simulation simulation) {
229 simulations.add(simulation);
230 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
233 public void addSimulation(Simulation simulation, int n) {
234 simulations.add(n, simulation);
235 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
238 public void removeSimulation(Simulation simulation) {
239 simulations.remove(simulation);
240 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
243 public Simulation removeSimulation(int n) {
244 Simulation simulation = simulations.remove(n);
245 fireDocumentChangeEvent(new SimulationChangeEvent(simulation));
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.
254 * @return the new name.
256 public String getNextSimulationName() {
257 // Generate unique name for the simulation
259 for (Simulation s : simulations) {
260 String name = s.getName();
261 if (name.startsWith(SIMULATION_NAME_PREFIX)) {
263 maxValue = Math.max(maxValue,
264 Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length())));
265 } catch (NumberFormatException ignore) {
269 return SIMULATION_NAME_PREFIX + (maxValue + 1);
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.
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.
283 * If this method is called successively without any change events occurring between the
284 * calls, only the last call will have any effect.
286 * @param description A short description of the following actions.
288 public void addUndoPosition(String description) {
290 if (storedDescription != null) {
291 logUndoError("addUndoPosition called while storedDescription=" + storedDescription +
292 " description=" + description);
295 // Check whether modifications have been done since last call
296 if (isCleanState()) {
298 log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state");
299 nextDescription = description;
303 log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state");
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.
309 if (undoPosition != undoHistory.size() - 1) {
310 logUndoError("undo position inconsistency");
312 while (undoPosition < undoHistory.size() - 1) {
313 undoHistory.removeLast();
314 undoDescription.removeLast();
318 // Add the current state to the undo history
319 undoHistory.add(rocket.copyWithOriginalID());
320 undoDescription.add(null);
321 nextDescription = description;
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();
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.
343 * @param description Description of the following undoable operations.
345 public void startUndo(String description) {
346 if (storedDescription != null) {
347 logUndoError("startUndo called while storedDescription=" + storedDescription +
348 " description=" + description);
350 log.info("Starting time-limited undoable operation '" + description + "' for " + this);
351 String store = nextDescription;
352 addUndoPosition(description);
353 storedDescription = store;
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
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);
371 * Clear the undo history.
373 public void clearUndo() {
374 log.info("Clearing undo history of " + this);
376 undoDescription.clear();
378 undoHistory.add(rocket.copyWithOriginalID());
379 undoDescription.add(null);
382 fireUndoRedoChangeEvent();
387 public void componentChanged(ComponentChangeEvent e) {
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());
395 // Remove any redo information if available
396 while (undoPosition < undoHistory.size() - 1) {
397 undoHistory.removeLast();
398 undoDescription.removeLast();
401 // Set the latest description
402 undoDescription.set(undoPosition, nextDescription);
405 fireUndoRedoChangeEvent();
410 * Return whether undo action is available.
411 * @return <code>true</code> if undo can be performed
413 public boolean isUndoAvailable() {
414 if (undoPosition > 0)
417 return !isCleanState();
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.
424 public String getUndoDescription() {
425 if (!isUndoAvailable())
428 if (isCleanState()) {
429 return undoDescription.get(undoPosition - 1);
431 return undoDescription.get(undoPosition);
437 * Return whether redo action is available.
438 * @return <code>true</code> if redo can be performed
440 public boolean isRedoAvailable() {
441 return undoPosition < undoHistory.size() - 1;
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.
448 public String getRedoDescription() {
449 if (!isRedoAvailable())
452 return undoDescription.get(undoPosition);
457 * Perform undo operation on the rocket.
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();
467 if (storedDescription != null) {
468 logUndoError("undo() called with storedDescription=" + storedDescription);
471 // Update history position
473 if (isCleanState()) {
474 // We are in a clean state, simply move backwards in history
477 if (undoPosition != undoHistory.size() - 1) {
478 logUndoError("undo position inconsistency");
480 // Modifications have been made, save the state and restore previous state
481 undoHistory.add(rocket.copyWithOriginalID());
482 undoDescription.add(null);
485 rocket.checkComponentStructure();
486 undoHistory.get(undoPosition).checkComponentStructure();
487 undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure();
488 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
489 rocket.checkComponentStructure();
494 * Perform redo operation on the rocket.
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();
504 if (storedDescription != null) {
505 logUndoError("redo() called with storedDescription=" + storedDescription);
510 rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID());
514 private boolean isCleanState() {
515 return rocket.getModID() == undoHistory.get(undoPosition).getModID();
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.
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());
529 if (!undoErrorReported) {
530 undoErrorReported = true;
531 Application.getExceptionHandler().handleErrorCondition("Undo/Redo error: " + error);
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.
542 * @return a copy of this document.
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));
558 public void addUndoRedoListener( UndoRedoListener listener ) {
559 undoRedoListeners.add(listener);
562 public void removeUndoRedoListener( UndoRedoListener listener ) {
563 undoRedoListeners.remove(listener);
566 private void fireUndoRedoChangeEvent() {
567 UndoRedoListener[] array = undoRedoListeners.toArray(new UndoRedoListener[0]);
568 for (UndoRedoListener l : array) {
574 public void addDocumentChangeListener(DocumentChangeListener listener) {
575 listeners.add(listener);
578 public void removeDocumentChangeListener(DocumentChangeListener listener) {
579 listeners.remove(listener);
582 protected void fireDocumentChangeEvent(DocumentChangeEvent event) {
583 DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]);
584 for (DocumentChangeListener l : array) {
585 l.documentChanged(event);