<classpathentry kind="src" path="src-extra"/>
<classpathentry kind="src" path="test"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
- <classpathentry kind="lib" path="/home/sampo/Projects/OpenRocket/lib/miglayout15-swing.jar"/>
- <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/JCommon 1.0.16"/>
- <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/JFreeChart 1.0.13"/>
<classpathentry kind="lib" path="lib-extra/RXTXcomm.jar"/>
<classpathentry kind="lib" path="lib-test/junit-4.7.jar"/>
+ <classpathentry kind="lib" path="lib/jfreechart-1.0.13.jar"/>
+ <classpathentry kind="lib" path="lib/jcommon-1.0.16.jar"/>
+ <classpathentry kind="lib" path="lib/miglayout15-swing.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
+2009-11-24 Sampo Niskanen
+
+ * Released version 0.9.4
+
+2009-11-24 Sampo Niskanen
+
+ * Close original window when opening example design
+
+2009-11-10 Sampo Niskanen
+
+ * [BUG] Fixed transition volume/mass computation
+ * [BUG] Simulations etc. using removed motor configuration IDs
+
+2009-10-11 Sampo Niskanen
+
+ * [BUG] Sorting motor selection dialog with ',' decimal separator
+
2009-10-10 Sampo Niskanen
* Removed non-ASCII characters from source code files
+OpenRocket 0.9.4 (2009-11-24):
+-------------------------------
+
+Added through-the-wall fin tabs, attaching components to tube
+couplers, material editing and automatic update checks, and fixed
+numerous of the most commonly occurring bugs.
+
+
OpenRocket 0.9.3 (2009-09-01):
-------------------------------
Must-have:
- Go through thrust curves and correct errors
-- Add styrofoam and depron materials
+or
+- Hide duplicate motors
Bugs:
-- Unit tests fail from ant script
Maybe:
-- Inform user about software updates
+- Re-investigate 15% reduction of three-fin CNa
+- Take into account all fins in interference effects
+- Add slight randomness to yaw moment
Postponed:
+- Integration with thrustcurve.org (syncing?)
+- Reading thrust curves from external directory
+- Plot motor thrust curve
+
+
- Windows executable wrapper (launch4j)
- Allow only one instance of OpenRocket running (RMI communication)
- Only schedule rocket figure update instead of each time updating it
- Simulate other branches
- Implement setDefaults() method for RocketComponent
- BUG: Inner tube cluster rotation, edit with spinner arrows, slider wrong
-- Reading thrust curves from external directory
- NAR/CNES/etc competition validity checking
- Running from command line
- Print support
- Allow editing user-defined materials
- [BUG] All configuration dialogs too high
- Simulation plot dialog forces dialog one button row too high (All/None)
+- Add styrofoam and depron materials
+- Inform user about software updates
# The OpenRocket build version
-build.version=0.9.4pre
+build.version=0.9.4
# The source of the package. When building a package for a specific
; Sachsen Feuerwerk / WECO Feuerwerk A8-3
-; Created by Sampo Niskanen
-; Data taken from:
+; Created by Sampo Niskanen for OpenRocket, released into the Public Domain
+; Thrust curve data taken from:
; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+; Mass measured by the author.
A8 18 70 3 0.00312 0.0153 SF
0.065 0.44
0.11 1.832
; Sachsen Feuerwerk / WECO Feuerwerk B4-0, B4-4
-; Created by Sampo Niskanen
-; Data taken from:
+; Created by Sampo Niskanen for OpenRocket, released into the Public Domain
+; Thrust curve data taken from:
; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+; Mass measured by the author from B4-4 motors.
B4 18 70 0-4 0.00833 0.0195 SF
0.088 0.542
0.167 3.007
; Sachsen Feuerwerk / WECO Feuerwerk Held 1000
-; Created by Sampo Niskanen
-; True propellant weight unknown
-; Data taken from:
+; Created by Sampo Niskanen for OpenRocket, released into the Public Domain
+; Thrust curve data taken from:
; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+; True propellant weight unknown, estimates from various sources
C2 15 95 P 0.012 0.024 SF
0.075 3.543
0.16 8.231
; Sachsen Feuerwerk / WECO Feuerwerk C6-0, C6-3, C6-5
-; Created by Sampo Niskanen
-; Data taken from:
+; Created by Sampo Niskanen for OpenRocket, released into the Public Domain
+; Thrust curve data taken from:
; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+; Mass measured by the author from C6-3 motors.
C6 18 70 0-3-5 0.01248 0.022 SF
0.096 0.579
0.152 2.441
; Sachsen Feuerwerk / WECO Feuerwerk D7-0, D7-3
-; Created by Sampo Niskanen
-; Data taken from:
+; Created by Sampo Niskanen for OpenRocket, released into the Public Domain
+; Thrust curve data taken from:
; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+; Mass measured by the author from D7-3 motors.
D7 25 70 0-3 0.019 0.043 SF
0.079 1.625
0.179 6.979
Redirect 307 /actions/reportbug http://sampo.kapsi.fi/openrocket/reportbug.php
+RewriteEngine On
+RewriteBase /actions/
+RewriteRule ^updates$ updates.php
+
if (preg_match("/^[a-zA-Z0-9. -]{1,30}$/", $version) &&
strlen($content) > 0) {
- if (mail($mailaddr, "Automatic bug report for OpenRocket " . $version,
- $content . $headers,
+ $subject = date("Y-m-d H:i:s") . " Automatic bug report for OpenRocket " . $version;
+ if (mail($mailaddr, $subject, $content . $headers,
"From: Automatic Bug Reports <".$mailaddr.">\r\n".
"Content-Type: text/plain; charset=utf-8")) {
--- /dev/null
+<?
+$logfiles = "/home/groups/o/op/openrocket/persistent/logs/access-";
+
+
+// getallheaders method
+if (!function_exists('getallheaders')) {
+ function getallheaders() {
+ foreach ($_SERVER as $name => $value) {
+ if (substr($name, 0, 5) == 'HTTP_') {
+ $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
+ }
+ }
+ return $headers;
+ }
+}
+
+
+// Parse + validate headers
+$orid = "";
+$orversion = "";
+$oros = "";
+$orjava = "";
+$orcountry = "";
+foreach (getallheaders() as $header => $value) {
+ if (preg_match("/^[a-zA-Z0-9 !$%&()*+,.\\/:=?@_~-]{1,40}$/", $value)) {
+ $h = strtolower($header);
+ if ($h == 'x-openrocket-version') {
+ $orversion = $value;
+ } else if ($h == 'x-openrocket-id') {
+ $orid = $value;
+ } else if ($h == 'x-openrocket-os') {
+ $oros = $value;
+ } else if ($h == 'x-openrocket-java') {
+ $orjava = $value;
+ } else if ($h == 'x-openrocket-country') {
+ $orcountry = $value;
+ }
+ }
+}
+
+// Log the request
+if (strlen($orversion) > 0 || strlen($orid) > 0 || strlen($oros) > 0
+ || strlen($orjava) > 0 || strlen($orcountry) > 0) {
+
+ $file = $logfiles . gmdate("Y-m");
+ $line = gmdate("Y-m-d H:i:s") . ";" . $orid . ";" . $orversion .
+ ";" . $oros . ";" . $orjava . ";" . $orcountry . "\n";
+
+ $fp = fopen($file, 'a');
+ if ($fp != FALSE) {
+ fwrite($fp, $line);
+ fclose($fp);
+ }
+}
+
+
+// Set HTTP content-type header
+header("Content-type: text/plain; charset=utf-8");
+
+$version = $_GET["version"];
+
+// No updates available
+header("HTTP/1.0 202 No Content");
+
+?>
\ No newline at end of file
BULK_MATERIAL.add(new Material.Bulk("Polystyrene", 1050, false));
BULK_MATERIAL.add(new Material.Bulk("PVC", 1390, false));
BULK_MATERIAL.add(new Material.Bulk("Spruce", 450, false));
- BULK_MATERIAL.add(new Material.Bulk("Styrofoam generic (EPS)", 20, false));
- BULK_MATERIAL.add(new Material.Bulk("Styrofoam / Blue Foam (XPS)", 32, false));
+ BULK_MATERIAL.add(new Material.Bulk("Styrofoam (generic EPS)", 20, false));
+// BULK_MATERIAL.add(new Material.Bulk("Styrofoam (Blue foam, XPS)", 32, false));
+ BULK_MATERIAL.add(new Material.Bulk("Styrofoam \"Blue foam\" (XPS)", 32, false));
BULK_MATERIAL.add(new Material.Bulk("Quantum tubing",1050, false));
SURFACE_MATERIAL.add(new Material.Surface("Ripstop nylon", 0.067, false));
import net.sf.openrocket.motor.Manufacturer;
import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.MotorDigest;
import net.sf.openrocket.motor.ThrustCurveMotor;
+import net.sf.openrocket.motor.MotorDigest.DataType;
import net.sf.openrocket.util.Coordinate;
public class RASPMotorLoader extends MotorLoader {
designation = removeDelay(designation);
+ // Create the motor digest from data available in RASP files
+ MotorDigest motorDigest = new MotorDigest();
+ motorDigest.update(DataType.TIME_ARRAY, timeArray);
+ motorDigest.update(DataType.MASS_SPECIFIC, totalW, totalW-propW);
+ motorDigest.update(DataType.FORCE_PER_TIME, thrustArray);
+ final String digest = motorDigest.getDigest();
+
+
try {
return new ThrustCurveMotor(Manufacturer.getManufacturer(manufacturer),
designation, comment, Motor.Type.UNKNOWN,
- delays, diameter, length, timeArray, thrustArray, cgArray);
+ delays, diameter, length, timeArray, thrustArray, cgArray, digest);
} catch (IllegalArgumentException e) {
import net.sf.openrocket.file.simplesax.SimpleSAX;
import net.sf.openrocket.motor.Manufacturer;
import net.sf.openrocket.motor.Motor;
+import net.sf.openrocket.motor.MotorDigest;
import net.sf.openrocket.motor.ThrustCurveMotor;
+import net.sf.openrocket.motor.MotorDigest.DataType;
import net.sf.openrocket.util.Coordinate;
import org.xml.sax.InputSource;
finalizeThrustCurve(time, force, mass, cg);
final int n = time.size();
+ if (hasIllegalValue(mass))
+ calculateMass = true;
+ if (hasIllegalValue(cg))
+ calculateCG = true;
+
if (calculateMass) {
mass = calculateMass(time, force, initMass, propMass);
}
}
}
- double[] timeArray = new double[n];
- double[] thrustArray = new double[n];
+ double[] timeArray = toArray(time);
+ double[] thrustArray = toArray(force);
Coordinate[] cgArray = new Coordinate[n];
for (int i=0; i < n; i++) {
- timeArray[i] = time.get(i);
- thrustArray[i] = force.get(i);
cgArray[i] = new Coordinate(cg.get(i),0,0,mass.get(i));
}
+
+ // Create the motor digest from all data available in the file
+ MotorDigest motorDigest = new MotorDigest();
+ motorDigest.update(DataType.TIME_ARRAY, timeArray);
+ if (!calculateMass) {
+ motorDigest.update(DataType.MASS_PER_TIME, toArray(mass));
+ } else {
+ motorDigest.update(DataType.MASS_SPECIFIC, initMass, initMass-propMass);
+ }
+ if (!calculateCG) {
+ motorDigest.update(DataType.CG_PER_TIME, toArray(cg));
+ }
+ motorDigest.update(DataType.FORCE_PER_TIME, thrustArray);
+ final String digest = motorDigest.getDigest();
+
+
try {
return new ThrustCurveMotor(Manufacturer.getManufacturer(manufacturer),
designation, description, type,
- delays, diameter, length, timeArray, thrustArray, cgArray);
+ delays, diameter, length, timeArray, thrustArray, cgArray, digest);
} catch (IllegalArgumentException e) {
throw new SAXException("Illegal motor data", e);
}
}
}
}
+
+
+
+ private static boolean hasIllegalValue(List<Double> list) {
+ for (Double d: list) {
+ if (d == null || d.isNaN() || d.isInfinite()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static double[] toArray(List<Double> list) {
+ final int n = list.size();
+ double[] array = new double[n];
+ for (int i=0; i < n; i++) {
+ array[i] = list.get(i);
+ }
+ return array;
+ }
}
}
// Implement a wrapper to the ChangeListeners
- ArrayList<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
+ ArrayList<PropertyChangeListener> propertyChangeListeners =
+ new ArrayList<PropertyChangeListener>();
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
- listeners.add(listener);
+ propertyChangeListeners.add(listener);
DoubleModel.this.addChangeListener(this);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
- listeners.remove(listener);
- if (listeners.isEmpty())
+ propertyChangeListeners.remove(listener);
+ if (propertyChangeListeners.isEmpty())
DoubleModel.this.removeChangeListener(this);
}
// If the value has changed, generate an event to the listeners
PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
oldValue,newValue);
oldValue = newValue;
- Object[] l = listeners.toArray();
+ Object[] l = propertyChangeListeners.toArray();
for (int i=0; i<l.length; i++) {
((PropertyChangeListener)l[i]).propertyChange(event);
}
} catch (IllegalAccessException e) {
throw new RuntimeException("BUG: Unable to invoke setMethod of "+this, e);
} catch (InvocationTargetException e) {
- throw new RuntimeException("BUG: Unable to invoke setMethod of "+this, e);
+ throw new RuntimeException("Setter method of "+this+" threw exception", e);
}
}
}
if (!(item instanceof Enum<?>)) {
- throw new IllegalArgumentException("Not String or Enum");
+ throw new IllegalArgumentException("Not String or Enum, item="+item);
}
// Comparison with == ok, since both are enums
if (!(o instanceof String))
return;
+ if (rowIndex < 0 || rowIndex >= finset.getFinPoints().length ||
+ columnIndex < 0 || columnIndex >= Columns.values().length) {
+ throw new IllegalArgumentException("Index out of bounds, row="+rowIndex+
+ " column="+columnIndex+" fin point count="+finset.getFinPoints().length);
+ }
+
String str = (String)o;
try {
} catch (IllegalFinPointException ignore) {
}
}
-
-
}
}
JPanel tab;
- tab = positionTab();
- tabbedPane.insertTab("Radial position", null, tab, "Radial position", 1);
-
tab = new MotorConfig((MotorMount)c);
- tabbedPane.insertTab("Motor", null, tab, "Motor mount configuration", 2);
+ tabbedPane.insertTab("Motor", null, tab, "Motor mount configuration", 1);
tab = clusterTab();
- tabbedPane.insertTab("Cluster", null, tab, "Cluster configuration", 3);
+ tabbedPane.insertTab("Cluster", null, tab, "Cluster configuration", 2);
+
+ tab = positionTab();
+ tabbedPane.insertTab("Radial position", null, tab, "Radial position", 3);
tabbedPane.setSelectedIndex(0);
}
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.communication.BugReporter;
-import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.components.SelectableLabel;
+import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.util.GUIUtil;
import net.sf.openrocket.util.JarUtil;
import net.sf.openrocket.util.Prefs;
BugReportDialog reportDialog =
new BugReportDialog(parent,
- "<html>You can report a bug in OpenRocket by filling in and submitting " +
- "the form below.<br>" +
+ "<html><b>You can report a bug in OpenRocket by filling in and submitting " +
+ "the form below.</b><br>" +
"You can also report bugs and include attachments on the project " +
"web site.", sb.toString());
reportDialog.setVisible(true);
sb.append('\n');
BugReportDialog reportDialog =
- new BugReportDialog(parent, "Please include a short description about " +
- "what you were doing when the exception occurred.", sb.toString());
+ new BugReportDialog(parent, "<html><b>Please include a short description about " +
+ "what you were doing when the exception occurred.</b>", sb.toString());
reportDialog.setVisible(true);
}
import java.text.Collator;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.Locale;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.motor.Motor;
import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.unit.Value;
+import net.sf.openrocket.unit.ValueComparator;
import net.sf.openrocket.util.GUIUtil;
import net.sf.openrocket.util.Prefs;
public String getValue(Motor m) {
return m.getManufacturer().getDisplayName();
}
-// @Override
-// public String getToolTipText(Motor m) {
-// return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
-// }
@Override
public Comparator<?> getComparator() {
return Collator.getInstance();
public String getValue(Motor m) {
return m.getDesignation();
}
-// @Override
-// public String getToolTipText(Motor m) {
-// return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
-// }
@Override
public Comparator<?> getComparator() {
return Motor.getDesignationComparator();
public String getValue(Motor m) {
return m.getMotorType().getName();
}
-// @Override
-// public String getToolTipText(Motor m) {
-// return m.getMotorType().getDescription();
-// }
@Override
public Comparator<?> getComparator() {
return Collator.getInstance();
},
DIAMETER("Diameter") {
@Override
- public String getValue(Motor m) {
- return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
- m.getDiameter());
+ public Object getValue(Motor m) {
+ return new Value(m.getDiameter(), UnitGroup.UNITS_MOTOR_DIMENSIONS);
}
@Override
public Comparator<?> getComparator() {
- return getNumericalComparator();
+ return ValueComparator.INSTANCE;
}
},
LENGTH("Length") {
@Override
- public String getValue(Motor m) {
- return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
- m.getLength());
+ public Object getValue(Motor m) {
+ return new Value(m.getLength(), UnitGroup.UNITS_MOTOR_DIMENSIONS);
}
@Override
public Comparator<?> getComparator() {
- return getNumericalComparator();
+ return ValueComparator.INSTANCE;
}
},
IMPULSE("Impulse") {
@Override
- public String getValue(Motor m) {
- return UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(
- m.getTotalImpulse());
+ public Object getValue(Motor m) {
+ return new Value(m.getTotalImpulse(), UnitGroup.UNITS_IMPULSE);
}
@Override
public Comparator<?> getComparator() {
- return getNumericalComparator();
+ return ValueComparator.INSTANCE;
}
},
TIME("Burn time") {
@Override
- public String getValue(Motor m) {
- return UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit(
- m.getAverageTime());
+ public Object getValue(Motor m) {
+ return new Value(m.getAverageTime(), UnitGroup.UNITS_SHORT_TIME);
}
@Override
public Comparator<?> getComparator() {
- return getNumericalComparator();
+ return ValueComparator.INSTANCE;
}
};
}
- public abstract String getValue(Motor m);
+ public abstract Object getValue(Motor m);
public abstract Comparator<?> getComparator();
public String getTitle() {
public boolean filterByString(Motor m) {
main: for (String s : searchTerms) {
for (MotorColumns col : MotorColumns.values()) {
- String str = col.getValue(m).toLowerCase();
+ String str = col.getValue(m).toString().toLowerCase();
if (str.indexOf(s) >= 0)
continue main;
}
}
}
-
- private static Comparator<String> numericalComparator = null;
- private static Comparator<String> getNumericalComparator() {
- if (numericalComparator == null)
- numericalComparator = new NumericalComparator();
- return numericalComparator;
- }
-
- private static class NumericalComparator implements Comparator<String> {
- private Pattern pattern =
- Pattern.compile("^\\s*([0-9]*[.,][0-9]+|[0-9]+[.,]?[0-9]*).*?$");
- private Collator collator = null;
- @Override
- public int compare(String s1, String s2) {
- Matcher m1, m2;
-
- m1 = pattern.matcher(s1);
- m2 = pattern.matcher(s2);
- if (m1.find() && m2.find()) {
- double d1 = Double.parseDouble(m1.group(1));
- double d2 = Double.parseDouble(m2.group(1));
-
- return (int)((d1-d2)*1000);
- }
-
- if (collator == null)
- collator = Collator.getInstance(Locale.US);
- return collator.compare(s1, s2);
- }
- }
-
}
"Delete", "Confirm", true)), "wrap 40lp, growx, sg combos");
- final JCheckBox softwareUpdateBox = new JCheckBox("Check for software updates");
+ final JCheckBox softwareUpdateBox = new JCheckBox("Check for software updates at startup");
softwareUpdateBox.setSelected(Prefs.getCheckUpdates());
softwareUpdateBox.addActionListener(new ActionListener() {
@Override
- private static boolean open(URL url, Window parent) {
+ private static boolean open(URL url, BasicFrame parent) {
String filename = null;
// Try using URI.getPath();
try {
InputStream is = url.openStream();
- open(is, filename, parent);
+ if (open(is, filename, parent)) {
+ // Close previous window if replacing
+ if (parent.replaceable && parent.document.isSaved()) {
+ parent.closeAction();
+ parent.replaceable = false;
+ }
+ }
} catch (IOException e) {
JOptionPane.showMessageDialog(parent,
"An error occurred while opening the file " + filename,
// Normal exception, show question dialog
String msg = e.getClass().getSimpleName() + ": " + e.getMessage();
if (msg.length() > 90) {
- msg = msg.substring(0, 90) + "...";
+ msg = msg.substring(0, 80) + "...";
}
private final String designation;
private final String description;
private final Type motorType;
+ private final String digest;
private final double[] delays;
* @param length length of the motor
*/
protected Motor(Manufacturer manufacturer, String designation, String description,
- Type type, double[] delays, double diameter, double length) {
+ Type type, double[] delays, double diameter, double length, String digest) {
if (manufacturer == null || designation == null || description == null ||
type == null || delays == null) {
this.delays = delays.clone();
this.diameter = diameter;
this.length = length;
+ this.digest = digest;
}
}
+ /**
+ * Return a digest string of this motor. This digest should be computed from all
+ * flight-affecting data. For example for thrust curve motors the thrust curve
+ * should be digested using suitable precision. The intention is that the combination
+ * of motor type, manufacturer, designation, diameter, length and digest uniquely
+ * identify any particular motor data file.
+ *
+ * @return a string digest of this motor (0-60 chars)
+ */
+ public String getDigestString() {
+ return digest;
+ }
+
+
/**
* Compares two <code>Motor</code> objects. The motors are considered equal
* if they have identical manufacturers, designations and types, near-identical
--- /dev/null
+package net.sf.openrocket.motor;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import net.sf.openrocket.util.TextUtil;
+
+public class MotorDigest {
+
+ private static final double EPSILON = 0.00000000001;
+
+ public enum DataType {
+ /** An array of time points at which data is available (in ms) */
+ TIME_ARRAY(0, 1000),
+ /** Mass data for a few specific points (normally initial and empty mass) (in 0.1g) */
+ MASS_SPECIFIC(1, 10000),
+ /** Mass per time (in 0.1g) */
+ MASS_PER_TIME(2, 10000),
+ /** CG position for a few specific points (normally initial and final CG) (in mm) */
+ CG_SPECIFIC(3, 1000),
+ /** CG position per time (in mm) */
+ CG_PER_TIME(4, 1000),
+ /** Thrust force per time (in mN) */
+ FORCE_PER_TIME(5, 1000);
+
+ private final int order;
+ private final int multiplier;
+ DataType(int order, int multiplier) {
+ this.order = order;
+ this.multiplier = multiplier;
+ }
+ public int getOrder() {
+ return order;
+ }
+ public int getMultiplier() {
+ return multiplier;
+ }
+ }
+
+
+ private final MessageDigest digest;
+ private boolean used = false;
+ private int lastOrder = -1;
+
+
+ public MotorDigest() {
+ try {
+ digest = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("MD5 digest not supported by JRE", e);
+ }
+ }
+
+
+ public void update(DataType type, int ... values) {
+
+ // Check for correct order
+ if (lastOrder >= type.getOrder()) {
+ throw new IllegalArgumentException("Called with type="+type+" order="+type.getOrder()+
+ " while lastOrder=" + lastOrder);
+ }
+ lastOrder = type.getOrder();
+
+ // Digest the type
+ digest.update(bytes(type.getOrder()));
+
+ // Digest the data length
+ digest.update(bytes(values.length));
+
+ // Digest the values
+ for (int v: values) {
+ digest.update(bytes(v));
+ }
+
+ }
+
+
+ private void update(DataType type, int multiplier, double ... values) {
+
+ int[] intValues = new int[values.length];
+ for (int i=0; i<values.length; i++) {
+ double v = values[i];
+ v = next(v);
+ v *= multiplier;
+ v = next(v);
+ intValues[i] = (int) Math.round(v);
+ }
+ update(type, intValues);
+ }
+
+ public void update(DataType type, double ... values) {
+ update(type, type.getMultiplier(), values);
+ }
+
+ private static double next(double v) {
+ return v + Math.signum(v) * EPSILON;
+ }
+
+
+ public String getDigest() {
+ if (used) {
+ throw new IllegalStateException("MotorDigest already used");
+ }
+ used = true;
+ byte[] result = digest.digest();
+ return TextUtil.hexString(result);
+ }
+
+
+
+ private byte[] bytes(int value) {
+ return new byte[] {
+ (byte) ((value>>>24) & 0xFF), (byte) ((value>>>16) & 0xFF),
+ (byte) ((value>>>8) & 0xFF), (byte) (value & 0xFF)
+ };
+ }
+
+
+
+
+ public static String digestComment(String comment) {
+ comment = comment.replaceAll("\\s+", " ").trim();
+
+ MessageDigest digest;
+ try {
+ digest = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("MD5 digest not supported by JRE", e);
+ }
+
+ try {
+ digest.update(comment.getBytes("UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new IllegalStateException("UTF-8 encoding not supported by JRE", e);
+ }
+
+ return TextUtil.hexString(digest.digest());
+ }
+
+}
*/
public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description,
Motor.Type type, double[] delays, double diameter, double length,
- double[] time, double[] thrust, Coordinate[] cg) {
- super(manufacturer, designation, description, type, delays, diameter, length);
+ double[] time, double[] thrust, Coordinate[] cg, String digest) {
+ super(manufacturer, designation, description, type, delays, diameter, length, digest);
double max = -1;
public Coordinate[] getCGPoints() {
return cg.clone();
}
-
+
}
@Override
public Motor getMotor(String id) {
+ if (id == null)
+ return null;
+
+ // Check whether the id is valid for the current rocket
+ RocketComponent root = this.getRoot();
+ if (!(root instanceof Rocket))
+ return null;
+ if (!((Rocket) root).isMotorConfigurationID(id))
+ return null;
+
return motors.get(id);
}
@Override
public void setMotor(String id, Motor motor) {
+ if (id == null) {
+ if (motor != null) {
+ throw new IllegalArgumentException("Cannot set non-null motor for id null");
+ }
+ }
Motor current = motors.get(id);
if ((motor == null && current == null) ||
(motor != null && motor.equals(current)))
/**
* Remove the fin point with the given index. The first and last fin points
- * cannot be removed, and will cause an <code>IllegalArgumentException</code>
+ * cannot be removed, and will cause an <code>IllegalFinPointException</code>
* if attempted.
*
* @param index the fin point index to remove
* <p>
* Note that this method enforces basic fin shape restrictions (non-negative y,
* first and last point locations) silently, but throws an
- * <code>IllegalArgumentException</code> if the point causes fin segments to
- * intersect. The calling method should always catch this exception.
+ * <code>IllegalFinPointException</code> if the point causes fin segments to
+ * intersect.
* <p>
* Moving of the first point in the X-axis is allowed, but this actually moves
* all of the other points the corresponding distance back.
@Override
public Motor getMotor(String id) {
+ if (id == null)
+ return null;
+
+ // Check whether the id is valid for the current rocket
+ RocketComponent root = this.getRoot();
+ if (!(root instanceof Rocket))
+ return null;
+ if (!((Rocket) root).isMotorConfigurationID(id))
+ return null;
+
return motors.get(id);
}
@Override
public void setMotor(String id, Motor motor) {
+ if (id == null) {
+ if (motor != null) {
+ throw new IllegalArgumentException("Cannot set non-null motor for id null");
+ }
+ }
Motor current = motors.get(id);
if ((motor == null && current == null) ||
(motor != null && motor.equals(current)))
fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
}
+ @Deprecated
@Override
public int getMotorCount() {
return getClusterCount();
/**
* Return the motor for the motor configuration. May return <code>null</code>
* if no motor has been set. This method must return <code>null</code> if ID
- * is <code>null</code>.
+ * is <code>null</code> or if the ID is not valid for the current rocket
+ * (or if the component is not part of any rocket).
*
* @param id the motor configuration ID
* @return the motor, or <code>null</code> if not set.
/**
* Get the number of similar motors clustered.
*
+ * TODO: HIGH: This should not be used, since the components themselves can be clustered
+ *
* @return the number of motors.
*/
+ @Deprecated
public int getMotorCount();
* @return whether any motors are defined for it.
*/
public boolean hasMotors(String id) {
+ if (id == null)
+ return false;
+
Iterator<RocketComponent> iterator = this.deepIterator();
while (iterator.hasNext()) {
RocketComponent c = iterator.next();
* @return the configuration name
*/
public String getMotorConfigurationName(String id) {
+ if (!isMotorConfigurationID(id))
+ return "";
String s = motorConfigurationNames.get(id);
if (s == null)
return "";
fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
}
-
+
/**
* Return either the motor configuration name (if set) or its description.
*
public String getMotorConfigurationNameOrDescription(String id) {
String name;
- name = motorConfigurationNames.get(id);
+ name = getMotorConfigurationName(id);
if (name != null && !name.equals(""))
return name;
-
+
return getMotorConfigurationDescription(id);
}
String name;
int motorCount = 0;
- if (!motorConfigurationIDs.contains(id)) {
- throw new IllegalArgumentException("Motor configuration ID does not exist: "+id);
- }
-
// Generate the description
// First iterate over each stage and store the designations of each motor
} else {
// Hollow piece
final double height = thickness*hyp/l;
- dV = pil*height*(r1+r2-height);
+ dV = MathUtil.max(pil*height*(r1+r2-height), 0);
}
// Add to the volume-related components
if (planArea > 0)
planCenter /= planArea;
- if (volume == 0) {
- cg = Coordinate.NUL;
+ if (volume < 0.0000000001) { // 0.1 mm^3
+ volume = 0;
+ cg = new Coordinate(length/2, 0, 0, 0);
} else {
// getComponentMass is safe now
- cg = new Coordinate(cgx/volume,0,0,getComponentMass());
+ // Use super.getComponentMass() to ensure only the transition shape mass
+ // is used, not the shoulders
+ cg = new Coordinate(cgx/volume,0,0,super.getComponentMass());
}
}
}
public void setType(Shape type) {
+ if (type == null) {
+ throw new IllegalArgumentException("BUG: setType called with null argument");
+ }
if (this.type == type)
return;
this.type = type;
--- /dev/null
+package net.sf.openrocket.unit;
+
+public class FrequencyUnit extends GeneralUnit {
+
+
+ public FrequencyUnit(double multiplier, String unit) {
+ super(multiplier, unit);
+ }
+
+
+
+ @Override
+ public double toUnit(double value) {
+ double hz = 1/value;
+ return hz / multiplier;
+ }
+
+
+ @Override
+ public double fromUnit(double value) {
+ double hz = value * multiplier;
+ return 1/hz;
+ }
+
+}
public static final UnitGroup UNITS_COEFFICIENT;
+// public static final UnitGroup UNITS_FREQUENCY;
+
public static final Map<String, UnitGroup> UNITS;
UNITS_COEFFICIENT = new UnitGroup();
UNITS_COEFFICIENT.addUnit(new FixedPrecisionUnit(""+ZWSP, 0.01)); // zero-width space
+
+ // This is not used by OpenRocket, and not extensively tested:
+// UNITS_FREQUENCY = new UnitGroup();
+// UNITS_FREQUENCY.addUnit(new GeneralUnit(1, "s"));
+// UNITS_FREQUENCY.addUnit(new GeneralUnit(0.001, "ms"));
+// UNITS_FREQUENCY.addUnit(new GeneralUnit(0.000001, MICRO + "s"));
+// UNITS_FREQUENCY.addUnit(new FrequencyUnit(1, "Hz"));
+// UNITS_FREQUENCY.addUnit(new FrequencyUnit(1000, "kHz"));
+// UNITS_FREQUENCY.setDefaultUnit(3);
+
HashMap<String,UnitGroup> map = new HashMap<String,UnitGroup>();
map.put("NONE", UNITS_NONE);
this.value = value;
this.unit = unit;
}
+
+
+ /**
+ * Creates a new Value object using unit group. Currently it simply uses the default
+ * unit of the group, but may later change.
+ *
+ * @param value the value to set.
+ * @param group the group the value belongs to.
+ */
+ public Value(double value, UnitGroup group) {
+ this(value, group.getDefaultUnit());
+ }
/**
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.util.Comparator;
+
+public class ValueComparator implements Comparator<Value> {
+
+ public static final ValueComparator INSTANCE = new ValueComparator();
+
+ @Override
+ public int compare(Value o1, Value o2) {
+ return o1.compareTo(o2);
+ }
+
+}
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
};
private static final char PAD = '=';
+
+// private static final byte[] REVERSE;
+// static {
+// REVERSE = new byte[128];
+// Arrays.fill(REVERSE, (byte)-1);
+// for (int i=0; i<64; i++) {
+// REVERSE[ALPHABET[i]] = (byte)i;
+// }
+// REVERSE['-'] = 62;
+// REVERSE['_'] = 63;
+// REVERSE[PAD] = 0;
+// }
private static final Map<Character,Integer> REVERSE = new HashMap<Character,Integer>();
static {
public static String getUniqueID() {
String id = PREFNODE.get("id", null);
if (id == null) {
- id = UniqueID.generateHashedID();
+ id = UniqueID.uuid();
PREFNODE.put("id", id);
}
return id;
public class TextUtil {
+ private static final char[] HEX = {
+ '0','1','2','3','4','5','6','7',
+ '8','9','a','b','c','d','e','f'
+ };
+
+
+ /**
+ * Return the bytes formatted as a hexadecimal string. The length of the
+ * string will be twice the number of bytes, with no spacing between the bytes
+ * and lowercase letters utilized.
+ *
+ * @param bytes the bytes to convert.
+ * @return the bytes in hexadecimal notation.
+ */
+ public static final String hexString(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (byte b: bytes) {
+ sb.append(HEX[(b >>> 4) & 0xF]);
+ sb.append(HEX[b & 0xF]);
+ }
+ return sb.toString();
+ }
/**
* Return a string of the double value with suitable precision (5 digits).
package net.sf.openrocket.util;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
-import net.sf.openrocket.gui.main.ExceptionHandler;
-
public class UniqueID {
private static AtomicInteger nextId = new AtomicInteger(1);
return UUID.randomUUID().toString();
}
-
- /**
- * Return a hashed unique ID that contains no information whatsoever of the
- * originating computer.
- *
- * @return a unique identifier string that contains no information about the computer.
- */
- public static String generateHashedID() {
- String id = UUID.randomUUID().toString();
-
- try {
- MessageDigest algorithm = MessageDigest.getInstance("MD5");
- algorithm.reset();
- algorithm.update(id.getBytes());
- byte[] digest = algorithm.digest();
-
- StringBuilder sb = new StringBuilder();
- for (byte b: digest) {
- sb.append(String.format("%02X", 0xFF & b));
- }
- id = sb.toString();
-
- } catch (NoSuchAlgorithmException e) {
- ExceptionHandler.handleErrorCondition(e);
- id = "" + id.hashCode();
- }
-
- return id;
- }
-
}
System.out.printf(" Total impulse: %.2f Ns\n", m.getTotalImpulse());
System.out.println(" Diameter: " + m.getDiameter()*1000 + " mm");
System.out.println(" Length: " + m.getLength()*1000 + " mm");
+ System.out.println(" Digest: " + m.getDigestString());
if (m instanceof ThrustCurveMotor) {
ThrustCurveMotor tc = (ThrustCurveMotor)m;
--- /dev/null
+package net.sf.openrocket.motor;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import net.sf.openrocket.motor.MotorDigest.DataType;
+import net.sf.openrocket.util.TextUtil;
+
+import org.junit.Test;
+
+
+public class MotorDigestTest {
+
+ private static final double[] timeArray = {
+ 0.0, 0.123456789, 0.4115, Math.nextAfter(Math.nextAfter(1.4445, 0), 0)
+ };
+
+ private static final double[] massArray = {
+ 0.54321, 0.43211
+ };
+
+ private static final double[] thrustArray = {
+ 0.0, 0.2345678, 9999.3335, 0.0
+ };
+
+ private static final int[] intData = {
+ // Time (ms)
+ 0, 4, 0, 123, 412, 1445,
+ // Mass specific (0.1g)
+ 1, 2, 5432, 4321,
+ // Thrust (mN)
+ 5, 4, 0, 235, 9999334, 0
+ };
+
+
+ @Test
+ public void testMotorDigest() throws NoSuchAlgorithmException {
+
+ MessageDigest correct = MessageDigest.getInstance("MD5");
+ for (int value: intData) {
+ correct.update((byte) ((value >>> 24) & 0xFF));
+ correct.update((byte) ((value >>> 16) & 0xFF));
+ correct.update((byte) ((value >>> 8) & 0xFF));
+ correct.update((byte) (value & 0xFF));
+ }
+
+ MotorDigest motor = new MotorDigest();
+ motor.update(DataType.TIME_ARRAY, timeArray);
+ motor.update(DataType.MASS_SPECIFIC, massArray);
+ motor.update(DataType.FORCE_PER_TIME, thrustArray);
+
+
+ assertEquals(TextUtil.hexString(correct.digest()), motor.getDigest());
+ }
+
+
+ @Test
+ public void testCommentDigest() throws NoSuchAlgorithmException, UnsupportedEncodingException {
+
+ assertEquals(md5("Hello world!"), MotorDigest.digestComment("Hello world! "));
+ assertEquals(md5("Hello world!"), MotorDigest.digestComment("\nHello\tworld!\n\r"));
+ assertEquals(md5("Hello world!"), MotorDigest.digestComment("Hello\r\r\r\nworld!"));
+ assertEquals(md5("Hello\u00e4 world!"), MotorDigest.digestComment("Hello\u00e4\r\r\nworld!"));
+
+ }
+
+
+ private static String md5(String source)
+ throws NoSuchAlgorithmException, UnsupportedEncodingException {
+ MessageDigest digest = MessageDigest.getInstance("MD5");
+ return TextUtil.hexString(digest.digest(source.getBytes("UTF-8")));
+ }
+}
import static java.lang.Math.PI;
import static org.junit.Assert.assertEquals;
+import java.util.Random;
+
import org.junit.Test;
public class TextUtilTest {
+
+ @Test
+ public void textHexString() {
+ assertEquals("", TextUtil.hexString(new byte[0]));
+ assertEquals("00", TextUtil.hexString(new byte[] { 0x00 }));
+ assertEquals("ff", TextUtil.hexString(new byte[] { (byte) 0xff }));
+
+ for (int i=0; i <= 0xff; i++) {
+ assertEquals(String.format("%02x", i), TextUtil.hexString(new byte[] { (byte) i }));
+ }
+
+ assertEquals("0f1e2d3c4b5a6978", TextUtil.hexString(new byte[] {
+ 0x0f, 0x1e, 0x2d, 0x3c, 0x4b, 0x5a, 0x69, 0x78
+ }));
+
+ Random rnd = new Random();
+ for (int count=0; count<10; count++) {
+ int n = rnd.nextInt(100);
+ byte[] bytes = new byte[n];
+ rnd.nextBytes(bytes);
+ StringBuilder sb = new StringBuilder();
+ for (byte b: bytes) {
+ sb.append(String.format("%02x", b & 0xFF));
+ }
+ assertEquals(sb.toString(), TextUtil.hexString(bytes));
+ }
+ }
@Test
public void specialCaseTest() {
assertNotSame(id, UniqueID.uuid());
}
- @Test
- public void hashedTest() {
- String id = UniqueID.generateHashedID();
- assertNotNull(id);
-
- boolean matchhigh = false;
- boolean matchlow = false;
- for (int i=0; i<100; i++) {
- String newid = UniqueID.generateHashedID();
- assertNotNull(newid);
- assertNotSame(id, newid);
- assertTrue(newid.matches("^[0-9a-fA-F]{32}$"));
-
- // Check that both high and low values occur
- matchhigh = matchhigh || newid.matches("^([0-9a-fA-F][0-9a-fA-F])*[A-F].*");
- matchlow = matchlow || newid.matches("^([0-9a-fA-F][0-9a-fA-F])*[0-4].*");
- }
- assertTrue(matchhigh);
- assertTrue(matchlow);
- }
-
}