+ */
+ public enum Platform {
+ WINDOWS,
+ MAC_OS,
+ UNIX;
+ }
+
+
+ /**
+ * Return the current operating system.
+ *
+ * @return the operating system of the current system.
+ */
+ public static Platform getPlatform() {
+ String os = System.getProperty("os.name").toLowerCase();
+
+ if (os.indexOf("win") >= 0) {
+ return Platform.WINDOWS;
+ } else if (os.indexOf("mac") >= 0) {
+ return Platform.MAC_OS;
+ } else {
+ /*
+ * Assume UNIX otherwise, e.g. "Linux", "Solaris", "AIX" etc.
+ */
+ return Platform.UNIX;
+ }
+ }
+
+
+
+
+ /**
+ * Return the application data directory of this user. The location depends
+ * on the current platform.
+ *
+ * The directory will not be created by this method.
+ *
+ * @return the application directory for OpenRocket
+ */
+ public static File getUserApplicationDirectory() {
+ final String homeDir = System.getProperty("user.home");
+ final File dir;
+
+ switch (getPlatform()) {
+ case WINDOWS:
+ String appdata = System.getenv("APPDATA");
+ if (appdata != null) {
+ dir = new File(appdata, "OpenRocket/");
+ } else {
+ dir = new File(homeDir, "OpenRocket/");
+ }
+ break;
+
+ case MAC_OS:
+ dir = new File(homeDir, "Library/Application Support/OpenRocket/");
+ break;
+
+ case UNIX:
+ dir = new File(homeDir, ".openrocket/");
+ break;
+
+ default:
+ throw new BugException("Not implemented for platform " + getPlatform());
+ }
+
+ return dir;
+ }
+
+}
diff --git a/src/net/sf/openrocket/communication/UpdateInfoRetriever.java b/src/net/sf/openrocket/communication/UpdateInfoRetriever.java
index 3f20c5ca6..772bc90c6 100644
--- a/src/net/sf/openrocket/communication/UpdateInfoRetriever.java
+++ b/src/net/sf/openrocket/communication/UpdateInfoRetriever.java
@@ -8,12 +8,15 @@ import java.io.Reader;
import java.net.HttpURLConnection;
import java.util.ArrayList;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.ComparablePair;
import net.sf.openrocket.util.LimitedInputStream;
import net.sf.openrocket.util.Prefs;
public class UpdateInfoRetriever {
-
+ private static final LogHelper log = Application.getLogger();
+
private UpdateInfoFetcher fetcher = null;
@@ -63,7 +66,7 @@ public class UpdateInfoRetriever {
}
-
+
/**
* Parse the data received from the server.
*
@@ -75,15 +78,15 @@ public class UpdateInfoRetriever {
static UpdateInfo parseUpdateInput(Reader r) throws IOException {
BufferedReader reader;
if (r instanceof BufferedReader) {
- reader = (BufferedReader)r;
+ reader = (BufferedReader) r;
} else {
reader = new BufferedReader(r);
}
-
+
String version = null;
- ArrayList> updates =
- new ArrayList>();
+ ArrayList> updates =
+ new ArrayList>();
String str = reader.readLine();
while (str != null) {
@@ -92,9 +95,9 @@ public class UpdateInfoRetriever {
} else if (str.matches("^[0-9]+:\\p{Print}+$")) {
int index = str.indexOf(':');
int value = Integer.parseInt(str.substring(0, index));
- String desc = str.substring(index+1).trim();
+ String desc = str.substring(index + 1).trim();
if (!desc.equals("")) {
- updates.add(new ComparablePair(value, desc));
+ updates.add(new ComparablePair(value, desc));
}
}
// Ignore anything else
@@ -109,14 +112,14 @@ public class UpdateInfoRetriever {
}
-
+
/**
* An asynchronous task that fetches and parses the update info.
*
* @author Sampo Niskanen
*/
private class UpdateInfoFetcher extends Thread {
-
+
private volatile UpdateInfo info = null;
@Override
@@ -124,14 +127,14 @@ public class UpdateInfoRetriever {
try {
doConnection();
} catch (IOException e) {
- System.out.println("fetching update failed: " + e);
+ log.info("Fetching update failed: " + e);
return;
}
}
private void doConnection() throws IOException {
- String url = Communicator.UPDATE_INFO_URL + "?" + Communicator.VERSION_PARAM + "="
+ String url = Communicator.UPDATE_INFO_URL + "?" + Communicator.VERSION_PARAM + "="
+ Communicator.encode(Prefs.getVersion());
HttpURLConnection connection = Communicator.connectionSource.getConnection(url);
@@ -141,48 +144,48 @@ public class UpdateInfoRetriever {
connection.setRequestMethod("GET");
connection.setUseCaches(false);
connection.setDoInput(true);
- connection.setRequestProperty("X-OpenRocket-Version",
+ connection.setRequestProperty("X-OpenRocket-Version",
Communicator.encode(Prefs.getVersion() + " " + Prefs.getBuildSource()));
- connection.setRequestProperty("X-OpenRocket-ID",
+ connection.setRequestProperty("X-OpenRocket-ID",
Communicator.encode(Prefs.getUniqueID()));
- connection.setRequestProperty("X-OpenRocket-OS",
- Communicator.encode(System.getProperty("os.name") + " " +
+ connection.setRequestProperty("X-OpenRocket-OS",
+ Communicator.encode(System.getProperty("os.name") + " " +
System.getProperty("os.arch")));
- connection.setRequestProperty("X-OpenRocket-Java",
- Communicator.encode(System.getProperty("java.vendor") + " " +
+ connection.setRequestProperty("X-OpenRocket-Java",
+ Communicator.encode(System.getProperty("java.vendor") + " " +
System.getProperty("java.version")));
- connection.setRequestProperty("X-OpenRocket-Country",
+ connection.setRequestProperty("X-OpenRocket-Country",
Communicator.encode(System.getProperty("user.country") + " " +
System.getProperty("user.timezone")));
+ connection.setRequestProperty("X-OpenRocket-CPUs", "" + Runtime.getRuntime().availableProcessors());
InputStream is = null;
try {
connection.connect();
- System.out.println("response code: " + connection.getResponseCode());
+ log.debug("Update response code: " + connection.getResponseCode());
if (connection.getResponseCode() == Communicator.UPDATE_INFO_NO_UPDATE_CODE) {
// No updates are available
+ log.info("No updates available");
info = new UpdateInfo();
return;
}
if (connection.getResponseCode() != Communicator.UPDATE_INFO_UPDATE_AVAILABLE) {
// Error communicating with server
- System.out.println("Unknown response code: " + connection.getResponseCode());
+ log.warn("Unknown server response code: " + connection.getResponseCode());
return;
}
String contentType = connection.getContentType();
- if (contentType == null ||
+ if (contentType == null ||
contentType.toLowerCase().indexOf(Communicator.UPDATE_INFO_CONTENT_TYPE) < 0) {
// Unknown response type
- System.out.println("Unknown Content-type received:"+contentType);
+ log.warn("Unknown Content-type received:" + contentType);
return;
}
- System.out.println("Update is available");
-
// Update is available, parse input
is = connection.getInputStream();
is = new LimitedInputStream(is, Communicator.MAX_INPUT_BYTES);
@@ -192,8 +195,8 @@ public class UpdateInfoRetriever {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, encoding));
String version = null;
- ArrayList> updates =
- new ArrayList>();
+ ArrayList> updates =
+ new ArrayList>();
String line = reader.readLine();
while (line != null) {
@@ -203,23 +206,23 @@ public class UpdateInfoRetriever {
} else if (line.matches("^[0-9]{1,9}:\\P{Cntrl}{1,300}$")) {
String[] split = line.split(":", 2);
int n = Integer.parseInt(split[0]);
- updates.add(new ComparablePair(n, split[1].trim()));
+ updates.add(new ComparablePair(n, split[1].trim()));
}
// Ignore line otherwise
line = reader.readLine();
}
// Check version input
- if (version == null || version.length() == 0 ||
+ if (version == null || version.length() == 0 ||
version.equalsIgnoreCase(Prefs.getVersion())) {
// Invalid response
- System.out.println("Invalid version received, ignoring.");
+ log.warn("Invalid version received, ignoring.");
return;
}
-
+
info = new UpdateInfo(version, updates);
- System.out.println("Found update: " + info);
+ log.info("Found update: " + info);
} finally {
try {
if (is != null)
diff --git a/src/net/sf/openrocket/file/DirectoryIterator.java b/src/net/sf/openrocket/file/DirectoryIterator.java
deleted file mode 100644
index 825987f9b..000000000
--- a/src/net/sf/openrocket/file/DirectoryIterator.java
+++ /dev/null
@@ -1,139 +0,0 @@
-package net.sf.openrocket.file;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
-import net.sf.openrocket.logging.LogHelper;
-import net.sf.openrocket.startup.Application;
-import net.sf.openrocket.util.JarUtil;
-import net.sf.openrocket.util.Pair;
-
-public abstract class DirectoryIterator implements Iterator> {
-
- private static final LogHelper logger = Application.getLogger();
-
- private Pair next = null;
-
- @Override
- public boolean hasNext() {
- if (next != null)
- return true;
-
- next = findNext();
- return (next != null);
- }
-
-
- @Override
- public Pair next() {
- if (next == null) {
- next = findNext();
- }
- if (next == null) {
- throw new NoSuchElementException("No more files");
- }
-
- Pair n = next;
- next = null;
- return n;
- }
-
-
- @Override
- public void remove() {
- throw new UnsupportedOperationException("remove() not supported");
- }
-
-
-
- /**
- * Closes the resources related to this iterator. This method should be
- * overridden if the iterator needs to close any resources of its own, but
- * must call this method as well.
- */
- public void close() {
- if (next != null) {
- try {
- next.getV().close();
- } catch (IOException e) {
- logger.error("Error closing file " + next.getU());
- }
- next = null;
- }
- }
-
- /**
- * Return the next pair of file name and InputStream.
- *
- * @return a pair with the file name and input stream reading the file.
- */
- protected abstract Pair findNext();
-
-
-
- /**
- * Return a DirectoryIterator for a directory that can be located either
- * within the containing JAR file, in the classpath or in the current directory
- * (searched in this order). The first place that contains matching files
- * will be iterated through.
- *
- * @param directory the directory to search for.
- * @param filter the filter for matching files in the directory.
- * @return a DirectoryIterator for iterating through the files in the
- * directory, or null
if no directory containing
- * matching files can be found.
- */
- public static DirectoryIterator findDirectory(String directory, FileFilter filter) {
- DirectoryIterator iterator = null;
-
- // Try to load from containing JAR file
- File jarFile = JarUtil.getCurrentJarFile();
- if (jarFile != null) {
- try {
- iterator = new ZipDirectoryIterator(jarFile, directory, filter);
- if (iterator.hasNext()) {
- return iterator;
- }
- iterator.close();
- } catch (IOException e) {
- logger.error("Error opening containing JAR file " + jarFile, e);
- }
- }
-
-
- // Try to find directory as a system resource
- URL url = ClassLoader.getSystemResource(directory);
- if (url != null) {
- try {
- File dir = JarUtil.urlToFile(url);
- iterator = new RegularDirectoryIterator(dir, filter);
- if (iterator.hasNext()) {
- return iterator;
- }
- iterator.close();
- } catch (Exception e1) {
- logger.error("Error opening directory from URL " + url);
- }
- }
-
-
- // Try to open directory as such
- try {
- iterator = new RegularDirectoryIterator(new File(directory), filter);
- if (iterator.hasNext()) {
- return iterator;
- }
- iterator.close();
- } catch (IOException e) {
- logger.error("Error opening directory " + directory);
- }
-
- return null;
- }
-
-}
diff --git a/src/net/sf/openrocket/file/GeneralMotorLoader.java b/src/net/sf/openrocket/file/GeneralMotorLoader.java
deleted file mode 100644
index 117672f53..000000000
--- a/src/net/sf/openrocket/file/GeneralMotorLoader.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package net.sf.openrocket.file;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Reader;
-import java.nio.charset.Charset;
-import java.util.List;
-
-import net.sf.openrocket.file.motor.RASPMotorLoader;
-import net.sf.openrocket.file.motor.RockSimMotorLoader;
-import net.sf.openrocket.motor.Motor;
-
-/**
- * A motor loader class that detects the file type based on the file name extension.
- *
- * @author Sampo Niskanen
- */
-public class GeneralMotorLoader extends MotorLoader {
-
- private static final MotorLoader RASP_LOADER = new RASPMotorLoader();
- private static final MotorLoader ROCKSIM_LOADER = new RockSimMotorLoader();
-
-
- @Override
- public List load(InputStream stream, String filename) throws IOException {
- return selectLoader(filename).load(stream, filename);
- }
-
- @Override
- public List load(Reader reader, String filename) throws IOException {
- return selectLoader(filename).load(reader, filename);
- }
-
-
- @Override
- protected Charset getDefaultCharset() {
- // Not used, may return null
- return null;
- }
-
-
- /**
- * Return the appropriate motor loader based on the file name.
- *
- * @param filename the file name (may be null
).
- * @return the appropriate motor loader to use for the file.
- * @throws IOException if the file type cannot be detected from the file name.
- */
- public static MotorLoader selectLoader(String filename) throws IOException {
- if (filename == null) {
- throw new IOException("Unknown file type.");
- }
-
- String ext = "";
- int point = filename.lastIndexOf('.');
-
- if (point > 0)
- ext = filename.substring(point+1);
-
- if (ext.equalsIgnoreCase("eng")) {
- return RASP_LOADER;
- } else if (ext.equalsIgnoreCase("rse")) {
- return ROCKSIM_LOADER;
- }
-
- throw new IOException("Unknown file type.");
- }
-
-}
diff --git a/src/net/sf/openrocket/file/RegularDirectoryIterator.java b/src/net/sf/openrocket/file/RegularDirectoryIterator.java
deleted file mode 100644
index 25f3545c8..000000000
--- a/src/net/sf/openrocket/file/RegularDirectoryIterator.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package net.sf.openrocket.file;
-
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-
-import net.sf.openrocket.logging.LogHelper;
-import net.sf.openrocket.startup.Application;
-import net.sf.openrocket.util.Pair;
-
-/**
- * A DirectoryIterator that scans for files within a directory in the file system.
- *
- * TODO: MEDIUM: Does not support recursive search.
- *
- * @author Sampo Niskanen
- */
-public class RegularDirectoryIterator extends DirectoryIterator {
-
- private static final LogHelper logger = Application.getLogger();
-
- private final File[] files;
- private int position = 0;
-
- /**
- * Sole constructor.
- *
- * @param directory the directory to read.
- * @param filter the filter for selecting files.
- * @throws IOException if the directory cannot be read.
- */
- public RegularDirectoryIterator(File directory, FileFilter filter)
- throws IOException {
- this.files = directory.listFiles(filter);
- if (this.files == null) {
- throw new IOException("not a directory or IOException occurred when listing files " +
- "from " + directory);
- }
- }
-
-
-
- @Override
- protected Pair findNext() {
- for (; position < files.length; position++) {
- try {
- InputStream is = new BufferedInputStream(new FileInputStream(files[position]));
- position++;
- return new Pair(files[position-1].getName(), is);
- } catch (FileNotFoundException e) {
- logger.warn("Error opening file " + files[position], e);
- }
- }
- return null;
- }
-
-}
diff --git a/src/net/sf/openrocket/file/UnknownFileTypeException.java b/src/net/sf/openrocket/file/UnknownFileTypeException.java
new file mode 100644
index 000000000..bcea2bbb6
--- /dev/null
+++ b/src/net/sf/openrocket/file/UnknownFileTypeException.java
@@ -0,0 +1,27 @@
+package net.sf.openrocket.file;
+
+import java.io.IOException;
+
+/**
+ * An exception marking that a file type was not supported.
+ *
+ * @author Sampo Niskanen
+ */
+public class UnknownFileTypeException extends IOException {
+
+ public UnknownFileTypeException() {
+ }
+
+ public UnknownFileTypeException(String message) {
+ super(message);
+ }
+
+ public UnknownFileTypeException(Throwable cause) {
+ super(cause);
+ }
+
+ public UnknownFileTypeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git a/src/net/sf/openrocket/file/iterator/DirectoryIterator.java b/src/net/sf/openrocket/file/iterator/DirectoryIterator.java
new file mode 100644
index 000000000..c934482c1
--- /dev/null
+++ b/src/net/sf/openrocket/file/iterator/DirectoryIterator.java
@@ -0,0 +1,187 @@
+package net.sf.openrocket.file.iterator;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.JarUtil;
+import net.sf.openrocket.util.Pair;
+
+/**
+ * A DirectoryIterator that scans for files within a directory in the file system
+ * matching a FileFilter. The scan is optionally recursive.
+ *
+ * @author Sampo Niskanen
+ */
+public class DirectoryIterator extends FileIterator {
+
+ private static final LogHelper logger = Application.getLogger();
+
+ private final FileFilter filter;
+ private final File[] files;
+ private final boolean recursive;
+ private int position = 0;
+ private DirectoryIterator subIterator = null;
+
+ /**
+ * Sole constructor.
+ *
+ * @param directory the directory to read.
+ * @param filter the filter for selecting files.
+ * @throws IOException if the directory cannot be read.
+ */
+ public DirectoryIterator(File directory, FileFilter filter, boolean recursive)
+ throws IOException {
+
+ this.filter = filter;
+ this.recursive = recursive;
+
+ this.files = directory.listFiles(new DirSelectionFileFilter(filter, recursive));
+ if (this.files == null) {
+ throw new IOException("not a directory or IOException occurred when listing files " +
+ "from " + directory);
+ }
+ }
+
+
+
+
+
+ @Override
+ protected Pair findNext() {
+
+ // Check if we're recursing
+ if (subIterator != null) {
+ if (subIterator.hasNext()) {
+ return subIterator.next();
+ } else {
+ subIterator.close();
+ subIterator = null;
+ }
+ }
+
+ // Scan through file entries
+ while (position < files.length) {
+ File file = files[position];
+ position++;
+
+ try {
+ if (recursive && file.isDirectory()) {
+ subIterator = new DirectoryIterator(file, filter, recursive);
+ if (subIterator.hasNext()) {
+ return subIterator.next();
+ } else {
+ subIterator.close();
+ subIterator = null;
+ continue;
+ }
+ }
+
+ InputStream is = new BufferedInputStream(new FileInputStream(file));
+ return new Pair(file.getName(), is);
+ } catch (IOException e) {
+ logger.warn("Error opening file/directory " + file, e);
+ }
+ }
+ return null;
+ }
+
+
+
+ /**
+ * Return a DirectoryIterator for a directory that can be located either
+ * within the containing JAR file, in the classpath or in the current directory
+ * (searched in this order). The first place that contains matching files
+ * will be iterated through.
+ *
+ * @param directory the directory to search for.
+ * @param filter the filter for matching files in the directory.
+ * @return a DirectoryIterator for iterating through the files in the
+ * directory, or null
if no directory containing
+ * matching files can be found.
+ */
+ public static FileIterator findDirectory(String directory, FileFilter filter) {
+ FileIterator iterator = null;
+
+ // Try to load from containing JAR file
+ File jarFile = JarUtil.getCurrentJarFile();
+ if (jarFile != null) {
+ try {
+ iterator = new ZipDirectoryIterator(jarFile, directory, filter);
+ if (iterator.hasNext()) {
+ return iterator;
+ }
+ iterator.close();
+ } catch (IOException e) {
+ logger.error("Error opening containing JAR file " + jarFile, e);
+ }
+ }
+
+
+ // Try to find directory as a system resource
+ URL url = ClassLoader.getSystemResource(directory);
+ if (url != null) {
+ try {
+ File dir = JarUtil.urlToFile(url);
+ iterator = new DirectoryIterator(dir, filter, true);
+ if (iterator.hasNext()) {
+ return iterator;
+ }
+ iterator.close();
+ } catch (Exception e1) {
+ logger.error("Error opening directory from URL " + url);
+ }
+ }
+
+
+ // Try to open directory as such
+ try {
+ iterator = new DirectoryIterator(new File(directory), filter, true);
+ if (iterator.hasNext()) {
+ return iterator;
+ }
+ iterator.close();
+ } catch (IOException e) {
+ logger.error("Error opening directory " + directory);
+ }
+
+ return null;
+ }
+
+
+
+ /**
+ * A FileFilter wrapper that accepts or discards directories.
+ */
+ private class DirSelectionFileFilter implements FileFilter {
+
+ private final boolean acceptDirs;
+ private final FileFilter parentFilter;
+
+
+ public DirSelectionFileFilter(FileFilter filter, boolean acceptDirs) {
+ this.acceptDirs = acceptDirs;
+ this.parentFilter = filter;
+ }
+
+
+ @Override
+ public boolean accept(File pathname) {
+ if (pathname.getName().startsWith(".")) {
+ return false;
+ }
+ if (pathname.isDirectory()) {
+ return acceptDirs;
+ }
+ return parentFilter.accept(pathname);
+ }
+
+ }
+
+}
diff --git a/src/net/sf/openrocket/file/iterator/FileIterator.java b/src/net/sf/openrocket/file/iterator/FileIterator.java
new file mode 100644
index 000000000..14d220a91
--- /dev/null
+++ b/src/net/sf/openrocket/file/iterator/FileIterator.java
@@ -0,0 +1,94 @@
+package net.sf.openrocket.file.iterator;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.Pair;
+
+/**
+ * An abstract class for iterating over files fulfilling some condition. The files are
+ * returned as pairs of open InputStreams and file names. Conditions can be for example
+ * files in a directory matching a specific FileFilter.
+ *
+ * Concrete implementations must implement the method {@link #findNext()} and possibly
+ * {@link #close()}.
+ *
+ * @author Sampo Niskanen
+ */
+public abstract class FileIterator implements Iterator> {
+ private static final LogHelper logger = Application.getLogger();
+
+ private Pair next = null;
+ private int fileCount = 0;
+
+ @Override
+ public boolean hasNext() {
+ if (next != null)
+ return true;
+
+ next = findNext();
+ return (next != null);
+ }
+
+
+ @Override
+ public Pair next() {
+ if (next == null) {
+ next = findNext();
+ }
+ if (next == null) {
+ throw new NoSuchElementException("No more files");
+ }
+
+ Pair n = next;
+ next = null;
+ fileCount++;
+ return n;
+ }
+
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("remove() not supported");
+ }
+
+
+
+ /**
+ * Closes the resources related to this iterator. This method should be
+ * overridden if the iterator needs to close any resources of its own, but
+ * must call this method as well.
+ */
+ public void close() {
+ if (next != null) {
+ try {
+ next.getV().close();
+ } catch (IOException e) {
+ logger.error("Error closing file " + next.getU());
+ }
+ next = null;
+ }
+ }
+
+
+ /**
+ * Return the number of files that have so far been returned by this iterator.
+ *
+ * @return the number of files that this iterator has returned so far.
+ */
+ public int getFileCount() {
+ return fileCount;
+ }
+
+ /**
+ * Return the next pair of file name and InputStream.
+ *
+ * @return a pair with the file name and input stream reading the file.
+ */
+ protected abstract Pair findNext();
+
+}
diff --git a/src/net/sf/openrocket/file/ZipDirectoryIterator.java b/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java
similarity index 96%
rename from src/net/sf/openrocket/file/ZipDirectoryIterator.java
rename to src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java
index f335a3fb4..3d3e6df5f 100644
--- a/src/net/sf/openrocket/file/ZipDirectoryIterator.java
+++ b/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java
@@ -1,4 +1,4 @@
-package net.sf.openrocket.file;
+package net.sf.openrocket.file.iterator;
import java.io.File;
import java.io.FileFilter;
@@ -20,7 +20,7 @@ import net.sf.openrocket.util.Pair;
*
* @author Sampo Niskanen
*/
-public class ZipDirectoryIterator extends DirectoryIterator {
+public class ZipDirectoryIterator extends FileIterator {
private static final LogHelper logger = Application.getLogger();
diff --git a/src/net/sf/openrocket/file/MotorLoader.java b/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java
similarity index 75%
rename from src/net/sf/openrocket/file/MotorLoader.java
rename to src/net/sf/openrocket/file/motor/AbstractMotorLoader.java
index 004b14f13..e9051f7a7 100644
--- a/src/net/sf/openrocket/file/MotorLoader.java
+++ b/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java
@@ -1,4 +1,4 @@
-package net.sf.openrocket.file;
+package net.sf.openrocket.file.motor;
import java.io.IOException;
import java.io.InputStream;
@@ -13,19 +13,14 @@ import java.util.List;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.util.MathUtil;
+public abstract class AbstractMotorLoader implements MotorLoader {
+
-public abstract class MotorLoader implements Loader {
-
-
/**
- * Load motors from the specified InputStream
. The file is read using
- * the default charset returned by {@link #getDefaultCharset()}.
- *
- * @param stream the source of the motor definitions.
- * @param filename the file name of the file, may be null
if not
- * applicable.
- * @return a list of motors contained in the file.
- * @throws IOException if an I/O exception occurs of the file format is invalid.
+ * {@inheritDoc}
+ *
+ * This method delegates the reading to the loaded from the Reader using the charset
+ * returned by {@link #getDefaultCharset()}.
*/
public List load(InputStream stream, String filename) throws IOException {
return load(new InputStreamReader(stream, getDefaultCharset()), filename);
@@ -41,10 +36,10 @@ public abstract class MotorLoader implements Loader {
* @return a list of motors contained in the file.
* @throws IOException if an I/O exception occurs of the file format is invalid.
*/
- public abstract List load(Reader reader, String filename) throws IOException;
+ protected abstract List load(Reader reader, String filename) throws IOException;
+
-
/**
* Return the default charset to use when loading rocket files of this type.
*
@@ -53,14 +48,14 @@ public abstract class MotorLoader implements Loader {
*
* @return the charset to use when loading the rocket file.
*/
- protected abstract Charset getDefaultCharset();
+ protected abstract Charset getDefaultCharset();
+
-
-
+
////////// Helper methods //////////
-
+
/**
* Calculate the mass of a motor at distinct points in time based on the
* initial total mass, propellant weight and thrust.
@@ -79,7 +74,7 @@ public abstract class MotorLoader implements Loader {
double total, double prop) {
List mass = new ArrayList();
List deltam = new ArrayList();
-
+
double t0, f0;
double totalMassChange = 0;
double scale;
@@ -87,11 +82,11 @@ public abstract class MotorLoader implements Loader {
// First calculate mass change between points
t0 = time.get(0);
f0 = thrust.get(0);
- for (int i=1; i < time.size(); i++) {
+ for (int i = 1; i < time.size(); i++) {
double t1 = time.get(i);
double f1 = thrust.get(i);
- double dm = 0.5*(f0+f1)*(t1-t0);
+ double dm = 0.5 * (f0 + f1) * (t1 - t0);
deltam.add(dm);
totalMassChange += dm;
t0 = t1;
@@ -101,8 +96,8 @@ public abstract class MotorLoader implements Loader {
// Scale mass change and calculate mass
mass.add(total);
scale = prop / totalMassChange;
- for (double dm: deltam) {
- total -= dm*scale;
+ for (double dm : deltam) {
+ total -= dm * scale;
mass.add(total);
}
@@ -125,12 +120,12 @@ public abstract class MotorLoader implements Loader {
}
-
+
/**
* Helper method to tokenize a string using whitespace as the delimiter.
*/
protected static String[] split(String str) {
- return split(str,"\\s+");
+ return split(str, "\\s+");
}
@@ -139,7 +134,7 @@ public abstract class MotorLoader implements Loader {
*/
protected static String[] split(String str, String delim) {
String[] pieces = str.split(delim);
- if (pieces.length==0 || !pieces[0].equals(""))
+ if (pieces.length == 0 || !pieces[0].equals(""))
return pieces;
return Arrays.copyOfRange(pieces, 1, pieces.length);
}
@@ -151,7 +146,7 @@ public abstract class MotorLoader implements Loader {
* @param primary the list to order.
* @param lists lists to order in the same permutation.
*/
- protected static void sortLists(List primary, List> ... lists) {
+ protected static void sortLists(List primary, List>... lists) {
// TODO: LOW: Very idiotic sort algorithm, but should be fast enough
// since the time should be sorted already
@@ -159,23 +154,23 @@ public abstract class MotorLoader implements Loader {
int index;
do {
- for (index=0; index < primary.size()-1; index++) {
- if (primary.get(index+1) < primary.get(index)) {
- Collections.swap(primary, index, index+1);
- for (List> l: lists) {
- Collections.swap(l, index, index+1);
+ for (index = 0; index < primary.size() - 1; index++) {
+ if (primary.get(index + 1) < primary.get(index)) {
+ Collections.swap(primary, index, index + 1);
+ for (List> l : lists) {
+ Collections.swap(l, index, index + 1);
}
break;
}
}
- } while (index < primary.size()-1);
+ } while (index < primary.size() - 1);
}
-
+
@SuppressWarnings("unchecked")
protected static void finalizeThrustCurve(List time, List thrust,
- List ... lists) {
+ List... lists) {
if (time.size() == 0)
return;
@@ -184,18 +179,18 @@ public abstract class MotorLoader implements Loader {
if (!MathUtil.equals(time.get(0), 0) || !MathUtil.equals(thrust.get(0), 0)) {
time.add(0, 0.0);
thrust.add(0, 0.0);
- for (List l: lists) {
+ for (List l : lists) {
Object o = l.get(0);
l.add(0, o);
}
}
// End
- int n = time.size()-1;
+ int n = time.size() - 1;
if (!MathUtil.equals(thrust.get(n), 0)) {
time.add(time.get(n));
thrust.add(0.0);
- for (List l: lists) {
+ for (List l : lists) {
Object o = l.get(n);
l.add(o);
}
diff --git a/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java b/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java
new file mode 100644
index 000000000..937c67903
--- /dev/null
+++ b/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java
@@ -0,0 +1,80 @@
+package net.sf.openrocket.file.motor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import net.sf.openrocket.file.UnknownFileTypeException;
+import net.sf.openrocket.motor.Motor;
+
+/**
+ * A motor loader class that detects the file type based on the file name extension.
+ *
+ * @author Sampo Niskanen
+ */
+public class GeneralMotorLoader implements MotorLoader {
+
+ private final MotorLoader RASP_LOADER = new RASPMotorLoader();
+ private final MotorLoader ROCKSIM_LOADER = new RockSimMotorLoader();
+ private final MotorLoader ZIP_LOADER;
+
+
+ public GeneralMotorLoader() {
+ // Must use this loader in order to avoid recursive instantiation
+ ZIP_LOADER = new ZipFileMotorLoader(this);
+ }
+
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * @throws UnknownFileTypeException if the file format is not supported
+ */
+ @Override
+ public List load(InputStream stream, String filename) throws IOException {
+ return selectLoader(filename).load(stream, filename);
+ }
+
+
+
+ /**
+ * Return an array containing the supported file extensions.
+ *
+ * @return an array of the supported file extensions.
+ */
+ public String[] getSupportedExtensions() {
+ return new String[] { "rse", "eng", "zip" };
+ }
+
+
+ /**
+ * Return the appropriate motor loader based on the file name.
+ *
+ * @param filename the file name (may be null
).
+ * @return the appropriate motor loader to use for the file.
+ * @throws UnknownFileTypeException if the file type cannot be detected from the file name.
+ */
+ private MotorLoader selectLoader(String filename) throws IOException {
+ if (filename == null) {
+ throw new UnknownFileTypeException("Unknown file type, filename=null");
+ }
+
+ String ext = "";
+ int point = filename.lastIndexOf('.');
+
+ if (point > 0)
+ ext = filename.substring(point + 1);
+
+ if (ext.equalsIgnoreCase("eng")) {
+ return RASP_LOADER;
+ } else if (ext.equalsIgnoreCase("rse")) {
+ return ROCKSIM_LOADER;
+ } else if (ext.equalsIgnoreCase("zip")) {
+ return ZIP_LOADER;
+ }
+
+ throw new UnknownFileTypeException("Unknown file type, filename=" + filename);
+ }
+
+}
diff --git a/src/net/sf/openrocket/file/motor/MotorLoader.java b/src/net/sf/openrocket/file/motor/MotorLoader.java
new file mode 100644
index 000000000..73e140391
--- /dev/null
+++ b/src/net/sf/openrocket/file/motor/MotorLoader.java
@@ -0,0 +1,24 @@
+package net.sf.openrocket.file.motor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import net.sf.openrocket.file.Loader;
+import net.sf.openrocket.motor.Motor;
+
+
+public interface MotorLoader extends Loader {
+
+ /**
+ * Load motors from the specified InputStream
.
+ *
+ * @param stream the source of the motor definitions.
+ * @param filename the file name of the file, may be null
if not
+ * applicable.
+ * @return a list of motors contained in the file.
+ * @throws IOException if an I/O exception occurs of the file format is invalid.
+ */
+ public List load(InputStream stream, String filename) throws IOException;
+
+}
diff --git a/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java b/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java
new file mode 100644
index 000000000..377ade46b
--- /dev/null
+++ b/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java
@@ -0,0 +1,110 @@
+package net.sf.openrocket.file.motor;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.openrocket.file.iterator.DirectoryIterator;
+import net.sf.openrocket.file.iterator.FileIterator;
+import net.sf.openrocket.gui.main.SimpleFileFilter;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.ThrustCurveMotor;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.Pair;
+
+public final class MotorLoaderHelper {
+
+ private static final LogHelper log = Application.getLogger();
+
+ private MotorLoaderHelper() {
+ // Prevent construction
+ }
+
+ /**
+ * Load a file or directory of thrust curves. Directories are loaded
+ * recursively. Any errors during loading are logged, but otherwise ignored.
+ *
+ * @param target the file or directory to load.
+ * @return a list of all motors in the file/directory.
+ */
+ public static List load(File target) {
+ GeneralMotorLoader loader = new GeneralMotorLoader();
+
+ if (target.isDirectory()) {
+
+ try {
+ return load(new DirectoryIterator(target, new SimpleFileFilter("", loader.getSupportedExtensions()), true));
+ } catch (IOException e) {
+ log.warn("Could not read directory " + target, e);
+ return Collections.emptyList();
+ }
+
+ } else {
+
+ InputStream is = null;
+ try {
+ is = new FileInputStream(target);
+ return loader.load(new BufferedInputStream(is), target.getName());
+ } catch (IOException e) {
+ log.warn("Could not load file " + target, e);
+ return Collections.emptyList();
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ log.error("Could not close file " + target, e);
+ }
+ }
+ }
+
+ }
+ }
+
+
+ /**
+ * Load motors from files iterated over by a FileIterator. Any errors during
+ * loading are logged, but otherwise ignored.
+ *
+ * The iterator is closed at the end of the operation.
+ *
+ * @param iterator the FileIterator that iterates of the files to load.
+ * @return a list of all motors loaded.
+ */
+ public static List load(FileIterator iterator) {
+ GeneralMotorLoader loader = new GeneralMotorLoader();
+ List list = new ArrayList();
+
+ while (iterator.hasNext()) {
+ final Pair input = iterator.next();
+ log.debug("Loading motors from file " + input.getU());
+ try {
+ List motors = loader.load(input.getV(), input.getU());
+ if (motors.size() == 0) {
+ log.warn("No motors found in file " + input.getU());
+ }
+ for (Motor m : motors) {
+ list.add((ThrustCurveMotor) m);
+ }
+ } catch (IOException e) {
+ log.warn("IOException when loading motor file " + input.getU(), e);
+ } finally {
+ try {
+ input.getV().close();
+ } catch (IOException e) {
+ log.error("IOException when closing InputStream", e);
+ }
+ }
+ }
+ iterator.close();
+
+ return list;
+ }
+
+}
diff --git a/src/net/sf/openrocket/file/motor/RASPMotorLoader.java b/src/net/sf/openrocket/file/motor/RASPMotorLoader.java
index 1f6477ed0..ee243d434 100644
--- a/src/net/sf/openrocket/file/motor/RASPMotorLoader.java
+++ b/src/net/sf/openrocket/file/motor/RASPMotorLoader.java
@@ -8,7 +8,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import net.sf.openrocket.file.MotorLoader;
import net.sf.openrocket.motor.Manufacturer;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.MotorDigest;
@@ -16,7 +15,7 @@ import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.motor.MotorDigest.DataType;
import net.sf.openrocket.util.Coordinate;
-public class RASPMotorLoader extends MotorLoader {
+public class RASPMotorLoader extends AbstractMotorLoader {
public static final String CHARSET_NAME = "ISO-8859-1";
@@ -161,10 +160,6 @@ public class RASPMotorLoader extends MotorLoader {
throw new IOException("Illegal file format.");
- } finally {
-
- in.close();
-
}
return motors;
diff --git a/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java b/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java
index 2e156f2d7..747adeb69 100644
--- a/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java
+++ b/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java
@@ -8,7 +8,6 @@ import java.util.HashMap;
import java.util.List;
import net.sf.openrocket.aerodynamics.WarningSet;
-import net.sf.openrocket.file.MotorLoader;
import net.sf.openrocket.file.simplesax.ElementHandler;
import net.sf.openrocket.file.simplesax.NullElementHandler;
import net.sf.openrocket.file.simplesax.PlainTextHandler;
@@ -23,7 +22,7 @@ import net.sf.openrocket.util.Coordinate;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
-public class RockSimMotorLoader extends MotorLoader {
+public class RockSimMotorLoader extends AbstractMotorLoader {
public static final String CHARSET_NAME = "UTF-8";
diff --git a/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java b/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java
new file mode 100644
index 000000000..804226f01
--- /dev/null
+++ b/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java
@@ -0,0 +1,85 @@
+package net.sf.openrocket.file.motor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import net.sf.openrocket.file.UnknownFileTypeException;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.UncloseableInputStream;
+
+/**
+ * A motor loader that loads motors from a ZIP file.
+ *
+ * @author Sampo Niskanen
+ */
+public class ZipFileMotorLoader implements MotorLoader {
+ private static final LogHelper log = Application.getLogger();
+
+ private final MotorLoader loader;
+
+
+ /**
+ * Construct a ZipFileMotorLoader that loads files using a
+ * {@link GeneralMotorLoader}.
+ */
+ public ZipFileMotorLoader() {
+ this(new GeneralMotorLoader());
+ }
+
+ /**
+ * Constructs a ZipFileMotorLoader that loads files using the provided motor loader.
+ *
+ * @param loader the motor loader to use when loading.
+ */
+ public ZipFileMotorLoader(MotorLoader loader) {
+ this.loader = loader;
+ }
+
+
+ @Override
+ public List load(InputStream stream, String filename) throws IOException {
+ List motors = new ArrayList();
+
+ ZipInputStream is = new ZipInputStream(stream);
+
+ // SAX seems to close the input stream, prevent it
+ InputStream uncloseable = new UncloseableInputStream(is);
+
+ while (true) {
+ ZipEntry entry = is.getNextEntry();
+ if (entry == null)
+ break;
+
+ if (entry.isDirectory())
+ continue;
+
+ // Get the file name of the entry
+ String name = entry.getName();
+ int index = name.lastIndexOf('/');
+ if (index < 0) {
+ index = name.lastIndexOf('\\');
+ }
+ if (index >= 0) {
+ name = name.substring(index + 1);
+ }
+
+ try {
+ List m = loader.load(uncloseable, entry.getName());
+ motors.addAll(m);
+ log.info("Loaded " + m.size() + " motors from ZIP entry " + entry.getName());
+ } catch (UnknownFileTypeException e) {
+ log.info("Could not read ZIP entry " + entry.getName() + ": " + e.getMessage());
+ }
+
+ }
+
+ return motors;
+ }
+
+}
diff --git a/src/net/sf/openrocket/gui/components/DescriptionArea.java b/src/net/sf/openrocket/gui/components/DescriptionArea.java
index d3d568b2d..616b3adcd 100644
--- a/src/net/sf/openrocket/gui/components/DescriptionArea.java
+++ b/src/net/sf/openrocket/gui/components/DescriptionArea.java
@@ -1,27 +1,43 @@
package net.sf.openrocket.gui.components;
+import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import javax.swing.JEditorPane;
+import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
public class DescriptionArea extends JScrollPane {
-
+
private final JEditorPane editorPane;
-
+
public DescriptionArea(int rows) {
this("", rows, -1);
}
+
public DescriptionArea(int rows, float size) {
this("", rows, size);
}
public DescriptionArea(String text, int rows, float size) {
+ this(text, rows, size, true);
+ }
+
+ /**
+ * Constructor with all options.
+ *
+ * @param text the text for the description area.
+ * @param rows the number of rows to set
+ * @param size the relative font size in points (positive or negative)
+ * @param opaque if false
the background color will be set to the background color
+ * of a default JPanel (simulation non-opaque)
+ */
+ public DescriptionArea(String text, int rows, float size, boolean opaque) {
super(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
@@ -30,6 +46,12 @@ public class DescriptionArea extends JScrollPane {
editorPane.setFont(font.deriveFont(font.getSize2D() + size));
editorPane.setEditable(false);
+ if (!opaque) {
+ Color bg = new JPanel().getBackground();
+ editorPane.setBackground(new Color(bg.getRed(), bg.getGreen(), bg.getBlue()));
+ this.setOpaque(true);
+ }
+
// Calculate correct height
editorPane.setText("abc");
Dimension oneline = editorPane.getPreferredSize();
@@ -52,14 +74,14 @@ public class DescriptionArea extends JScrollPane {
editorPane.setText(txt);
editorPane.revalidate();
SwingUtilities.invokeLater(new Runnable() {
-
+
@Override
public void run() {
- editorPane.scrollRectToVisible(new Rectangle(0,0,1,1));
+ editorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
}
});
- editorPane.scrollRectToVisible(new Rectangle(0,0,1,1));
+ editorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
}
}
diff --git a/src/net/sf/openrocket/gui/configdialog/RocketConfig.java b/src/net/sf/openrocket/gui/configdialog/RocketConfig.java
index 2e3131601..cf62fd08b 100644
--- a/src/net/sf/openrocket/gui/configdialog/RocketConfig.java
+++ b/src/net/sf/openrocket/gui/configdialog/RocketConfig.java
@@ -16,27 +16,26 @@ import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.util.GUIUtil;
public class RocketConfig extends RocketComponentConfig {
-
+
private TextFieldListener textFieldListener;
private JTextArea designerTextArea;
private JTextArea revisionTextArea;
-
+
private final Rocket rocket;
public RocketConfig(RocketComponent c) {
super(c);
- rocket = (Rocket)c;
+ rocket = (Rocket) c;
this.removeAll();
setLayout(new MigLayout("fill"));
-
+
this.add(new JLabel("Design name:"), "top, pad 4lp, gapright 10lp");
this.add(componentNameField, "growx, wrap para");
-
-
+
this.add(new JLabel("Designer:"), "top, pad 4lp, gapright 10lp");
textFieldListener = new TextFieldListener();
@@ -46,13 +45,13 @@ public class RocketConfig extends RocketComponentConfig {
designerTextArea.setEditable(true);
GUIUtil.setTabToFocusing(designerTextArea);
designerTextArea.addFocusListener(textFieldListener);
- this.add(new JScrollPane(designerTextArea), "wmin 300lp, hmin 45lp, grow 30, wrap para");
-
+ this.add(new JScrollPane(designerTextArea), "wmin 400lp, height 60lp:60lp:, grow 30, wrap para");
+
this.add(new JLabel("Comments:"), "top, pad 4lp, gapright 10lp");
- this.add(new JScrollPane(commentTextArea), "wmin 300lp, hmin 105lp, grow 100, wrap para");
-
+ this.add(new JScrollPane(commentTextArea), "wmin 400lp, height 155lp:155lp:, grow 100, wrap para");
+
this.add(new JLabel("Revision history:"), "top, pad 4lp, gapright 10lp");
revisionTextArea = new JTextArea(rocket.getRevision());
revisionTextArea.setLineWrap(true);
@@ -61,9 +60,9 @@ public class RocketConfig extends RocketComponentConfig {
GUIUtil.setTabToFocusing(revisionTextArea);
revisionTextArea.addFocusListener(textFieldListener);
- this.add(new JScrollPane(revisionTextArea), "wmin 300lp, hmin 45lp, grow 30, wrap para");
-
+ this.add(new JScrollPane(revisionTextArea), "wmin 400lp, height 60lp:60lp:, grow 30, wrap para");
+
addButtons();
}
@@ -73,10 +72,14 @@ public class RocketConfig extends RocketComponentConfig {
public void actionPerformed(ActionEvent e) {
setName();
}
- public void focusGained(FocusEvent e) { }
+
+ public void focusGained(FocusEvent e) {
+ }
+
public void focusLost(FocusEvent e) {
setName();
}
+
private void setName() {
if (!rocket.getDesigner().equals(designerTextArea.getText())) {
rocket.setDesigner(designerTextArea.getText());
@@ -87,6 +90,6 @@ public class RocketConfig extends RocketComponentConfig {
}
}
-
-
+
+
}
diff --git a/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java b/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
index 0f46cfe97..58f5664bb 100644
--- a/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
+++ b/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
@@ -6,6 +6,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
+import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -15,41 +16,52 @@ import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
+import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
import javax.swing.Timer;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.communication.UpdateInfo;
import net.sf.openrocket.communication.UpdateInfoRetriever;
+import net.sf.openrocket.gui.components.DescriptionArea;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.dialogs.UpdateInfoDialog;
+import net.sf.openrocket.gui.main.SimpleFileFilter;
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.Unit;
import net.sf.openrocket.unit.UnitGroup;
import net.sf.openrocket.util.GUIUtil;
import net.sf.openrocket.util.Prefs;
public class PreferencesDialog extends JDialog {
+ private static final LogHelper log = Application.getLogger();
private final List unitSelectors = new ArrayList();
-
+
+ private File defaultDirectory = null;
+
private PreferencesDialog() {
- super((Window)null, "Preferences", Dialog.ModalityType.APPLICATION_MODAL);
+ super((Window) null, "Preferences", Dialog.ModalityType.APPLICATION_MODAL);
+
+ JPanel panel = new JPanel(new MigLayout("fill, gap unrel", "[grow]", "[grow][]"));
- JPanel panel = new JPanel(new MigLayout("fill, gap unrel","[grow]","[grow][]"));
-
JTabbedPane tabbedPane = new JTabbedPane();
- panel.add(tabbedPane,"grow, wrap");
+ panel.add(tabbedPane, "grow, wrap");
tabbedPane.addTab("Units", null, unitsPane(), "Default units");
tabbedPane.addTab("Materials", null, new MaterialEditPanel(), "Custom materials");
tabbedPane.addTab("Options", null, optionsPane(), "Miscellaneous options");
-
+
JButton close = new JButton("Close");
close.addActionListener(new ActionListener() {
@Override
@@ -58,7 +70,7 @@ public class PreferencesDialog extends JDialog {
PreferencesDialog.this.dispose();
}
});
- panel.add(close,"span, right, tag close");
+ panel.add(close, "span, right, tag close");
this.setContentPane(panel);
pack();
@@ -70,7 +82,7 @@ public class PreferencesDialog extends JDialog {
Prefs.storeDefaultUnits();
}
});
-
+
GUIUtil.setDisposableDialogOptions(this, close);
}
@@ -78,7 +90,7 @@ public class PreferencesDialog extends JDialog {
private JPanel optionsPane() {
JPanel panel = new JPanel(new MigLayout("fillx, ins 30lp n n n"));
-
+
panel.add(new JLabel("Position to insert new body components:"), "gapright para");
panel.add(new JComboBox(new PrefChoiseSelector(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY,
"Always ask", "Insert in middle", "Add to end")), "wrap para, growx, sg combos");
@@ -87,7 +99,104 @@ public class PreferencesDialog extends JDialog {
panel.add(new JComboBox(new PrefBooleanSelector(Prefs.CONFIRM_DELETE_SIMULATION,
"Delete", "Confirm", true)), "wrap 40lp, growx, sg combos");
+
+ panel.add(new JLabel("User-defined thrust curves:"), "spanx, wrap");
+ final JTextField field = new JTextField();
+ List files = Prefs.getUserThrustCurveFiles();
+ String str = "";
+ for (File file : files) {
+ if (str.length() > 0) {
+ str += ";";
+ }
+ str += file.getAbsolutePath();
+ }
+ field.setText(str);
+ field.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ changed();
+ }
+
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ changed();
+ }
+
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ changed();
+ }
+
+ private void changed() {
+ String text = field.getText();
+ List list = new ArrayList();
+ for (String s : text.split(";")) {
+ s = s.trim();
+ if (s.length() > 0) {
+ list.add(new File(s));
+ }
+ }
+ Prefs.setUserThrustCurveFiles(list);
+ }
+ });
+ panel.add(field, "w 100px, gapright unrel, spanx, growx, split");
+ JButton button = new JButton("Add");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ JFileChooser chooser = new JFileChooser();
+ SimpleFileFilter filter = new SimpleFileFilter("All thrust curve files (*.eng; *.rse; *.zip; directories)",
+ true, "eng", "rse", "zip");
+ chooser.addChoosableFileFilter(filter);
+ chooser.addChoosableFileFilter(new SimpleFileFilter("RASP motor files (*.eng)",
+ true, "eng"));
+ chooser.addChoosableFileFilter(new SimpleFileFilter("RockSim engine files (*.rse)",
+ true, "rse"));
+ chooser.addChoosableFileFilter(new SimpleFileFilter("ZIP archives (*.zip)",
+ true, "zip"));
+ chooser.setFileFilter(filter);
+ chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+ if (defaultDirectory != null) {
+ chooser.setCurrentDirectory(defaultDirectory);
+ }
+
+ int returnVal = chooser.showDialog(PreferencesDialog.this, "Add");
+ if (returnVal == JFileChooser.APPROVE_OPTION) {
+ log.user("Adding user thrust curve: " + chooser.getSelectedFile());
+ defaultDirectory = chooser.getCurrentDirectory();
+ String text = field.getText().trim();
+ if (text.length() > 0) {
+ text += ";";
+ }
+ text += chooser.getSelectedFile().getAbsolutePath();
+ field.setText(text);
+ }
+ }
+ });
+ panel.add(button, "gapright unrel");
+
+ button = new JButton("Reset");
+
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ // First one sets to the default, but does not un-set the pref
+ field.setText(Prefs.getDefaultUserThrustCurveFile().getAbsolutePath());
+ Prefs.setUserThrustCurveFiles(null);
+ }
+ });
+ panel.add(button, "wrap");
+
+ DescriptionArea desc = new DescriptionArea("Add directories, RASP motor files (*.eng), " +
+ "RockSim engine files (*.rse) or ZIP archives separated by a semicolon (;) to load external " +
+ "thrust curves. Changes will take effect the next time you start OpenRocket.", 3, -3, false);
+ desc.setBackground(getBackground());
+ panel.add(desc, "spanx, growx, wrap 40lp");
+
+
+
+
final JCheckBox softwareUpdateBox = new JCheckBox("Check for software updates at startup");
softwareUpdateBox.setSelected(Prefs.getCheckUpdates());
softwareUpdateBox.addActionListener(new ActionListener() {
@@ -98,7 +207,7 @@ public class PreferencesDialog extends JDialog {
});
panel.add(softwareUpdateBox);
- JButton button = new JButton("Check now");
+ button = new JButton("Check now");
button.setToolTipText("Check for software updates now");
button.addActionListener(new ActionListener() {
@Override
@@ -108,19 +217,17 @@ public class PreferencesDialog extends JDialog {
});
panel.add(button, "right, wrap");
-
+
return panel;
}
-
-
private JPanel unitsPane() {
JPanel panel = new JPanel(new MigLayout("", "[][]40lp[][]"));
JComboBox combo;
panel.add(new JLabel("Select your preferred units:"), "span, wrap paragraph");
-
+
panel.add(new JLabel("Rocket dimensions:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH));
panel.add(combo, "sizegroup boxes");
@@ -129,8 +236,8 @@ public class PreferencesDialog extends JDialog {
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_LINE));
panel.add(combo, "sizegroup boxes, wrap");
-
-
+
+
panel.add(new JLabel("Motor dimensions:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MOTOR_DIMENSIONS));
panel.add(combo, "sizegroup boxes");
@@ -140,7 +247,7 @@ public class PreferencesDialog extends JDialog {
panel.add(combo, "sizegroup boxes, wrap");
-
+
panel.add(new JLabel("Distance:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE));
panel.add(combo, "sizegroup boxes");
@@ -150,26 +257,26 @@ public class PreferencesDialog extends JDialog {
panel.add(combo, "sizegroup boxes, wrap");
-
+
panel.add(new JLabel("Velocity:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_VELOCITY));
panel.add(combo, "sizegroup boxes");
-
+
panel.add(new JLabel("Surface roughness:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROUGHNESS));
panel.add(combo, "sizegroup boxes, wrap");
-
-
+
+
panel.add(new JLabel("Acceleration:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ACCELERATION));
panel.add(combo, "sizegroup boxes");
-
+
panel.add(new JLabel("Area:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_AREA));
panel.add(combo, "sizegroup boxes, wrap");
-
+
panel.add(new JLabel("Mass:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MASS));
@@ -180,7 +287,7 @@ public class PreferencesDialog extends JDialog {
panel.add(combo, "sizegroup boxes, wrap");
-
+
panel.add(new JLabel("Force:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE));
panel.add(combo, "sizegroup boxes");
@@ -190,7 +297,7 @@ public class PreferencesDialog extends JDialog {
panel.add(combo, "sizegroup boxes, wrap");
-
+
panel.add(new JLabel("Total impulse:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_IMPULSE));
panel.add(combo, "sizegroup boxes");
@@ -200,23 +307,23 @@ public class PreferencesDialog extends JDialog {
panel.add(combo, "sizegroup boxes, wrap");
-
+
panel.add(new JLabel("Stability:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_STABILITY));
panel.add(combo, "sizegroup boxes");
-
+
panel.add(new JLabel("Pressure:"));
combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_PRESSURE));
panel.add(combo, "sizegroup boxes, wrap para");
-
-
+
+
JButton button = new JButton("Default metric");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
UnitGroup.setDefaultMetricUnits();
- for (DefaultUnitSelector s: unitSelectors)
+ for (DefaultUnitSelector s : unitSelectors)
s.fireChange();
}
});
@@ -227,14 +334,14 @@ public class PreferencesDialog extends JDialog {
@Override
public void actionPerformed(ActionEvent e) {
UnitGroup.setDefaultImperialUnits();
- for (DefaultUnitSelector s: unitSelectors)
+ for (DefaultUnitSelector s : unitSelectors)
s.fireChange();
}
});
panel.add(button, "grow, wrap para");
-
- panel.add(new StyledLabel("The effects will take place the next time you open a window.",-2),
+
+ panel.add(new StyledLabel("The effects will take place the next time you open a window.", -2),
"spanx, wrap");
@@ -242,12 +349,13 @@ public class PreferencesDialog extends JDialog {
}
-
-
-
+
+
+
private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel {
private final UnitGroup group;
+
public DefaultUnitSelector(UnitGroup group) {
this.group = group;
unitSelectors.add(this);
@@ -257,6 +365,7 @@ public class PreferencesDialog extends JDialog {
public Object getSelectedItem() {
return group.getDefaultUnit();
}
+
@Override
public void setSelectedItem(Object item) {
if (item == null) {
@@ -264,14 +373,16 @@ public class PreferencesDialog extends JDialog {
return;
}
if (!(item instanceof Unit)) {
- throw new IllegalArgumentException("Illegal argument "+item);
+ throw new IllegalArgumentException("Illegal argument " + item);
}
- group.setDefaultUnit(group.getUnitIndex((Unit)item));
+ group.setDefaultUnit(group.getUnitIndex((Unit) item));
}
+
@Override
public Object getElementAt(int index) {
return group.getUnit(index);
}
+
@Override
public int getSize() {
return group.getUnitCount();
@@ -283,13 +394,13 @@ public class PreferencesDialog extends JDialog {
}
}
-
+
private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel {
private final String preference;
private final String[] descriptions;
- public PrefChoiseSelector(String preference, String ... descriptions) {
+ public PrefChoiseSelector(String preference, String... descriptions) {
this.preference = preference;
this.descriptions = descriptions;
}
@@ -306,15 +417,15 @@ public class PreferencesDialog extends JDialog {
return;
}
if (!(item instanceof String)) {
- throw new IllegalArgumentException("Illegal argument "+item);
+ throw new IllegalArgumentException("Illegal argument " + item);
}
int index;
for (index = 0; index < descriptions.length; index++) {
- if (((String)item).equalsIgnoreCase(descriptions[index]))
+ if (((String) item).equalsIgnoreCase(descriptions[index]))
break;
}
if (index >= descriptions.length) {
- throw new IllegalArgumentException("Illegal argument "+item);
+ throw new IllegalArgumentException("Illegal argument " + item);
}
Prefs.putChoise(preference, index);
@@ -324,19 +435,20 @@ public class PreferencesDialog extends JDialog {
public Object getElementAt(int index) {
return descriptions[index];
}
+
@Override
public int getSize() {
return descriptions.length;
}
}
-
+
private class PrefBooleanSelector extends AbstractListModel implements ComboBoxModel {
private final String preference;
private final String trueDesc, falseDesc;
private final boolean def;
- public PrefBooleanSelector(String preference, String falseDescription,
+ public PrefBooleanSelector(String preference, String falseDescription,
String trueDescription, boolean defaultState) {
this.preference = preference;
this.trueDesc = trueDescription;
@@ -360,7 +472,7 @@ public class PreferencesDialog extends JDialog {
return;
}
if (!(item instanceof String)) {
- throw new IllegalArgumentException("Illegal argument "+item);
+ throw new IllegalArgumentException("Illegal argument " + item);
}
if (trueDesc.equals(item)) {
@@ -368,7 +480,7 @@ public class PreferencesDialog extends JDialog {
} else if (falseDesc.equals(item)) {
Prefs.NODE.putBoolean(preference, false);
} else {
- throw new IllegalArgumentException("Illegal argument "+item);
+ throw new IllegalArgumentException("Illegal argument " + item);
}
}
@@ -377,14 +489,15 @@ public class PreferencesDialog extends JDialog {
switch (index) {
case 0:
return def ? trueDesc : falseDesc;
-
+
case 1:
- return def ? falseDesc: trueDesc;
+ return def ? falseDesc : trueDesc;
default:
- throw new IndexOutOfBoundsException("Boolean asked for index="+index);
+ throw new IndexOutOfBoundsException("Boolean asked for index=" + index);
}
}
+
@Override
public int getSize() {
return 2;
@@ -396,7 +509,7 @@ public class PreferencesDialog extends JDialog {
final UpdateInfoRetriever retriever = new UpdateInfoRetriever();
retriever.start();
-
+
// Progress dialog
final JDialog dialog = new JDialog(this, ModalityType.APPLICATION_MODAL);
JPanel panel = new JPanel(new MigLayout());
@@ -419,15 +532,15 @@ public class PreferencesDialog extends JDialog {
GUIUtil.setDisposableDialogOptions(dialog, cancel);
-
+
// Timer to monitor progress
final Timer timer = new Timer(100, null);
final long startTime = System.currentTimeMillis();
-
+
ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
- if (!retriever.isRunning() || startTime+10000 < System.currentTimeMillis()) {
+ if (!retriever.isRunning() || startTime + 10000 < System.currentTimeMillis()) {
timer.stop();
dialog.dispose();
}
@@ -436,22 +549,22 @@ public class PreferencesDialog extends JDialog {
timer.addActionListener(listener);
timer.start();
-
+
// Wait for action
dialog.setVisible(true);
-
+
// Check result
UpdateInfo info = retriever.getUpdateInfo();
if (info == null) {
- JOptionPane.showMessageDialog(this,
- "An error occurred while communicating with the server.",
+ JOptionPane.showMessageDialog(this,
+ "An error occurred while communicating with the server.",
"Unable to retrieve update information", JOptionPane.WARNING_MESSAGE, null);
- } else if (info.getLatestVersion() == null ||
+ } else if (info.getLatestVersion() == null ||
info.getLatestVersion().equals("") ||
Prefs.getVersion().equalsIgnoreCase(info.getLatestVersion())) {
- JOptionPane.showMessageDialog(this,
- "You are running the latest version of OpenRocket.",
+ JOptionPane.showMessageDialog(this,
+ "You are running the latest version of OpenRocket.",
"No updates available", JOptionPane.INFORMATION_MESSAGE, null);
} else {
UpdateInfoDialog infoDialog = new UpdateInfoDialog(info);
@@ -478,5 +591,5 @@ public class PreferencesDialog extends JDialog {
dialog.setVisible(true);
}
-
+
}
diff --git a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/src/net/sf/openrocket/gui/main/ComponentAddButtons.java
index 1f22c2e9a..3fe592677 100644
--- a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java
+++ b/src/net/sf/openrocket/gui/main/ComponentAddButtons.java
@@ -148,7 +148,6 @@ public class ComponentAddButtons extends JPanel implements Scrollable {
}
// Set all buttons to maximum size
- System.out.println("Setting w=" + w + " h=" + h);
width = w;
height = h;
Dimension d = new Dimension(width, height);
diff --git a/src/net/sf/openrocket/optimization/Function.java b/src/net/sf/openrocket/optimization/Function.java
new file mode 100644
index 000000000..636e8a4a7
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/Function.java
@@ -0,0 +1,40 @@
+package net.sf.openrocket.optimization;
+
+/**
+ * An interface defining an optimizable function.
+ *
+ * Some function optimizers require that the function is thread-safe.
+ *
+ * @author Sampo Niskanen
+ */
+public interface Function {
+
+ /**
+ * Evaluate the function at the specified point.
+ *
+ * If the function evaluation is slow, then this method should abort the computation if
+ * the thread is interrupted.
+ *
+ * @param point the point at which to evaluate the function.
+ * @return the function value.
+ * @throws InterruptedException if the thread was interrupted before function evaluation was completed.
+ */
+ public double evaluate(Point point) throws InterruptedException;
+
+
+ /**
+ * Return a cached value of the function at the specified point. This allows efficient
+ * caching of old values even between calls to optimization methods. This method should
+ * NOT evaluate the function except in special cases (e.g. the point is outside of the
+ * function domain).
+ *
+ * Note that it is allowed to always allowed to return Double.NaN
, especially
+ * for functions that are fast to evaluate.
+ *
+ * @param point the point of function evaluation.
+ * @return the function value, or Double.NaN
if the function value has not been
+ * evaluated at this point.
+ */
+ public double preComputed(Point point);
+
+}
diff --git a/src/net/sf/openrocket/optimization/FunctionCache.java b/src/net/sf/openrocket/optimization/FunctionCache.java
new file mode 100644
index 000000000..faa080bcc
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/FunctionCache.java
@@ -0,0 +1,13 @@
+package net.sf.openrocket.optimization;
+
+public interface FunctionCache {
+
+ public double getValue(Point point);
+
+ public void clearCache();
+
+ public Function getFunction();
+
+ public void setFunction(Function function);
+
+}
diff --git a/src/net/sf/openrocket/optimization/FunctionCacheComparator.java b/src/net/sf/openrocket/optimization/FunctionCacheComparator.java
new file mode 100644
index 000000000..11e722988
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/FunctionCacheComparator.java
@@ -0,0 +1,26 @@
+package net.sf.openrocket.optimization;
+
+import java.util.Comparator;
+
+/**
+ * A comparator that orders Points in a function value order, smallest first.
+ *
+ * @author Sampo Niskanen
+ */
+public class FunctionCacheComparator implements Comparator {
+
+ private final FunctionCache cache;
+
+ public FunctionCacheComparator(FunctionCache cache) {
+ this.cache = cache;
+ }
+
+ @Override
+ public int compare(Point o1, Point o2) {
+ double v1 = cache.getValue(o1);
+ double v2 = cache.getValue(o2);
+
+ return Double.compare(v1, v2);
+ }
+
+}
diff --git a/src/net/sf/openrocket/optimization/FunctionCallable.java b/src/net/sf/openrocket/optimization/FunctionCallable.java
new file mode 100644
index 000000000..315466530
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/FunctionCallable.java
@@ -0,0 +1,34 @@
+package net.sf.openrocket.optimization;
+
+import java.util.concurrent.Callable;
+
+/**
+ * A Callable that computes the value of a function at a specific point.
+ *
+ * @author Sampo Niskanen
+ */
+public class FunctionCallable implements Callable {
+
+ private final Function function;
+ private final Point point;
+
+ /**
+ * Sole constructor.
+ *
+ * @param function the function to evaluate
+ * @param point the point at which to evaluate the function
+ */
+ public FunctionCallable(Function function, Point point) {
+ this.function = function;
+ this.point = point;
+ }
+
+ /**
+ * Evaluate the function and return the result.
+ */
+ @Override
+ public Double call() throws InterruptedException {
+ return function.evaluate(point);
+ }
+
+}
diff --git a/src/net/sf/openrocket/optimization/FunctionDecorator.java b/src/net/sf/openrocket/optimization/FunctionDecorator.java
new file mode 100644
index 000000000..0214b2cea
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/FunctionDecorator.java
@@ -0,0 +1,22 @@
+package net.sf.openrocket.optimization;
+
+public class FunctionDecorator implements Function {
+
+ private final Function function;
+
+ public FunctionDecorator(Function function) {
+ this.function = function;
+ }
+
+
+ @Override
+ public double evaluate(Point x) throws InterruptedException {
+ return function.evaluate(x);
+ }
+
+ @Override
+ public double preComputed(Point x) {
+ return function.preComputed(x);
+ }
+
+}
diff --git a/src/net/sf/openrocket/optimization/FunctionOptimizer.java b/src/net/sf/openrocket/optimization/FunctionOptimizer.java
new file mode 100644
index 000000000..8c151eaa3
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/FunctionOptimizer.java
@@ -0,0 +1,18 @@
+package net.sf.openrocket.optimization;
+
+public interface FunctionOptimizer {
+
+ public void optimize(Point initial, OptimizationController control);
+
+
+ public Point getOptimumPoint();
+
+ public double getOptimumValue();
+
+
+ public FunctionCache getFunctionCache();
+
+ public void setFunctionCache(FunctionCache functionCache);
+
+
+}
diff --git a/src/net/sf/openrocket/optimization/MultidirectionalSearchOptimizer.java b/src/net/sf/openrocket/optimization/MultidirectionalSearchOptimizer.java
new file mode 100644
index 000000000..9125c32af
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/MultidirectionalSearchOptimizer.java
@@ -0,0 +1,290 @@
+package net.sf.openrocket.optimization;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import net.sf.openrocket.logging.LogHelper;
+import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.util.Statistics;
+
+/**
+ * A customized implementation of the parallel multidirectional search algorithm by Dennis and Torczon.
+ *
+ * This is a parallel pattern search optimization algorithm. The function evaluations are performed
+ * using an ExecutorService. By default a ThreadPoolExecutor is used that has as many thread defined
+ * as the system has processors.
+ */
+public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Statistics {
+ private static final LogHelper log = Application.getLogger();
+
+ private List simplex = new ArrayList();
+
+ private ParallelFunctionCache functionExecutor;
+
+ private boolean useExpansion = false;
+
+ private int stepCount = 0;
+ private int reflectionAcceptance = 0;
+ private int expansionAcceptance = 0;
+ private int coordinateAcceptance = 0;
+ private int reductionFallback = 0;
+
+
+ public MultidirectionalSearchOptimizer() {
+ // No-op
+ }
+
+ public MultidirectionalSearchOptimizer(ParallelFunctionCache functionCache) {
+ this.functionExecutor = functionCache;
+ }
+
+
+
+ @Override
+ public void optimize(Point initial, OptimizationController control) {
+ FunctionCacheComparator comparator = new FunctionCacheComparator(functionExecutor);
+
+ final List pattern = SearchPattern.square(initial.dim());
+ log.info("Starting optimization at " + initial + " with pattern " + pattern);
+
+ try {
+
+ boolean simplexComputed = false;
+ double step = 0.5;
+
+ // Set up the current simplex
+ simplex.clear();
+ simplex.add(initial);
+ for (Point p : pattern) {
+ simplex.add(initial.add(p.mul(step)));
+ }
+
+ // Normal iterations
+ List reflection = new ArrayList(simplex.size());
+ List expansion = new ArrayList(simplex.size());
+ List coordinateSearch = new ArrayList(simplex.size());
+ Point current;
+ double currentValue;
+ do {
+
+ log.debug("Starting optimization step with simplex " + simplex +
+ (simplexComputed ? "" : " (not computed)"));
+ stepCount++;
+
+ if (!simplexComputed) {
+ // TODO: Could something be computed in parallel?
+ functionExecutor.compute(simplex);
+ functionExecutor.waitFor(simplex);
+ Collections.sort(simplex, comparator);
+ simplexComputed = true;
+ }
+
+ current = simplex.get(0);
+ currentValue = functionExecutor.getValue(current);
+
+ /*
+ * Compute and queue the next points in likely order of usefulness.
+ * Expansion is unlikely as we're mainly dealing with bounded optimization.
+ */
+ createReflection(simplex, reflection);
+ createCoordinateSearch(current, step, coordinateSearch);
+ if (useExpansion)
+ createExpansion(simplex, expansion);
+
+ functionExecutor.compute(reflection);
+ functionExecutor.compute(coordinateSearch);
+ if (useExpansion)
+ functionExecutor.compute(expansion);
+
+ // Check reflection acceptance
+ log.debug("Computing reflection");
+ functionExecutor.waitFor(reflection);
+
+ if (accept(reflection, currentValue)) {
+
+ log.debug("Reflection was successful, aborting coordinate search, " +
+ (useExpansion ? "computing" : "skipping") + " expansion");
+
+ functionExecutor.abort(coordinateSearch);
+
+ simplex.clear();
+ simplex.add(current);
+ simplex.addAll(reflection);
+ Collections.sort(simplex, comparator);
+
+ if (useExpansion) {
+
+ /*
+ * Assume expansion to be unsuccessful, queue next reflection while computing expansion.
+ */
+ createReflection(simplex, reflection);
+
+ functionExecutor.compute(reflection);
+ functionExecutor.waitFor(expansion);
+
+ if (accept(expansion, currentValue)) {
+ log.debug("Expansion was successful, aborting reflection");
+ functionExecutor.abort(reflection);
+
+ simplex.clear();
+ simplex.add(current);
+ simplex.addAll(expansion);
+ step *= 2;
+ Collections.sort(simplex, comparator);
+ expansionAcceptance++;
+ } else {
+ log.debug("Expansion failed");
+ reflectionAcceptance++;
+ }
+
+ } else {
+ reflectionAcceptance++;
+ }
+
+ } else {
+
+ log.debug("Reflection was unsuccessful, aborting expansion, computing coordinate search");
+
+ functionExecutor.abort(expansion);
+
+ /*
+ * Assume coordinate search to be unsuccessful, queue contraction step while computing.
+ */
+ halveStep(simplex);
+ functionExecutor.compute(simplex);
+ functionExecutor.waitFor(coordinateSearch);
+
+ if (accept(coordinateSearch, currentValue)) {
+
+ log.debug("Coordinate search successful, reseting simplex");
+ List toAbort = new LinkedList(simplex);
+ simplex.clear();
+ simplex.add(current);
+ for (Point p : pattern) {
+ simplex.add(current.add(p.mul(step)));
+ }
+ toAbort.removeAll(simplex);
+ functionExecutor.abort(toAbort);
+ simplexComputed = false;
+ coordinateAcceptance++;
+
+ } else {
+ log.debug("Coordinate search unsuccessful, halving step.");
+ step /= 2;
+ reductionFallback++;
+ }
+
+ }
+
+ log.debug("Ending optimization step with simplex " + simplex);
+
+ if (Thread.interrupted()) {
+ throw new InterruptedException();
+ }
+
+ } while (control.stepTaken(current, currentValue, simplex.get(0),
+ functionExecutor.getValue(simplex.get(0)), step));
+
+ } catch (InterruptedException e) {
+ log.info("Optimization was interrupted with InterruptedException");
+ }
+
+ log.info("Finishing optimization at point " + simplex.get(0) + " value = " +
+ functionExecutor.getValue(simplex.get(0)));
+ }
+
+
+
+ private void createReflection(List base, List reflection) {
+ Point current = base.get(0);
+ reflection.clear();
+ for (int i = 1; i < base.size(); i++) {
+ Point p = current.mul(2).sub(base.get(i));
+ reflection.add(p);
+ }
+ }
+
+ private void createExpansion(List base, List expansion) {
+ Point current = base.get(0);
+ expansion.clear();
+ for (int i = 1; i < base.size(); i++) {
+ Point p = current.mul(3).sub(base.get(i).mul(2));
+ expansion.add(p);
+ }
+ }
+
+ private void halveStep(List base) {
+ Point current = base.get(0);
+ for (int i = 1; i < base.size(); i++) {
+ Point p = base.get(i);
+ p = p.add(current).mul(0.5);
+ base.set(i, p);
+ }
+ }
+
+ private void createCoordinateSearch(Point current, double step, List coordinateDirections) {
+ coordinateDirections.clear();
+ for (int i = 0; i < current.dim(); i++) {
+ Point p = new Point(current.dim());
+ p = p.set(i, step);
+ coordinateDirections.add(current.add(p));
+ coordinateDirections.add(current.sub(p));
+ }
+ }
+
+
+ private boolean accept(List points, double currentValue) {
+ for (Point p : points) {
+ if (functionExecutor.getValue(p) < currentValue) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+
+ @Override
+ public Point getOptimumPoint() {
+ return simplex.get(0);
+ }
+
+ @Override
+ public double getOptimumValue() {
+ return functionExecutor.getValue(getOptimumPoint());
+ }
+
+ @Override
+ public FunctionCache getFunctionCache() {
+ return functionExecutor;
+ }
+
+ @Override
+ public void setFunctionCache(FunctionCache functionCache) {
+ if (!(functionCache instanceof ParallelFunctionCache)) {
+ throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache);
+ }
+ this.functionExecutor = (ParallelFunctionCache) functionCache;
+ }
+
+ @Override
+ public String getStatistics() {
+ return "MultidirectionalSearchOptimizer[stepCount=" + stepCount +
+ ", reflectionAcceptance=" + reflectionAcceptance +
+ ", expansionAcceptance=" + expansionAcceptance +
+ ", coordinateAcceptance=" + coordinateAcceptance +
+ ", reductionFallback=" + reductionFallback;
+ }
+
+ @Override
+ public void resetStatistics() {
+ stepCount = 0;
+ reflectionAcceptance = 0;
+ expansionAcceptance = 0;
+ coordinateAcceptance = 0;
+ reductionFallback = 0;
+ }
+
+}
diff --git a/src/net/sf/openrocket/optimization/MultipleOptimizationController.java b/src/net/sf/openrocket/optimization/MultipleOptimizationController.java
new file mode 100644
index 000000000..50e51b9f7
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/MultipleOptimizationController.java
@@ -0,0 +1,41 @@
+package net.sf.openrocket.optimization;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An OptimizationController that delegates control actions to multiple other controllers.
+ * The optimization is stopped if any of the controllers stops it.
+ *
+ * @author Sampo Niskanen
+ */
+public class MultipleOptimizationController implements OptimizationController {
+
+ private final List controllers = new ArrayList();
+
+ public MultipleOptimizationController(OptimizationController... controllers) {
+ for (OptimizationController c : controllers) {
+ this.controllers.add(c);
+ }
+ }
+
+ public MultipleOptimizationController(Collection controllers) {
+ this.controllers.addAll(controllers);
+ }
+
+
+ @Override
+ public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) {
+ boolean ret = true;
+
+ for (OptimizationController c : controllers) {
+ if (!c.stepTaken(oldPoint, oldValue, newPoint, newValue, stepSize)) {
+ ret = false;
+ }
+ }
+
+ return ret;
+ }
+
+}
diff --git a/src/net/sf/openrocket/optimization/OptimizationController.java b/src/net/sf/openrocket/optimization/OptimizationController.java
new file mode 100644
index 000000000..314624358
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/OptimizationController.java
@@ -0,0 +1,19 @@
+package net.sf.openrocket.optimization;
+
+public interface OptimizationController {
+
+ /**
+ * Control for whether to continue the optimization. This method is called after
+ * every full step taken by the optimization algorithm.
+ *
+ * @param oldPoint the old position.
+ * @param oldValue the value of the function at the old position.
+ * @param newPoint the new position.
+ * @param newValue the value of the function at the new position.
+ * @param stepSize the step length that is used to search for smaller function values (when applicable).
+ * @return true
to continue optimization, false
to stop.
+ */
+ public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue,
+ double stepSize);
+
+}
diff --git a/src/net/sf/openrocket/optimization/ParallelExecutorCache.java b/src/net/sf/openrocket/optimization/ParallelExecutorCache.java
new file mode 100644
index 000000000..d1c4f6b72
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/ParallelExecutorCache.java
@@ -0,0 +1,226 @@
+package net.sf.openrocket.optimization;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class that evaluates function values in parallel and caches them.
+ * This allows pre-calculating possibly required function values beforehand.
+ * If values are not required after all, the computation can be aborted assuming
+ * the function evaluation supports it.
+ *
+ * @author Sampo Niskanen
+ */
+public class ParallelExecutorCache implements ParallelFunctionCache {
+
+ private final Map functionCache = new HashMap();
+ private final Map> futureMap = new HashMap>();
+
+ private ExecutorService executor;
+
+ private Function function;
+
+
+
+ public ParallelExecutorCache() {
+ this(Runtime.getRuntime().availableProcessors());
+ }
+
+ public ParallelExecutorCache(int threadCount) {
+ executor = new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS,
+ new LinkedBlockingQueue(),
+ new ThreadFactory() {
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = new Thread(r);
+ t.setDaemon(true);
+ return t;
+ }
+ });
+ }
+
+ public ParallelExecutorCache(ExecutorService executor) {
+ this.executor = executor;
+ }
+
+
+
+ /**
+ * Queue a list of function evaluations at the specified points.
+ *
+ * @param points the points at which to evaluate the function.
+ */
+ public void compute(Collection points) {
+ for (Point p : points) {
+ compute(p);
+ }
+ }
+
+
+ /**
+ * Queue function evaluation for the specified point.
+ *
+ * @param point the point at which to evaluate the function.
+ */
+ public void compute(Point point) {
+ if (functionCache.containsKey(point)) {
+ // Function has already been evaluated at the point
+ return;
+ }
+
+ if (futureMap.containsKey(point)) {
+ // Function is being evaluated at the point
+ return;
+ }
+
+ double value = function.preComputed(point);
+ if (!Double.isNaN(value)) {
+ // Function value was in function cache
+ functionCache.put(point, value);
+ return;
+ }
+
+ // Submit point for evaluation
+ FunctionCallable callable = new FunctionCallable(function, point);
+ Future future = executor.submit(callable);
+ futureMap.put(point, future);
+ }
+
+
+ /**
+ * Wait for a collection of points to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param points the points to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Collection points) throws InterruptedException {
+ for (Point p : points) {
+ waitFor(p);
+ }
+ }
+
+ /**
+ * Wait for a point to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param point the point to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Point point) throws InterruptedException {
+ if (functionCache.containsKey(point)) {
+ return;
+ }
+
+ Future future = futureMap.get(point);
+ if (future == null) {
+ throw new IllegalStateException("waitFor called for " + point + " but it is not being computed");
+ }
+
+ try {
+ double value = future.get();
+ functionCache.put(point, value);
+ } catch (ExecutionException e) {
+ throw new IllegalStateException("Function threw exception while processing", e.getCause());
+ }
+ }
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param points the points to abort.
+ * @return a list of the points that have been computed anyway
+ */
+ public List abort(Collection points) {
+ List computed = new ArrayList(Math.min(points.size(), 10));
+
+ for (Point p : points) {
+ if (abort(p)) {
+ computed.add(p);
+ }
+ }
+
+ return computed;
+ }
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param point the point to abort.
+ * @return true
if the point has been computed anyway, false
if not.
+ */
+ public boolean abort(Point point) {
+ if (functionCache.containsKey(point)) {
+ return true;
+ }
+
+ Future future = futureMap.remove(point);
+ if (future == null) {
+ throw new IllegalStateException("abort called for " + point + " but it is not being computed");
+ }
+
+ if (future.isDone()) {
+ // Evaluation has been completed, store value in cache
+ try {
+ double value = future.get();
+ functionCache.put(point, value);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ } else {
+ // Cancel the evaluation
+ future.cancel(true);
+ return false;
+ }
+ }
+
+
+ public double getValue(Point point) {
+ Double d = functionCache.get(point);
+ if (d == null) {
+ throw new IllegalStateException(point.toString() + " is not in function cache. " +
+ "functionCache=" + functionCache + " futureMap=" + futureMap);
+ }
+ return d;
+ }
+
+
+
+ @Override
+ public Function getFunction() {
+ return function;
+ }
+
+ @Override
+ public void setFunction(Function function) {
+ this.function = function;
+ clearCache();
+ }
+
+ @Override
+ public void clearCache() {
+ List list = new ArrayList(futureMap.keySet());
+ abort(list);
+ functionCache.clear();
+ }
+
+ public ExecutorService getExecutor() {
+ return executor;
+ }
+
+}
diff --git a/src/net/sf/openrocket/optimization/ParallelFunctionCache.java b/src/net/sf/openrocket/optimization/ParallelFunctionCache.java
new file mode 100644
index 000000000..c035d9e2b
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/ParallelFunctionCache.java
@@ -0,0 +1,59 @@
+package net.sf.openrocket.optimization;
+
+import java.util.Collection;
+import java.util.List;
+
+public interface ParallelFunctionCache extends FunctionCache {
+
+ /**
+ * Queue a list of function evaluations at the specified points.
+ *
+ * @param points the points at which to evaluate the function.
+ */
+ public void compute(Collection points);
+
+ /**
+ * Queue function evaluation for the specified point.
+ *
+ * @param point the point at which to evaluate the function.
+ */
+ public void compute(Point point);
+
+ /**
+ * Wait for a collection of points to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param points the points to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Collection points) throws InterruptedException;
+
+ /**
+ * Wait for a point to be computed. After calling this method
+ * the function values are available by calling XXX
+ *
+ * @param point the point to wait for.
+ * @throws InterruptedException if this thread was interrupted while waiting.
+ */
+ public void waitFor(Point point) throws InterruptedException;
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param points the points to abort.
+ * @return a list of the points that have been computed anyway
+ */
+ public List abort(Collection points);
+
+
+ /**
+ * Abort the computation of the specified point. If computation has ended,
+ * the result is stored in the function cache anyway.
+ *
+ * @param point the point to abort.
+ * @return true
if the point has been computed anyway, false
if not.
+ */
+ public boolean abort(Point point);
+}
diff --git a/src/net/sf/openrocket/optimization/Point.java b/src/net/sf/openrocket/optimization/Point.java
new file mode 100644
index 000000000..41140f5ff
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/Point.java
@@ -0,0 +1,182 @@
+package net.sf.openrocket.optimization;
+
+import java.util.Arrays;
+
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * An immutable n-dimensional coordinate point.
+ *
+ * @author Sampo Niskanen
+ */
+public final class Point {
+
+ private final double[] point;
+ private double length = -1;
+ private double length2 = -1;
+
+
+ public Point(int dim) {
+ if (dim <= 0) {
+ throw new IllegalArgumentException("Invalid dimensionality " + dim);
+ }
+ point = new double[dim];
+ }
+
+ public Point(int dim, double value) {
+ this(dim);
+ Arrays.fill(point, value);
+ }
+
+ public Point(double... value) {
+ if (value.length == 0) {
+ throw new IllegalArgumentException("Zero-dimensional point not allowed");
+ }
+ point = value.clone();
+ }
+
+ private Point(Point p) {
+ point = p.point.clone();
+ }
+
+
+
+ /**
+ * Return the point dimensionality.
+ *
+ * @return the point dimensionality
+ */
+ public int dim() {
+ return point.length;
+ }
+
+
+
+ public double get(int i) {
+ return point[i];
+ }
+
+ public Point set(int i, double v) {
+ Point p = new Point(this);
+ p.point[i] = v;
+ return p;
+ }
+
+
+ /**
+ * Return a new point that is the sum of two points.
+ *
+ * @param other the point to add to this point.
+ * @return the sum of these points.
+ */
+ public Point add(Point other) {
+ Point p = new Point(this);
+ for (int i = 0; i < point.length; i++) {
+ p.point[i] += other.point[i];
+ }
+ return p;
+ }
+
+
+ /**
+ * Return a new point that is the subtraction of two points.
+ *
+ * @param other the point to subtract from this point.
+ * @return the value of this - other.
+ */
+ public Point sub(Point other) {
+ Point p = new Point(this);
+ for (int i = 0; i < point.length; i++) {
+ p.point[i] -= other.point[i];
+ }
+ return p;
+ }
+
+
+ /**
+ * Return this point multiplied by a scalar value.
+ *
+ * @param v the scalar to multiply with
+ * @return the scaled point
+ */
+ public Point mul(double v) {
+ Point p = new Point(this);
+ for (int i = 0; i < point.length; i++) {
+ p.point[i] *= v;
+ }
+ return p;
+ }
+
+
+ /**
+ * Return the length of this coordinate.
+ *
+ * @return the length.
+ */
+ public double length() {
+ if (length < 0) {
+ length = Math.sqrt(length2());
+ }
+ return length;
+ }
+
+
+ /**
+ * Return the squared length of this coordinate.
+ *
+ * @return the square of the length of thie coordinate.
+ */
+ public double length2() {
+ if (length2 < 0) {
+ length2 = 0;
+ for (double p : point) {
+ length2 += p * p;
+ }
+ }
+ return length2;
+ }
+
+
+ /**
+ * Return the point as an array.
+ *
+ * @return the point as an array.
+ */
+ public double[] asArray() {
+ return point.clone();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+
+ if (!(obj instanceof Point))
+ return false;
+
+ Point other = (Point) obj;
+ if (this.point.length != other.point.length)
+ return false;
+
+ for (int i = 0; i < point.length; i++) {
+ if (!MathUtil.equals(this.point[i], other.point[i]))
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int n = 0;
+ for (double d : point) {
+ n *= 37;
+ n += (int) (d * 1000);
+ }
+ return n;
+ }
+
+ @Override
+ public String toString() {
+ return "Point" + Arrays.toString(point);
+ }
+}
diff --git a/src/net/sf/openrocket/optimization/SearchPattern.java b/src/net/sf/openrocket/optimization/SearchPattern.java
new file mode 100644
index 000000000..8082928cd
--- /dev/null
+++ b/src/net/sf/openrocket/optimization/SearchPattern.java
@@ -0,0 +1,89 @@
+package net.sf.openrocket.optimization;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.util.MathUtil;
+
+public class SearchPattern {
+
+ /**
+ * Create a square search pattern with the specified dimensionality.
+ *
+ * @param dimensionality the dimensionality
+ */
+ public static List square(int dimensionality) {
+ List pattern = new ArrayList(dimensionality);
+
+ for (int i = 0; i < dimensionality; i++) {
+ double[] p = new double[dimensionality];
+ p[i] = 1.0;
+ pattern.add(new Point(p));
+ }
+ return pattern;
+ }
+
+
+
+ /**
+ * Create a regular simplex search pattern with the specified dimensionality.
+ *
+ * @param dimensionality the dimensionality
+ */
+ public static List regularSimplex(int dimensionality) {
+ if (dimensionality <= 0) {
+ throw new IllegalArgumentException("Illegal dimensionality " + dimensionality);
+ }
+
+ List pattern = new ArrayList(dimensionality);
+
+ double[] coordinates = new double[dimensionality];
+ double dot = -1.0 / dimensionality;
+
+ /*
+ * First construct an origin-centered regular simplex.
+ * http://en.wikipedia.org/wiki/Simplex#Cartesian_coordinates_for_regular_n-dimensional_simplex_in_Rn
+ */
+
+ for (int i = 0; i < dimensionality; i++) {
+ // Compute the next point coordinate
+ double value = 1;
+
+ for (int j = 0; j < i; j++) {
+ value -= MathUtil.pow2(coordinates[j]);
+ }
+ value = Math.sqrt(value);
+
+ coordinates[i] = value;
+ pattern.add(new Point(coordinates));
+
+ // Compute the i-coordinate for all next points
+ value = dot;
+ for (int j = 0; j < i; j++) {
+ value -= MathUtil.pow2(coordinates[j]);
+ }
+ value = value / coordinates[i];
+
+ coordinates[i] = value;
+ }
+
+ // Minimum point
+ Point min = pattern.get(dimensionality - 1);
+ min = min.set(dimensionality - 1, -min.get(dimensionality - 1));
+
+
+ /*
+ * Shift simplex to have a corner at the origin and scale to unit length.
+ */
+ if (dimensionality > 1) {
+ double scale = 1.0 / (pattern.get(1).sub(pattern.get(0)).length());
+ for (int i = 0; i < dimensionality; i++) {
+ Point p = pattern.get(i);
+ p = p.sub(min).mul(scale);
+ pattern.set(i, p);
+ }
+ }
+
+ return pattern;
+ }
+}
diff --git a/src/net/sf/openrocket/startup/Startup.java b/src/net/sf/openrocket/startup/Startup.java
index 971a5e94c..2d2d492e0 100644
--- a/src/net/sf/openrocket/startup/Startup.java
+++ b/src/net/sf/openrocket/startup/Startup.java
@@ -4,8 +4,6 @@ import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
import java.io.PrintStream;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -20,8 +18,9 @@ import net.sf.openrocket.communication.UpdateInfoRetriever;
import net.sf.openrocket.database.Databases;
import net.sf.openrocket.database.ThrustCurveMotorSet;
import net.sf.openrocket.database.ThrustCurveMotorSetDatabase;
-import net.sf.openrocket.file.DirectoryIterator;
-import net.sf.openrocket.file.GeneralMotorLoader;
+import net.sf.openrocket.file.iterator.DirectoryIterator;
+import net.sf.openrocket.file.iterator.FileIterator;
+import net.sf.openrocket.file.motor.MotorLoaderHelper;
import net.sf.openrocket.gui.dialogs.UpdateInfoDialog;
import net.sf.openrocket.gui.main.BasicFrame;
import net.sf.openrocket.gui.main.ExceptionHandler;
@@ -35,7 +34,6 @@ import net.sf.openrocket.logging.PrintStreamLogger;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.util.GUIUtil;
-import net.sf.openrocket.util.Pair;
import net.sf.openrocket.util.Prefs;
@@ -86,8 +84,8 @@ public class Startup {
log.info("Startup complete");
- // Block motor loading for 2 seconds to allow window painting
- blockLoading.set(2000);
+ // Block motor loading for 1.5 seconds to allow window painting
+ blockLoading.set(1500);
}
@@ -125,7 +123,6 @@ public class Startup {
Prefs.loadDefaultUnits();
// Load motors etc.
- // TODO: HIGH: Use new motor loading
log.info("Loading databases");
loadMotor();
Databases.fakeMethod();
@@ -145,74 +142,70 @@ public class Startup {
private static void loadMotor() {
- log.info("Starting motor loading from " + THRUSTCURVE_DIRECTORY +
- " in background thread.");
+ log.info("Starting motor loading from " + THRUSTCURVE_DIRECTORY + " in background thread.");
ThrustCurveMotorSetDatabase db = new ThrustCurveMotorSetDatabase(true) {
@Override
protected void loadMotors() {
+ // Block loading until timeout occurs or database is taken into use
log.info("Blocking motor loading while starting up");
-
- // Block for 100ms a time until timeout or database in use
while (!inUse && blockLoading.addAndGet(-100) > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
}
-
log.info("Blocking ended, inUse=" + inUse + " slowLoadingCount=" + blockLoading.get());
- log.info("Started to load motors from " + THRUSTCURVE_DIRECTORY);
+ // Start loading
+ log.info("Loading motors from " + THRUSTCURVE_DIRECTORY);
long t0 = System.currentTimeMillis();
+ int fileCount;
+ int thrustCurveCount;
- int fileCount = 0;
- int thrustCurveCount = 0;
- int distinctMotorCount = 0;
- int distinctThrustCurveCount = 0;
-
- GeneralMotorLoader loader = new GeneralMotorLoader();
- DirectoryIterator iterator = DirectoryIterator.findDirectory(THRUSTCURVE_DIRECTORY,
+ // Load the packaged thrust curves
+ List list;
+ FileIterator iterator = DirectoryIterator.findDirectory(THRUSTCURVE_DIRECTORY,
new SimpleFileFilter("", false, "eng", "rse"));
if (iterator == null) {
- throw new IllegalStateException("No thrust curves found, distribution built wrong");
+ throw new IllegalStateException("Thrust curve directory " + THRUSTCURVE_DIRECTORY +
+ "not found, distribution built wrong");
}
- while (iterator.hasNext()) {
- final Pair input = iterator.next();
- log.debug("Loading motors from file " + input.getU());
- fileCount++;
- try {
- List motors = loader.load(input.getV(), input.getU());
- if (motors.size() == 0) {
- log.warn("No motors found in file " + input.getU());
- }
- for (Motor m : motors) {
- thrustCurveCount++;
- this.addMotor((ThrustCurveMotor) m);
- }
- } catch (IOException e) {
- log.warn("IOException when loading motor file " + input.getU(), e);
- } finally {
- try {
- input.getV().close();
- } catch (IOException e) {
- log.error("IOException when closing InputStream", e);
- }
+ list = MotorLoaderHelper.load(iterator);
+ for (Motor m : list) {
+ this.addMotor((ThrustCurveMotor) m);
+ }
+ fileCount = iterator.getFileCount();
+
+ thrustCurveCount = list.size();
+
+ // Load the user-defined thrust curves
+ for (File file : Prefs.getUserThrustCurveFiles()) {
+ // TODO: LOW: This counts a directory as one file
+ log.info("Loading motors from " + file);
+ list = MotorLoaderHelper.load(file);
+ for (Motor m : list) {
+ this.addMotor((ThrustCurveMotor) m);
}
-
+ fileCount++;
+ thrustCurveCount += list.size();
}
long t1 = System.currentTimeMillis();
// Count statistics
+ int distinctMotorCount = 0;
+ int distinctThrustCurveCount = 0;
distinctMotorCount = motorSets.size();
for (ThrustCurveMotorSet set : motorSets) {
distinctThrustCurveCount += set.getMotorCount();
}
log.info("Motor loading done, took " + (t1 - t0) + " ms to load "
- + fileCount + " files containing " + thrustCurveCount + " thrust curves which contained "
- + distinctMotorCount + " distinct motors with " + distinctThrustCurveCount + " thrust curves.");
+ + fileCount + " files/directories containing "
+ + thrustCurveCount + " thrust curves which contained "
+ + distinctMotorCount + " distinct motors with "
+ + distinctThrustCurveCount + " distinct thrust curves.");
}
};
diff --git a/src/net/sf/openrocket/util/MathUtil.java b/src/net/sf/openrocket/util/MathUtil.java
index c120b763a..58585d771 100644
--- a/src/net/sf/openrocket/util/MathUtil.java
+++ b/src/net/sf/openrocket/util/MathUtil.java
@@ -1,15 +1,21 @@
package net.sf.openrocket.util;
-public class MathUtil {
- public static final double EPSILON = 0.00000001; // 10mm^3 in m^3
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+public class MathUtil {
+ public static final double EPSILON = 0.00000001; // 10mm^3 in m^3
+
/**
* The square of x (x^2). On Sun's JRE using this method is as fast as typing x*x.
* @param x x
* @return x^2
*/
public static double pow2(double x) {
- return x*x;
+ return x * x;
}
/**
@@ -18,11 +24,11 @@ public class MathUtil {
* @return x^3
*/
public static double pow3(double x) {
- return x*x*x;
+ return x * x * x;
}
public static double pow4(double x) {
- return (x*x)*(x*x);
+ return (x * x) * (x * x);
}
/**
@@ -73,11 +79,11 @@ public class MathUtil {
if (equals(toMin, toMax))
return toMin;
if (equals(fromMin, fromMax)) {
- throw new IllegalArgumentException("from range is singular and to range is not: "+
+ throw new IllegalArgumentException("from range is singular and to range is not: " +
"value=" + value + " fromMin=" + fromMin + " fromMax=" + fromMax +
"toMin=" + toMin + " toMax=" + toMax);
}
- return (value - fromMin)/(fromMax-fromMin) * (toMax - toMin) + toMin;
+ return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin;
}
@@ -97,14 +103,14 @@ public class MathUtil {
if (toMin.equals(toMax))
return toMin;
if (equals(fromMin, fromMax)) {
- throw new IllegalArgumentException("from range is singular and to range is not: "+
+ throw new IllegalArgumentException("from range is singular and to range is not: " +
"value=" + value + " fromMin=" + fromMin + " fromMax=" + fromMax +
"toMin=" + toMin + " toMax=" + toMax);
}
- double a = (value - fromMin)/(fromMax-fromMin);
- return toMax.multiply(a).add(toMin.multiply(1-a));
+ double a = (value - fromMin) / (fromMax - fromMin);
+ return toMax.multiply(a).add(toMin.multiply(1 - a));
}
-
+
/**
* Compute the minimum of two values. This is performed by direct comparison.
@@ -135,9 +141,9 @@ public class MathUtil {
*/
public static double min(double x, double y, double z) {
if (x < y || Double.isNaN(y)) {
- return min(x,z);
+ return min(x, z);
} else {
- return min(y,z);
+ return min(y, z);
}
}
@@ -148,9 +154,9 @@ public class MathUtil {
*/
public static double max(double x, double y, double z) {
if (x > y || Double.isNaN(y)) {
- return max(x,z);
+ return max(x, z);
} else {
- return max(y,z);
+ return max(y, z);
}
}
@@ -159,19 +165,19 @@ public class MathUtil {
* faster than Math.hypot(x,y)
.
*/
public static double hypot(double x, double y) {
- return Math.sqrt(x*x + y*y);
+ return Math.sqrt(x * x + y * y);
}
-
+
/**
* Reduce the angle x to the range 0 - 2*PI.
* @param x Original angle.
* @return The equivalent angle in the range 0 ... 2*PI.
*/
public static double reduce360(double x) {
- double d = Math.floor(x / (2*Math.PI));
- return x - d*2*Math.PI;
+ double d = Math.floor(x / (2 * Math.PI));
+ return x - d * 2 * Math.PI;
}
-
+
/**
* Reduce the angle x to the range -PI - PI.
*
@@ -181,19 +187,19 @@ public class MathUtil {
* @return The equivalent angle in the range -PI ... PI.
*/
public static double reduce180(double x) {
- double d = Math.rint(x / (2*Math.PI));
- return x - d*2*Math.PI;
+ double d = Math.rint(x / (2 * Math.PI));
+ return x - d * 2 * Math.PI;
}
public static boolean equals(double a, double b) {
double absb = Math.abs(b);
- if (absb < EPSILON/2) {
+ if (absb < EPSILON / 2) {
// Near zero
- return Math.abs(a) < EPSILON/2;
+ return Math.abs(a) < EPSILON / 2;
}
- return Math.abs(a-b) < EPSILON*absb;
+ return Math.abs(a - b) < EPSILON * absb;
}
@@ -207,15 +213,66 @@ public class MathUtil {
* @return -1.0 if x<0; 1.0 if x>0; otherwise either -1.0 or 1.0.
*/
public static double sign(double x) {
- return (x<0) ? -1.0 : 1.0;
+ return (x < 0) ? -1.0 : 1.0;
}
-
+
/* Math.abs() is about 3x as fast as this:
public static double abs(double x) {
return (x<0) ? -x : x;
}
*/
+
+
+ public static double average(Collection extends Number> values) {
+ if (values.isEmpty()) {
+ return Double.NaN;
+ }
+
+ double avg = 0.0;
+ int count = 0;
+ for (Number n : values) {
+ avg += n.doubleValue();
+ count++;
+ }
+ return avg / count;
+ }
+ public static double stddev(Collection extends Number> values) {
+ if (values.size() < 2) {
+ return Double.NaN;
+ }
+
+ double avg = average(values);
+ double stddev = 0.0;
+ int count = 0;
+ for (Number n : values) {
+ stddev += pow2(n.doubleValue() - avg);
+ count++;
+ }
+ stddev = Math.sqrt(stddev / (count - 1));
+ return stddev;
+ }
+
+ public static double median(Collection extends Number> values) {
+ if (values.isEmpty()) {
+ return Double.NaN;
+ }
+
+ List sorted = new ArrayList(values);
+ Collections.sort(sorted, new Comparator() {
+ @Override
+ public int compare(Number o1, Number o2) {
+ return Double.compare(o1.doubleValue(), o2.doubleValue());
+ }
+ });
+
+ int n = sorted.size();
+ if (n % 2 == 0) {
+ return (sorted.get(n / 2).doubleValue() + sorted.get(n / 2 - 1).doubleValue()) / 2;
+ } else {
+ return sorted.get(n / 2).doubleValue();
+ }
+ }
}
diff --git a/src/net/sf/openrocket/util/Prefs.java b/src/net/sf/openrocket/util/Prefs.java
index 53d9ff262..ed7cb2c85 100644
--- a/src/net/sf/openrocket/util/Prefs.java
+++ b/src/net/sf/openrocket/util/Prefs.java
@@ -7,8 +7,10 @@ import java.awt.Toolkit;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
@@ -16,6 +18,7 @@ import java.util.Set;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
+import net.sf.openrocket.arch.SystemInfo;
import net.sf.openrocket.database.Databases;
import net.sf.openrocket.document.Simulation;
import net.sf.openrocket.gui.main.ExceptionHandler;
@@ -39,6 +42,9 @@ import net.sf.openrocket.unit.UnitGroup;
public class Prefs {
private static final LogHelper log = Application.getLogger();
+ private static final String SPLIT_CHARACTER = "|";
+
+
/**
* Whether to use the debug-node instead of the normal node.
*/
@@ -64,6 +70,7 @@ public class Prefs {
*/
private static class BuildPropertyHolder {
+ public static final Properties PROPERTIES;
public static final String BUILD_VERSION;
public static final String BUILD_SOURCE;
public static final boolean DEFAULT_CHECK_UPDATES;
@@ -78,11 +85,11 @@ public class Prefs {
"build.properties", "build.version");
}
- Properties props = new Properties();
- props.load(is);
+ PROPERTIES = new Properties();
+ PROPERTIES.load(is);
is.close();
- String version = props.getProperty("build.version");
+ String version = PROPERTIES.getProperty("build.version");
if (version == null) {
throw new MissingResourceException(
"build.version not found in property file",
@@ -90,14 +97,14 @@ public class Prefs {
}
BUILD_VERSION = version.trim();
- BUILD_SOURCE = props.getProperty("build.source");
+ BUILD_SOURCE = PROPERTIES.getProperty("build.source");
if (BUILD_SOURCE == null) {
throw new MissingResourceException(
"build.source not found in property file",
"build.properties", "build.source");
}
- String value = props.getProperty("build.checkupdates");
+ String value = PROPERTIES.getProperty("build.checkupdates");
if (value != null)
DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
else
@@ -113,7 +120,8 @@ public class Prefs {
public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
-
+ public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
+
public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
// Preferences related to data export
@@ -287,9 +295,13 @@ public class Prefs {
* Set a string preference.
*
* @param key the preference key
- * @param value the value to set
+ * @param value the value to set, or null
to remove the key
*/
public static void putString(String key, String value) {
+ if (value == null) {
+ PREFNODE.remove(key);
+ return;
+ }
PREFNODE.put(key, value);
storeVersion();
}
@@ -361,6 +373,70 @@ public class Prefs {
}
+ /**
+ * Return a list of files/directories to be loaded as custom thrust curves.
+ *
+ * If this property has not been set, the directory "ThrustCurves" in the user
+ * application directory will be used. The directory will be created if it does not
+ * exist.
+ *
+ * @return a list of files to load as thrust curves.
+ */
+ public static List getUserThrustCurveFiles() {
+ List list = new ArrayList();
+
+ String files = getString(USER_THRUST_CURVES_KEY, null);
+ if (files == null) {
+ // Default to application directory
+ File tcdir = getDefaultUserThrustCurveFile();
+ if (!tcdir.isDirectory()) {
+ tcdir.mkdirs();
+ }
+ list.add(tcdir);
+ } else {
+ for (String file : files.split("\\" + SPLIT_CHARACTER)) {
+ file = file.trim();
+ if (file.length() > 0) {
+ list.add(new File(file));
+ }
+ }
+ }
+
+ return list;
+ }
+
+ public static File getDefaultUserThrustCurveFile() {
+ File appdir = SystemInfo.getUserApplicationDirectory();
+ File tcdir = new File(appdir, "ThrustCurves");
+ return tcdir;
+ }
+
+
+ /**
+ * Set the list of files/directories to be loaded as custom thrust curves.
+ *
+ * @param files the files to load, or null
to reset to default value.
+ */
+ public static void setUserThrustCurveFiles(List files) {
+ if (files == null) {
+ putString(USER_THRUST_CURVES_KEY, null);
+ return;
+ }
+
+ String str = "";
+
+ for (File file : files) {
+ if (str.length() > 0) {
+ str += SPLIT_CHARACTER;
+ }
+ str += file.getAbsolutePath();
+ }
+ putString(USER_THRUST_CURVES_KEY, str);
+ }
+
+
+
+
public static Color getDefaultColor(Class extends RocketComponent> c) {
String color = get("componentColors", c, DEFAULT_COLORS);
diff --git a/src/net/sf/openrocket/util/Statistics.java b/src/net/sf/openrocket/util/Statistics.java
new file mode 100644
index 000000000..f96adf195
--- /dev/null
+++ b/src/net/sf/openrocket/util/Statistics.java
@@ -0,0 +1,9 @@
+package net.sf.openrocket.util;
+
+public interface Statistics {
+
+ public String getStatistics();
+
+ public void resetStatistics();
+
+}
diff --git a/src/net/sf/openrocket/util/TestRockets.java b/src/net/sf/openrocket/util/TestRockets.java
index cf6b2787a..1748fcd8b 100644
--- a/src/net/sf/openrocket/util/TestRockets.java
+++ b/src/net/sf/openrocket/util/TestRockets.java
@@ -349,9 +349,9 @@ public class TestRockets {
String id = rocket.newMotorConfigurationID();
bodytube.setMotorMount(true);
- Motor m = Application.getMotorSetDatabase().findMotors(null, null, "F12J", Double.NaN, Double.NaN).get(0);
- bodytube.setMotor(id, m);
- bodytube.setMotorOverhang(0.005);
+ // Motor m = Application.getMotorSetDatabase().findMotors(null, null, "F12J", Double.NaN, Double.NaN).get(0);
+ // bodytube.setMotor(id, m);
+ // bodytube.setMotorOverhang(0.005);
rocket.getDefaultConfiguration().setMotorConfigurationID(id);
rocket.getDefaultConfiguration().setAllStages();
@@ -552,9 +552,9 @@ public class TestRockets {
String id = rocket.newMotorConfigurationID();
tube3.setMotorMount(true);
- Motor m = Application.getMotorSetDatabase().findMotors(null, null, "L540", Double.NaN, Double.NaN).get(0);
- tube3.setMotor(id, m);
- tube3.setMotorOverhang(0.02);
+ // Motor m = Application.getMotorSetDatabase().findMotors(null, null, "L540", Double.NaN, Double.NaN).get(0);
+ // tube3.setMotor(id, m);
+ // tube3.setMotorOverhang(0.02);
rocket.getDefaultConfiguration().setMotorConfigurationID(id);
// tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER);
diff --git a/src/net/sf/openrocket/util/UncloseableInputStream.java b/src/net/sf/openrocket/util/UncloseableInputStream.java
new file mode 100644
index 000000000..ca0779fc8
--- /dev/null
+++ b/src/net/sf/openrocket/util/UncloseableInputStream.java
@@ -0,0 +1,23 @@
+package net.sf.openrocket.util;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An InputStream filter that prevents closing the source stream. The
+ * {@link #close()} method is overridden to do nothing.
+ *
+ * @author Sampo Niskanen
+ */
+public class UncloseableInputStream extends FilterInputStream {
+
+ public UncloseableInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ // No-op
+ }
+}
diff --git a/src/net/sf/openrocket/utils/GraphicalMotorSelector.java b/src/net/sf/openrocket/utils/GraphicalMotorSelector.java
index 44d35eece..1087c5df5 100644
--- a/src/net/sf/openrocket/utils/GraphicalMotorSelector.java
+++ b/src/net/sf/openrocket/utils/GraphicalMotorSelector.java
@@ -8,7 +8,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import net.sf.openrocket.file.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.util.Pair;
diff --git a/src/net/sf/openrocket/utils/MotorCheck.java b/src/net/sf/openrocket/utils/MotorCheck.java
index 50cbeb0ae..5710486d8 100644
--- a/src/net/sf/openrocket/utils/MotorCheck.java
+++ b/src/net/sf/openrocket/utils/MotorCheck.java
@@ -5,8 +5,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.List;
-import net.sf.openrocket.file.GeneralMotorLoader;
-import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.MotorLoader;
import net.sf.openrocket.motor.Manufacturer;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.ThrustCurveMotor;
diff --git a/src/net/sf/openrocket/utils/MotorCompare.java b/src/net/sf/openrocket/utils/MotorCompare.java
index db229bbea..dcb16aebc 100644
--- a/src/net/sf/openrocket/utils/MotorCompare.java
+++ b/src/net/sf/openrocket/utils/MotorCompare.java
@@ -6,8 +6,8 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
-import net.sf.openrocket.file.GeneralMotorLoader;
-import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.MotorLoader;
import net.sf.openrocket.motor.Manufacturer;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.ThrustCurveMotor;
diff --git a/src/net/sf/openrocket/utils/MotorCompareAll.java b/src/net/sf/openrocket/utils/MotorCompareAll.java
index cc7866849..f3a6f8a7b 100644
--- a/src/net/sf/openrocket/utils/MotorCompareAll.java
+++ b/src/net/sf/openrocket/utils/MotorCompareAll.java
@@ -9,8 +9,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import net.sf.openrocket.file.GeneralMotorLoader;
-import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.MotorLoader;
import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.util.Pair;
diff --git a/src/net/sf/openrocket/utils/MotorCorrelation.java b/src/net/sf/openrocket/utils/MotorCorrelation.java
index aab9b171e..3394a59e0 100644
--- a/src/net/sf/openrocket/utils/MotorCorrelation.java
+++ b/src/net/sf/openrocket/utils/MotorCorrelation.java
@@ -6,8 +6,8 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
-import net.sf.openrocket.file.GeneralMotorLoader;
-import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.MotorLoader;
import net.sf.openrocket.logging.LogLevel;
import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
import net.sf.openrocket.motor.Motor;
diff --git a/src/net/sf/openrocket/utils/MotorDigester.java b/src/net/sf/openrocket/utils/MotorDigester.java
index d9e53a9b2..754d4075a 100644
--- a/src/net/sf/openrocket/utils/MotorDigester.java
+++ b/src/net/sf/openrocket/utils/MotorDigester.java
@@ -5,8 +5,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.List;
-import net.sf.openrocket.file.GeneralMotorLoader;
-import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.MotorLoader;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.MotorDigest;
import net.sf.openrocket.motor.ThrustCurveMotor;
diff --git a/src/net/sf/openrocket/utils/MotorPlot.java b/src/net/sf/openrocket/utils/MotorPlot.java
index 6fdeee8f6..1f3eb90cf 100644
--- a/src/net/sf/openrocket/utils/MotorPlot.java
+++ b/src/net/sf/openrocket/utils/MotorPlot.java
@@ -19,7 +19,7 @@ import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
-import net.sf.openrocket.file.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.motor.ThrustCurveMotor;
import net.sf.openrocket.util.GUIUtil;
diff --git a/src/net/sf/openrocket/utils/MotorPrinter.java b/src/net/sf/openrocket/utils/MotorPrinter.java
index 44b78ed41..0ca4e9871 100644
--- a/src/net/sf/openrocket/utils/MotorPrinter.java
+++ b/src/net/sf/openrocket/utils/MotorPrinter.java
@@ -6,9 +6,10 @@ import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
-import net.sf.openrocket.file.GeneralMotorLoader;
-import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.file.motor.GeneralMotorLoader;
+import net.sf.openrocket.file.motor.MotorLoader;
import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.MotorDigest;
import net.sf.openrocket.motor.ThrustCurveMotor;
public class MotorPrinter {
@@ -38,11 +39,16 @@ public class MotorPrinter {
System.out.printf(" Total impulse: %.2f Ns\n", m.getTotalImpulseEstimate());
System.out.println(" Diameter: " + m.getDiameter() * 1000 + " mm");
System.out.println(" Length: " + m.getLength() * 1000 + " mm");
- // System.out.println(" Digest: " + m.getDigestString());
+ System.out.println(" Digest: " + MotorDigest.digestMotor(m));
if (m instanceof ThrustCurveMotor) {
ThrustCurveMotor tc = (ThrustCurveMotor) m;
System.out.println(" Data points: " + tc.getTimePoints().length);
+ for (int i = 0; i < m.getTimePoints().length; i++) {
+ double time = m.getTimePoints()[i];
+ double thrust = m.getThrustPoints()[i];
+ System.out.printf(" t=%.3f F=%.3f\n", time, thrust);
+ }
}
System.out.println(" Comment:");
diff --git a/src/net/sf/openrocket/utils/TestFunctionOptimizer.java b/src/net/sf/openrocket/utils/TestFunctionOptimizer.java
new file mode 100644
index 000000000..4eacec339
--- /dev/null
+++ b/src/net/sf/openrocket/utils/TestFunctionOptimizer.java
@@ -0,0 +1,127 @@
+package net.sf.openrocket.utils;
+
+import net.sf.openrocket.optimization.Function;
+import net.sf.openrocket.optimization.FunctionOptimizer;
+import net.sf.openrocket.optimization.MultidirectionalSearchOptimizer;
+import net.sf.openrocket.optimization.OptimizationController;
+import net.sf.openrocket.optimization.ParallelExecutorCache;
+import net.sf.openrocket.optimization.ParallelFunctionCache;
+import net.sf.openrocket.optimization.Point;
+
+
+
+
+public class TestFunctionOptimizer {
+
+ private static final int LOOP_COUNT = 1000000;
+
+ private volatile int evaluations = 0;
+ private volatile int aborted = 0;
+ private volatile int stepCount = 0;
+
+
+
+ private void go(final ParallelFunctionCache functionCache,
+ final FunctionOptimizer optimizer, final Point optimum, final int maxSteps) {
+
+ Function function = new Function() {
+ @Override
+ public double evaluate(Point p) throws InterruptedException {
+ if (loop(LOOP_COUNT)) {
+ evaluations++;
+ return p.sub(optimum).length2();
+ } else {
+ aborted++;
+ return Double.NaN;
+ }
+ }
+
+ @Override
+ public double preComputed(Point p) {
+ for (double d : p.asArray()) {
+ if (d < 0 || d > 1)
+ return Double.MAX_VALUE;
+ }
+ return Double.NaN;
+ }
+ };
+
+ OptimizationController control = new OptimizationController() {
+
+ @Override
+ public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) {
+ stepCount++;
+ // System.out.println("CSV " + count + ", " + evaluations + ", " + newPoint.sub(optimum).length());
+ // System.out.println("Steps: " + count + " Function evaluations: " + evaluations);
+ // System.out.println("Distance: " + newPoint.sub(optimum).length() + " " + newPoint + " value=" + newValue);
+ return stepCount < maxSteps;
+ }
+ };
+ ;
+
+ functionCache.setFunction(function);
+ optimizer.setFunctionCache(functionCache);
+ optimizer.optimize(new Point(optimum.dim(), 0.5), control);
+ System.err.println("Result: " + optimizer.getOptimumPoint() + " value=" + optimizer.getOptimumValue());
+ System.err.println("Steps: " + stepCount + " Evaluations: " + evaluations);
+ }
+
+
+ public static double counter;
+
+ private static boolean loop(int count) {
+ counter = 1.0;
+ for (int i = 0; i < count; i++) {
+ counter += Math.sin(counter);
+ if (i % 1024 == 0) {
+ if (Thread.interrupted()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+
+ public static void main(String[] args) throws InterruptedException {
+
+ System.err.println("Number of processors: " + Runtime.getRuntime().availableProcessors());
+
+ for (int i = 0; i < 20; i++) {
+ long t0 = System.currentTimeMillis();
+ loop(LOOP_COUNT);
+ long t1 = System.currentTimeMillis();
+ System.err.println("Loop delay at startup: " + (t1 - t0) + "ms");
+ }
+ System.err.println();
+
+ for (int threadCount = 1; threadCount <= 10; threadCount++) {
+
+ System.err.println("THREAD COUNT: " + threadCount);
+ TestFunctionOptimizer test = new TestFunctionOptimizer();
+
+ ParallelExecutorCache executor = new ParallelExecutorCache(threadCount);
+ MultidirectionalSearchOptimizer optimizer = new MultidirectionalSearchOptimizer();
+ long t0 = System.currentTimeMillis();
+ test.go(executor, optimizer, new Point(0.2, 0.3, 0.85), 30);
+ long t1 = System.currentTimeMillis();
+
+ System.err.println("Optimization took " + (t1 - t0) + "ms");
+ System.err.println("" + test.stepCount + " steps, " + test.evaluations +
+ " function evaluations, " + test.aborted + " aborted evaluations");
+ System.err.println("Statistics: " + optimizer.getStatistics());
+
+ executor.getExecutor().shutdownNow();
+ Thread.sleep(1000);
+
+ t0 = System.currentTimeMillis();
+ loop(LOOP_COUNT);
+ t1 = System.currentTimeMillis();
+ System.err.println("Loop delay afterwards: " + (t1 - t0) + "ms");
+ System.err.println();
+ }
+ }
+
+
+
+}
diff --git a/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java b/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java
new file mode 100644
index 000000000..5a81d10f6
--- /dev/null
+++ b/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java
@@ -0,0 +1,109 @@
+package net.sf.openrocket.utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import net.sf.openrocket.optimization.Function;
+import net.sf.openrocket.optimization.FunctionOptimizer;
+import net.sf.openrocket.optimization.MultidirectionalSearchOptimizer;
+import net.sf.openrocket.optimization.OptimizationController;
+import net.sf.openrocket.optimization.ParallelExecutorCache;
+import net.sf.openrocket.optimization.Point;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class TestFunctionOptimizerLoop {
+
+ private static final double PRECISION = 0.01;
+
+ private Point optimum;
+ private int stepCount = 0;
+ private int evaluations = 0;
+
+
+
+ private void go(final FunctionOptimizer optimizer, final Point optimum, final int maxSteps, ExecutorService executor) {
+
+ Function function = new Function() {
+ @Override
+ public double evaluate(Point p) throws InterruptedException {
+ evaluations++;
+ return p.sub(optimum).length2();
+ }
+
+ @Override
+ public double preComputed(Point p) {
+ for (double d : p.asArray()) {
+ if (d < 0 || d > 1)
+ return Double.MAX_VALUE;
+ }
+ return Double.NaN;
+ }
+ };
+
+ OptimizationController control = new OptimizationController() {
+
+ @Override
+ public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) {
+ stepCount++;
+ if (stepCount % 1000 == 0) {
+ System.err.println("WARNING: Over " + stepCount + " steps required for optimum=" + optimum +
+ " position=" + newPoint);
+ }
+ double distance = newPoint.sub(optimum).length();
+ return distance >= PRECISION;
+ }
+ };
+ ;
+
+ ParallelExecutorCache cache = new ParallelExecutorCache(executor);
+ cache.setFunction(function);
+ optimizer.setFunctionCache(cache);
+ optimizer.optimize(new Point(optimum.dim(), 0.5), control);
+ }
+
+
+ public static void main(String[] args) {
+
+ System.err.println("PRECISION = " + PRECISION);
+
+ ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, new ArrayBlockingQueue(100));
+
+ for (int dim = 1; dim <= 10; dim++) {
+
+ List stepCount = new ArrayList();
+ List functionCount = new ArrayList();
+
+ MultidirectionalSearchOptimizer optimizer = new MultidirectionalSearchOptimizer();
+ for (int count = 0; count < 200; count++) {
+ TestFunctionOptimizerLoop test = new TestFunctionOptimizerLoop();
+ double[] point = new double[dim];
+ for (int i = 0; i < dim; i++) {
+ point[i] = Math.random();
+ }
+ // point[0] = 0.7;
+ test.go(optimizer, new Point(point), 20, executor);
+ stepCount.add(test.stepCount);
+ functionCount.add(test.evaluations);
+ }
+
+ // System.err.println("StepCount = " + stepCount);
+
+ System.out.printf("dim=%d Steps avg=%5.2f dev=%5.2f median=%.1f " +
+ "Evaluations avg=%5.2f dev=%5.2f median=%.1f\n",
+ dim, MathUtil.average(stepCount), MathUtil.stddev(stepCount), MathUtil.median(stepCount),
+ MathUtil.average(functionCount), MathUtil.stddev(functionCount), MathUtil.median(functionCount));
+ System.out.println("stat: " + optimizer.getStatistics());
+
+ }
+
+ executor.shutdownNow();
+ }
+
+
+
+}
diff --git a/test/net/sf/openrocket/arch/TestSystemInfo.java b/test/net/sf/openrocket/arch/TestSystemInfo.java
new file mode 100644
index 000000000..80c655846
--- /dev/null
+++ b/test/net/sf/openrocket/arch/TestSystemInfo.java
@@ -0,0 +1,73 @@
+package net.sf.openrocket.arch;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+
+import org.junit.Test;
+
+/*
+ * Note: These tests have not been tested on Windows, they might fail there
+ * due to a different directory separator character.
+ */
+public class TestSystemInfo {
+
+ private String osname;
+ private String userhome;
+
+
+ public void setup() {
+ this.osname = System.getProperty("os.name");
+ this.userhome = System.getProperty("user.home");
+ }
+
+ public void tearDown() {
+ System.setProperty("os.name", this.osname);
+ System.setProperty("user.home", this.userhome);
+ }
+
+ @Test
+ public void testWindows() {
+ setup();
+
+ System.setProperty("os.name", "Windows Me");
+ System.setProperty("user.home", "C:/Users/my user");
+ assertEquals(SystemInfo.Platform.WINDOWS, SystemInfo.getPlatform());
+ if (System.getenv("APPDATA") != null) {
+ assertEquals(new File(System.getenv("APPDATA") + "/OpenRocket/"), SystemInfo.getUserApplicationDirectory());
+ } else {
+ assertEquals(new File("C:/Users/my user/OpenRocket/"), SystemInfo.getUserApplicationDirectory());
+ }
+
+ tearDown();
+ }
+
+ @Test
+ public void testMacOS() {
+ setup();
+
+ System.setProperty("os.name", "Mac OS X");
+ System.setProperty("user.home", "/Users/My User");
+ assertEquals(SystemInfo.Platform.MAC_OS, SystemInfo.getPlatform());
+ assertEquals(new File("/Users/My User/Library/Application Support/OpenRocket/"),
+ SystemInfo.getUserApplicationDirectory());
+
+ tearDown();
+ }
+
+ @Test
+ public void testUnix() {
+ setup();
+
+ System.setProperty("user.home", "/home/myuser");
+ for (String os : new String[] { "Linux", "Solaris", "Foobar" }) {
+ System.setProperty("os.name", os);
+
+ assertEquals(SystemInfo.Platform.UNIX, SystemInfo.getPlatform());
+ assertEquals(new File("/home/myuser/.openrocket"), SystemInfo.getUserApplicationDirectory());
+ }
+
+ tearDown();
+ }
+
+}
diff --git a/test/net/sf/openrocket/file/iterator/TestDirectoryIterator.java b/test/net/sf/openrocket/file/iterator/TestDirectoryIterator.java
new file mode 100644
index 000000000..dce148617
--- /dev/null
+++ b/test/net/sf/openrocket/file/iterator/TestDirectoryIterator.java
@@ -0,0 +1,25 @@
+package net.sf.openrocket.file.iterator;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class TestDirectoryIterator {
+
+ @Test
+ public void testDirectoryIterator() throws IOException {
+ DirectoryIterator iterator = new DirectoryIterator(new File("test/net/sf/openrocket/file"), new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.getName().matches("^Test(Directory|File)Iterator.java");
+ }
+ }, true);
+
+ while (iterator.hasNext()) {
+ System.out.println("" + iterator.next());
+ }
+
+ }
+}
diff --git a/test/net/sf/openrocket/file/iterator/TestFileIterator.java b/test/net/sf/openrocket/file/iterator/TestFileIterator.java
new file mode 100644
index 000000000..c3e424ee0
--- /dev/null
+++ b/test/net/sf/openrocket/file/iterator/TestFileIterator.java
@@ -0,0 +1,41 @@
+package net.sf.openrocket.file.iterator;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import net.sf.openrocket.util.Pair;
+
+import org.junit.Test;
+
+public class TestFileIterator {
+
+ @Test
+ public void testFileIterator() {
+ final Pair one = new Pair("one", new ByteArrayInputStream(new byte[] { 1 }));
+ final Pair two = new Pair("two", new ByteArrayInputStream(new byte[] { 2 }));
+
+ FileIterator iterator = new FileIterator() {
+ private int count = 0;
+
+ @Override
+ protected Pair findNext() {
+ count++;
+ switch (count) {
+ case 1:
+ return one;
+ case 2:
+ return two;
+ default:
+ return null;
+ }
+ }
+ };
+
+ assertTrue(iterator.hasNext());
+ assertEquals(one, iterator.next());
+ assertEquals(two, iterator.next());
+ assertFalse(iterator.hasNext());
+ }
+}
diff --git a/test/net/sf/openrocket/file/motor/TestMotorLoader.java b/test/net/sf/openrocket/file/motor/TestMotorLoader.java
new file mode 100644
index 000000000..8200310a0
--- /dev/null
+++ b/test/net/sf/openrocket/file/motor/TestMotorLoader.java
@@ -0,0 +1,63 @@
+package net.sf.openrocket.file.motor;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+
+import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.MotorDigest;
+import net.sf.openrocket.motor.ThrustCurveMotor;
+
+import org.junit.Test;
+
+public class TestMotorLoader {
+
+ @Test
+ public void testGeneralMotorLoader() throws IOException {
+ MotorLoader loader = new GeneralMotorLoader();
+
+ test(loader, "test1.eng", "c056cf25df6751f7bb8a94bc4f64750f");
+ test(loader, "test2.rse", "b2fe203ee319ae28b9ccdad26a8f21de");
+ test(loader, "test.zip", "b2fe203ee319ae28b9ccdad26a8f21de", "c056cf25df6751f7bb8a94bc4f64750f");
+
+ }
+
+ @Test
+ public void testRASPMotorLoader() throws IOException {
+ test(new RASPMotorLoader(), "test1.eng", "c056cf25df6751f7bb8a94bc4f64750f");
+ }
+
+ @Test
+ public void testRocksimMotorLoader() throws IOException {
+ test(new RockSimMotorLoader(), "test2.rse", "b2fe203ee319ae28b9ccdad26a8f21de");
+ }
+
+ @Test
+ public void testZipMotorLoader() throws IOException {
+ test(new ZipFileMotorLoader(), "test.zip", "b2fe203ee319ae28b9ccdad26a8f21de", "c056cf25df6751f7bb8a94bc4f64750f");
+ }
+
+
+ private void test(MotorLoader loader, String file, String... digests) throws IOException {
+ List motors;
+
+ InputStream is = this.getClass().getResourceAsStream(file);
+ assertNotNull("File " + file + " not found", is);
+ motors = loader.load(is, file);
+ is.close();
+ assertEquals(digests.length, motors.size());
+
+ String[] d = new String[digests.length];
+ for (int i = 0; i < motors.size(); i++) {
+ d[i] = MotorDigest.digestMotor((ThrustCurveMotor) motors.get(i));
+ }
+
+ Arrays.sort(digests);
+ Arrays.sort(d);
+ assertTrue(Arrays.equals(d, digests));
+ }
+
+}
diff --git a/test/net/sf/openrocket/file/motor/test.txt b/test/net/sf/openrocket/file/motor/test.txt
new file mode 100644
index 000000000..4b43be8fa
--- /dev/null
+++ b/test/net/sf/openrocket/file/motor/test.txt
@@ -0,0 +1 @@
+boo bah
diff --git a/test/net/sf/openrocket/file/motor/test.zip b/test/net/sf/openrocket/file/motor/test.zip
new file mode 100644
index 000000000..eeb5ef7c4
Binary files /dev/null and b/test/net/sf/openrocket/file/motor/test.zip differ
diff --git a/test/net/sf/openrocket/file/motor/test1.eng b/test/net/sf/openrocket/file/motor/test1.eng
new file mode 100644
index 000000000..06dc100ea
--- /dev/null
+++ b/test/net/sf/openrocket/file/motor/test1.eng
@@ -0,0 +1,24 @@
+D10 18 70 7 0.009800000000000001 0.0259 AT
+ 0.0070 23.0
+ 0.018 25.0
+ 0.027 20.25
+ 0.066 20.25
+ 0.073 18.5
+ 0.094 20.25
+ 0.112 20.75
+ 0.137 19.75
+ 0.163 21.5
+ 0.202 20.75
+ 0.231 20.75
+ 0.254 22.75
+ 0.27 20.75
+ 0.504 20.0
+ 0.536 18.25
+ 0.607 17.0
+ 0.687 14.75
+ 0.751 14.25
+ 0.84 11.25
+ 0.998 8.25
+ 1.024 8.25
+ 1.248 2.5
+ 1.385 0.0
diff --git a/test/net/sf/openrocket/file/motor/test2.rse b/test/net/sf/openrocket/file/motor/test2.rse
new file mode 100644
index 000000000..a77408f87
--- /dev/null
+++ b/test/net/sf/openrocket/file/motor/test2.rse
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/net/sf/openrocket/optimization/TestSearchPattern.java b/test/net/sf/openrocket/optimization/TestSearchPattern.java
new file mode 100644
index 000000000..7ba59656d
--- /dev/null
+++ b/test/net/sf/openrocket/optimization/TestSearchPattern.java
@@ -0,0 +1,40 @@
+package net.sf.openrocket.optimization;
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.junit.Test;
+
+public class TestSearchPattern {
+
+ @Test
+ public void testRegularSimplex() {
+ for (int dim = 1; dim < 20; dim++) {
+ List points = SearchPattern.regularSimplex(dim);
+ assertEquals(dim, points.size());
+
+ for (int i = 0; i < dim; i++) {
+ // Test dot product
+ for (int j = i + 1; j < dim; j++) {
+ double[] x = points.get(i).asArray();
+ double[] y = points.get(j).asArray();
+ double dot = 0;
+ for (int k = 0; k < dim; k++) {
+ dot += x[k] * y[k];
+ }
+ assertEquals(0.5, dot, 0.000000001);
+ }
+
+ // Test positive coordinates
+ for (int j = 0; j < dim; j++) {
+ assertTrue(points.get(i).get(j) >= 0);
+ }
+
+ // Test length
+ assertEquals(1.0, points.get(i).length(), 0.000000001);
+ }
+ }
+ }
+
+}
diff --git a/test/net/sf/openrocket/util/MathUtilTest.java b/test/net/sf/openrocket/util/MathUtilTest.java
index df1524271..a794a0907 100644
--- a/test/net/sf/openrocket/util/MathUtilTest.java
+++ b/test/net/sf/openrocket/util/MathUtilTest.java
@@ -4,22 +4,25 @@ import static java.lang.Double.NaN;
import static java.lang.Math.PI;
import static org.junit.Assert.*;
+import java.util.ArrayList;
+import java.util.List;
+
import org.junit.Test;
public class MathUtilTest {
public static final double EPS = 0.00000000001;
-
+
@Test
public void miscMathTest() {
- assertEquals(PI*PI, MathUtil.pow2(PI), EPS);
- assertEquals(PI*PI*PI, MathUtil.pow3(PI), EPS);
- assertEquals(PI*PI*PI*PI, MathUtil.pow4(PI), EPS);
+ assertEquals(PI * PI, MathUtil.pow2(PI), EPS);
+ assertEquals(PI * PI * PI, MathUtil.pow3(PI), EPS);
+ assertEquals(PI * PI * PI * PI, MathUtil.pow4(PI), EPS);
assertEquals(1.0, MathUtil.clamp(0.9999, 1.0, 2.0), 0);
assertEquals(1.23, MathUtil.clamp(1.23, 1.0, 2.0), 0);
- assertEquals(2.0, MathUtil.clamp(2 + EPS/100, 1.0, 2.0), 0);
+ assertEquals(2.0, MathUtil.clamp(2 + EPS / 100, 1.0, 2.0), 0);
assertEquals(1.0f, MathUtil.clamp(0.9999f, 1.0f, 2.0f), 0);
assertEquals(1.23f, MathUtil.clamp(1.23f, 1.0f, 2.0f), 0);
@@ -32,17 +35,17 @@ public class MathUtilTest {
assertEquals(-1.0, MathUtil.sign(Double.NEGATIVE_INFINITY), EPS);
assertEquals(-1.0, MathUtil.sign(-100), EPS);
assertEquals(-1.0, MathUtil.sign(Math.nextAfter(0.0, -1.0)), EPS);
- assertEquals( 1.0, MathUtil.sign(Math.nextUp(0.0)), EPS);
- assertEquals( 1.0, MathUtil.sign(100), EPS);
- assertEquals( 1.0, MathUtil.sign(Double.POSITIVE_INFINITY), EPS);
+ assertEquals(1.0, MathUtil.sign(Math.nextUp(0.0)), EPS);
+ assertEquals(1.0, MathUtil.sign(100), EPS);
+ assertEquals(1.0, MathUtil.sign(Double.POSITIVE_INFINITY), EPS);
}
@Test
public void hypotTest() {
- for (int i=0; i<10000; i++) {
- double x = Math.random()*100 - 50;
- double y = Math.random()*i - i/2;
+ for (int i = 0; i < 10000; i++) {
+ double x = Math.random() * 100 - 50;
+ double y = Math.random() * i - i / 2;
double z = Math.hypot(x, y);
assertEquals(z, MathUtil.hypot(x, y), EPS);
}
@@ -52,15 +55,15 @@ public class MathUtilTest {
@Test
public void reduceTest() {
- for (int i=-1000; i<1000; i++) {
- double angle = Math.random() * 2*PI;
- double shift = angle + i*2*PI;
+ for (int i = -1000; i < 1000; i++) {
+ double angle = Math.random() * 2 * PI;
+ double shift = angle + i * 2 * PI;
assertEquals(angle, MathUtil.reduce360(shift), EPS);
}
- for (int i=-1000; i<1000; i++) {
- double angle = Math.random() * 2*PI - PI;
- double shift = angle + i*2*PI;
+ for (int i = -1000; i < 1000; i++) {
+ double angle = Math.random() * 2 * PI - PI;
+ double shift = angle + i * 2 * PI;
assertEquals(angle, MathUtil.reduce180(shift), EPS);
}
@@ -73,7 +76,7 @@ public class MathUtilTest {
assertEquals(1.0, MathUtil.min(NaN, 1.0), 0);
assertEquals(1.0, MathUtil.min(1.0, NaN), 0);
assertEquals(NaN, MathUtil.min(NaN, NaN), 0);
-
+
assertEquals(Math.nextUp(1.0), MathUtil.max(1.0, Math.nextUp(1.0)), 0);
assertEquals(Double.POSITIVE_INFINITY, MathUtil.max(1.0, Double.POSITIVE_INFINITY), 0);
assertEquals(1.0, MathUtil.max(NaN, 1.0), 0);
@@ -107,44 +110,45 @@ public class MathUtilTest {
try {
MathUtil.map(6.0, 1.0, Math.nextUp(1.0), 1.0, 2.0);
fail("Should not be reached.");
- } catch (IllegalArgumentException normal) { }
-
+ } catch (IllegalArgumentException normal) {
+ }
+
assertEquals(7.0, MathUtil.map(Math.nextUp(1.0), 0.0, 5.0, 9.0, -1.0), EPS);
}
@Test
public void mapCoordinateTest() {
- assertEquals(new Coordinate(0.8, 2.0, 1.6, 4.0),
+ assertEquals(new Coordinate(0.8, 2.0, 1.6, 4.0),
MathUtil.map(1.0, 0.0, 5.0, new Coordinate(0, 1, 2, 3), new Coordinate(4, 6, 0, 8)));
}
@Test
public void equalsTest() {
- assertTrue(MathUtil.equals(1.0, 1.0 + MathUtil.EPSILON/3));
- assertFalse(MathUtil.equals(1.0, 1.0 + MathUtil.EPSILON*2));
- assertTrue(MathUtil.equals(-1.0, -1.0 + MathUtil.EPSILON/3));
- assertFalse(MathUtil.equals(-1.0, -1.0 + MathUtil.EPSILON*2));
+ assertTrue(MathUtil.equals(1.0, 1.0 + MathUtil.EPSILON / 3));
+ assertFalse(MathUtil.equals(1.0, 1.0 + MathUtil.EPSILON * 2));
+ assertTrue(MathUtil.equals(-1.0, -1.0 + MathUtil.EPSILON / 3));
+ assertFalse(MathUtil.equals(-1.0, -1.0 + MathUtil.EPSILON * 2));
- for (double zero: new double[] { 0.0, MathUtil.EPSILON/10, -MathUtil.EPSILON/10 }) {
-
- assertTrue(MathUtil.equals(zero, MathUtil.EPSILON/3));
- assertTrue(MathUtil.equals(zero, -MathUtil.EPSILON/3));
- assertFalse(MathUtil.equals(zero, MathUtil.EPSILON*2));
- assertFalse(MathUtil.equals(zero, -MathUtil.EPSILON*2));
-
- assertTrue(MathUtil.equals(MathUtil.EPSILON/3, zero));
- assertTrue(MathUtil.equals(-MathUtil.EPSILON/3, zero));
- assertFalse(MathUtil.equals(MathUtil.EPSILON*2, zero));
- assertFalse(MathUtil.equals(-MathUtil.EPSILON*2, zero));
-
+ for (double zero : new double[] { 0.0, MathUtil.EPSILON / 10, -MathUtil.EPSILON / 10 }) {
+
+ assertTrue(MathUtil.equals(zero, MathUtil.EPSILON / 3));
+ assertTrue(MathUtil.equals(zero, -MathUtil.EPSILON / 3));
+ assertFalse(MathUtil.equals(zero, MathUtil.EPSILON * 2));
+ assertFalse(MathUtil.equals(zero, -MathUtil.EPSILON * 2));
+
+ assertTrue(MathUtil.equals(MathUtil.EPSILON / 3, zero));
+ assertTrue(MathUtil.equals(-MathUtil.EPSILON / 3, zero));
+ assertFalse(MathUtil.equals(MathUtil.EPSILON * 2, zero));
+ assertFalse(MathUtil.equals(-MathUtil.EPSILON * 2, zero));
+
}
- for (double value: new double[] { PI*1e20, -PI*1e20 }) {
+ for (double value : new double[] { PI * 1e20, -PI * 1e20 }) {
assertTrue("value=" + value, MathUtil.equals(value, value + 1));
assertTrue("value=" + value, MathUtil.equals(value, Math.nextUp(value)));
- assertTrue("value=" + value, MathUtil.equals(value, value * (1+MathUtil.EPSILON)));
+ assertTrue("value=" + value, MathUtil.equals(value, value * (1 + MathUtil.EPSILON)));
}
assertFalse(MathUtil.equals(NaN, 0.0));
@@ -152,4 +156,54 @@ public class MathUtilTest {
assertFalse(MathUtil.equals(NaN, NaN));
}
+ @Test
+ public void testAverageStddev() {
+ List ints = new ArrayList();
+ List doubles = new ArrayList();
+
+ ints.add(3);
+ ints.add(4);
+ ints.add(7);
+ ints.add(5);
+
+ doubles.add(3.4);
+ doubles.add(2.9);
+ doubles.add(7.5);
+ doubles.add(5.43);
+ doubles.add(2.8);
+ doubles.add(6.6);
+
+ assertEquals(4.75, MathUtil.average(ints), EPS);
+ assertEquals(1.707825127659933, MathUtil.stddev(ints), EPS);
+ assertEquals(4.771666666666667, MathUtil.average(doubles), EPS);
+ assertEquals(2.024454659078999, MathUtil.stddev(doubles), EPS);
+ }
+
+ @Test
+ public void testMedian() {
+ List ints = new ArrayList();
+ List doubles = new ArrayList();
+
+ ints.add(3);
+ ints.add(4);
+ ints.add(7);
+ ints.add(5);
+
+ doubles.add(3.4);
+ doubles.add(2.9);
+ doubles.add(7.5);
+ doubles.add(5.43);
+ doubles.add(2.8);
+ doubles.add(6.6);
+
+ assertEquals(4.5, MathUtil.median(ints), EPS);
+ assertEquals(4.415, MathUtil.median(doubles), EPS);
+
+ ints.add(9);
+ doubles.add(10.0);
+
+ assertEquals(5, MathUtil.median(ints), EPS);
+ assertEquals(5.43, MathUtil.median(doubles), EPS);
+ }
+
}
diff --git a/web/html/actions/updates.php b/web/html/actions/updates.php
index 94c5049f1..5a8d9994a 100644
--- a/web/html/actions/updates.php
+++ b/web/html/actions/updates.php
@@ -21,6 +21,7 @@ $orversion = "";
$oros = "";
$orjava = "";
$orcountry = "";
+$orcores = "";
foreach (getallheaders() as $header => $value) {
if (preg_match("/^[a-zA-Z0-9 !$%&()*+,.\\/:=?@_~-]{1,40}$/", $value)) {
$h = strtolower($header);
@@ -34,20 +35,23 @@ foreach (getallheaders() as $header => $value) {
$orjava = $value;
} else if ($h == 'x-openrocket-country') {
$orcountry = $value;
+ } else if ($h == 'x-openrocket-cpus') {
+ $orcores = $value;
}
}
}
// Log the request
if ((strlen($orversion) > 0 || strlen($orid) > 0 || strlen($oros) > 0
- || strlen($orjava) > 0 || strlen($orcountry) > 0) &&
-
+ || strlen($orjava) > 0 || strlen($orcountry) > 0
+ || strlen($orcores) > 0) &&
(strlen($orversion) < 20 && strlen($orid) < 50 && strlen($oros) < 50
- && strlen($orjava) < 50 && strlen($orcountry) < 50)) {
+ && strlen($orjava) < 50 && strlen($orcountry) < 50)
+ && strlen($orcores) < 10) {
$file = $logfiles . gmdate("Y-m");
$line = gmdate("Y-m-d H:i:s") . ";" . $orid . ";" . $orversion .
- ";" . $oros . ";" . $orjava . ";" . $orcountry . "\n";
+ ";" . $oros . ";" . $orjava . ";" . $orcountry . ";" . $orcores . "\n";
$fp = fopen($file, 'a');
if ($fp != FALSE) {
@@ -65,15 +69,23 @@ header("Content-type: text/plain");
/*
* Currently all old versions are handled manually.
* Update checking was introduced in OpenRocket 0.9.4
+ *
+ * We ignore "pre" versions, they are handled exacly like
+ * their non-pre counterparts.
*/
$version = $_GET["version"];
$updates = "";
-if (preg_match("/^0\.9\.6/",$version)) {
+if (preg_match("/^1\.1\.0/", $version)) {
+ $updates = "Version: 1.1.1\n" .
+ "6: Enhanced motor selection\n" .
+ "5: Rewritten simulation code" .
+ "4: Bug fixes";
+} else if (preg_match("/^0\.9\.6/", $version)) {
$updates = "Version: 1.0.0\n" .
"6: Hundreds of new thrustcurves\n" .
"5: Bug fixes";
-} else if (preg_match("/^0\.9\.(4|5pre|5|6pre)/",$version)) {
+} else if (preg_match("/^0\.9\.[45]/", $version)) {
$updates = "Version: 1.0.0\n" .
"7: Hundreds of new thrustcurves\n" .
"6: Aerodynamic computation updates\n" .