updates for 0.9.4

This commit is contained in:
Sampo Niskanen 2009-11-24 19:56:40 +00:00
parent 4dff922640
commit fd4fa42e8a
49 changed files with 659 additions and 191 deletions

View File

@ -4,10 +4,10 @@
<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>

View File

@ -1,3 +1,20 @@
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

View File

@ -1,4 +1,12 @@
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):
-------------------------------

16
TODO
View File

@ -5,21 +5,28 @@ Feature roadmap for OpenRocket 1.0
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
@ -30,7 +37,6 @@ Postponed:
- 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
@ -79,4 +85,6 @@ In 0.9.4:
- 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

View File

@ -1,7 +1,7 @@
# 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

View File

@ -1,7 +1,8 @@
; 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

View File

@ -1,7 +1,8 @@
; 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

View File

@ -1,8 +1,8 @@
; 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

View File

@ -1,7 +1,8 @@
; 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

View File

@ -1,7 +1,8 @@
; 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

Binary file not shown.

BIN
dists/OpenRocket-0.9.4.jar Normal file

Binary file not shown.

View File

@ -8,3 +8,7 @@
Redirect 307 /actions/reportbug http://sampo.kapsi.fi/openrocket/reportbug.php
RewriteEngine On
RewriteBase /actions/
RewriteRule ^updates$ updates.php

View File

@ -31,8 +31,8 @@ header("Content-type: text/plain; charset=utf-8");
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")) {

65
html/actions/updates.php Normal file
View File

@ -0,0 +1,65 @@
<?
$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");
?>

View File

@ -90,8 +90,9 @@ public class Databases {
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));

View File

@ -10,7 +10,9 @@ import java.util.List;
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 {
@ -195,11 +197,19 @@ 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) {

View File

@ -14,7 +14,9 @@ import net.sf.openrocket.file.simplesax.PlainTextHandler;
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;
@ -321,6 +323,11 @@ public class RockSimMotorLoader extends MotorLoader {
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);
}
@ -330,19 +337,33 @@ public class RockSimMotorLoader extends MotorLoader {
}
}
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);
}
@ -417,4 +438,24 @@ public class RockSimMotorLoader extends MotorLoader {
}
}
}
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;
}
}

View File

@ -379,16 +379,17 @@ public class DoubleModel implements ChangeListener, ChangeSource {
}
// 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
@ -399,7 +400,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
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);
}
@ -619,7 +620,7 @@ public class DoubleModel implements ChangeListener, ChangeSource {
} 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);
}
}

View File

@ -83,7 +83,7 @@ public class EnumModel<T extends Enum<T>> extends AbstractListModel
}
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

View File

@ -450,6 +450,12 @@ public class FreeformFinSetConfig extends FinSetConfig {
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 {
@ -466,7 +472,5 @@ public class FreeformFinSetConfig extends FinSetConfig {
} catch (IllegalFinPointException ignore) {
}
}
}
}

View File

@ -50,14 +50,14 @@ public class InnerTubeConfig extends ThicknessRingComponentConfig {
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);
}

View File

@ -26,8 +26,8 @@ import javax.swing.JTextArea;
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;
@ -179,8 +179,8 @@ public class BugReportDialog extends JDialog {
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);
@ -239,8 +239,8 @@ public class BugReportDialog extends JDialog {
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);
}

View File

@ -12,9 +12,6 @@ import java.awt.event.MouseEvent;
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;
@ -40,6 +37,8 @@ import net.sf.openrocket.database.Databases;
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;
@ -383,10 +382,6 @@ public class MotorChooserDialog extends JDialog {
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();
@ -397,10 +392,6 @@ public class MotorChooserDialog extends JDialog {
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();
@ -411,10 +402,6 @@ public class MotorChooserDialog extends JDialog {
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();
@ -422,46 +409,42 @@ public class MotorChooserDialog extends JDialog {
},
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;
}
};
@ -479,7 +462,7 @@ public class MotorChooserDialog extends JDialog {
}
public abstract String getValue(Motor m);
public abstract Object getValue(Motor m);
public abstract Comparator<?> getComparator();
public String getTitle() {
@ -623,7 +606,7 @@ public class MotorChooserDialog extends JDialog {
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;
}
@ -664,35 +647,4 @@ public class MotorChooserDialog extends JDialog {
}
}
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);
}
}
}

View File

@ -88,7 +88,7 @@ public class PreferencesDialog extends JDialog {
"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

View File

@ -752,7 +752,7 @@ public class BasicFrame extends JFrame {
private static boolean open(URL url, Window parent) {
private static boolean open(URL url, BasicFrame parent) {
String filename = null;
// Try using URI.getPath();
@ -780,7 +780,13 @@ public class BasicFrame extends JFrame {
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,

View File

@ -163,7 +163,7 @@ public class ExceptionHandler implements Thread.UncaughtExceptionHandler {
// 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) + "...";
}

View File

@ -98,6 +98,7 @@ public abstract class Motor implements Comparable<Motor> {
private final String designation;
private final String description;
private final Type motorType;
private final String digest;
private final double[] delays;
@ -126,7 +127,7 @@ public abstract class Motor implements Comparable<Motor> {
* @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) {
@ -140,6 +141,7 @@ public abstract class Motor implements Comparable<Motor> {
this.delays = delays.clone();
this.diameter = diameter;
this.length = length;
this.digest = digest;
}
@ -435,6 +437,20 @@ public abstract class Motor implements Comparable<Motor> {
}
/**
* 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

View File

@ -0,0 +1,141 @@
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());
}
}

View File

@ -35,8 +35,8 @@ public class ThrustCurveMotor extends Motor {
*/
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;
@ -157,5 +157,5 @@ public class ThrustCurveMotor extends Motor {
public Coordinate[] getCGPoints() {
return cg.clone();
}
}

View File

@ -290,11 +290,26 @@ public class BodyTube extends SymmetricComponent implements MotorMount {
@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)))

View File

@ -124,7 +124,7 @@ public class FreeformFinSet extends FinSet {
/**
* 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
@ -173,8 +173,8 @@ public class FreeformFinSet extends FinSet {
* <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.

View File

@ -201,11 +201,26 @@ implements Clusterable, RadialParent, MotorMount {
@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)))
@ -228,6 +243,7 @@ implements Clusterable, RadialParent, MotorMount {
fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
}
@Deprecated
@Override
public int getMotorCount() {
return getClusterCount();

View File

@ -88,7 +88,8 @@ public interface MotorMount extends ChangeSource {
/**
* 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.
@ -107,8 +108,11 @@ public interface MotorMount extends ChangeSource {
/**
* 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();

View File

@ -563,6 +563,9 @@ public class Rocket extends RocketComponent {
* @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();
@ -588,6 +591,8 @@ public class Rocket extends RocketComponent {
* @return the configuration name
*/
public String getMotorConfigurationName(String id) {
if (!isMotorConfigurationID(id))
return "";
String s = motorConfigurationNames.get(id);
if (s == null)
return "";
@ -607,7 +612,7 @@ public class Rocket extends RocketComponent {
fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
}
/**
* Return either the motor configuration name (if set) or its description.
*
@ -617,10 +622,10 @@ public class Rocket extends RocketComponent {
public String getMotorConfigurationNameOrDescription(String id) {
String name;
name = motorConfigurationNames.get(id);
name = getMotorConfigurationName(id);
if (name != null && !name.equals(""))
return name;
return getMotorConfigurationDescription(id);
}
@ -636,10 +641,6 @@ public class Rocket extends RocketComponent {
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

View File

@ -308,7 +308,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
} 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
@ -334,11 +334,14 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial
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());
}
}

View File

@ -171,6 +171,9 @@ public class Transition extends SymmetricComponent {
}
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;

View File

@ -0,0 +1,25 @@
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;
}
}

View File

@ -57,6 +57,8 @@ public class UnitGroup {
public static final UnitGroup UNITS_COEFFICIENT;
// public static final UnitGroup UNITS_FREQUENCY;
public static final Map<String, UnitGroup> UNITS;
@ -203,6 +205,16 @@ public class UnitGroup {
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);

View File

@ -27,6 +27,18 @@ public class Value implements Comparable<Value> {
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());
}
/**

View File

@ -0,0 +1,14 @@
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);
}
}

View File

@ -16,6 +16,18 @@ public class Base64 {
'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 {

View File

@ -203,7 +203,7 @@ public class Prefs {
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;

View File

@ -2,6 +2,28 @@ package net.sf.openrocket.util;
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).

View File

@ -1,12 +1,8 @@
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);
@ -34,34 +30,4 @@ public class UniqueID {
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;
}
}

View File

@ -37,6 +37,7 @@ public class MotorPrinter {
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;

View File

@ -0,0 +1,76 @@
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")));
}
}

View File

@ -3,9 +3,38 @@ package net.sf.openrocket.util;
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() {

View File

@ -26,25 +26,4 @@ public class UniqueIDTest {
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);
}
}