Initial commit tags/Release_0.9.0@2 trunk@3
authorplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Sun, 24 May 2009 18:11:38 +0000 (18:11 +0000)
committerplaa <plaa@180e2498-e6e9-4542-8430-84ac67f01cd8>
Sun, 24 May 2009 18:11:38 +0000 (18:11 +0000)
git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/OpenRocket@2 180e2498-e6e9-4542-8430-84ac67f01cd8

894 files changed:
.classpath [new file with mode: 0644]
.project [new file with mode: 0644]
ChangeLog [new file with mode: 0644]
LICENSE.TXT [new file with mode: 0644]
README.TXT [new file with mode: 0644]
TODO [new file with mode: 0644]
build.xml [new file with mode: 0644]
datafiles/thrustcurves/00INDEX.txt [new file with mode: 0644]
datafiles/thrustcurves/AMW_I195.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_I220.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_I271.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_I285.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_I315.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_I325.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_I375.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J357.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J365.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J370.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J400.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J440.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J450.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J450_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J480.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_J500.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K1000.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K1075.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K365.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K450.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K470.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K475.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K530.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K555.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K560.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K570.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K600.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K600_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K605.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K650.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K670.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K670_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K700.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K800.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K950.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K950_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_K975.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1060.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1060_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1080.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1100.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1111.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1300.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L1400.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L666.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L700.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L777.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L777_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_L900.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M1350.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M1480.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M1730.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M1850.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M1850_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M1900.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M2500.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_M3000.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_N2020.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_N2600.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_N2700.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_N2800.eng [new file with mode: 0644]
datafiles/thrustcurves/AMW_N4000.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_D13.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_D15.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_D21.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_D24.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_D7.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_D9.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E11.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E12.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E15.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E15_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E16.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E18.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E23.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E28.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E30.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E6.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_E7.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F10.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F12.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F13.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F16.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F20.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F21.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F22.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F23.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F23_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F24.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F25.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F26.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F27.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F32.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F35.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F37.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F39.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F40.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F42.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F50.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F52.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F62.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_F72.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G101.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G104.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G12.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G25.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G33.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G339.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G35.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G38.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G40.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G53.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G54.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G55.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G61.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G64.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G67.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G69.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G71.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G71_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G75.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G75_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G76.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G76_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G77.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G78.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G79.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G80.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G80_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_G80_2.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H112.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H123.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H125.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H128.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H148.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H165.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H180.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H210.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H220.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H238.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H242.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H242_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H250.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H268.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H45.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H55.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H669.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H70.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H73.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H97.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_H999.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I115.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I117.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I1299.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I132.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I154.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I161.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I195.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I195_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I200.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I211.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I215.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I218.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I225.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I229.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I245.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I284.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I284_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I285.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I300.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I305.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I357.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I364.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I366.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I435.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I435_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I599.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I600.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_I65.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J125.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J1299.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J135.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J145.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J180.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J1999.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J210.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J250.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J260.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J275.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J315.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J350.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J350_1.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J390.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J415.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J420.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J460.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J500.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J540.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J570.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J575.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J800.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J825.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_J90.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K1050.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K1100.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K1275.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K1499.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K185.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K1999.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K250.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K270.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K458.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K485.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K550.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K560.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K650.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K680.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K695.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K700.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K780.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_K828.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L1120.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L1150.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L1300.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L1420.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L1500.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L850.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_L952.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1297.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1315.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1419.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1550.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1600.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1850.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M1939.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M2000.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M2400.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M2500.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M650.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M750.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_M845.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_N2000.eng [new file with mode: 0644]
datafiles/thrustcurves/AeroTech_N4800.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_1/2A2.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_1/4A2.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_A2.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_B2.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_B7.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_C10.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_C4.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_C6.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_D10.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_D3.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_E6.eng [new file with mode: 0644]
datafiles/thrustcurves/Apogee_F10.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_G60.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_G69.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_G69_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_G79.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_G79_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_H120.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_H143.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_H153.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_H565.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I170.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I205.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I212.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I240.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I285.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I287.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I350.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I360.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_I540.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J210.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J280.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J285.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J295.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J300.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J330.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J360.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J380.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J400.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_J410.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K445.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K510.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K510_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K530.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K570.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K575.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K650.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_K660.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L1090.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L1115.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L1115_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L610.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L730.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L800.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L800_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_L890.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M1060.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M1400.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M1400_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M1450.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M2505.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M520.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_M795.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_N1100.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_N2500.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_O5100.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_O5800.eng [new file with mode: 0644]
datafiles/thrustcurves/Cesaroni_O8000.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_G100.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_G123.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_G130.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_G234.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_G300.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H121.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H141.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H211.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H222.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H246.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H277.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H300.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H303.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_H340.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I155.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I210.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I221.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I290.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I307.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I333.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I400.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I500.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I727.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_I747.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J150.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J222.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J234.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J242.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J245.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J246.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J272.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J292.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J333.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J345.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J355.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J358.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J416.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J555.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J642.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_J800.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K234.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K265.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K300.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K321.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K404.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K456.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K630.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K678.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K707.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_K777.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_L1222.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_L2525.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_L369.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_L800.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_M1575.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_M2700.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_M2800.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_M711.eng [new file with mode: 0644]
datafiles/thrustcurves/Contrail_O6300.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_G20.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_G35.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_G37.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_H275.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_H48.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_H50.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_I130.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_I134.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_I150.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_I160.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_I230.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_I69.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_J110.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_J148.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_J228.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_J270.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_J330.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_K475.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_L330.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_L600.eng [new file with mode: 0644]
datafiles/thrustcurves/Ellis_M1000.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_1/2A3.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_1/2A6.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_1/4A3.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_A10.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_A3.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_A8.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_B4.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_B6.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_C11.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_C5.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_C6.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_D11.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_D12.eng [new file with mode: 0644]
datafiles/thrustcurves/Estes_E9.eng [new file with mode: 0644]
datafiles/thrustcurves/FALSE-apogee.eng [new file with mode: 0644]
datafiles/thrustcurves/GR_K555.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I130.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I136.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I145.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I205.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I222.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I225.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I260.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_I310.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J115.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J120.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J150.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J170.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J190.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J220.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J250.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J250_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J270.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J295.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J317.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J330.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_J330_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_K240.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L200.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L225.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L350.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L355.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L475.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L535.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L540.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L540_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L550.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L570.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L570_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L575.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L575_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L610.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L625.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L625_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L740.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_L970.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1000.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1000_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1001.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1010.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1010_1.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1015.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M1040.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M740.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M956.eng [new file with mode: 0644]
datafiles/thrustcurves/Hypertek_M960.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I170.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I280.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I301.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I310.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I370.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I450.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_I550.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_J405.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_J605.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_K1750.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_K400.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_K600.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_K750.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_L1000.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_L1400.eng [new file with mode: 0644]
datafiles/thrustcurves/KBA_M1450.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_H144.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_H500.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_I405.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_J525.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_J528.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_K250.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_K350.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_K960.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_L1400.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_L930.eng [new file with mode: 0644]
datafiles/thrustcurves/Loki_M1882.eng [new file with mode: 0644]
datafiles/thrustcurves/PML_F50.eng [new file with mode: 0644]
datafiles/thrustcurves/PML_G40.eng [new file with mode: 0644]
datafiles/thrustcurves/PML_G80.eng [new file with mode: 0644]
datafiles/thrustcurves/PP_H70.eng [new file with mode: 0644]
datafiles/thrustcurves/PP_I160.eng [new file with mode: 0644]
datafiles/thrustcurves/PP_I80.eng [new file with mode: 0644]
datafiles/thrustcurves/PP_J140.eng [new file with mode: 0644]
datafiles/thrustcurves/Quest_A6.eng [new file with mode: 0644]
datafiles/thrustcurves/Quest_B6.eng [new file with mode: 0644]
datafiles/thrustcurves/Quest_C6.eng [new file with mode: 0644]
datafiles/thrustcurves/Quest_D5.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_H70.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_I80.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_I90.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_J160.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_K240.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_L600.eng [new file with mode: 0644]
datafiles/thrustcurves/RATT_M900.eng [new file with mode: 0644]
datafiles/thrustcurves/RV_F32.eng [new file with mode: 0644]
datafiles/thrustcurves/RV_F72.eng [new file with mode: 0644]
datafiles/thrustcurves/RV_G55.eng [new file with mode: 0644]
datafiles/thrustcurves/Roadrunner_E25.eng [new file with mode: 0644]
datafiles/thrustcurves/Roadrunner_F35.eng [new file with mode: 0644]
datafiles/thrustcurves/Roadrunner_F45.eng [new file with mode: 0644]
datafiles/thrustcurves/Roadrunner_F60.eng [new file with mode: 0644]
datafiles/thrustcurves/Roadrunner_G80.eng [new file with mode: 0644]
datafiles/thrustcurves/SF_A8.eng [new file with mode: 0644]
datafiles/thrustcurves/SF_B4.eng [new file with mode: 0644]
datafiles/thrustcurves/SF_C2.eng [new file with mode: 0644]
datafiles/thrustcurves/SF_C6.eng [new file with mode: 0644]
datafiles/thrustcurves/SF_D7.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_G125.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_G63.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_G69.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_H124.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_H155.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_H78.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_I117.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_I119.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_I147.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_J144.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_J261.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_J263.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_J337.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_J348.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_K257.eng [new file with mode: 0644]
datafiles/thrustcurves/SkyR_K347.eng [new file with mode: 0644]
datafiles/thrustcurves/WCH_I110.eng [new file with mode: 0644]
extra-lib/RXTXcomm.jar [new file with mode: 0644]
extra-src/altimeter/Alt15K.java [new file with mode: 0644]
extra-src/altimeter/AltData.java [new file with mode: 0644]
extra-src/altimeter/RotationLogger.java [new file with mode: 0644]
extra-src/altimeter/SerialDownload.java [new file with mode: 0644]
html/contact.html [new file with mode: 0644]
html/documentation.html [new file with mode: 0644]
html/download.html [new file with mode: 0644]
html/features.html [new file with mode: 0644]
html/index.html [new file with mode: 0644]
html/layout.css [new file with mode: 0644]
html/license.html [new file with mode: 0644]
html/report.html [new file with mode: 0644]
html/screenshots.html [new file with mode: 0644]
html/shots-small/dialog-analysis.jpg [new file with mode: 0644]
html/shots-small/dialog-edit.jpg [new file with mode: 0644]
html/shots-small/dialog-plot-options.jpg [new file with mode: 0644]
html/shots-small/dialog-plot.jpg [new file with mode: 0644]
html/shots-small/main.jpg [new file with mode: 0644]
html/shots/dialog-analysis.png [new file with mode: 0644]
html/shots/dialog-edit.png [new file with mode: 0644]
html/shots/dialog-plot-options.png [new file with mode: 0644]
html/shots/dialog-plot.png [new file with mode: 0644]
html/shots/main.png [new file with mode: 0644]
html/valid-xhtml10.png [new file with mode: 0644]
html/vcss.gif [new file with mode: 0644]
lib/jcommon-1.0.16.jar [new file with mode: 0644]
lib/jfreechart-1.0.13.jar [new file with mode: 0644]
lib/miglayout15-swing.jar [new file with mode: 0644]
pix-src/componenticons/bodyoutline.xcf.gz [new file with mode: 0644]
pix-src/componenticons/bodytube.xcf.gz [new file with mode: 0644]
pix-src/componenticons/bulkhead.xcf.gz [new file with mode: 0644]
pix-src/componenticons/centeringring.xcf.gz [new file with mode: 0644]
pix-src/componenticons/ellipticalfin.xcf.gz [new file with mode: 0644]
pix-src/componenticons/engineblock.xcf.gz [new file with mode: 0644]
pix-src/componenticons/freeformfin.xcf.gz [new file with mode: 0644]
pix-src/componenticons/innertube.xcf.gz [new file with mode: 0644]
pix-src/componenticons/launchlug.xcf.gz [new file with mode: 0644]
pix-src/componenticons/mass.xcf.gz [new file with mode: 0644]
pix-src/componenticons/nosecone.xcf.gz [new file with mode: 0644]
pix-src/componenticons/parachute.xcf.gz [new file with mode: 0644]
pix-src/componenticons/shockcord.xcf.gz [new file with mode: 0644]
pix-src/componenticons/siiveke.fig [new file with mode: 0644]
pix-src/componenticons/siiveke.svg [new file with mode: 0644]
pix-src/componenticons/streamer.xcf.gz [new file with mode: 0644]
pix-src/componenticons/transition.xcf.gz [new file with mode: 0644]
pix-src/componenticons/trapezoidfin.xcf.gz [new file with mode: 0644]
pix-src/componenticons/tubecoupler.xcf.gz [new file with mode: 0644]
pix-src/spheres/blue-16x16.png [new file with mode: 0644]
pix-src/spheres/blue-cyan-large.png [new file with mode: 0644]
pix-src/spheres/copyright.txt [new file with mode: 0644]
pix-src/spheres/gray-16x16.png [new file with mode: 0644]
pix-src/spheres/gray-large.xcf.gz [new file with mode: 0644]
pix-src/spheres/green-16x16.png [new file with mode: 0644]
pix-src/spheres/green-large.xcf.gz [new file with mode: 0644]
pix-src/spheres/red-16x16.png [new file with mode: 0644]
pix-src/spheres/red-large.xcf.gz [new file with mode: 0644]
pix-src/spheres/step4c.png [new file with mode: 0644]
pix-src/spheres/yellow-16x16.png [new file with mode: 0644]
pix-src/spheres/yellow-large.xcf.gz [new file with mode: 0644]
pix-src/splashscreen.xcf.gz [new file with mode: 0644]
pix/componenticons/bodytube-large.png [new file with mode: 0644]
pix/componenticons/bodytube-small.png [new file with mode: 0644]
pix/componenticons/bulkhead-large.png [new file with mode: 0644]
pix/componenticons/bulkhead-small.png [new file with mode: 0644]
pix/componenticons/centeringring-large.png [new file with mode: 0644]
pix/componenticons/centeringring-small.png [new file with mode: 0644]
pix/componenticons/ellipticalfin-large.png [new file with mode: 0644]
pix/componenticons/ellipticalfin-small.png [new file with mode: 0644]
pix/componenticons/engineblock-large.png [new file with mode: 0644]
pix/componenticons/engineblock-small.png [new file with mode: 0644]
pix/componenticons/freeformfin-large.png [new file with mode: 0644]
pix/componenticons/freeformfin-small.png [new file with mode: 0644]
pix/componenticons/innertube-large.png [new file with mode: 0644]
pix/componenticons/innertube-small.png [new file with mode: 0644]
pix/componenticons/launchlug-large.png [new file with mode: 0644]
pix/componenticons/launchlug-small.png [new file with mode: 0644]
pix/componenticons/mass-large.png [new file with mode: 0644]
pix/componenticons/mass-small.png [new file with mode: 0644]
pix/componenticons/nosecone-large.png [new file with mode: 0644]
pix/componenticons/nosecone-small.png [new file with mode: 0644]
pix/componenticons/parachute-large.png [new file with mode: 0644]
pix/componenticons/parachute-small.png [new file with mode: 0644]
pix/componenticons/shockcord-large.png [new file with mode: 0644]
pix/componenticons/shockcord-small.png [new file with mode: 0644]
pix/componenticons/streamer-large.png [new file with mode: 0644]
pix/componenticons/streamer-small.png [new file with mode: 0644]
pix/componenticons/transition-large.png [new file with mode: 0644]
pix/componenticons/transition-small.png [new file with mode: 0644]
pix/componenticons/trapezoidfin-large.png [new file with mode: 0644]
pix/componenticons/trapezoidfin-small.png [new file with mode: 0644]
pix/componenticons/tubecoupler-large.png [new file with mode: 0644]
pix/componenticons/tubecoupler-small.png [new file with mode: 0644]
pix/icons/application-exit.png [new file with mode: 0644]
pix/icons/copyright.txt [new file with mode: 0644]
pix/icons/document-close.png [new file with mode: 0644]
pix/icons/document-new.png [new file with mode: 0644]
pix/icons/document-open.png [new file with mode: 0644]
pix/icons/document-save-as.png [new file with mode: 0644]
pix/icons/document-save.png [new file with mode: 0644]
pix/icons/edit-copy.png [new file with mode: 0644]
pix/icons/edit-cut.png [new file with mode: 0644]
pix/icons/edit-delete.png [new file with mode: 0644]
pix/icons/edit-paste.png [new file with mode: 0644]
pix/icons/edit-redo.png [new file with mode: 0644]
pix/icons/edit-undo.png [new file with mode: 0644]
pix/icons/preferences.png [new file with mode: 0644]
pix/icons/zoom-in.png [new file with mode: 0644]
pix/icons/zoom-out.png [new file with mode: 0644]
pix/spheres/blue-16x16.png [new file with mode: 0644]
pix/spheres/gray-16x16.png [new file with mode: 0644]
pix/spheres/green-16x16.png [new file with mode: 0644]
pix/spheres/red-16x16.png [new file with mode: 0644]
pix/spheres/yellow-16x16.png [new file with mode: 0644]
pix/splashscreen.png [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/AerodynamicForces.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/AtmosphericConditions.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/AtmosphericModel.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/ConeDragTest.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/ExactAtmosphericConditions.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/ExtendedISAModel.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/FlightConditions.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/GravityModel.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/Warning.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/WarningSet.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/WindSimulator.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java [new file with mode: 0644]
src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java [new file with mode: 0644]
src/net/sf/openrocket/database/Database.java [new file with mode: 0644]
src/net/sf/openrocket/database/Databases.java [new file with mode: 0644]
src/net/sf/openrocket/document/OpenRocketDocument.java [new file with mode: 0644]
src/net/sf/openrocket/document/Simulation.java [new file with mode: 0644]
src/net/sf/openrocket/document/StorageOptions.java [new file with mode: 0644]
src/net/sf/openrocket/file/GeneralRocketLoader.java [new file with mode: 0644]
src/net/sf/openrocket/file/Loader.java [new file with mode: 0644]
src/net/sf/openrocket/file/MotorLoader.java [new file with mode: 0644]
src/net/sf/openrocket/file/OpenRocketLoader.java [new file with mode: 0644]
src/net/sf/openrocket/file/OpenRocketSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/RocketLoadException.java [new file with mode: 0644]
src/net/sf/openrocket/file/RocketLoader.java [new file with mode: 0644]
src/net/sf/openrocket/file/RocketSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/BodyComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/BodyTubeSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/BulkheadSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/CenteringRingSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/ComponentAssemblySaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/EllipticalFinSetSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/EngineBlockSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/ExternalComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/FinSetSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/FreeformFinSetSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/InnerTubeSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/InternalComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/LaunchLugSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/MassComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/MassObjectSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/NoseConeSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/ParachuteSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/RadiusRingComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/RecoveryDeviceSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/RingComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/RocketComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/RocketSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/ShockCordSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/StageSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/StreamerSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/StructuralComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/SymmetricComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/ThicknessRingComponentSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/TransitionSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/TrapezoidFinSetSaver.java [new file with mode: 0644]
src/net/sf/openrocket/file/openrocket/TubeCouplerSaver.java [new file with mode: 0644]
src/net/sf/openrocket/gui/BasicSlider.java [new file with mode: 0644]
src/net/sf/openrocket/gui/ComponentAnalysisDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/DescriptionArea.java [new file with mode: 0644]
src/net/sf/openrocket/gui/DetailDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/PreferencesDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/Resettable.java [new file with mode: 0644]
src/net/sf/openrocket/gui/ResizeLabel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/SpinnerEditor.java [new file with mode: 0644]
src/net/sf/openrocket/gui/StageSelector.java [new file with mode: 0644]
src/net/sf/openrocket/gui/StorageOptionChooser.java [new file with mode: 0644]
src/net/sf/openrocket/gui/TextFieldListener.java [new file with mode: 0644]
src/net/sf/openrocket/gui/UnitSelector.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/BooleanModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/Column.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/DoubleModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/EnumModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/IntegerModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/MaterialModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/FinSetConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/MotorConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/RocketConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/SleeveConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/StreamerConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/TransitionConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java [new file with mode: 0644]
src/net/sf/openrocket/gui/figureelements/CGCaret.java [new file with mode: 0644]
src/net/sf/openrocket/gui/figureelements/CPCaret.java [new file with mode: 0644]
src/net/sf/openrocket/gui/figureelements/Caret.java [new file with mode: 0644]
src/net/sf/openrocket/gui/figureelements/FigureElement.java [new file with mode: 0644]
src/net/sf/openrocket/gui/figureelements/RocketInfo.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/AboutDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/BareComponentTreeModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/BasicFrame.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/ComponentAddButtons.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/ComponentIcons.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/ComponentTree.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/ComponentTreeModel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/ComponentTreeRenderer.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/LicenseDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/MotorChooserDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/RocketActions.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/SimulationEditDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/SimulationPanel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/SimulationRunDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/main/SimulationWorker.java [new file with mode: 0644]
src/net/sf/openrocket/gui/plot/Axis.java [new file with mode: 0644]
src/net/sf/openrocket/gui/plot/PlotConfiguration.java [new file with mode: 0644]
src/net/sf/openrocket/gui/plot/PlotDialog.java [new file with mode: 0644]
src/net/sf/openrocket/gui/plot/PlotPanel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/MassObjectShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/RingComponentShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/RocketFigure.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/RocketPanel.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java [new file with mode: 0644]
src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java [new file with mode: 0644]
src/net/sf/openrocket/material/Material.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/BodyComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/BodyTube.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Bulkhead.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/CenteringRing.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Clusterable.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ComponentAssembly.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Configuration.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/EngineBlock.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ExternalComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/FinSet.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/InnerTube.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/InternalComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/LaunchLug.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/MassComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/MassObject.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Motor.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/MotorMount.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/NoseCone.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Parachute.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/RadialParent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ReferenceType.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/RingComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Rocket.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/RocketComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ShockCord.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Sleeve.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Stage.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Streamer.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/StructuralComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/ThrustCurveMotor.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/Transition.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java [new file with mode: 0644]
src/net/sf/openrocket/rocketcomponent/TubeCoupler.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/EulerSimulator.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/FlightData.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/FlightDataBranch.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/FlightEvent.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/FlightSimulator.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/RK4SimulationStatus.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/RK4Simulator.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/SimulationConditions.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/SimulationListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/SimulationStatus.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/exception/SimulationException.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/exception/SimulationListenerException.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/ApogeeEndListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/CSVSaveListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/InterruptListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/PrintSimulationListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/RollSaveListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/StopSimulationListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/haisu/HaisuCatoListener.java [new file with mode: 0644]
src/net/sf/openrocket/simulation/listeners/haisu/RollControlListener.java [new file with mode: 0644]
src/net/sf/openrocket/unit/CaliberUnit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/DegreeUnit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/FixedPrecisionUnit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/GeneralUnit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/RadianUnit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/TemperatureUnit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/Tick.java [new file with mode: 0644]
src/net/sf/openrocket/unit/Unit.java [new file with mode: 0644]
src/net/sf/openrocket/unit/UnitGroup.java [new file with mode: 0644]
src/net/sf/openrocket/util/Analysis.java [new file with mode: 0644]
src/net/sf/openrocket/util/Base64.java [new file with mode: 0644]
src/net/sf/openrocket/util/ChangeSource.java [new file with mode: 0644]
src/net/sf/openrocket/util/Coordinate.java [new file with mode: 0644]
src/net/sf/openrocket/util/GUIUtil.java [new file with mode: 0644]
src/net/sf/openrocket/util/Icons.java [new file with mode: 0644]
src/net/sf/openrocket/util/LineStyle.java [new file with mode: 0644]
src/net/sf/openrocket/util/LinearInterpolator.java [new file with mode: 0644]
src/net/sf/openrocket/util/MathUtil.java [new file with mode: 0644]
src/net/sf/openrocket/util/MutableCoordinate.java [new file with mode: 0644]
src/net/sf/openrocket/util/Pair.java [new file with mode: 0644]
src/net/sf/openrocket/util/PinkNoise.java [new file with mode: 0644]
src/net/sf/openrocket/util/PolyInterpolator.java [new file with mode: 0644]
src/net/sf/openrocket/util/Prefs.java [new file with mode: 0644]
src/net/sf/openrocket/util/Quaternion.java [new file with mode: 0644]
src/net/sf/openrocket/util/QuaternionMultiply.java [new file with mode: 0644]
src/net/sf/openrocket/util/Reflection.java [new file with mode: 0644]
src/net/sf/openrocket/util/Rotation2D.java [new file with mode: 0644]
src/net/sf/openrocket/util/Test.java [new file with mode: 0644]
src/net/sf/openrocket/util/Transformation.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..290b5a5
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="src" path="extra-src"/>
+       <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="/home/sampo/Projects/OpenRocket/extra-lib/RXTXcomm.jar"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..977f748
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>OpenRocket</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/ChangeLog b/ChangeLog
new file mode 100644 (file)
index 0000000..9e11801
--- /dev/null
+++ b/ChangeLog
@@ -0,0 +1,4 @@
+2009-05-24  Sampo Niskanen  <sampo.niskanen@iki.fi>
+
+       * Initial release 0.9.0
+
diff --git a/LICENSE.TXT b/LICENSE.TXT
new file mode 100644 (file)
index 0000000..b8b2c1e
--- /dev/null
@@ -0,0 +1,700 @@
+OpenRocket - A model rocket simulator
+
+Copyright (C) 2007-2009 Sampo Niskanen
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or (at
+your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License (below) for more details.
+
+
+Additional permission under GNU GPL version 3 section 7:
+
+The licensors grant additional permission to package this Program, or
+any covered work, along with any non-compilable data files (such as
+thrust curves or component databases) and convey the resulting work.
+
+
+------------------------------------------------------------------------
+
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                      TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+  
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
diff --git a/README.TXT b/README.TXT
new file mode 100644 (file)
index 0000000..017418e
--- /dev/null
@@ -0,0 +1,20 @@
+
+OpenRocket - an Open Source model rocket simulator
+--------------------------------------------------
+
+Copyright (C) 2007-2009  Sampo Niskanen
+
+
+For license information see the file LICENSE.TXT.
+
+For more information see http://openrocket.sourceforge.net/
+
+
+
+To start the software run the class 
+
+    net.sf.openrocket.gui.main.BasicFrame
+
+or from the JAR file run
+
+    $ java -jar OpenRocket-<VERSION>.jar
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..a39c664
--- /dev/null
+++ b/TODO
@@ -0,0 +1,126 @@
+
+GUI:
+
+- Preferences dialog
+
+
+BUGS:
+
+
+COMPUTATION:
+
+
+FILE/STORAGE:
+
+
+OTHER:
+
+- web-sivut
+
+
+DIPPA:
+
+
+
+
+-------------------
+
+LATER:
+
+- Simulation delete/copy/paste hotkeys
+  (either component or simulation selected, but not both)
+- Add BodyComponent at end of rocket when no component is selected
+- Showing events in plot (maybe future)
+- Search field in motor selection dialog
+- Through-the-wall fins
+- Store materials
+
+- Streamer CD estimation
+
+- exporting (maybe later)
+
+- Make ThicknessRingComponent implement RadialParent and allow
+  attaching components to a TubeCoupler
+
+
+
+
+DONE:
+
+- Automatic diameters of body components
+- Copy/paste
+
+18.4.:
+- Esc, Ctrl-Z and Y etc.
+- Look and feel
+
+19.4.:
+- Nose cone and transition shoulders in GUI
+- zoom, cut/copy/paste etc. icons
+
+23.4.:
+- Figure or rocket not updating when using a new BasicFrame
+
+24.4.:
+- File save and load
+- Motor configuration editing          (pre-alpha)
+- Save simulations
+
+25.4.:
+- Multi-stages simulation              (pre-alpha)
+- Make sure simulations end
+- Mass and CG overrides                        (pre-alpha)
+- General loader
+
+26.4.:
+- Centering ring inner diameter automatics     (pre-alpha)
+- Landing simulation                   (pre-alpha ??)
+- Parachute/Streamer editing in GUI    (pre-alpha)
+- Launch lug editing in GUI            (pre-alpha)
+
+29.4.:
+- Actual plotting done
+- Refactored source code packages
+
+2.5.:
+- Plotting                             (pre-alpha)
+- Gravity model
+- More units and specific custom units (angle, temperature, ...)
+- Transition/Nose cone description text wrapping
+- Fin set CP jumps at Mach 0.9
+
+- Error dialogs for load/save/etc
+
+3.5.:
+- More materials                       (pre-alpha)
+- File opening from command line
+
+9.5.:
+- Rocket configuration dialog
+- Warnings in poor conditions (transition supersonic)
+- New or old fin-body interference?
+- poista tiedot laminaarisesta vastuksesta
+- vertailuosio
+
+11.5.:
+- Better default values for components
+- Component analysis dialog show zero total mass and CG
+- Compression support in save
+- Simulation storage options
+
+12.5.:
+- Load simulations
+- Update file version to 1.0
+
+13.5.:
+- statistiikat softasta
+
+17.5.:
+- jonkin verran TODOja
+- conclusion
+- viitteet
+- Draw the component icons
+- splashscreen
+
+18.5.:
+- About dialog + version number
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..f563965
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,63 @@
+<project name="OpenRocket" basedir=".">
+
+       <property name="src.dir"     value="src"/>                      <!-- Source directory -->
+       <property name="build.dir"   value="build"/>            <!-- Build directory -->
+       
+       <!-- Distribution directory, from which stuff is jar'ed -->
+       <property name="dist.dir"    value="${build.dir}/dist"/> 
+       
+       <property name="classes.dir" value="${dist.dir}"/>      <!-- Directory for classes -->
+       <property name="jar.dir"     value="${build.dir}/jar"/> <!-- Directory for built jar's -->
+       <property name="lib.dir"     value="lib"/>                              <!-- Library source directory -->
+
+       
+       <!-- The main class of the application -->
+       <property name="main-class"  value="net.sf.openrocket.gui.main.BasicFrame"/>
+
+       
+       <!-- Classpath definition -->
+       <path id="classpath">
+               <fileset dir="${lib.dir}" includes="**/*.jar"/>
+       </path>
+       
+       
+       <!-- CLEAN -->
+       <target name="clean">
+               <delete dir="${build.dir}"/>
+       </target>
+               
+       <!-- BUILD -->
+       <target name="build">
+               <mkdir dir="${classes.dir}"/>
+               <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"/>
+       </target>
+       
+       <!-- JAR -->
+       <target name="jar" depends="build">
+               <copy todir="${dist.dir}/">
+                       <fileset dir="." includes="LICENSE.TXT" />
+                       <fileset dir="." includes="README.TXT" />
+                       <fileset dir="." includes="datafiles/**/* pix/**/*" />
+               </copy>
+               <mkdir dir="${jar.dir}"/>
+               <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${dist.dir}">
+                       <manifest>
+                               <attribute name="Main-Class" value="${main-class}"/>
+                               <attribute name="SplashScreen-Image" value="pix/splashscreen.png"/>
+                       </manifest>
+                       <zipfileset src="lib/miglayout15-swing.jar" />
+                       <zipfileset src="lib/jcommon-1.0.16.jar" />
+                       <zipfileset src="lib/jfreechart-1.0.13.jar" />
+               </jar>
+       </target>
+       
+       <!-- RUN -->
+       <target name="run" depends="jar">
+               <java fork="true" classname="${main-class}">
+                       <classpath>
+                               <path location="${jar.dir}/${ant.project.name}.jar"/>
+                       </classpath>
+               </java>
+       </target>
+
+</project>
\ No newline at end of file
diff --git a/datafiles/thrustcurves/00INDEX.txt b/datafiles/thrustcurves/00INDEX.txt
new file mode 100644 (file)
index 0000000..8837613
--- /dev/null
@@ -0,0 +1,3685 @@
+Rocket motor simulation data downloaded from ThrustCurve.org.\r
+This ZIP file contains 526 simulator data files.\r
+For more info, please see http://www.thrustcurve.org/\r
+\r
+AMW_I195.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WW-38-390\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John DeMar\r
+\r
+AMW_I220.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-38-390\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John DeMar\r
+\r
+AMW_I271.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-38-390\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AMW_I285.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-38-390\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AMW_I315.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-38-640\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Koen Loeven\r
+\r
+AMW_I325.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WW-38-640\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John DeMar\r
+\r
+AMW_I375.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-38-640\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Robert DeHate\r
+\r
+AMW_J357.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-54-1050\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_J365.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-54-1400\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Robert DeHate\r
+\r
+AMW_J370.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-54-1050\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_J400.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-54-1050\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_J440.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-38-640\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Robert DeHate\r
+\r
+AMW_J450.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-54-1050\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_J450_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-54-1050\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_J480.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-54-1050\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_J500.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  J500ST\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K1000.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-54-2550\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K1075.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-54-2550\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K365.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-75-1700\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K450.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-75-1700\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K470.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-75-1700\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K475.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-54-1400\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K530.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-54-1400\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K555.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Koen Loeven\r
+\r
+AMW_K560.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-54-1400\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K570.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_K600.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-75-2500\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_K600_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-75-2500\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_K605.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-75-2500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K650.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Koen Loeven\r
+\r
+AMW_K670.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_K670_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_K700.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-54-1400\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_K800.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Koen Loeven\r
+\r
+AMW_K950.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_K950_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-54-1750\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_K975.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-54-2550\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_L1060.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_L1060_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_L1080.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_L1100.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-54-2550\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_L1111.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_L1300.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-54-2550\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_L1400.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-75-6000\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John DeMar\r
+\r
+AMW_L666.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Joel Rogers\r
+\r
+AMW_L700.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-75-2500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_L777.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_L777_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_L900.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-75-3500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_M1350.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-75-6000\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_M1480.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  RR-75-6000\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_M1730.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  SK-98-11000\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Joel Rogers\r
+\r
+AMW_M1850.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-75-6000\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_M1850_1.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-75-6000\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_M1900.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-75-6000\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_M2500.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-75-7600\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Carl Tulanko\r
+\r
+AMW_M3000.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  ST-75-7600\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Conway Stevens\r
+\r
+AMW_N2020.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WT-98-11000\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Joel Rogers\r
+\r
+AMW_N2600.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  GG-98-11000\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_N2700.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-98-11000\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AMW_N2800.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  WW-98-17500\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John DeMar\r
+\r
+AMW_N4000.eng\r
+    Manufacturer: Animal Motor Works\r
+    Designation:  BB-98-17500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Robert DeHate\r
+\r
+AeroTech_D13.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  D13\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_D15.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  D15\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_D21.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  D21\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_D24.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  D24\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_D7.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  D7\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_D9.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  D9\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E11.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E11J\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_E12.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E12J\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_E15.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E15\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_E15_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E15\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E16.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E16\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E18.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E18\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E23.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E23\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E28.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E28\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E30.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E30\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_E6.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E6\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_E7.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  E7\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F10.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F10\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_F12.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F12\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_F13.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F13-RC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F16.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F16-RC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F20.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F20\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F21.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F21W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_F22.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F22\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F23.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F23FJ\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F23_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F23-RC-SK\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F24.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F24\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_F25.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F25W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F26.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F26FJ\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F27.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F27R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F32.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F32\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F35.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F35W\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_F37.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F37\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F39.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F39\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_F40.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F40\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F42.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F42T\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F50.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F50\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F52.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F52\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_F62.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F62T\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_F72.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  F72\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G101.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G101T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_G104.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G104T\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_G12.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G12-RC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G25.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G25\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G33.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G33\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G339.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G339N-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Bill Wagstaff\r
+\r
+AeroTech_G35.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G35\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G38.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G38FJ\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G40.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G40W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G53.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G53FJ\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_G54.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G54\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G55.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G55\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G61.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G61W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G64.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G64\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G67.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G67R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_G69.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G69N\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_G71.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G71R\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_G71_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G71R\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Edward K. Chess\r
+\r
+AeroTech_G75.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G75J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G75_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G75J\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_G76.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G76G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_G76_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G76G\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John DeMar\r
+\r
+AeroTech_G77.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G77R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Stan Hemphill\r
+\r
+AeroTech_G78.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G78G\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G79.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G79W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_G80.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G80\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John DeMar\r
+\r
+AeroTech_G80_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G80\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John DeMar\r
+\r
+AeroTech_G80_2.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  G80\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John DeMar\r
+\r
+AeroTech_H112.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H112J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H123.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H123W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H125.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H125W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H128.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H128W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H148.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H148R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H165.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H165R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H180.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H180W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H210.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H210R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H220.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H220T\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H238.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H238T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H242.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H242T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H242_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H242T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H250.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H250G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Jim Yehle\r
+\r
+AeroTech_H268.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H268R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H45.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H45W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H55.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H55W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H669.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H669N-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_H70.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H70W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H73.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H73J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H97.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H97J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_H999.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  H999\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_I115.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I115W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_I117.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I117FJ\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_I1299.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I1299N-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Jim Yehle\r
+\r
+AeroTech_I132.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I132W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I154.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I154J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I161.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I161W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I195.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I195J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I195_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I195J\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I200.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I200W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I211.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I211W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I215.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I215R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_I218.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I218R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I225.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I225FJ\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_I229.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I229T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_I245.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I245G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Jim Yehle\r
+\r
+AeroTech_I284.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I284W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I284_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I284W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I285.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I285R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I300.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I300T\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I305.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I305FJ\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_I357.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I357T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I364.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I364FJ\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_I366.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I366R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I435.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I435T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I435_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I435T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I599.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I599N\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_I600.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I600R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_I65.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  I65W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J125.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J125W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J1299.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J1299N-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_J135.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J135W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J145.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J145H 2-jet std.\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J180.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J180T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J1999.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J1999N-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_J210.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J210H 4-jet std.\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J250.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J250FJ\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_J260.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J260HW 3-jet EFX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J275.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J275W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J315.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J315R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J350.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J350W-L\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J350_1.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J350W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J390.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J390-turbo\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J415.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J415W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J420.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J420R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J460.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J460T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J500.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J500G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Jim Yehle\r
+\r
+AeroTech_J540.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J540R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J570.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J570W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J575.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J575FJ\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Simon Crafts\r
+\r
+AeroTech_J800.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J800T-PS\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_J825.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J825R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_J90.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  J90W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K1050.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K1050W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K1100.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K1100T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K1275.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K1275\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K1499.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K1499N-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Jim Yehle\r
+\r
+AeroTech_K185.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K185W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K1999.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K1999N-P\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Christopher Kobel\r
+\r
+AeroTech_K250.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K250W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K270.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K270W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_K458.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K458W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K485.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K485H (3 jet)\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K550.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K550W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K560.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K560W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K650.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K650T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K680.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K680R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K695.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K695R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K700.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K700W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K780.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K780R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_K828.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  K828FJ\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+AeroTech_L1120.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L1120W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_L1150.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L1150R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_L1300.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L1300R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_L1420.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L1420R\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_L1500.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L1500T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_L850.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L850W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_L952.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  L952W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M1297.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1297W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_M1315.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1315W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M1419.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1419W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M1550.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1550R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M1600.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1600R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M1850.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1850W-PS\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_M1939.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M1939W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M2000.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M2000R\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M2400.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M2400T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M2500.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M2500T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_M650.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M650W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_M750.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M750W\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Greg Gardner\r
+\r
+AeroTech_M845.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  M845\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+AeroTech_N2000.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  N2000W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+AeroTech_N4800.eng\r
+    Manufacturer: AeroTech\r
+    Designation:  N4800T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Apogee_1/2A2.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  1/2A2\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_1/4A2.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  1/4A2\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_A2.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  A2\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_B2.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  B2\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_B7.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  B7\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_C10.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  C10\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_C4.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  C4\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_C6.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  C6\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_D10.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  D10\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_D3.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  D3\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_E6.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  E6\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Apogee_F10.eng\r
+    Manufacturer: Apogee Components\r
+    Designation:  F10\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_G60.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  134 G60-14A\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_G69.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  121 G69-14A\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Pete Carr\r
+\r
+Cesaroni_G69_1.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  121 G69-14A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_G79.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  129 G79SS-13A\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  Pete Carr\r
+\r
+Cesaroni_G79_1.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  129 G79SS-13A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_H120.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  261 H120-14A\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Len Bryan\r
+\r
+Cesaroni_H143.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  247 H143SS-13A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_H153.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  H153\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_H565.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  H565\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_I170.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  382 I170-14A\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_I205.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  I205\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_I212.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  364 I212SS-14A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_I240.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  I240\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_I285.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  I285\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_I287.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  486 I287SS-15A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_I350.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  601 I350SS-16A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_I360.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  I360\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_I540.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  634I540WT\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_J210.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  836 J210-16A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J280.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  J280SS\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J285.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  648 J285-15A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J295.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  1195 J295-15A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J300.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  J300\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_J330.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  765 J330-16A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J360.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  J360\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_J380.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  1043 J380SS-16A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J400.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  J400SS\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_J410.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  774 J410-16A\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Len Bryan\r
+\r
+Cesaroni_K445.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  1635 K445-A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_K510.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  2486 K510-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Len Lekx\r
+\r
+Cesaroni_K510_1.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  2486 K510-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_K530.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  1412 K530SS-16A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_K570.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  2060 K570-A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_K575.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  2493 K575-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_K650.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  1750 K650SS-16A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_K660.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  2437 K660-17A\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_L1090.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  4815 L1090-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_L1115.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  5015 L1115-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Len Lekx\r
+\r
+Cesaroni_L1115_1.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  5015 L1115-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_L610.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  4842 L610-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_L730.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  2765 L730-P\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_L800.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  3757 L800-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Len Lekx\r
+\r
+Cesaroni_L800_1.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  3757 L800-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_L890.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  3762 L890-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_M1060.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  7441 M1060-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_M1400.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  6251 M1400-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Len Lekx\r
+\r
+Cesaroni_M1400_1.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  6251 M1400-P-U\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_M1450.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  9955 M1450-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_M2505.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  7450 M2505-P\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_M520.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  7400 M520-P\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_M795.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  10133 M795-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_N1100.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  14005 N1100-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_N2500.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  13766 N2500-P\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Casey Hatch\r
+\r
+Cesaroni_O5100.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  29990 O5100-P\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Cesaroni_O5800.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  30605 O5800-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Len Bryan\r
+\r
+Cesaroni_O8000.eng\r
+    Manufacturer: Cesaroni Technology Inc.\r
+    Designation:  40960 O8000-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Len Bryan\r
+\r
+Contrail_G100.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  G100-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_G123.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  G123-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_G130.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  G130-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_G234.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  G234-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_G300.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  G300-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H121.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H121-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H141.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H141-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H211.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H211-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H222.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H222-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H246.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H246-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H277.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H277-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H300.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H300-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H303.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H303-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_H340.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  H340-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I155.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I155-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I210.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I210-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I221.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I221-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I290.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I290-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I307.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I307-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I333.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I333-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I400.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I400-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I500.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I500-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I727.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I727-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_I747.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  I747-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J150.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J150-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J222.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J222-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J234.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J234-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J242.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J242-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J245.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J245-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J246.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J246-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J272.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J272-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J292.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J292-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J333.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J333-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J345.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J345-PVC\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J355.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J355-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J358.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J358-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J416.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J416-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J555.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J555-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J642.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J642-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_J800.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  J800-HP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K234.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K234-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K265.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K265-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K300.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K300-BS\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K321.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K321-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K404.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K404-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K456.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K456-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K630.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K630-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K678.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K678-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K707.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K707-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_K777.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  K777-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_L1222.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  L1222-SM\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_L2525.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  L2525-GF\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_L369.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  L369-SP\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_L800.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  L800-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_M1575.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  M1575-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_M2700.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  M2700-BS\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_M2800.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  M2800-BG\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_M711.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  M711-BS\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Contrail_O6300.eng\r
+    Manufacturer: Contrail Rockets LLC\r
+    Designation:  O6300-BS\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_G20.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  G20\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_G35.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  G35\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_G37.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  G37\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_H275.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  H275\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_H48.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  H48\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_H50.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  H50\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_I130.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  I130\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_I134.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  I134\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_I150.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  I150\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_I160.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  I160\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_I230.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  I230\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_I69.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  I69\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_J110.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  J110\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_J148.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  J148\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_J228.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  J228\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_J270.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  J270\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_J330.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  J330\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_K475.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  K475\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Ellis_L330.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  L330\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_L600.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  L600\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Ellis_M1000.eng\r
+    Manufacturer: Ellis Mountain Rocket Works\r
+    Designation:  M1000\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Estes_1/2A3.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  1/2A3\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_1/2A6.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  1/2A6\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_1/4A3.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  1/4A3\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_A10.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  A10\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_A3.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  A3\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_A8.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  A8\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_B4.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  B4\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_B6.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  B6\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+Estes_C11.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  C11\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_C5.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  C5\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_C6.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  C6\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_D11.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  D11\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_D12.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  D12\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Estes_E9.eng\r
+    Manufacturer: Estes Industries\r
+    Designation:  E9\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+GR_K555.eng\r
+    Manufacturer: Gorilla Rocket Motors, Inc.\r
+    Designation:  K555GT\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Mark Koelsch\r
+\r
+Hypertek_I130.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  300CC098J - I130\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I136.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  300CC098J2 - I136\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I145.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  300CC098JFX - I145FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I205.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  300CC125J - I205\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I222.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  300CC125J2 - I222\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I225.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  300CC125JFX - I225FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I260.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC172J - I260\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_I310.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC172J - I310\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J115.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC076J - J115\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J120.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC076JFX - J120FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J150.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC086J - J150\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J170.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC098J - J170\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J190.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC098JFX - J190FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J220.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC110J - J220\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J250.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC125J - J250\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J250_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC125J - J250\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J270.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC125JFX - J270FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J295.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  440CC172JFX - J295FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J317.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  835CC172J - J317\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J330.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  835CC172JFX - J330FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_J330_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  835CC172JFX - J330FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_K240.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  835CC125J - K240\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L200.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CC098L - L200\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L225.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CC098LFX - L225FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L350.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CC125L - L350\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L355.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CC125LFX - L355FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L475.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CC172L - L475\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L535.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CC172LFX - L535FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L540.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC172L - L540\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L540_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC172L - L540\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L550.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CCRGL - L550\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L570.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC172LFX - L570FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L570_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC172LFX - L570FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L575.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CCRGL - L575\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L575_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CCRGL - L575\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L610.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  1685CCRGLFX - L610FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L625.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CCRGLFX - L625FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L625_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CCRGLFX - L625FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L740.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC200MFX - L740FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_L970.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC300M - L970\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1000.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  4630CCRGM - M1000\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1000_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  4630CCRGM - M1000\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1001.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  5478CCRGM - M1001\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1010.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  4630CCRGMFX - M1010FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1010_1.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  4630CCRGMFX - M1010FX\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1015.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  3500CCRGMFX - M1015FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M1040.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  4630CCRGMFX - M1040FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M740.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC200M - M740\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M956.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  3500CCRGM - M956\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Hypertek_M960.eng\r
+    Manufacturer: Hypertek\r
+    Designation:  2800CC300MFX - M960FX\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_I170.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I170S\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_I280.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I280F\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_I301.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I301W\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Mark Koelsch\r
+\r
+KBA_I310.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I310S\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_I370.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I370F\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_I450.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I450F\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_I550.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  I550R\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Mark Koelsch\r
+\r
+KBA_J405.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  J405S\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_J605.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  J605F\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_K1750.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  K1750R\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Mark Koelsch\r
+\r
+KBA_K400.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  K400S\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_K600.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  K600F\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+KBA_K750.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  K750W\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Mark Koelsch\r
+\r
+KBA_L1000.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  L1000S\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+KBA_L1400.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  L1400F\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+KBA_M1450.eng\r
+    Manufacturer: Kosdon by AeroTech\r
+    Designation:  M1450W\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Mark Koelsch\r
+\r
+Loki_H144.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  H144-LW\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_H500.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  H500\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Loki_I405.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  I405LW\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_J525.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  J525LW\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_J528.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  J528LW\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_K250.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  K250LWM\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_K350.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  K350LWM\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_K960.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  K960LWB\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_L1400.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  L1400LW\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_L930.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  L930LWB\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+Loki_M1882.eng\r
+    Manufacturer: Loki Research\r
+    Designation:  M1882LW\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  William Carney\r
+\r
+PML_F50.eng\r
+    Manufacturer: Public Missiles, Ltd.\r
+    Designation:  F50T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+PML_G40.eng\r
+    Manufacturer: Public Missiles, Ltd.\r
+    Designation:  G40W\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+PML_G80.eng\r
+    Manufacturer: Public Missiles, Ltd.\r
+    Designation:  G80T\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+PP_H70.eng\r
+    Manufacturer: Propulsion Polymers\r
+    Designation:  240NS-H70\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+PP_I160.eng\r
+    Manufacturer: Propulsion Polymers\r
+    Designation:  484NS-I160\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+PP_I80.eng\r
+    Manufacturer: Propulsion Polymers\r
+    Designation:  460NS-I80\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+PP_J140.eng\r
+    Manufacturer: Propulsion Polymers\r
+    Designation:  664NS-J140\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Quest_A6.eng\r
+    Manufacturer: Quest Aerospace\r
+    Designation:  A6\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Quest_B6.eng\r
+    Manufacturer: Quest Aerospace\r
+    Designation:  B6\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+Quest_C6.eng\r
+    Manufacturer: Quest Aerospace\r
+    Designation:  C6\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+Quest_D5.eng\r
+    Manufacturer: Quest Aerospace\r
+    Designation:  D5-P\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+RATT_H70.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  H70\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+RATT_I80.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  I80\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+RATT_I90.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  I90L\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+RATT_J160.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  J160\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+RATT_K240.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  K240H\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  John Coker\r
+\r
+RATT_L600.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  L600\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+RATT_M900.eng\r
+    Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+    Designation:  M900\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+RV_F32.eng\r
+    Manufacturer: Rocketvision Flight-Star\r
+    Designation:  F32\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+RV_F72.eng\r
+    Manufacturer: Rocketvision Flight-Star\r
+    Designation:  F72\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+RV_G55.eng\r
+    Manufacturer: Rocketvision Flight-Star\r
+    Designation:  G55\r
+    Data Format:  RASP\r
+    Data Source:  cert\r
+    Contributor:  Mark Koelsch\r
+\r
+Roadrunner_E25.eng\r
+    Manufacturer: Roadrunner Rocketry\r
+    Designation:  E25\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Roadrunner Rocketry\r
+\r
+Roadrunner_F35.eng\r
+    Manufacturer: Roadrunner Rocketry\r
+    Designation:  F35\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Roadrunner Rocketry\r
+\r
+Roadrunner_F45.eng\r
+    Manufacturer: Roadrunner Rocketry\r
+    Designation:  F45\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Roadrunner Rocketry\r
+\r
+Roadrunner_F60.eng\r
+    Manufacturer: Roadrunner Rocketry\r
+    Designation:  F60\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Roadrunner Rocketry\r
+\r
+Roadrunner_G80.eng\r
+    Manufacturer: Roadrunner Rocketry\r
+    Designation:  G80\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Roadrunner Rocketry\r
+\r
+SkyR_G125.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  G125\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+SkyR_G63.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  G63\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+SkyR_G69.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  G69\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+SkyR_H124.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  H124\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
+\r
+SkyR_H155.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  H155\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
+\r
+SkyR_H78.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  H78\r
+    Data Format:  RASP\r
+    Data Source:  user\r
+    Contributor:  John Coker\r
+\r
+SkyR_I117.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  I117\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
+\r
+SkyR_I119.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  I119\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
+\r
+SkyR_I147.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  I147\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
+\r
+SkyR_J144.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  J144\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
+\r
+SkyR_J261.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  J261G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John Coker\r
+\r
+SkyR_J263.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  J263G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John Coker\r
+\r
+SkyR_J337.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  J337B\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John Coker\r
+\r
+SkyR_J348.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  J348B\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John Coker\r
+\r
+SkyR_K257.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  K257G\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John Coker\r
+\r
+SkyR_K347.eng\r
+    Manufacturer: Sky Ripper Systems\r
+    Designation:  K347B\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  John Coker\r
+\r
+WCH_I110.eng\r
+    Manufacturer: West Coast Hybrids\r
+    Designation:  499 I110-P\r
+    Data Format:  RASP\r
+    Data Source:  mfr\r
+    Contributor:  Andrew MacMillen\r
diff --git a/datafiles/thrustcurves/AMW_I195.eng b/datafiles/thrustcurves/AMW_I195.eng
new file mode 100644 (file)
index 0000000..2aacd4a
--- /dev/null
@@ -0,0 +1,33 @@
+;Animal Motor Works 38-390\r
+I195WT 38 249 17 0.193 0.495 AMW\r
+  0.0020     10.548\r
+  0.018      42.653\r
+  0.046      136.214\r
+  0.064      179.784\r
+  0.072      191.248\r
+  0.078      197.211\r
+  0.088      198.587\r
+  0.126      198.587\r
+  0.175      207.759\r
+  0.217      211.887\r
+  0.349      216.931\r
+  0.401      221.059\r
+  0.554      225.646\r
+  0.586      228.856\r
+  0.626      228.48\r
+  0.65       230.232\r
+  1.013      231.607\r
+  1.105      230.691\r
+  1.2        225.187\r
+  1.356      210.511\r
+  1.392      207.759\r
+  1.441      206.842\r
+  1.457      205.007\r
+  1.519      181.159\r
+  1.563      155.936\r
+  1.693      49.992\r
+  1.727      28.435\r
+  1.756      16.512\r
+  1.798      7.338\r
+  1.86       1.376\r
+  1.89       0.0\r
diff --git a/datafiles/thrustcurves/AMW_I220.eng b/datafiles/thrustcurves/AMW_I220.eng
new file mode 100644 (file)
index 0000000..c07bea7
--- /dev/null
@@ -0,0 +1,29 @@
+;Animal Motor Works 38-390\r
+I220SK 38 249 20 0.202 0.495 AMW\r
+  0.0050     12.747\r
+  0.019      45.25\r
+  0.036      79.666\r
+  0.052      125.554\r
+  0.069      162.519\r
+  0.076      169.53\r
+  0.095      174.629\r
+  0.167      176.541\r
+  0.229      191.199\r
+  0.447      235.175\r
+  0.602      260.668\r
+  0.733      288.073\r
+  0.85       302.095\r
+  0.974      301.457\r
+  1.094      289.985\r
+  1.184      268.954\r
+  1.268      240.273\r
+  1.302      219.879\r
+  1.388      177.178\r
+  1.418      147.224\r
+  1.435      127.467\r
+  1.473      91.139\r
+  1.504      65.645\r
+  1.543      40.789\r
+  1.593      19.12\r
+  1.622      10.197\r
+  1.65       0.0\r
diff --git a/datafiles/thrustcurves/AMW_I271.eng b/datafiles/thrustcurves/AMW_I271.eng
new file mode 100644 (file)
index 0000000..0827496
--- /dev/null
@@ -0,0 +1,27 @@
+;\r
+; AMW 38-390\r
+I271BB 38 258 0 0.189 0.493 AMW\r
+0.011 119.530\r
+0.035 213.907\r
+0.050 245.903\r
+0.074 262.705\r
+0.115 269.446\r
+0.225 267.736\r
+0.346 282.929\r
+0.465 296.411\r
+0.584 303.152\r
+0.727 311.504\r
+0.916 318.245\r
+1.054 324.986\r
+1.162 331.400\r
+1.201 326.696\r
+1.225 313.214\r
+1.242 286.249\r
+1.268 240.990\r
+1.294 188.888\r
+1.323 136.833\r
+1.346 87.565\r
+1.368 45.467\r
+1.392 18.523\r
+1.430 0.000\r
+;\r
diff --git a/datafiles/thrustcurves/AMW_I285.eng b/datafiles/thrustcurves/AMW_I285.eng
new file mode 100644 (file)
index 0000000..ea373d5
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+; AMW 38-390\r
+I285GG 38 258 0 0.206 0.515 AMW\r
+0.013 61.575\r
+0.032 119.327\r
+0.055 164.575\r
+0.076 191.004\r
+0.094 201.014\r
+0.139 212.326\r
+0.232 231.247\r
+0.357 258.876\r
+0.456 267.686\r
+0.592 278.998\r
+0.716 289.358\r
+0.841 291.200\r
+0.936 290.310\r
+1.051 285.204\r
+1.139 277.696\r
+1.204 280.199\r
+1.243 278.998\r
+1.265 268.887\r
+1.286 242.559\r
+1.319 187.200\r
+1.359 134.443\r
+1.387 86.702\r
+1.407 52.776\r
+1.428 31.413\r
+1.448 16.337\r
+1.465 5.026\r
+1.480 0.000\r
+;\r
diff --git a/datafiles/thrustcurves/AMW_I315.eng b/datafiles/thrustcurves/AMW_I315.eng
new file mode 100644 (file)
index 0000000..3769d80
--- /dev/null
@@ -0,0 +1,34 @@
+; This file my be used or given away.  All I ask is that this header \r
+; is maintained to give credit to NAR S&T.  Thank you, Jack Kane\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file.  The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited \r
+; number of points (32) allowed with wRASP up to v1.6.\r
+;Animal Motor Works 38-640 \r
+I315SK 38 369 20 0.3829 0.7166 AMW\r
+0.011    314.573\r
+0.030    312.796\r
+0.066    300.786\r
+0.084    300.502\r
+0.120    304.087\r
+0.175    312.998\r
+0.266    324.086\r
+0.356    332.224\r
+0.447    347.855\r
+0.538    371.972\r
+0.629    382.833\r
+0.719    385.552\r
+0.810    385.586\r
+0.901    384.836\r
+0.992    382.296\r
+1.082    378.323\r
+1.173    370.837\r
+1.264    357.564\r
+1.355    347.122\r
+1.445    328.332\r
+1.536    202.733\r
+1.627    90.867\r
+1.718    35.427\r
+1.808    8.192\r
+1.815    0.000\r
diff --git a/datafiles/thrustcurves/AMW_I325.eng b/datafiles/thrustcurves/AMW_I325.eng
new file mode 100644 (file)
index 0000000..7194597
--- /dev/null
@@ -0,0 +1,31 @@
+;Animal Motor Works 38-640 \r
+I325WT 38 370 17 0.317 0.712 AMW\r
+  0.014      68.710\r
+  0.022      113.038\r
+  0.026      153.671\r
+  0.037      244.545\r
+  0.045      299.216\r
+  0.055      330.246\r
+  0.065      350.194\r
+  0.079      365.709\r
+  0.094      376.79\r
+  0.124      381.963\r
+  0.185      373.836\r
+  0.252      373.836\r
+  0.35       381.224\r
+  0.47       382.701\r
+  0.622      388.611\r
+  1.102      384.179\r
+  1.366      364.971\r
+  1.379      360.537\r
+  1.415      331.724\r
+  1.49       223.119\r
+  1.505      211.298\r
+  1.551      187.657\r
+  1.592      162.538\r
+  1.688      80.529\r
+  1.726      50.978\r
+  1.775      27.336\r
+  1.806      16.993\r
+  1.834      9.605\r
+  1.901      0.0\r
diff --git a/datafiles/thrustcurves/AMW_I375.eng b/datafiles/thrustcurves/AMW_I375.eng
new file mode 100644 (file)
index 0000000..92aa13e
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;Animal Motor Works 38-640 \r
+I375GG 38 369 20 0.3936 0.7338 AMW\r
+0.013  223.878\r
+0.045  273.929\r
+0.092  312.421\r
+0.140  334.383\r
+0.219  357.983\r
+0.298  381.992\r
+0.377  410.267\r
+0.457  431.141\r
+0.536  454.458\r
+0.615  476.825\r
+0.694  495.473\r
+0.773  504.665\r
+0.852  510.942\r
+0.931  511.972\r
+1.011  489.639\r
+1.090  441.350\r
+1.169  392.762\r
+1.248  354.753\r
+1.327  292.385\r
+1.406  177.309\r
+1.486  63.879\r
+1.565  14.901\r
+1.583  0.000\r
diff --git a/datafiles/thrustcurves/AMW_J357.eng b/datafiles/thrustcurves/AMW_J357.eng
new file mode 100644 (file)
index 0000000..b387aa7
--- /dev/null
@@ -0,0 +1,35 @@
+; AMW Animal Motor Works  fixed by dberez 12/08/03\r
+;\r
+;Animal Motor Works J357 White Wolf\r
+J357WW 54 326 0 0.5481 1.2101 AMW\r
+0.02 129.64\r
+0.03 205.95\r
+0.05 265.00\r
+0.06 316.51\r
+0.09 326.05\r
+0.13 314.60\r
+0.18 301.25\r
+0.24 299.34\r
+0.35 312.69\r
+0.50 326.05\r
+0.66 333.68\r
+0.87 345.13\r
+1.07 358.48\r
+1.46 383.18\r
+1.77 398.45\r
+1.86 400.36\r
+1.98 402.35\r
+2.18 398.45\r
+2.29 390.82\r
+2.41 369.93\r
+2.51 354.67\r
+2.55 352.76\r
+2.60 347.03\r
+2.65 335.59\r
+2.69 310.79\r
+2.75 249.73\r
+2.81 175.43\r
+2.84 108.65\r
+2.90 53.38\r
+2.92 20.98\r
+2.95 0.00\r
diff --git a/datafiles/thrustcurves/AMW_J365.eng b/datafiles/thrustcurves/AMW_J365.eng
new file mode 100644 (file)
index 0000000..05fe775
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;Animal Motor Works 54-1400 \r
+J365SK 54 403 0 0.7571 1.4593 AMW\r
+0.029  389.731\r
+0.123  360.219\r
+0.218  334.200\r
+0.376  326.150\r
+0.534  334.217\r
+0.692  341.669\r
+0.850  347.676\r
+1.007  359.408\r
+1.165  370.043\r
+1.323  383.343\r
+1.481  399.248\r
+1.639  417.477\r
+1.797  443.735\r
+1.955  472.683\r
+2.112  501.668\r
+2.270  497.077\r
+2.428  425.371\r
+2.586  349.017\r
+2.744  262.068\r
+2.902  107.073\r
+3.060  41.821\r
+3.157  0.000\r
diff --git a/datafiles/thrustcurves/AMW_J370.eng b/datafiles/thrustcurves/AMW_J370.eng
new file mode 100644 (file)
index 0000000..78837f3
--- /dev/null
@@ -0,0 +1,44 @@
+;\r
+;Animal Motor Works 54-1050\r
+;AMW J370GG RASP.ENG file made from NAR data\r
+;File produced FEB 20, 2003\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+J370GG 54 326 100 0.5983 1.2491 Animal_Motor_Works \r
+0.008 185.496\r
+0.024 149.516\r
+0.063 225.273\r
+0.087 272.647\r
+0.122 304.829\r
+0.158 304.829\r
+0.273 335.212\r
+0.431 363.796\r
+0.573 390.381\r
+0.707 413.168\r
+0.877 428.459\r
+1.019 441.852\r
+1.126 441.852\r
+1.224 458.46\r
+1.284 443.951\r
+1.386 440.153\r
+1.572 438.454\r
+1.651 438.554\r
+1.813 417.765\r
+2.022 404.673\r
+2.141 385.883\r
+2.212 385.883\r
+2.255 374.59\r
+2.299 387.882\r
+2.362 357.599\r
+2.401 384.184\r
+2.421 348.204\r
+2.457 316.122\r
+2.559 251.758\r
+2.697 115.635\r
+2.753 45.624\r
+2.82 0\r
diff --git a/datafiles/thrustcurves/AMW_J400.eng b/datafiles/thrustcurves/AMW_J400.eng
new file mode 100644 (file)
index 0000000..52e017f
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+;AMW J400 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+J400RR 54 326 100 0.558 1.2314 Animal_Motor_Works \r
+0.043 246.55\r
+0.06 317.381\r
+0.081 344.709\r
+0.107 358.372\r
+0.15 356.06\r
+0.201 365.204\r
+0.308 392.532\r
+0.568 435.734\r
+0.863 458.339\r
+1.094 469.24\r
+1.209 467.18\r
+1.466 460.148\r
+1.705 443.972\r
+1.923 423.275\r
+2.132 409.411\r
+2.303 413.831\r
+2.402 420.563\r
+2.47 413.831\r
+2.517 395.445\r
+2.543 347.421\r
+2.568 265.238\r
+2.598 128.198\r
+2.615 68.801\r
+2.632 25.398\r
+2.66 0\r
diff --git a/datafiles/thrustcurves/AMW_J440.eng b/datafiles/thrustcurves/AMW_J440.eng
new file mode 100644 (file)
index 0000000..40adabd
--- /dev/null
@@ -0,0 +1,27 @@
+;Animal Motor Works 38-640 \r
+J440BB 38 369 20 0.3853 0.6985 AMW\r
+0.007  468.505\r
+0.022  509.996\r
+0.037  527.687\r
+0.052  532.792\r
+0.082  530.181\r
+0.127  525.586\r
+0.202  521.566\r
+0.277  519.840\r
+0.352  521.522\r
+0.426  525.414\r
+0.501  531.248\r
+0.576  538.724\r
+0.651  541.761\r
+0.726  538.508\r
+0.801  531.072\r
+0.876  516.175\r
+0.950  494.942\r
+1.025  477.251\r
+1.100  433.297\r
+1.175  313.900\r
+1.250  187.467\r
+1.325  101.546\r
+1.400  45.751\r
+1.474  22.083\r
+1.497  0.000\r
diff --git a/datafiles/thrustcurves/AMW_J450.eng b/datafiles/thrustcurves/AMW_J450.eng
new file mode 100644 (file)
index 0000000..01a2efa
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+;AMW J450 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+J450 54 326 P .5331 1.1964 AMW\r
+   0.009 251.586\r
+   0.016 376.074\r
+   0.030 413.450\r
+   0.051 430.832\r
+   0.094 423.296\r
+   0.162 413.149\r
+   0.262 395.566\r
+   0.402 420.182\r
+   0.495 444.898\r
+   0.805 504.078\r
+   1.048 536.028\r
+   1.223 550.597\r
+   1.299 563.180\r
+   1.334 555.319\r
+   1.470 560.042\r
+   1.588 559.841\r
+   1.764 546.980\r
+   1.921 516.838\r
+   1.993 496.743\r
+   2.025 499.154\r
+   2.047 479.160\r
+   2.086 414.354\r
+   2.115 344.525\r
+   2.141 252.290\r
+   2.177 140.161\r
+   2.213 82.780\r
+   2.239 50.347\r
+   2.271 27.861\r
+   2.296 12.860\r
+   2.330 0.000\r
diff --git a/datafiles/thrustcurves/AMW_J450_1.eng b/datafiles/thrustcurves/AMW_J450_1.eng
new file mode 100644 (file)
index 0000000..5b444e7
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Animal Motor Works J450 Super Tiger\r
+J450ST 54 326 0 0.5331 1.1964 AMW\r
+0.009 251.586\r
+0.016 376.074\r
+0.030 413.450\r
+0.051 430.832\r
+0.094 423.296\r
+0.162 413.149\r
+0.262 395.566\r
+0.402 420.182\r
+0.495 444.898\r
+0.805 504.078\r
+1.048 536.028\r
+1.223 550.597\r
+1.299 563.180\r
+1.334 555.319\r
+1.470 560.042\r
+1.588 559.841\r
+1.764 546.980\r
+1.921 516.838\r
+1.993 496.743\r
+2.025 499.154\r
+2.047 479.160\r
+2.086 414.354\r
+2.115 344.525\r
+2.141 252.290\r
+2.177 140.161\r
+2.213 82.780\r
+2.239 50.347\r
+2.271 27.861\r
+2.296 12.860\r
+2.330 0.000\r
diff --git a/datafiles/thrustcurves/AMW_J480.eng b/datafiles/thrustcurves/AMW_J480.eng
new file mode 100644 (file)
index 0000000..0eca688
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;AMW J480 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+J480BB 54 326 100 0.556 1.2131 Animal_Motor_Works \r
+0.015 225.429\r
+0.041 348.18\r
+0.071 388.127\r
+0.194 422.453\r
+0.385 459.49\r
+0.699 502.347\r
+0.968 528.042\r
+1.2 536.573\r
+1.454 543.15\r
+1.674 533.763\r
+1.887 522.321\r
+2.044 519.41\r
+2.108 525.131\r
+2.164 528.042\r
+2.197 488.095\r
+2.25 419.543\r
+2.283 333.928\r
+2.328 231.15\r
+2.354 176.95\r
+2.392 111.309\r
+2.418 68.501\r
+2.436 37.106\r
+2.49 0\r
diff --git a/datafiles/thrustcurves/AMW_J500.eng b/datafiles/thrustcurves/AMW_J500.eng
new file mode 100644 (file)
index 0000000..f5a0889
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;J500ST entered by Tim Van Milligan\r
+;For RockSim - http://www.rocksim.com\r
+;Based on TRA Certification paperwork from 06-01-2002\r
+;Initial Mass from Jim Robinson at AMW\r
+;Not approved by TRA or AMW.\r
+J500ST 38 370 20 0.3265 0.744 Animal_Motor_Works \r
+0.006 444.822\r
+0.025 475.651\r
+0.04 418.397\r
+0.053 466.843\r
+0.059 409.589\r
+0.071 458.035\r
+0.077 409.589\r
+0.1 444.822\r
+0.127 506.481\r
+0.204 590.16\r
+0.25 644.992\r
+0.3 678.244\r
+0.34 709.073\r
+0.402 735.498\r
+0.445 766.327\r
+0.516 783.944\r
+0.6 787.335\r
+0.637 770.732\r
+0.68 744.306\r
+0.76 620.989\r
+0.859 475.651\r
+1.00464 303.888\r
+1.122 171.763\r
+1.227 52.8502\r
+1.3 0\r
diff --git a/datafiles/thrustcurves/AMW_K1000.eng b/datafiles/thrustcurves/AMW_K1000.eng
new file mode 100644 (file)
index 0000000..09d0d32
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+;AMW K1000 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K1000SK 54 728 100 1.297 2.556 Animal_Motor_Works \r
+0.019 1155.06\r
+0.045 1426.12\r
+0.094 1248.23\r
+0.161 1112.99\r
+0.239 1128.02\r
+0.343 1113.99\r
+0.377 1149.05\r
+0.44 1121\r
+0.544 1221.18\r
+0.633 1178.11\r
+0.674 1221.18\r
+0.737 1193.13\r
+0.883 1200.14\r
+1.009 1194.13\r
+1.057 1236.21\r
+1.188 1137.03\r
+1.299 1145.05\r
+1.396 1087.94\r
+1.516 954.104\r
+1.631 855.228\r
+1.717 827.077\r
+1.777 650.061\r
+1.848 465.932\r
+1.93 303.141\r
+2.023 147.463\r
+2.083 83.879\r
+2.132 41.484\r
+2.18 0\r
diff --git a/datafiles/thrustcurves/AMW_K1075.eng b/datafiles/thrustcurves/AMW_K1075.eng
new file mode 100644 (file)
index 0000000..6a1cbb4
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+;Animal Motor Works K1075 RASP.ENG file made from NAR data\r
+;File produced Feb 22, 2003\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K1075GG 54 726 100 1.3999 2.6658 Animal_Motor_Works \r
+0.009 672.664\r
+0.015 963.511\r
+0.022 860.518\r
+0.047 987.857\r
+0.075 975.835\r
+0.106 921.332\r
+0.215 958.001\r
+0.529 1092.05\r
+0.878 1220.29\r
+1.077 1269.39\r
+1.158 1311.47\r
+1.235 1293.43\r
+1.448 1330.5\r
+1.577 1318.48\r
+1.672 1319.48\r
+1.721 1337.52\r
+1.759 1337.52\r
+1.805 1337.52\r
+1.829 1331.5\r
+1.856 1384.67\r
+1.889 1277.4\r
+1.906 1216.29\r
+1.938 1052.98\r
+1.96 871.338\r
+1.988 659.239\r
+2.027 453.352\r
+2.062 301.967\r
+2.115 138.46\r
+2.168 41.608\r
+2.2 0\r
diff --git a/datafiles/thrustcurves/AMW_K365.eng b/datafiles/thrustcurves/AMW_K365.eng
new file mode 100644 (file)
index 0000000..25fe1b1
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;AMW K365RR RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K365RR 75 111 100 0.946 2.3456 Animal_Motor_Works \r
+0.049 138.157\r
+0.068 381.241\r
+0.084 454.75\r
+0.106 481.536\r
+0.164 488.182\r
+0.291 514.867\r
+0.435 545.982\r
+0.666 561.49\r
+0.868 565.73\r
+1.082 565.518\r
+1.296 550.111\r
+1.591 529.871\r
+1.805 509.731\r
+1.828 536.517\r
+1.886 498.554\r
+2.124 467.237\r
+2.501 411.35\r
+2.924 328.677\r
+3.296 241.573\r
+3.638 172.293\r
+3.969 100.798\r
+4.195 56.098\r
+4.265 51.607\r
+4.346 35.959\r
+4.433 15.859\r
+4.51 0\r
diff --git a/datafiles/thrustcurves/AMW_K450.eng b/datafiles/thrustcurves/AMW_K450.eng
new file mode 100644 (file)
index 0000000..ece0bac
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;AMW K450BB RASP.ENG file made from NAR published data\r
+;File produced Aug 19, 2003\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to NAR S&T. Thank you, Jack Kane.\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K450BB 75 302 100 0.8816 2.8349 Animal_Motor_Works \r
+0.03 78.903\r
+0.045 227.9\r
+0.064 449.955\r
+0.069 508.417\r
+0.094 555.187\r
+0.151 563.956\r
+0.362 625.442\r
+0.562 651.85\r
+0.825 660.62\r
+1.134 652.254\r
+1.453 626.147\r
+1.793 594.296\r
+2.113 538.958\r
+2.458 469.106\r
+2.798 384.538\r
+3.165 276.686\r
+3.201 279.609\r
+3.325 232.94\r
+3.51 171.757\r
+3.732 107.65\r
+3.861 58.018\r
+3.959 40.55\r
+4.036 20.149\r
+4.11 0\r
diff --git a/datafiles/thrustcurves/AMW_K470.eng b/datafiles/thrustcurves/AMW_K470.eng
new file mode 100644 (file)
index 0000000..bd780c1
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;AMW K470ST RASP.ENG file made from Tripoli published data\r
+;File produced May 15, 2004\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to the people who produced the data.\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K470ST 75 302 100 0.826 2.779 Animal_Motor_Works \r
+0.028 699.309\r
+0.039 799.337\r
+0.09 765.845\r
+0.157 770.311\r
+0.258 785.941\r
+0.41 804\r
+0.572 794.425\r
+0.707 794.425\r
+0.886 792.192\r
+0.998 783.261\r
+1.15 752.002\r
+1.318 709.579\r
+1.447 655.992\r
+1.593 595.707\r
+1.728 522.025\r
+1.885 444.101\r
+2.092 354.923\r
+2.356 270.167\r
+2.664 187.554\r
+2.945 131.734\r
+3.27 78.058\r
+3.433 55.686\r
+3.478 48.987\r
+3.556 28.909\r
+3.7 0\r
diff --git a/datafiles/thrustcurves/AMW_K475.eng b/datafiles/thrustcurves/AMW_K475.eng
new file mode 100644 (file)
index 0000000..8551ed5
--- /dev/null
@@ -0,0 +1,38 @@
+;\r
+;Animal Motor Works K475 RASP.ENG file made from NAR data\r
+;File produced Feb 22, 2003\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K475WW 54 403 100 0.7286 1.4925 Animal_Motor_Works \r
+0.022 127.831\r
+0.041 386.016\r
+0.063 548.326\r
+0.096 521.308\r
+0.134 499.129\r
+0.18 486.83\r
+0.285 486.83\r
+0.478 501.649\r
+0.731 523.727\r
+1.096 553.266\r
+1.433 577.962\r
+1.601 588.29\r
+1.756 582.704\r
+1.895 580.284\r
+1.958 575.344\r
+2.063 550.746\r
+2.209 518.788\r
+2.344 477.051\r
+2.495 417.974\r
+2.561 354.058\r
+2.582 334.399\r
+2.599 331.98\r
+2.62 297.501\r
+2.67 226.226\r
+2.707 157.37\r
+2.74 98.353\r
+2.799 49.176\r
+2.853 17.208\r
+2.94 0\r
diff --git a/datafiles/thrustcurves/AMW_K530.eng b/datafiles/thrustcurves/AMW_K530.eng
new file mode 100644 (file)
index 0000000..e1c460f
--- /dev/null
@@ -0,0 +1,43 @@
+;\r
+;Animal Motor Works 54-1400\r
+;AMW K530GG RASP.ENG file made from NAR data\r
+;File produced Feb 25, 2003\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K530GG 54 403 1000 0.7967 1.616 Animal_Motor_Works \r
+0.013 129.764\r
+0.054 171.852\r
+0.096 284.122\r
+0.138 392.892\r
+0.171 455.975\r
+0.217 501.662\r
+0.238 498.063\r
+0.326 508.66\r
+0.542 564.745\r
+0.755 613.831\r
+1.01 645.423\r
+1.17 657.23\r
+1.273 648.922\r
+1.51 638.425\r
+1.656 634.925\r
+1.702 606.833\r
+1.803 606.833\r
+1.857 585.839\r
+1.936 589.338\r
+1.974 575.242\r
+2.015 589.338\r
+2.04 564.745\r
+2.132 536.652\r
+2.207 540.251\r
+2.291 522.656\r
+2.357 487.566\r
+2.42 375.297\r
+2.478 242.033\r
+2.529 140.361\r
+2.583 66.651\r
+2.66 0\r
diff --git a/datafiles/thrustcurves/AMW_K555.eng b/datafiles/thrustcurves/AMW_K555.eng
new file mode 100644 (file)
index 0000000..522291a
--- /dev/null
@@ -0,0 +1,33 @@
+;Animal Motor Works 54-1750 K555 skidmark\r
+;File provide by Joel Rogers of AMW\r
+K555SK 54 492 0 0.8707 1.7343 AMW\r
+0.063  507.328\r
+0.144  535.181\r
+0.226  559.826\r
+0.308  585.793\r
+0.389  607.239\r
+0.471  629.034\r
+0.553  664.586\r
+0.634  683.688\r
+0.716  697.625\r
+0.798  719.618\r
+0.879  756.521\r
+0.961  777.700\r
+1.043  789.004\r
+1.124  797.934\r
+1.206  801.689\r
+1.288  804.331\r
+1.369  799.414\r
+1.451  768.014\r
+1.533  704.469\r
+1.614  641.709\r
+1.696  568.727\r
+1.778  481.013\r
+1.859  401.614\r
+1.941  333.897\r
+2.023  277.226\r
+2.104  205.009\r
+2.186  129.425\r
+2.268  73.717\r
+2.349  22.380\r
+2.368    0.000\r
diff --git a/datafiles/thrustcurves/AMW_K560.eng b/datafiles/thrustcurves/AMW_K560.eng
new file mode 100644 (file)
index 0000000..cebab56
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+;AMW K560 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K560RR 54 430 100 0.75 1.5866 Animal_Motor_Works \r
+0.023 229.13\r
+0.046 415.135\r
+0.059 485.264\r
+0.078 512.268\r
+0.106 525.67\r
+0.154 523.05\r
+0.211 528.39\r
+0.261 536.451\r
+0.369 560.734\r
+0.511 587.738\r
+0.657 603.86\r
+0.77 612.022\r
+1.096 625.75\r
+1.358 620.083\r
+1.627 612.022\r
+1.839 603.86\r
+2.057 590.459\r
+2.218 598.52\r
+2.335 609.301\r
+2.385 601.24\r
+2.407 585.018\r
+2.426 533.831\r
+2.467 385.511\r
+2.507 283.037\r
+2.542 164.441\r
+2.576 67.399\r
+2.595 29.653\r
+2.62 0\r
diff --git a/datafiles/thrustcurves/AMW_K570.eng b/datafiles/thrustcurves/AMW_K570.eng
new file mode 100644 (file)
index 0000000..7c32b5d
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;Animal Motor Works K570 White Wolf\r
+K570WW 54 492 0 0.9146 1.8151 AMW\r
+0.020 364.42\r
+0.030 664.79\r
+0.051 751.47\r
+0.071 745.81\r
+0.096 705.25\r
+0.137 674.93\r
+0.284 661.38\r
+0.528 651.24\r
+0.913 644.51\r
+1.192 651.24\r
+1.430 651.24\r
+1.649 651.24\r
+1.872 644.51\r
+2.176 624.23\r
+2.318 600.64\r
+2.394 597.33\r
+2.455 546.63\r
+2.501 485.89\r
+2.562 421.84\r
+2.597 340.83\r
+2.638 266.54\r
+2.734 175.48\r
+2.836 97.86\r
+2.927 47.24\r
+3.040 0.00\r
diff --git a/datafiles/thrustcurves/AMW_K600.eng b/datafiles/thrustcurves/AMW_K600.eng
new file mode 100644 (file)
index 0000000..0d1e531
--- /dev/null
@@ -0,0 +1,40 @@
+;\r
+; Animal Motor Works K600 RASP.ENG file made from NAR data\r
+; File produced August 22, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K600 75 368 P 1.2233 2.9129 AMW\r
+   0.010 412.229\r
+   0.029 522.21\r
+   0.059 547.215\r
+   0.083 524.8\r
+   0.122 497.305\r
+   0.181 484.852\r
+   0.333 495.113\r
+   0.690 560.464\r
+   1.195 643.548\r
+   1.400 673.833\r
+   1.420 708.799\r
+   1.508 701.427\r
+   1.591 721.551\r
+   1.782 731.712\r
+   2.017 752.035\r
+   2.174 756.816\r
+   2.257 765.2\r
+   2.502 766.44\r
+   2.727 752.931\r
+   2.918 738.187\r
+   3.143 705.91\r
+   3.408 643.847\r
+   3.603 569.131\r
+   3.692 526.793\r
+   3.745 439.426\r
+   3.799 289.596\r
+   3.883 112.272\r
+   3.922 64.862\r
+   3.971 37.437\r
+   3.995 22.474\r
+   4.070 0\r
diff --git a/datafiles/thrustcurves/AMW_K600_1.eng b/datafiles/thrustcurves/AMW_K600_1.eng
new file mode 100644 (file)
index 0000000..2d078aa
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+;Animal Motor Works K600 White Wolf\r
+K600WW 75 368 0 1.2233 2.9129 AMW\r
+0.010 412.229\r
+0.029 522.21\r
+0.059 547.215\r
+0.083 524.8\r
+0.122 497.305\r
+0.181 484.852\r
+0.333 495.113\r
+0.690 560.464\r
+1.195 643.548\r
+1.400 673.833\r
+1.420 708.799\r
+1.508 701.427\r
+1.591 721.551\r
+1.782 731.712\r
+2.017 752.035\r
+2.174 756.816\r
+2.257 765.2\r
+2.502 766.44\r
+2.727 752.931\r
+2.918 738.187\r
+3.143 705.91\r
+3.408 643.847\r
+3.603 569.131\r
+3.692 526.793\r
+3.745 439.426\r
+3.799 289.596\r
+3.883 112.272\r
+3.922 64.862\r
+3.971 37.437\r
+3.995 22.474\r
+4.070 0\r
diff --git a/datafiles/thrustcurves/AMW_K605.eng b/datafiles/thrustcurves/AMW_K605.eng
new file mode 100644 (file)
index 0000000..da7701a
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+;AMW K605 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K605RR 75 368 100 1.231 2.7688 Animal_Motor_Works \r
+0.03 165.845\r
+0.053 309.12\r
+0.077 361.916\r
+0.142 392.042\r
+0.527 501.412\r
+0.988 606.905\r
+1.515 682.37\r
+2.101 730.593\r
+2.355 737.58\r
+2.692 731.289\r
+3 712.497\r
+3.361 671.036\r
+3.503 663.479\r
+3.586 659.701\r
+3.645 633.353\r
+3.692 573\r
+3.734 444.838\r
+3.775 297.785\r
+3.828 162.066\r
+3.864 98.015\r
+3.905 41.471\r
+3.95 0\r
diff --git a/datafiles/thrustcurves/AMW_K650.eng b/datafiles/thrustcurves/AMW_K650.eng
new file mode 100644 (file)
index 0000000..e3789b2
--- /dev/null
@@ -0,0 +1,38 @@
+; Animal Motor Works 54-1750\r
+; AMW K650RR RASP.ENG file made from NAR published data\r
+; File produced April 19, 2004\r
+; This file my be used or given away.  All I ask is that this header \r
+; is maintained to give credit to NAR S&T.  Thank you, Jack Kane\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file.  The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited \r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K650RR    54   492   0   0.931   1.8087   AMW\r
+0.022    308.257\r
+0.045    566.480\r
+0.058    620.440\r
+0.081    639.668\r
+0.135    639.668\r
+0.229    643.494\r
+0.351    662.823\r
+0.594    701.380\r
+0.810    724.434\r
+0.999    743.763\r
+1.151    751.220\r
+1.381    747.588\r
+1.610    736.001\r
+1.835    709.031\r
+2.073    685.876\r
+2.244    674.400\r
+2.334    682.051\r
+2.429    685.876\r
+2.469    666.648\r
+2.528    597.285\r
+2.573    481.714\r
+2.609    358.391\r
+2.631    250.471\r
+2.681    146.477\r
+2.721    65.507\r
+2.748    23.124\r
+2.770    0.000\r
diff --git a/datafiles/thrustcurves/AMW_K670.eng b/datafiles/thrustcurves/AMW_K670.eng
new file mode 100644 (file)
index 0000000..910d0dc
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+; AMW K670 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K670 54 492 P- 1.0140 1.9145 AMW\r
+   0.016 294.05\r
+   0.035 398.577\r
+   0.086 506.292\r
+   0.153 496.428\r
+   0.264 506.093\r
+   0.461 558.108\r
+   0.722 629.553\r
+   0.983 688.044\r
+   1.116 714.051\r
+   1.193 785.795\r
+   1.409 788.784\r
+   1.737 804.56\r
+   2.074 781.41\r
+   2.195 764.87\r
+   2.226 781.211\r
+   2.277 764.77\r
+   2.398 751.517\r
+   2.440 744.941\r
+   2.468 718.834\r
+   2.484 666.521\r
+   2.525 418.107\r
+   2.551 218.818\r
+   2.573 120.768\r
+   2.595 52.143\r
+   2.620 0\r
diff --git a/datafiles/thrustcurves/AMW_K670_1.eng b/datafiles/thrustcurves/AMW_K670_1.eng
new file mode 100644 (file)
index 0000000..fecc65a
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;Animal Motor Works K670 Green Gorilla\r
+K670GG 54 492 0 1.0140 1.9145 AMW\r
+0.016 294.05\r
+0.035 398.577\r
+0.086 506.292\r
+0.153 496.428\r
+0.264 506.093\r
+0.461 558.108\r
+0.722 629.553\r
+0.983 688.044\r
+1.116 714.051\r
+1.193 785.795\r
+1.409 788.784\r
+1.737 804.56\r
+2.074 781.41\r
+2.195 764.87\r
+2.226 781.211\r
+2.277 764.77\r
+2.398 751.517\r
+2.440 744.941\r
+2.468 718.834\r
+2.484 666.521\r
+2.525 418.107\r
+2.551 218.818\r
+2.573 120.768\r
+2.595 52.143\r
+2.620 0\r
diff --git a/datafiles/thrustcurves/AMW_K700.eng b/datafiles/thrustcurves/AMW_K700.eng
new file mode 100644 (file)
index 0000000..5529b06
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;AMW K700 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K700BB 54 430 100 0.754 1.4831 Animal_Motor_Works \r
+0.014 359.559\r
+0.022 625.425\r
+0.03 737.756\r
+0.047 771.505\r
+0.082 786.516\r
+0.106 771.505\r
+0.144 775.233\r
+0.272 786.516\r
+0.477 812.71\r
+0.693 842.632\r
+0.97 847.06\r
+1.283 838.904\r
+1.516 816.438\r
+1.706 801.427\r
+1.779 793.972\r
+1.811 775.233\r
+1.841 726.573\r
+1.873 625.425\r
+1.909 509.367\r
+1.95 393.208\r
+1.982 337.093\r
+2.035 292.16\r
+2.073 228.489\r
+2.111 153.535\r
+2.155 86.137\r
+2.193 37.446\r
+2.24 0\r
diff --git a/datafiles/thrustcurves/AMW_K800.eng b/datafiles/thrustcurves/AMW_K800.eng
new file mode 100644 (file)
index 0000000..19f99de
--- /dev/null
@@ -0,0 +1,35 @@
+; This file my be used or given away.  All I ask is that this header \r
+; is maintained to give credit to NAR S&T.  Thank you, Jack Kane\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file.  The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited \r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K800BB    54   492   0   0.9140   1.7866   AMW\r
+0.017    516.316\r
+0.035    745.845\r
+0.046    817.592\r
+0.090    860.560\r
+0.191    889.338\r
+0.270    908.424\r
+0.438    918.017\r
+0.689    945.892\r
+0.996    955.090\r
+1.325    922.713\r
+1.557    894.035\r
+1.726    874.949\r
+1.849    884.542\r
+1.920    894.035\r
+1.954    894.035\r
+1.984    855.863\r
+2.011    741.048\r
+2.049    592.859\r
+2.079    492.433\r
+2.113    430.280\r
+2.154    377.719\r
+2.196    329.854\r
+2.237    243.818\r
+2.275    152.986\r
+2.309    71.716\r
+2.339    33.465\r
+2.380    0.000\r
diff --git a/datafiles/thrustcurves/AMW_K950.eng b/datafiles/thrustcurves/AMW_K950.eng
new file mode 100644 (file)
index 0000000..0ea38ba
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+; AMW K950 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K950 54 492 P .8874 1.7949 AMW\r
+   0.011 771.836\r
+   0.025 1204.520\r
+   0.039 1083.244\r
+   0.053 1158.054\r
+   0.067 1036.364\r
+   0.085 1110.176\r
+   0.099 1022.399\r
+   0.135 982.102\r
+   0.220 968.835\r
+   0.404 1010.430\r
+   0.566 1044.343\r
+   0.701 1079.254\r
+   0.867 1106.186\r
+   0.995 1134.115\r
+   1.211 1114.166\r
+   1.313 1101.199\r
+   1.430 1067.285\r
+   1.529 1020.404\r
+   1.579 993.772\r
+   1.642 892.430\r
+   1.674 818.119\r
+   1.717 757.273\r
+   1.738 621.918\r
+   1.766 466.313\r
+   1.791 351.306\r
+   1.823 249.864\r
+   1.865 175.553\r
+   1.908 87.696\r
+   1.943 33.654\r
+   1.970 0.000\r
diff --git a/datafiles/thrustcurves/AMW_K950_1.eng b/datafiles/thrustcurves/AMW_K950_1.eng
new file mode 100644 (file)
index 0000000..78854cc
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Animal Motor Works K950 Super Tiger\r
+K950ST 54 492 0 .8874 1.7949 AMW\r
+0.011 771.836\r
+0.025 1204.520\r
+0.039 1083.244\r
+0.053 1158.054\r
+0.067 1036.364\r
+0.085 1110.176\r
+0.099 1022.399\r
+0.135 982.102\r
+0.220 968.835\r
+0.404 1010.430\r
+0.566 1044.343\r
+0.701 1079.254\r
+0.867 1106.186\r
+0.995 1134.115\r
+1.211 1114.166\r
+1.313 1101.199\r
+1.430 1067.285\r
+1.529 1020.404\r
+1.579 993.772\r
+1.642 892.430\r
+1.674 818.119\r
+1.717 757.273\r
+1.738 621.918\r
+1.766 466.313\r
+1.791 351.306\r
+1.823 249.864\r
+1.865 175.553\r
+1.908 87.696\r
+1.943 33.654\r
+1.970 0.000\r
diff --git a/datafiles/thrustcurves/AMW_K975.eng b/datafiles/thrustcurves/AMW_K975.eng
new file mode 100644 (file)
index 0000000..1d4be16
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;Animal Motor Works K975 White Wolf\r
+K975WW 54 728 0 1.357 2.5985 AMW\r
+0.017 526.644\r
+0.029 901.850\r
+0.038 1098.918\r
+0.046 1151.722\r
+0.076 1112.867\r
+0.130 1060.063\r
+0.219 1053.089\r
+0.336 1053.089\r
+0.479 1059.066\r
+0.609 1091.944\r
+0.866 1136.778\r
+1.046 1176.630\r
+1.164 1175.634\r
+1.202 1228.437\r
+1.239 1208.511\r
+1.315 1215.486\r
+1.353 1267.293\r
+1.387 1228.437\r
+1.487 1241.389\r
+1.538 1260.319\r
+1.634 1290.900\r
+1.723 1281.241\r
+1.794 1266.297\r
+1.836 1207.515\r
+1.933 1049.103\r
+1.992 851.437\r
+2.080 666.923\r
+2.118 640.521\r
+2.193 462.582\r
+2.269 212.311\r
+2.378 119.854\r
+2.510 0.000\r
diff --git a/datafiles/thrustcurves/AMW_L1060.eng b/datafiles/thrustcurves/AMW_L1060.eng
new file mode 100644 (file)
index 0000000..b382e11
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+; AMW L1060 RASP.ENG file made from NAR published data\r
+; File produced August 22, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+L1060 75 497 P- 1.9188 3.9388 AMW\r
+   0.020 258.773\r
+   0.024 368.235\r
+   0.032 328.386\r
+   0.076 427.96\r
+   0.100 567.284\r
+   0.116 751.352\r
+   0.128 791.202\r
+   0.169 816.071\r
+   0.225 816.071\r
+   0.309 875.795\r
+   0.518 985.257\r
+   0.763 1079.639\r
+   1.024 1174.519\r
+   1.308 1229.45\r
+   1.606 1288.375\r
+   1.782 1298.25\r
+   1.983 1293.369\r
+   2.256 1239.437\r
+   2.525 1184.506\r
+   2.822 1129.576\r
+   3.038 1069.651\r
+   3.111 1044.683\r
+   3.135 995.145\r
+   3.183 835.946\r
+   3.239 552.303\r
+   3.299 268.661\r
+   3.327 164.193\r
+   3.339 84.593\r
+   3.360 44.783\r
+   3.400 0\r
diff --git a/datafiles/thrustcurves/AMW_L1060_1.eng b/datafiles/thrustcurves/AMW_L1060_1.eng
new file mode 100644 (file)
index 0000000..4f83de1
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Animal Motor Works L1060 Green Gorilla\r
+L1060GG 75 497 0 1.9188 3.9388 AMW\r
+0.020 258.773\r
+0.024 368.235\r
+0.032 328.386\r
+0.076 427.96\r
+0.100 567.284\r
+0.116 751.352\r
+0.128 791.202\r
+0.169 816.071\r
+0.225 816.071\r
+0.309 875.795\r
+0.518 985.257\r
+0.763 1079.639\r
+1.024 1174.519\r
+1.308 1229.45\r
+1.606 1288.375\r
+1.782 1298.25\r
+1.983 1293.369\r
+2.256 1239.437\r
+2.525 1184.506\r
+2.822 1129.576\r
+3.038 1069.651\r
+3.111 1044.683\r
+3.135 995.145\r
+3.183 835.946\r
+3.239 552.303\r
+3.299 268.661\r
+3.327 164.193\r
+3.339 84.593\r
+3.360 44.783\r
+3.400 0\r
diff --git a/datafiles/thrustcurves/AMW_L1080.eng b/datafiles/thrustcurves/AMW_L1080.eng
new file mode 100644 (file)
index 0000000..985fc4e
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;AMW L1080 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L1080BB 75 497 100 1.717 3.5922 Animal_Motor_Works \r
+0.024 406.295\r
+0.043 812.489\r
+0.052 895.202\r
+0.088 929.641\r
+0.314 991.55\r
+0.626 1087.69\r
+0.988 1163.44\r
+1.346 1218.99\r
+1.638 1246.25\r
+1.864 1257.91\r
+2.247 1254.84\r
+2.6 1218.99\r
+2.766 1211.92\r
+2.851 1197.78\r
+2.942 1204.85\r
+3.002 1226.06\r
+3.033 1204.85\r
+3.089 1040.23\r
+3.124 874.499\r
+3.15 660.999\r
+3.191 461.336\r
+3.232 275.408\r
+3.268 144.622\r
+3.303 75.744\r
+3.339 41.316\r
+3.39 0\r
diff --git a/datafiles/thrustcurves/AMW_L1100.eng b/datafiles/thrustcurves/AMW_L1100.eng
new file mode 100644 (file)
index 0000000..7ab349d
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;AMW L1100 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L1100RR 75 728 100 1.346 2.5881 Animal_Motor_Works \r
+0.013 681.489\r
+0.029 1116.88\r
+0.041 1196.3\r
+0.079 1210.38\r
+0.147 1203.34\r
+0.257 1218.42\r
+0.366 1225.45\r
+0.567 1254.61\r
+0.824 1282.76\r
+1.059 1311.91\r
+1.267 1340.23\r
+1.459 1311.91\r
+1.622 1297.84\r
+1.713 1290.8\r
+1.785 1268.68\r
+1.83 1218.42\r
+1.886 1080.69\r
+1.969 819.214\r
+2.048 558.24\r
+2.108 376.985\r
+2.156 246.498\r
+2.205 144.963\r
+2.269 72.501\r
+2.35 0\r
diff --git a/datafiles/thrustcurves/AMW_L1111.eng b/datafiles/thrustcurves/AMW_L1111.eng
new file mode 100644 (file)
index 0000000..e8b36e8
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;L1111ST entered by Tim Van Milligan\r
+;For RockSim - http://www.rocksim.com\r
+;Based on TRA Certification paperwork from 06-01-2002\r
+;Initial Mass from Jim Robinson at AMW\r
+;Not approved by TRA or AMW.\r
+L1111ST 75 497 100 1.642 3.517 Animal_Motor_Works \r
+0.015 1023.97\r
+0.1 924.878\r
+0.147 902.857\r
+0.502 1034.98\r
+0.75 1156.1\r
+1.005 1266.2\r
+1.229 1354.29\r
+1.492 1398.33\r
+1.739 1398.33\r
+2.009 1354.29\r
+2.272 1244.18\r
+2.504 1123.07\r
+2.728 968.92\r
+2.782 902.857\r
+2.836 770.732\r
+2.98 363.345\r
+3.053 99.094\r
+3.083 22.021\r
+3.14 0\r
diff --git a/datafiles/thrustcurves/AMW_L1300.eng b/datafiles/thrustcurves/AMW_L1300.eng
new file mode 100644 (file)
index 0000000..f36b59f
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;AMW L1300 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L1300BB 75 728 100 1.314 2.5454 Animal_Motor_Works \r
+0.014 710.467\r
+0.025 1247.64\r
+0.039 1384.13\r
+0.053 1447.83\r
+0.074 1420.53\r
+0.12 1447.83\r
+0.276 1474.12\r
+0.475 1519.61\r
+0.712 1555\r
+0.942 1586.74\r
+1.147 1562.08\r
+1.36 1534.78\r
+1.484 1551.97\r
+1.537 1551.97\r
+1.569 1497.37\r
+1.59 1406.38\r
+1.604 1451.87\r
+1.615 1333.58\r
+1.64 1168.78\r
+1.689 986.687\r
+1.753 767.749\r
+1.824 512.503\r
+1.891 275.512\r
+1.933 147.816\r
+1.987 74.737\r
+2.06 0\r
diff --git a/datafiles/thrustcurves/AMW_L1400.eng b/datafiles/thrustcurves/AMW_L1400.eng
new file mode 100644 (file)
index 0000000..b83ca81
--- /dev/null
@@ -0,0 +1,35 @@
+; @File: SK-75-6000.txt, @Pts-I: 3609, @Pts-O: 31, @Sm: 6, @CO: 5%\r
+; @TI: 4740.56, @TIa: 4732.91, @TIe: 0.0%, @ThMax: 1908.398, @ThAvg: 1382.678, @Tb: 3.423\r
+; Exported using ThrustCurveTool, www.ThrustGear.com, by John DeMar\r
+L1400SK 75 785 P 2.8267 5.1985 AMW\r
+  0.0        68.1234\r
+  0.0040     193.7893\r
+  0.016      690.259\r
+  0.021      814.579\r
+  0.027      900.741\r
+  0.045      997.475\r
+  0.076      1251.156\r
+  0.092      1354.553\r
+  0.107      1405.971\r
+  0.132      1440.082\r
+  0.169      1453.774\r
+  0.368      1397.446\r
+  0.525      1411.875\r
+  0.705      1488.288\r
+  1.082      1734.489\r
+  1.414      1906.629\r
+  1.556      1875.238\r
+  1.766      1882.261\r
+  1.899      1803.008\r
+  2.142      1745.497\r
+  2.34       1659.082\r
+  2.504      1522.458\r
+  2.58       1402.287\r
+  2.819      844.839\r
+  2.847      841.674\r
+  2.893      730.795\r
+  3.068      406.536\r
+  3.176      265.8\r
+  3.425      94.9644\r
+  3.608      0.874524\r
+  3.609      0\r
diff --git a/datafiles/thrustcurves/AMW_L666.eng b/datafiles/thrustcurves/AMW_L666.eng
new file mode 100644 (file)
index 0000000..7379b89
--- /dev/null
@@ -0,0 +1,34 @@
+;Animal Motor Works 75-3500\r
+L666SK 75 497 0 1.8877 3.5344 AMW\r
+0.096  105.880\r
+0.175  509.783\r
+0.312  549.481\r
+0.449  577.319\r
+0.586  602.900\r
+0.722  615.605\r
+0.859  632.540\r
+0.996  652.072\r
+1.133  671.418\r
+1.270  685.671\r
+1.407  701.286\r
+1.543  718.069\r
+1.680  734.116\r
+1.817  753.292\r
+1.954  771.589\r
+2.091  790.453\r
+2.228  819.222\r
+2.364  846.663\r
+2.501  874.629\r
+2.638  890.083\r
+2.775  898.271\r
+2.912  899.312\r
+3.049  881.683\r
+3.185  845.157\r
+3.322  768.451\r
+3.459  672.771\r
+3.596  525.466\r
+3.733  304.694\r
+3.870  86.663\r
+3.968  0.000\r
+;\r
+;\r
diff --git a/datafiles/thrustcurves/AMW_L700.eng b/datafiles/thrustcurves/AMW_L700.eng
new file mode 100644 (file)
index 0000000..0f7f0ce
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+;\r
+L700BB  75.0 368.00 100 1.19310 2.73200 AMW\r
+   0.02     221.87 \r
+   0.03     399.33 \r
+   0.05     467.56 \r
+   0.08     494.89 \r
+   0.13     498.41 \r
+   0.24     535.99 \r
+   0.48     614.67 \r
+   0.77     683.20 \r
+   1.23     755.25 \r
+   1.62     789.72 \r
+   1.92     810.42 \r
+   2.26     821.14 \r
+   2.58     817.85 \r
+   2.91     801.07 \r
+   3.14     773.94 \r
+   3.25     750.13 \r
+   3.32     743.39 \r
+   3.37     729.83 \r
+   3.42     688.83 \r
+   3.46     593.37 \r
+   3.50     484.14 \r
+   3.53     368.18 \r
+   3.57     248.80 \r
+   3.62     149.82 \r
+   3.66      61.13 \r
+   3.72       0.00 \r
diff --git a/datafiles/thrustcurves/AMW_L777.eng b/datafiles/thrustcurves/AMW_L777.eng
new file mode 100644 (file)
index 0000000..0e6c65c
--- /dev/null
@@ -0,0 +1,40 @@
+;\r
+; AMW L777 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+L777 75 497 P 1.7623 3.6987 AMW\r
+   0.025 140.882\r
+   0.064 209.474\r
+   0.108 360.055\r
+   0.204 652.185\r
+   0.360 641.518\r
+   0.373 693.745\r
+   0.418 683.28\r
+   0.528 730.073\r
+   0.670 761.268\r
+   0.761 781.998\r
+   0.787 802.828\r
+   0.871 802.728\r
+   1.065 854.754\r
+   1.338 911.811\r
+   1.668 963.636\r
+   1.914 989.498\r
+   2.115 1000.16\r
+   2.368 962.831\r
+   2.647 926\r
+   2.985 878.603\r
+   3.303 805.143\r
+   3.472 752.815\r
+   3.550 705.72\r
+   3.602 648.26\r
+   3.647 611.631\r
+   3.693 512.409\r
+   3.779 334.897\r
+   3.857 178.216\r
+   3.935 89.379\r
+   3.981 26.687\r
+   4.050 0\r
diff --git a/datafiles/thrustcurves/AMW_L777_1.eng b/datafiles/thrustcurves/AMW_L777_1.eng
new file mode 100644 (file)
index 0000000..a908a0d
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+;Animal Motor Works L777 White Wolf\r
+L777WW 75 497 0 1.7623 3.6987 AMW\r
+0.025 140.882\r
+0.064 209.474\r
+0.108 360.055\r
+0.204 652.185\r
+0.360 641.518\r
+0.373 693.745\r
+0.418 683.28\r
+0.528 730.073\r
+0.670 761.268\r
+0.761 781.998\r
+0.787 802.828\r
+0.871 802.728\r
+1.065 854.754\r
+1.338 911.811\r
+1.668 963.636\r
+1.914 989.498\r
+2.115 1000.16\r
+2.368 962.831\r
+2.647 926\r
+2.985 878.603\r
+3.303 805.143\r
+3.472 752.815\r
+3.550 705.72\r
+3.602 648.26\r
+3.647 611.631\r
+3.693 512.409\r
+3.779 334.897\r
+3.857 178.216\r
+3.935 89.379\r
+3.981 26.687\r
+4.050 0\r
diff --git a/datafiles/thrustcurves/AMW_L900.eng b/datafiles/thrustcurves/AMW_L900.eng
new file mode 100644 (file)
index 0000000..4299eed
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+;AMW L900 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L900RR 75 497 100 1.771 3.5888 Animal_Motor_Works \r
+0.029 464.292\r
+0.053 630.937\r
+0.059 684.506\r
+0.096 702.328\r
+0.133 696.387\r
+0.201 714.311\r
+0.486 803.524\r
+0.777 910.661\r
+1.099 988.093\r
+1.26 1041.16\r
+1.284 1071.37\r
+1.378 1053.24\r
+1.607 1101.57\r
+1.917 1142.86\r
+2.208 1173.56\r
+2.413 1160.98\r
+2.624 1107.62\r
+2.866 976.211\r
+3.053 886.897\r
+3.208 839.27\r
+3.314 827.388\r
+3.382 809.465\r
+3.432 720.252\r
+3.495 547.564\r
+3.57 345.273\r
+3.627 214.273\r
+3.714 77.382\r
+3.79 0\r
diff --git a/datafiles/thrustcurves/AMW_M1350.eng b/datafiles/thrustcurves/AMW_M1350.eng
new file mode 100644 (file)
index 0000000..c3799a6
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;Animal Motor Works M1350 White Wolf\r
+M1350WW  75 781 0 2.92700 5.40300 AMW\r
+0.03 1197.771588\r
+0.04 1465.181058\r
+0.07 1660.167131\r
+0.09 1665.738162\r
+0.16 1587.743733\r
+0.45 1587.743733\r
+0.61 1576.601671\r
+1.86 1649.02507\r
+2.27 1643.454039\r
+2.64 1598.885794\r
+3.18 1504.178273\r
+3.29 1353.760446\r
+3.41 991.643454\r
+3.49 841.2256267\r
+3.62 646.2395543\r
+3.74 428.9693593\r
+3.90 373.2590529\r
+4.22 0\r
diff --git a/datafiles/thrustcurves/AMW_M1480.eng b/datafiles/thrustcurves/AMW_M1480.eng
new file mode 100644 (file)
index 0000000..10efe8a
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+;AMW M1480 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+M1480RR 75 785 100 3 5.5248 Animal_Motor_Works \r
+0.022 713.002\r
+0.032 1254.68\r
+0.055 1473.37\r
+0.078 1569.11\r
+0.156 1569.11\r
+0.352 1559.03\r
+0.642 1597.33\r
+0.974 1644.69\r
+1.289 1702.13\r
+1.52 1739.42\r
+1.918 1796.87\r
+2.279 1814.83\r
+2.481 1796.87\r
+2.707 1739.42\r
+2.968 1644.69\r
+3.058 1616.47\r
+3.135 1520.73\r
+3.218 1378.64\r
+3.284 1217.39\r
+3.332 1065.22\r
+3.344 1140.8\r
+3.368 1016.85\r
+3.41 741.522\r
+3.5 522.935\r
+3.613 275.727\r
+3.691 171.12\r
+3.768 66.553\r
+3.85 0\r
diff --git a/datafiles/thrustcurves/AMW_M1730.eng b/datafiles/thrustcurves/AMW_M1730.eng
new file mode 100644 (file)
index 0000000..5c636e2
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;Animal Motor Works 98-11000\r
+M1730SK 98 870 0 4.9452 9.8718 AMW\r
+0.040  682.642\r
+0.064  1153.387\r
+0.221  1354.665\r
+0.269  1414.771\r
+0.381  1458.026\r
+0.541  1526.924\r
+0.701  1589.200\r
+0.861  1675.203\r
+1.021  1732.669\r
+1.181  1802.227\r
+1.341  1886.644\r
+1.500  1973.713\r
+1.660  2070.514\r
+1.820  2183.822\r
+1.980  2299.313\r
+2.140  2433.862\r
+2.300  2568.119\r
+2.460  2679.423\r
+2.620  2638.376\r
+2.780  2484.185\r
+2.940  2306.038\r
+3.099  2173.849\r
+3.259  2074.688\r
+3.419  1961.303\r
+3.579  1807.810\r
+3.739  1640.258\r
+3.899  1303.035\r
+4.059  940.600\r
+4.219  567.152\r
+4.379  309.143\r
+4.539  188.981\r
+4.637    0.000\r
+;\r
diff --git a/datafiles/thrustcurves/AMW_M1850.eng b/datafiles/thrustcurves/AMW_M1850.eng
new file mode 100644 (file)
index 0000000..72764e0
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+; Animal Motor Works M1850GG\r
+; estimated from TRA graph by John DeMar jsdemar@syr.edu\r
+; motor mass is a guess based on similar types\r
+M1850GG  75 781 0 3.3750 4.5000 AMW\r
+   0.08     979.00 \r
+   0.13    1180.00 \r
+   0.28    1290.00 \r
+   0.33    1468.00 \r
+   0.73    1936.00 \r
+   1.33    2202.00 \r
+   1.73    2279.00 \r
+   2.58    2105.00 \r
+   2.83    2007.00 \r
+   2.88    1860.00 \r
+   3.08     538.00 \r
+   3.20     174.00 \r
+   3.30       0.00 \r
diff --git a/datafiles/thrustcurves/AMW_M1850_1.eng b/datafiles/thrustcurves/AMW_M1850_1.eng
new file mode 100644 (file)
index 0000000..48a109a
--- /dev/null
@@ -0,0 +1,30 @@
+;\r
+;Animal Motor Works M1850 Green Gorilla\r
+M1850GG  75 781 0 3.37000 5.85100 AMW\r
+0.12 1201.01994\r
+0.25 1321.121934\r
+0.37 1579.11881\r
+0.50 1699.220804\r
+0.62 1846.01213\r
+0.75 1930.528348\r
+0.87 1997.251678\r
+1.00 2059.526786\r
+1.12 2126.250116\r
+1.25 2192.973446\r
+1.37 2224.111\r
+1.50 2246.35211\r
+1.62 2268.59322\r
+1.75 2277.489664\r
+1.87 2268.59322\r
+2.00 2246.35211\r
+2.12 2224.111\r
+2.25 2192.973446\r
+2.37 2166.284114\r
+2.50 2144.043004\r
+2.62 2099.560784\r
+2.75 2046.18212\r
+2.87 1912.73546\r
+3.00 831.817514\r
+3.12 311.37554\r
+3.25 84.516218\r
+3.3 0.000\r
diff --git a/datafiles/thrustcurves/AMW_M1900.eng b/datafiles/thrustcurves/AMW_M1900.eng
new file mode 100644 (file)
index 0000000..8b85d34
--- /dev/null
@@ -0,0 +1,38 @@
+;\r
+;AMW M1900 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+M1900BB 75 785 100 2.733 5.4225 Animal_Motor_Works \r
+0.018 1109.21\r
+0.044 1761.75\r
+0.061 1910.65\r
+0.085 1938.62\r
+0.159 1929.63\r
+0.29 1956.62\r
+0.409 2031.56\r
+0.438 1974.6\r
+0.569 2011.58\r
+0.815 2104.51\r
+1.073 2197.44\r
+1.401 2280.39\r
+1.688 2324.7\r
+1.905 2297.37\r
+2.073 2241.41\r
+2.254 2138.49\r
+2.397 2063.54\r
+2.479 2016.57\r
+2.54 2025.57\r
+2.581 2006.58\r
+2.63 1885.67\r
+2.716 1493.94\r
+2.805 1120.21\r
+2.887 840.605\r
+2.972 569.996\r
+3.046 299.488\r
+3.119 150.193\r
+3.168 56.829\r
+3.23 0\r
diff --git a/datafiles/thrustcurves/AMW_M2500.eng b/datafiles/thrustcurves/AMW_M2500.eng
new file mode 100644 (file)
index 0000000..6634e31
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Animal Motor Works M2500 Green Gorilla\r
+M2500GG 75 1039 0 4.248 7.5515 AMW\r
+0.026 1288.791\r
+0.053 2021.398\r
+0.079 2140.011\r
+0.123 2105.125\r
+0.207 2117.086\r
+0.540 2309.458\r
+0.971 2560.637\r
+1.265 2727.094\r
+1.480 2836.736\r
+1.678 2920.462\r
+1.757 2980.267\r
+1.946 2995.51\r
+2.047 2959.335\r
+2.240 2889.563\r
+2.310 2854.677\r
+2.486 2820.788\r
+2.526 2880.593\r
+2.592 2773.941\r
+2.653 2821.785\r
+2.706 2752.012\r
+2.758 2752.012\r
+2.807 2763.973\r
+2.842 2504.82\r
+2.886 2115.092\r
+2.930 1630.674\r
+2.987 1051.565\r
+3.040 437.571\r
+3.057 284.072\r
+3.079 142.434\r
+3.110 0\r
diff --git a/datafiles/thrustcurves/AMW_M3000.eng b/datafiles/thrustcurves/AMW_M3000.eng
new file mode 100644 (file)
index 0000000..c5b66ce
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+; Animal Motor Works M3000ST\r
+; estimated from TRA graph by Rob Bazinet rbazinet66@hotmail.com\r
+; motor mass is a guess based on similar types\r
+M3000ST  75 1038 0 3.8190 6.72 AMW\r
+   0.032    2494.225\r
+   0.113    2621.05  \r
+   0.242    2705.6\r
+   0.355    2811.288\r
+   0.435    2895.838\r
+   0.5      2959.25\r
+   0.645    3128.35\r
+   0.75     3297.45\r
+   0.871    3382\r
+   0.968    3551.1\r
+   1.032    3656.788\r
+   1.145    3804.75\r
+   1.355    3973.85\r
+   1.452    4037.263\r
+   1.629    4079.538\r
+   1.742    4142.95\r
+   1.903    4185.225\r
+   1.935    3847.025\r
+   2.081    3424.275\r
+   2.129    2959.25\r
+   2.177    2536.5\r
+   2.194    2113.75\r
+   2.226    1691\r
+   2.274    1268.25\r
+   2.323    845.5\r
+   2.403    422.75\r
+   2.5      0\r
diff --git a/datafiles/thrustcurves/AMW_N2020.eng b/datafiles/thrustcurves/AMW_N2020.eng
new file mode 100644 (file)
index 0000000..d26216d
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;Animal Motor Works 98-11000\r
+N2020WT 98 870 0 5.1609 9.9693 AMW\r
+.106   1941.344\r
+0.221  2151.149\r
+0.381  2253.406\r
+0.541  2340.792\r
+0.701  2400.847\r
+0.861  2453.821\r
+1.021  2506.314\r
+1.181  2556.306\r
+1.341  2607.251\r
+1.500  2652.790\r
+1.660  2688.660\r
+1.820  2710.675\r
+1.980  2729.797\r
+2.140  2733.895\r
+2.300  2704.255\r
+2.460  2634.582\r
+2.620  2532.160\r
+2.780  2433.380\r
+2.940  2329.740\r
+3.099  2234.246\r
+3.259  2165.804\r
+3.419  2099.684\r
+3.579  2028.350\r
+3.739  1951.013\r
+3.899  1871.316\r
+4.059  1558.113\r
+4.219  1053.376\r
+4.379  890.506\r
+4.539  636.689\r
+4.998  0.000\r
+\r
+;\r
diff --git a/datafiles/thrustcurves/AMW_N2600.eng b/datafiles/thrustcurves/AMW_N2600.eng
new file mode 100644 (file)
index 0000000..93afa68
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;Animal Motor Works 98-11000\r
+N2600GG 98 870 1000 4.8812 10.4726 Animal_Motor_Works \r
+0.024 1674.37\r
+0.064 1949.62\r
+0.104 2039.52\r
+0.306 2189.98\r
+0.508 2334.45\r
+0.709 2491.23\r
+0.911 2668.93\r
+1.113 2874.7\r
+1.314 3038.83\r
+1.516 3191.29\r
+1.718 3266.01\r
+1.92 3318.98\r
+2.121 3336.18\r
+2.323 3229.26\r
+2.525 3089.68\r
+2.726 2943.98\r
+2.928 2847.69\r
+3.13 2751.68\r
+3.331 2682.22\r
+3.533 2463.48\r
+3.735 1339.63\r
+3.937 269.834\r
+4.034 0\r
diff --git a/datafiles/thrustcurves/AMW_N2700.eng b/datafiles/thrustcurves/AMW_N2700.eng
new file mode 100644 (file)
index 0000000..56d7f90
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;Animal Motor Works 98-11000\r
+N2700BB 98 870 1000 4.7837 9.9308 Animal_Motor_Works \r
+0.027 2229.53\r
+0.069 2476.18\r
+0.111 2539.74\r
+0.36 2723.21\r
+0.527 2863.83\r
+0.735 3016.48\r
+0.943 3141.25\r
+1.151 3241.72\r
+1.359 3335.56\r
+1.567 3519.92\r
+1.775 3425.88\r
+1.983 3420.56\r
+2.191 3356.08\r
+2.399 3270.48\r
+2.607 3182.6\r
+2.815 3098.31\r
+3.023 3002.95\r
+3.231 2888.73\r
+3.439 2266.61\r
+3.647 1498.26\r
+3.855 780.04\r
+4.063 233.545\r
+4.16 0\r
diff --git a/datafiles/thrustcurves/AMW_N2800.eng b/datafiles/thrustcurves/AMW_N2800.eng
new file mode 100644 (file)
index 0000000..1c5f954
--- /dev/null
@@ -0,0 +1,35 @@
+; @File: N2800b.txt, @Pts-I: 5383, @Pts-O: 31, @Sm: 8, @CO: 5%\r
+; @TI: 14810.26, @TIa: 14792.71, @TIe: 0.0%, @ThMax: 3650.74, @ThAvg: 2770.17, @Tb: 5.34\r
+; Exported using ThrustCurveTool, www.ThrustGear.com, by John DeMar\r
+N2800 98 1213 100 7.6947 13.8 AMW\r
+  0.0        93.0947\r
+  0.0020     168.347\r
+  0.0060     387.836\r
+  0.019      1271.166\r
+  0.029      1776.342\r
+  0.043      2298.6\r
+  0.062      2841.03\r
+  0.072      3021.31\r
+  0.084      3128.89\r
+  0.14       3296.17\r
+  0.277      3483.35\r
+  0.293      3431.67\r
+  0.369      3495.76\r
+  0.978      3598.65\r
+  1.973      3655.16\r
+  2.977      3534.8\r
+  3.3        3437.12\r
+  3.497      3308.46\r
+  3.583      3193.8\r
+  3.651      3015.65\r
+  3.748      2548.37\r
+  3.836      2223.91\r
+  4.109      1644.077\r
+  4.245      1443.685\r
+  4.272      1447.012\r
+  4.397      1163.584\r
+  4.489      1022.953\r
+  4.516      1057.203\r
+  4.574      883.885\r
+  4.647      776.407\r
+  5.569      0.0\r
diff --git a/datafiles/thrustcurves/AMW_N4000.eng b/datafiles/thrustcurves/AMW_N4000.eng
new file mode 100644 (file)
index 0000000..e001573
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;Animal Motor Works 98-17500\r
+N4000BB 98 1213 0 6.1026 13.6683 AMW\r
+0.029  4207.591\r
+0.071  4709.549\r
+0.113  4906.310\r
+0.155  5007.780\r
+0.239  5041.557\r
+0.323  4993.595\r
+0.534  5046.912\r
+0.744  5145.819\r
+0.954  5248.063\r
+1.165  5293.196\r
+1.375  5232.456\r
+1.585  5209.528\r
+1.796  5165.473\r
+2.006  5047.698\r
+2.216  4913.086\r
+2.427  4783.447\r
+2.637  4659.163\r
+2.847  4195.994\r
+3.058  2850.731\r
+3.268  1981.973\r
+3.478  1295.536\r
+3.689  907.699\r
+3.899  490.196\r
+4.110  316.338\r
+4.207  0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_D13.eng b/datafiles/thrustcurves/AeroTech_D13.eng
new file mode 100644 (file)
index 0000000..eec171a
--- /dev/null
@@ -0,0 +1,40 @@
+; Aerotech D13 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+D13W   18   70   4-7-10   0.0098   0.0326 AT\r
+   0.030   13.462\r
+   0.061   21.171\r
+   0.085   20.618\r
+   0.127   21.605\r
+   0.158   21.042\r
+   0.182   22.306\r
+   0.217   22.592\r
+   0.227   23.610\r
+   0.248   21.891\r
+   0.279   23.155\r
+   0.317   22.039\r
+   0.366   21.338\r
+   0.383   21.901\r
+   0.449   20.648\r
+   0.462   21.486\r
+   0.480   19.947\r
+   0.507   19.947\r
+   0.521   20.509\r
+   0.559   18.693\r
+   0.580   19.118\r
+   0.660   17.578\r
+   0.743   15.337\r
+   0.861   12.406\r
+   0.947    9.329\r
+   1.068    5.834\r
+   1.155    4.158\r
+   1.172    4.720\r
+   1.231    2.762\r
+   1.328    1.928\r
+   1.404    1.093\r
+   1.520    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_D15.eng b/datafiles/thrustcurves/AeroTech_D15.eng
new file mode 100644 (file)
index 0000000..c863bf2
--- /dev/null
@@ -0,0 +1,26 @@
+; Aerotech D15 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+D15T  24   70   4-6-8   .0089   .0440   AT\r
+   0.014   11.480\r
+   0.049   26.272\r
+   0.081   30.087\r
+   0.107   31.261\r
+   0.121   31.249\r
+   0.159   31.360\r
+   0.208   31.249\r
+   0.283   29.583\r
+   0.439   23.353\r
+   0.551   18.484\r
+   0.675   13.430\r
+   0.863    6.422\r
+   0.938    3.892\r
+   1.010    2.335\r
+   1.085    0.778\r
+   1.142    0.389\r
+   1.150    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_D21.eng b/datafiles/thrustcurves/AeroTech_D21.eng
new file mode 100644 (file)
index 0000000..7dcc85a
--- /dev/null
@@ -0,0 +1,33 @@
+;Aerotech D21 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D21T 18 70 4-7 0.0096 0.025 AT \r
+   0.01   1.367\r
+   0.021 19.367\r
+   0.029 32.12\r
+   0.037 31.667\r
+   0.051 30.528\r
+   0.094 30.074\r
+   0.115 31.213\r
+   0.133 30.074\r
+   0.177 30.76\r
+   0.189 29.842\r
+   0.203 30.528\r
+   0.226 29.842\r
+   0.275 28.935\r
+   0.296 29.389\r
+   0.331 28.027\r
+   0.421 25.971\r
+   0.478 24.146\r
+   0.579 20.728\r
+   0.659 17.774\r
+   0.739 14.356\r
+   0.799  9.569\r
+   0.852  4.557\r
+   0.899  1.139\r
+   0.94   0\r
diff --git a/datafiles/thrustcurves/AeroTech_D24.eng b/datafiles/thrustcurves/AeroTech_D24.eng
new file mode 100644 (file)
index 0000000..51f63ae
--- /dev/null
@@ -0,0 +1,48 @@
+; AeroTech D24T (Blue Thunder) RASP.ENG file made from\r
+; manufacturers published data.\r
+;\r
+; File was produced May 07, 2004 by Stanley_Hemphill@Hotmail.com.\r
+;\r
+; The motor is listed in the www.thrustcurve.org database as an\r
+; engine certified by NAR, but there is "no data" at the weblink\r
+; to the NAR file database.\r
+;\r
+; The author has created this file by extracting the manufacturers\r
+; Thrust-Time curve from The AeroTech-2002 Catalog, and then deploting\r
+; 32 points using the distance measuring tools in Paint Shop Pro 8.\r
+; The file was then created in RockSim 7 and the motor and static values\r
+; were read from the RockSim Engine Editor.\r
+;\r
+; Motor Dia Len Delay Propellant Total Manufacturer\r
+D24BT_CO_SU 18.00 70.00 4-7-10 0.00870 0.03200 AeroTech\r
+0.0380 39.6000\r
+0.0550 36.5000\r
+0.0760 34.4000\r
+0.1220 32.4000\r
+0.1640 31.1000\r
+0.2190 30.3000\r
+0.2610 29.3000\r
+0.3080 28.7000\r
+0.3290 27.9000\r
+0.3580 26.9000\r
+0.3920 25.8000\r
+0.4340 25.0000\r
+0.4850 24.0000\r
+0.5390 23.2000\r
+0.5770 22.3000\r
+0.6200 20.9000\r
+0.6660 19.6000\r
+0.6950 18.0000\r
+0.7210 16.5000\r
+0.7500 15.5000\r
+0.7540 14.3000\r
+0.7590 12.4000\r
+0.7600 11.0000\r
+0.7630 09.1000\r
+0.7634 07.5000\r
+0.7710 05.9000\r
+0.7920 04.0000\r
+0.8300 02.6000\r
+0.8680 01.8000\r
+0.9000 01.1000\r
+0.9400 00.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_D7.eng b/datafiles/thrustcurves/AeroTech_D7.eng
new file mode 100644 (file)
index 0000000..3b46001
--- /dev/null
@@ -0,0 +1,23 @@
+;Aerotech D7 RASP.ENG file made from NAR published data\r
+D7 24 70 100 0.0105 0.0422 AT\r
+0.036 3.336\r
+0.084 9.326\r
+0.101 10.281\r
+0.143 10.827\r
+0.213 10.99\r
+0.271 10.887\r
+0.359 10.685\r
+0.471 10.13\r
+0.506 10.342\r
+0.535 9.929\r
+0.81 8.697\r
+1.226 6.713\r
+1.589 5.138\r
+1.8 4.861\r
+2.151 4.581\r
+2.649 4.57\r
+2.696 3.887\r
+2.748 2.388\r
+2.807 0.889\r
+2.842 0.207\r
+2.87 0\r
diff --git a/datafiles/thrustcurves/AeroTech_D9.eng b/datafiles/thrustcurves/AeroTech_D9.eng
new file mode 100644 (file)
index 0000000..9d3b607
--- /dev/null
@@ -0,0 +1,23 @@
+; Aerotech D9 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel  (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+D9W  24   70   4-7   0.0101   0.045   AT               \r
+   0.1    13.7                 \r
+   0.15           15.4                 \r
+   0.2    16.3                 \r
+   0.25           16.8                 \r
+   0.35           17.2                 \r
+   0.40           17.2                 \r
+   0.50           16.8                 \r
+   0.65           15.9                 \r
+   0.80           14.5                 \r
+   1.10            9.2                 \r
+   1.25            7.0                 \r
+   1.40            4.8                 \r
+   1.60            2.5                 \r
+   1.90            0.0                 \r
diff --git a/datafiles/thrustcurves/AeroTech_E11.eng b/datafiles/thrustcurves/AeroTech_E11.eng
new file mode 100644 (file)
index 0000000..4bdcce9
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;Based On NAR Test Data\r
+;12/23/93\r
+E11J 24 70 4 0.025 0.0624 Aerotech\r
+0.0725446 14.3704\r
+0.16183 17.6296\r
+0.206473 18.3704\r
+0.418527 19.2593\r
+0.731027 18.3704\r
+1.31696 14.2222\r
+1.91964 9.03704\r
+2.51116 2.22222\r
+2.83 0\r
diff --git a/datafiles/thrustcurves/AeroTech_E12.eng b/datafiles/thrustcurves/AeroTech_E12.eng
new file mode 100644 (file)
index 0000000..7ba80db
--- /dev/null
@@ -0,0 +1,42 @@
+;\r
+;\r
+;Aerotech E12JRC RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+E12JRC 24 70 100 0.0303 0.0594 AT\r
+0.054 16.764\r
+0.095 18.33\r
+0.197 16.545\r
+0.313 16.654\r
+0.36 17.211\r
+0.401 16.316\r
+0.442 17.55\r
+0.476 16.206\r
+0.578 16.316\r
+0.666 16.764\r
+0.7 15.649\r
+0.768 16.316\r
+0.89 16.097\r
+1.019 15.649\r
+1.162 14.983\r
+1.23 14.983\r
+1.25 13.968\r
+1.291 14.754\r
+1.332 13.749\r
+1.373 14.197\r
+1.434 13.53\r
+1.488 13.749\r
+1.597 12.635\r
+1.726 11.401\r
+1.828 10.615\r
+1.889 9.613\r
+1.957 9.613\r
+1.998 8.495\r
+2.093 8.607\r
+2.277 7.042\r
+2.487 5.813\r
+3.05 0\r
diff --git a/datafiles/thrustcurves/AeroTech_E15.eng b/datafiles/thrustcurves/AeroTech_E15.eng
new file mode 100644 (file)
index 0000000..7cd6163
--- /dev/null
@@ -0,0 +1,46 @@
+; E15W-4,7,P from NAR data\r
+E15W 24 70 4-7-P 0.020100000000000003 0.0502 AT\r
+   0.012 9.918\r
+   0.018 20.205\r
+   0.027 25.257\r
+   0.039 28.152\r
+   0.055 28.768\r
+   0.088 27.29\r
+   0.197 24.517\r
+   0.297 22.977\r
+   0.467 20.945\r
+   0.561 19.959\r
+   0.679 20.021\r
+   0.722 19.22\r
+   0.761 18.789\r
+   0.807 20.021\r
+   0.84 18.234\r
+   0.904 18.727\r
+   0.995 17.926\r
+   1.034 18.172\r
+   1.104 16.756\r
+   1.147 17.248\r
+   1.256 16.386\r
+   1.377 15.77\r
+   1.411 14.846\r
+   1.426 16.324\r
+   1.45 15.031\r
+   1.547 14.353\r
+   1.559 16.016\r
+   1.589 13.86\r
+   1.62 14.23\r
+   1.693 13.121\r
+   1.72 13.429\r
+   1.829 12.936\r
+   1.866 11.951\r
+   1.944 11.951\r
+   2.005 10.965\r
+   2.093 10.472\r
+   2.236 8.316\r
+   2.26 9.055\r
+   2.278 7.207\r
+   2.378 4.99\r
+   2.442 2.71\r
+   2.499 1.602\r
+   2.548 1.047\r
+   2.618 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_E15_1.eng b/datafiles/thrustcurves/AeroTech_E15_1.eng
new file mode 100644 (file)
index 0000000..2bd38b2
--- /dev/null
@@ -0,0 +1,41 @@
+; Aerotech E15 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E15W 24 70 4-7 .0201 .0501 AT\r
+   0.020 23.330\r
+   0.036 27.318\r
+   0.058 28.840\r
+   0.079 27.171\r
+   0.139 25.638\r
+   0.183 24.263\r
+   0.237 24.106\r
+   0.297 22.426\r
+   0.373 21.964\r
+   0.400 20.894\r
+   0.443 21.355\r
+   0.487 20.442\r
+   0.617 19.833\r
+   0.742 18.457\r
+   0.812 20.000\r
+   0.850 18.006\r
+   0.899 18.467\r
+   1.035 17.711\r
+   1.100 16.945\r
+   1.160 16.945\r
+   1.377 15.736\r
+   1.426 14.656\r
+   1.436 16.198\r
+   1.463 14.813\r
+   1.550 14.361\r
+   1.572 15.432\r
+   1.610 13.752\r
+   1.827 12.839\r
+   2.126 10.098\r
+   2.337 6.116\r
+   2.538 1.369\r
+   2.600 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_E16.eng b/datafiles/thrustcurves/AeroTech_E16.eng
new file mode 100644 (file)
index 0000000..95afca8
--- /dev/null
@@ -0,0 +1,32 @@
+; Aerotech E16 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file.  The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E16W   29   124   4-7-10   .0190   .107   AT \r
+ 0.132    32.223 \r
+ 0.221    37.200 \r
+ 0.255    36.699 \r
+ 0.306    36.699 \r
+ 0.371    35.357 \r
+ 0.414    33.785 \r
+ 0.437    34.906 \r
+ 0.472    33.785 \r
+ 0.530    32.894 \r
+ 0.553    31.772 \r
+ 0.576    32.443 \r
+ 0.638    29.309 \r
+ 0.720    27.296 \r
+ 0.867    23.942 \r
+ 1.083    19.245 \r
+ 1.273    14.319 \r
+ 1.458    9.397\r
+ 1.513    8.055 \r
+ 1.524    8.279 \r
+ 1.555    6.936 \r
+ 1.656    4.474 \r
+ 1.814    1.790 \r
+ 2.000    0.000 \r
diff --git a/datafiles/thrustcurves/AeroTech_E18.eng b/datafiles/thrustcurves/AeroTech_E18.eng
new file mode 100644 (file)
index 0000000..cb73dfb
--- /dev/null
@@ -0,0 +1,37 @@
+; Aerotech E18 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E18W   24   70   4-8-10   .0207   .057 AT\r
+   0.016    6.586\r
+   0.042   18.004\r
+   0.073   27.138\r
+   0.098   29.815\r
+   0.134   30.357\r
+   0.170   30.347\r
+   0.195   31.080\r
+   0.236   30.347\r
+   0.287   30.878\r
+   0.338   30.337\r
+   0.368   30.878\r
+   0.404   29.795\r
+   0.424   30.688\r
+   0.465   29.976\r
+   0.526   29.785\r
+   0.592   29.063\r
+   0.669   28.341\r
+   0.786   26.908\r
+   0.908   23.850\r
+   1.025   21.163\r
+   1.157   17.905\r
+   1.284   14.857\r
+   1.462   11.338\r
+   1.660    7.106\r
+   1.838    3.470\r
+   2.006    1.309\r
+   2.083    0.588\r
+   2.140    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_E23.eng b/datafiles/thrustcurves/AeroTech_E23.eng
new file mode 100644 (file)
index 0000000..5417dd7
--- /dev/null
@@ -0,0 +1,30 @@
+; Aerotech E23 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E23T   29   124   5-8   .0174   .1039   AT\r
+   0.024   16.299\r
+   0.035   21.959\r
+   0.067   30.785\r
+   0.090   35.774\r
+   0.153   37.577\r
+   0.200   38.220\r
+   0.240   37.357\r
+   0.322   37.577\r
+   0.393   35.093\r
+   0.534   32.378\r
+   0.727   27.168\r
+   0.766   26.938\r
+   0.798   25.125\r
+   0.908   21.729\r
+   1.057   16.980\r
+   1.187   12.682\r
+   1.336    7.471\r
+   1.450    3.169\r
+   1.497    1.584\r
+   1.532    0.679\r
+   1.570    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_E28.eng b/datafiles/thrustcurves/AeroTech_E28.eng
new file mode 100644 (file)
index 0000000..8ded5e2
--- /dev/null
@@ -0,0 +1,36 @@
+; Aerotech E28 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E28T  24   70   2-5-8   .0184   .0545   A\r
+   0.010   29.8620\r
+   0.018   45.1390\r
+   0.038   47.5620\r
+   0.081   50.5200\r
+   0.106   48.9530\r
+   0.146   48.2630\r
+   0.161   48.9530\r
+   0.197   48.9530\r
+   0.242   47.5620\r
+   0.313   46.1800\r
+   0.411   43.0570\r
+   0.494   40.6240\r
+   0.527   39.5830\r
+   0.542   40.2740\r
+   0.562   38.5420\r
+   0.633   36.4600\r
+   0.683   34.3770\r
+   0.743   31.2440\r
+   0.799   29.1620\r
+   0.877   26.0380\r
+   0.970   20.8320\r
+   1.006   17.3590\r
+   1.046   11.4620\r
+   1.089    6.9430\r
+   1.132    3.8190\r
+   1.172    1.7350\r
+   1.220    0.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_E30.eng b/datafiles/thrustcurves/AeroTech_E30.eng
new file mode 100644 (file)
index 0000000..e0a8ce8
--- /dev/null
@@ -0,0 +1,34 @@
+; Aerotech E30 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E30T 24 70 4-7 .0193 .0433 AT\r
+   0.013  38.8470\r
+   0.020  45.6210\r
+   0.041  48.2700\r
+   0.059  46.5020\r
+   0.110  46.5020\r
+   0.166  45.9120\r
+   0.184  46.7920\r
+   0.217  45.9120\r
+   0.265  45.9120\r
+   0.319  45.0310\r
+   0.383  44.1500\r
+   0.482  42.0890\r
+   0.594  38.8470\r
+   0.615  39.4370\r
+   0.628  37.3760\r
+   0.684  35.3140\r
+   0.742  33.2630\r
+   0.804  30.0210\r
+   0.880  25.6070\r
+   0.962  20.0140\r
+   1.038  12.9490\r
+   1.089  7.3580\r
+   1.151  3.2370\r
+   1.186  1.1760\r
+   1.200  0.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_E6.eng b/datafiles/thrustcurves/AeroTech_E6.eng
new file mode 100644 (file)
index 0000000..9a8ef48
--- /dev/null
@@ -0,0 +1,20 @@
+; Aerotech E6T single use from NAR cert data\r
+E6T 24 70 2-4-8-P 0.021500000000000002 0.0463 AT\r
+   0.011 18.085\r
+   0.109 19.681\r
+   0.217 16.312\r
+   0.315 13.475\r
+   0.457 11.348\r
+   0.63 9.043\r
+   0.804 7.801\r
+   0.989 6.738\r
+   1.272 6.028\r
+   2.0 5.851\r
+   3.0 5.496\r
+   4.0 5.496\r
+   4.446 4.965\r
+   5.011 4.965\r
+   5.533 4.787\r
+   5.609 6.56\r
+   5.707 4.255\r
+   6.033 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_E7.eng b/datafiles/thrustcurves/AeroTech_E7.eng
new file mode 100644 (file)
index 0000000..a8f0097
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+;Aerotech E7TRC RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+E7RC 24 70 100 0.0171 0.0484 AT\r
+0.038 6.636\r
+0.063 10.056\r
+0.087 11.019\r
+0.134 11.42\r
+0.206 11.58\r
+0.312 11.149\r
+0.466 10.738\r
+0.667 9.777\r
+0.94 8.132\r
+1.223 6.281\r
+1.484 5.182\r
+1.709 4.701\r
+2.112 4.423\r
+2.776 4.279\r
+3.31 4.205\r
+3.926 4.266\r
+4.401 4.192\r
+4.638 4.258\r
+4.744 4.119\r
+5.124 3.979\r
+5.219 3.977\r
+5.266 3.156\r
+5.313 1.992\r
+5.36 0.965\r
+5.43 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F10.eng b/datafiles/thrustcurves/AeroTech_F10.eng
new file mode 100644 (file)
index 0000000..879934c
--- /dev/null
@@ -0,0 +1,30 @@
+;\r
+F10  29.0  92.00 4-6-8 0.04000 0.08300 Aerotech\r
+   0.01      16.81 \r
+   0.03      22.34 \r
+   0.11      22.23 \r
+   0.26      21.49 \r
+   0.37      20.00 \r
+   0.47      20.21 \r
+   0.67      18.09 \r
+   0.99      15.74 \r
+   1.31      13.40 \r
+   1.81      10.85 \r
+   2.49      10.21 \r
+   3.13       8.94 \r
+   3.60       8.83 \r
+   4.11       8.62 \r
+   4.95       8.62 \r
+   5.45       8.62 \r
+   5.58       8.51 \r
+   5.88       8.72 \r
+   6.22       8.51 \r
+   6.46       8.51 \r
+   6.60       7.77 \r
+   6.71       7.02 \r
+   6.79       5.64 \r
+   6.91       3.83 \r
+   6.95       2.23 \r
+   7.00       0.96 \r
+   7.05       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_F12.eng b/datafiles/thrustcurves/AeroTech_F12.eng
new file mode 100644 (file)
index 0000000..cd06f44
--- /dev/null
@@ -0,0 +1,40 @@
+; Aerotech F12 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (4/6/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F12J  24   70   2-5   .0303   .0667 AT\r
+   0.037   20.894\r
+   0.054   22.152\r
+   0.101   22.152\r
+   0.148   22.571\r
+   0.165   23.409\r
+   0.200   22.421\r
+   0.281   22.142\r
+   0.369   22.132\r
+   0.474   22.271\r
+   0.526   23.540\r
+   0.549   21.982\r
+   0.637   22.122\r
+   0.724   21.842\r
+   0.800   21.413\r
+   0.823   22.251\r
+   0.846   20.714\r
+   0.881   21.553\r
+   0.945   21.123\r
+   1.021   20.704\r
+   1.114   20.554\r
+   1.213   19.296\r
+   1.382   18.298\r
+   1.481   18.019\r
+   1.737   15.343\r
+   1.790   17.300\r
+   1.883   13.936\r
+   2.051   11.260\r
+   2.220    7.468\r
+   2.447    3.671\r
+   2.709    1.135\r
+   2.930    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_F13.eng b/datafiles/thrustcurves/AeroTech_F13.eng
new file mode 100644 (file)
index 0000000..7de9988
--- /dev/null
@@ -0,0 +1,38 @@
+;\r
+;Aerotech F13RCJ RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F13RCJ 32 107 100 0.0323 0.1105 AT\r
+0.048 15.309\r
+0.084 18.629\r
+0.143 19.98\r
+0.311 18.968\r
+0.538 18.172\r
+0.729 17.138\r
+0.992 15.428\r
+1.279 13.828\r
+1.673 12.456\r
+1.984 11.879\r
+2.044 12.227\r
+2.139 11.313\r
+2.378 11.193\r
+2.51 11.084\r
+2.558 12.108\r
+2.641 10.855\r
+2.976 10.736\r
+3.49 10.627\r
+3.873 10.507\r
+3.992 10.965\r
+4.028 10.627\r
+4.41 10.507\r
+4.625 10.736\r
+4.769 9.941\r
+4.829 8.684\r
+4.865 6.742\r
+4.96 3.199\r
+5.02 1.485\r
+5.1 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F16.eng b/datafiles/thrustcurves/AeroTech_F16.eng
new file mode 100644 (file)
index 0000000..898c017
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Aerotech F16RCJ RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F16RCJ 32 107 100 0.0625 0.1404 AT\r
+0.046 26.35\r
+0.116 22.388\r
+0.139 21.374\r
+0.185 21.886\r
+0.22 20.54\r
+0.301 19.696\r
+0.498 18.35\r
+0.579 19.194\r
+0.637 16.492\r
+0.718 18.35\r
+0.834 18.35\r
+0.95 18.35\r
+1.054 19.194\r
+1.147 17.848\r
+1.181 18.853\r
+1.263 17.336\r
+1.436 18.009\r
+1.633 17.165\r
+1.784 17.336\r
+1.865 18.682\r
+1.934 16.834\r
+1.981 17.336\r
+2.178 16.332\r
+2.375 16.332\r
+2.502 18.18\r
+2.664 15.659\r
+2.896 15.488\r
+3.29 13.8\r
+3.718 11.611\r
+4.181 9.426\r
+4.888 5.891\r
+5.69 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F20.eng b/datafiles/thrustcurves/AeroTech_F20.eng
new file mode 100644 (file)
index 0000000..18edb27
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;\r
+F20EJ 29 83 4-7 0.03 0.0746 AeroTech\r
+0.01 52.08\r
+0.03 49.81\r
+0.06 46.98\r
+0.1 45.56\r
+0.15 44.49\r
+0.18 45.55\r
+0.21 43.42\r
+0.24 43.78\r
+0.32 43.77\r
+0.36 44.11\r
+0.44 43.04\r
+0.45 40.58\r
+0.53 39.86\r
+0.62 38.08\r
+0.76 36.3\r
+0.8 37.35\r
+0.84 34.88\r
+0.89 36.99\r
+0.9 33.46\r
+1.03 30.61\r
+1.06 32.02\r
+1.09 29.55\r
+1.23 26\r
+1.32 22.45\r
+1.35 23.16\r
+1.36 21.39\r
+1.58 16.42\r
+1.8 11.1\r
+2.01 6.48\r
+2.19 3.63\r
+2.39 1.13\r
+2.68 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F21.eng b/datafiles/thrustcurves/AeroTech_F21.eng
new file mode 100644 (file)
index 0000000..c4cdedc
--- /dev/null
@@ -0,0 +1,60 @@
+; Aerotech F21W (White Lightning) RASP.ENG file.\r
+; File produced Jun 22 2004.\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the Tripoli.org motor pdf file.\r
+;\r
+; The F21W cannot be found on thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill\r
+; Contact at stanley_hemphill@hotmail.com\r
+;\r
+; Motor ## Dia  Len  Delays    Prop   Motor  Company\r
+F21WL_CO_SU 24 96 6-8 0.0300 0.064 AeroTech\r
+0.0045 037.2266\r
+0.0090 042.1474\r
+0.0180 042.5040\r
+0.0270 040.5071\r
+0.0337 038.8669\r
+0.0427 038.2250\r
+0.0517 037.7258\r
+0.0607 036.8700\r
+0.0720 036.4422\r
+0.0877 036.4422\r
+0.1102 035.3724\r
+0.1350 035.8716\r
+0.1552 035.4437\r
+0.1732 036.2282\r
+0.2025 035.6577\r
+0.2452 037.2979\r
+0.2835 036.5848\r
+0.3195 038.0111\r
+0.3375 037.4406\r
+0.3757 038.5816\r
+0.3960 038.2250\r
+0.4297 039.3661\r
+0.4454 038.0111\r
+0.4747 038.7242\r
+0.4882 037.7971\r
+0.5084 038.1537\r
+0.5354 038.5103\r
+0.5647 037.9398\r
+0.5849 037.2266\r
+0.6007 037.8685\r
+0.6389 036.8700\r
+0.6704 037.1553\r
+0.7649 035.9429\r
+0.9201 032.9477\r
+1.0056 030.3090\r
+1.0709 029.7385\r
+1.2643 024.0333\r
+1.2868 024.0333\r
+1.3723 020.8241\r
+1.3926 020.8241\r
+1.5883 015.4754\r
+1.6108 015.4754\r
+2.0112 005.0634\r
+2.1192 003.4231\r
+2.2407 002.4247\r
+2.3780 001.4976\r
+2.4927 000.9271\r
+2.5152 000.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_F22.eng b/datafiles/thrustcurves/AeroTech_F22.eng
new file mode 100644 (file)
index 0000000..8cd24a5
--- /dev/null
@@ -0,0 +1,38 @@
+;\r
+;Aerotech F22 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F22 29 125 4-7 0.0463 0.1342 AT\r
+0.014 11.527\r
+0.075 20.126\r
+0.157 26.572\r
+0.293 29.113\r
+0.382 30.278\r
+0.45 29.69\r
+0.539 30.667\r
+0.614 30.089\r
+0.662 31.15\r
+0.771 30.478\r
+0.948 29.89\r
+0.996 28.714\r
+1.078 28.136\r
+1.187 27.738\r
+1.289 26.761\r
+1.337 26.96\r
+1.412 25.984\r
+1.474 25.008\r
+1.515 26.173\r
+1.542 24.808\r
+1.706 22.856\r
+1.938 20.903\r
+2.101 18.173\r
+2.129 19.338\r
+2.251 16.21\r
+2.402 13.48\r
+2.64 8.791\r
+2.961 3.32\r
+3.31 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F23.eng b/datafiles/thrustcurves/AeroTech_F23.eng
new file mode 100644 (file)
index 0000000..e6c5c2b
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;F23FJ Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;file produced March 2, 2005\r
+;Based on data supplied by Aerotech for the newer molded case F23 econojet.\r
+F23FJ 29 83 4-7 0.033 0.0839 AeroTech\r
+0.03 48.7\r
+0.05 43.11\r
+0.08 41.41\r
+0.1 42.26\r
+0.13 40.84\r
+0.17 39.42\r
+0.23 38.85\r
+0.27 38.85\r
+0.3 37.44\r
+0.31 38.57\r
+0.36 37.72\r
+0.43 36.59\r
+0.5 36.02\r
+0.56 36.02\r
+0.59 34.6\r
+0.69 33.18\r
+0.77 32.61\r
+0.85 31.2\r
+0.94 29.5\r
+1.04 27.79\r
+1.18 24.39\r
+1.2 25.24\r
+1.25 22.97\r
+1.37 20.98\r
+1.53 16.73\r
+1.69 12.48\r
+1.83 9.07\r
+1.95 5.11\r
+2.07 2.27\r
+2.22 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F23_1.eng b/datafiles/thrustcurves/AeroTech_F23_1.eng
new file mode 100644 (file)
index 0000000..ae21a8c
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Aerotech F23RCWSK RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F23-RC-SK 32 107 100 0.0378 0.1287 AT\r
+0.042 22.644\r
+0.133 28.191\r
+0.161 27.261\r
+0.189 29.57\r
+0.252 31.419\r
+0.343 32.578\r
+0.399 32.348\r
+0.441 33.737\r
+0.476 30.729\r
+0.539 33.507\r
+0.609 34.197\r
+0.777 34.886\r
+0.826 34.656\r
+0.896 36\r
+0.938 34.656\r
+1.015 34.656\r
+1.071 34.197\r
+1.12 33.038\r
+1.218 32.578\r
+1.267 29.81\r
+1.351 29.34\r
+1.393 27.731\r
+1.54 26.802\r
+1.645 24.263\r
+1.799 21.255\r
+1.862 19.866\r
+2.051 15.479\r
+2.317 11.552\r
+2.618 6.7\r
+2.884 3.234\r
+3.185 1.386\r
+3.47 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F24.eng b/datafiles/thrustcurves/AeroTech_F24.eng
new file mode 100644 (file)
index 0000000..81698fc
--- /dev/null
@@ -0,0 +1,34 @@
+; Aerotech F24 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (4/6/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F24W   24   70   4-7-10   .0253   .062  AT\r
+   0.033    16.442\r
+   0.112    40.646\r
+   0.125    41.450\r
+   0.180    40.927\r
+   0.245    40.626\r
+   0.281    41.017\r
+   0.355    40.024\r
+   0.438    39.713\r
+   0.543    38.227\r
+   0.603    37.032\r
+   0.658    33.779\r
+   0.685    34.663\r
+   0.726    29.934\r
+   0.772    30.216\r
+   0.951    26.953\r
+   1.071    25.166\r
+   1.107    23.088\r
+   1.185    21.311\r
+   1.383    17.144\r
+   1.649    10.910\r
+   1.828    5.869\r
+   1.938    2.903\r
+   1.988    2.306\r
+   2.048    1.412\r
+   2.130    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_F25.eng b/datafiles/thrustcurves/AeroTech_F25.eng
new file mode 100644 (file)
index 0000000..b6e3f77
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;F25 Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;file produced March 2, 2005\r
+;Based on data supplied by Aerotech for the newer molded case F25.\r
+F25 29 98 4-6-9 0.0388 0.0972 Aerotech\r
+0.039 57.631\r
+0.187 53.491\r
+0.342 51.239\r
+0.5 47.86\r
+1 33.806\r
+1.5 22.94\r
+2 10.135\r
+2.207 4.504\r
+2.69 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F26.eng b/datafiles/thrustcurves/AeroTech_F26.eng
new file mode 100644 (file)
index 0000000..53613c0
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+;F26FJ Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;File created March 2, 2005\r
+;Based on data supplied by Aerotech prior to NAR certification.\r
+F26FJ 29 98 6-9 0.0431 0.1007 Aerotech\r
+0.041 38.289\r
+0.114 36.318\r
+0.293 34.347\r
+0.497 32.939\r
+0.774 32.376\r
+1 31.25\r
+1.254 28.716\r
+1.498 25.338\r
+1.743 22.241\r
+2.003 17.737\r
+2.077 15.484\r
+2.304 5.349\r
+2.484 1.689\r
+2.61 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F27.eng b/datafiles/thrustcurves/AeroTech_F27.eng
new file mode 100644 (file)
index 0000000..88339ab
--- /dev/null
@@ -0,0 +1,36 @@
+; Exported using ThrustCurveTool, www.ThrustGear.com \r
+; @File: 060603WF27Composite.txt, @Pts-I: 1001, @Pts-O: 32, @Sm: 5, @CO: 5% \r
+; @TI: 49.5446, @TIa: 49.299, @TIe: 0.0%, @ThMax: 36.2491, @ThAvg: 24.1799, @Tb: 2.049\r
+F27 29 83 4,8 0.0284 0.08 Aerotech \r
+  0.0 4.84718 \r
+  0.0125 15.5374 \r
+  0.0175 18.81827 \r
+  0.025 22.5311 \r
+  0.0325 25.2547 \r
+  0.04 27.3204 \r
+  0.0575 30.3657 \r
+  0.0725 31.4597 \r
+  0.0975 32.2507 \r
+  0.265 35.0238 \r
+  0.295 35.9203 \r
+  0.4675 35.9684 \r
+  0.59 35.065 \r
+  0.8 32.0145 \r
+  0.825 31.2773 \r
+  0.8575 31.1102 \r
+  0.9025 29.9308 \r
+  0.955 29.7244 \r
+  1.045 27.2951 \r
+  1.085 27.2663 \r
+  1.1175 25.9881 \r
+  1.1475 26.0014 \r
+  1.235 23.2853 \r
+  1.28 22.9001 \r
+  1.3425 21.0771 \r
+  1.52 17.256 \r
+  1.8075 6.85478 \r
+  1.9175 4.16387 \r
+  2.05 1.783863 \r
+  2.1775 0.488016 \r
+  2.4225 0.44385 \r
+  2.425 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_F32.eng b/datafiles/thrustcurves/AeroTech_F32.eng
new file mode 100644 (file)
index 0000000..d4b5b65
--- /dev/null
@@ -0,0 +1,40 @@
+;\r
+;Aerotech F32 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F32 24 124 5-10-15 0.0377 0.0814 AeroTech\r
+0.025 46.699\r
+0.031 51.846\r
+0.061 55.64\r
+0.085 52.868\r
+0.126 47.37\r
+0.245 45.637\r
+0.34 44.946\r
+0.394 42.873\r
+0.447 42.873\r
+0.572 41.14\r
+0.72 39.408\r
+0.744 40.78\r
+0.786 38.026\r
+1.041 35.592\r
+1.136 33.179\r
+1.177 34.541\r
+1.225 32.818\r
+1.379 31.436\r
+1.474 30.394\r
+1.635 28.311\r
+1.676 27.28\r
+1.694 29.683\r
+1.712 26.929\r
+1.854 25.537\r
+1.943 23.815\r
+2.092 21.051\r
+2.187 18.287\r
+2.276 13.82\r
+2.382 7.281\r
+2.525 2.457\r
+2.72 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F35.eng b/datafiles/thrustcurves/AeroTech_F35.eng
new file mode 100644 (file)
index 0000000..6f00123
--- /dev/null
@@ -0,0 +1,34 @@
+; Curve fit of AT Instruction Sheet by C. Kobel  7/29/08\r
+F35W 24 95 5-8-11 0.03 0.085 AT\r
+   0.007 39.452\r
+   0.012 51.842\r
+   0.019 49.885\r
+   0.034 57.873\r
+   0.048 58.363\r
+   0.058 57.221\r
+   0.077 54.45\r
+   0.098 54.939\r
+   0.106 53.961\r
+   0.201 53.472\r
+   0.299 53.309\r
+   0.398 52.005\r
+   0.498 52.005\r
+   0.549 49.559\r
+   0.601 48.907\r
+   0.653 47.277\r
+   0.702 45.647\r
+   0.752 44.669\r
+   0.802 43.201\r
+   0.898 39.778\r
+   0.946 39.615\r
+   0.984 36.843\r
+   1.003 37.332\r
+   1.102 33.583\r
+   1.144 30.159\r
+   1.200 22.334\r
+   1.298 10.923\r
+   1.346 6.521\r
+   1.398 3.260\r
+   1.448 1.793\r
+   1.497 0.978\r
+   1.600 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_F37.eng b/datafiles/thrustcurves/AeroTech_F37.eng
new file mode 100644 (file)
index 0000000..89c087f
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+;Aerotech F37 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F37 29 99 6-10-14 0.0282 0.1086 AT\r
+0.018 7.251\r
+0.053 13.626\r
+0.088 22.331\r
+0.106 25.227\r
+0.141 26.385\r
+0.183 28.411\r
+0.26 37.685\r
+0.31 41.449\r
+0.422 44.035\r
+0.524 45.183\r
+0.59 46.47\r
+0.682 45.153\r
+0.864 43.386\r
+0.934 40.471\r
+1.042 35.23\r
+1.151 29.699\r
+1.246 25.037\r
+1.354 19.796\r
+1.445 13.397\r
+1.498 7.586\r
+1.54 3.226\r
+1.6 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F39.eng b/datafiles/thrustcurves/AeroTech_F39.eng
new file mode 100644 (file)
index 0000000..8ea0d81
--- /dev/null
@@ -0,0 +1,40 @@
+; Aerotech F39 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (4/6/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F39T  24   70   3-6-9   .0227   .059   AT\r
+   0.010   45.057\r
+   0.016   54.131\r
+   0.046   58.321\r
+   0.079   59.470\r
+   0.103   58.311\r
+   0.130   57.253\r
+   0.172   55.491\r
+   0.235   53.738\r
+   0.321   51.271\r
+   0.363   50.566\r
+   0.387   49.509\r
+   0.408   50.203\r
+   0.426   48.804\r
+   0.453   47.746\r
+   0.480   47.041\r
+   0.680   41.059\r
+   0.716   39.649\r
+   0.752   38.944\r
+   0.809   36.487\r
+   0.860   34.382\r
+   0.893   33.324\r
+   0.917   32.619\r
+   1.000   28.752\r
+   1.075   25.247\r
+   1.105   22.095\r
+   1.126   17.201\r
+   1.144   13.001\r
+   1.174    8.109\r
+   1.219    4.606\r
+   1.261    2.500\r
+   1.330    0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_F40.eng b/datafiles/thrustcurves/AeroTech_F40.eng
new file mode 100644 (file)
index 0000000..ec729de
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+;Aerotech F40 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F40 29 124 4-7-10 0.04 0.126 AT\r
+0.015 17.776\r
+0.049 41.016\r
+0.089 58.793\r
+0.124 62.9\r
+0.148 65.173\r
+0.183 62.442\r
+0.242 68.07\r
+0.292 60.617\r
+0.321 61.524\r
+0.415 60.617\r
+0.524 58.334\r
+0.741 52.412\r
+0.87 48.314\r
+0.889 49.221\r
+0.914 47.397\r
+1.102 40.109\r
+1.285 33.728\r
+1.492 25.064\r
+1.665 15.952\r
+1.808 8.659\r
+1.942 3.19\r
+2.06 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F42.eng b/datafiles/thrustcurves/AeroTech_F42.eng
new file mode 100644 (file)
index 0000000..092f2ee
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;F42T Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;Based on data supplied by Aerotech prior to NAR certification.\r
+F42T 29 83 4-8 0.027 0.076 Aerotech\r
+0.01 68.694\r
+0.029 65.879\r
+0.202 62.5\r
+0.511 51.802\r
+0.739 43.356\r
+0.993 31.532\r
+1.02 29.279\r
+1.072 23.086\r
+1.199 9.572\r
+1.262 4.505\r
+1.319 2.815\r
+1.47 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F50.eng b/datafiles/thrustcurves/AeroTech_F50.eng
new file mode 100644 (file)
index 0000000..9b33bfb
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;Aerotech F50 RASP.ENG file made by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Thrust curve supplied by Aerotech for the molded case F50T motors.\r
+F50T 29 98 4-6-9 0.0336 0.0898 AeroTech\r
+0.013 73.762\r
+0.0326 70.383\r
+0.267 69.82\r
+0.518 67.005\r
+0.792 56.87\r
+0.906 50.676\r
+1 44.482\r
+1.036 39.978\r
+1.107 23.649\r
+1.199 6.194\r
+1.316 1.126\r
+1.43 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F52.eng b/datafiles/thrustcurves/AeroTech_F52.eng
new file mode 100644 (file)
index 0000000..e808de9
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+;Aerotech F52 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F52 29 124 5-8-11 0.0366 0.1214 AT\r
+0.012 46.899\r
+0.033 61.778\r
+0.056 69.441\r
+0.097 73.483\r
+0.115 76.636\r
+0.13 74.381\r
+0.153 74.82\r
+0.168 78.422\r
+0.182 78.95\r
+0.206 77.963\r
+0.238 77.504\r
+0.258 73.892\r
+0.314 72.974\r
+0.39 72.046\r
+0.428 70.679\r
+0.501 65.699\r
+0.565 62.975\r
+0.688 58.874\r
+0.749 56.15\r
+0.837 52.517\r
+0.901 49.793\r
+0.971 46.161\r
+1.088 39.365\r
+1.144 34.386\r
+1.173 29.417\r
+1.222 20.376\r
+1.275 13.151\r
+1.339 5.461\r
+1.389 1.838\r
+1.42 0\r
diff --git a/datafiles/thrustcurves/AeroTech_F62.eng b/datafiles/thrustcurves/AeroTech_F62.eng
new file mode 100644 (file)
index 0000000..f00317c
--- /dev/null
@@ -0,0 +1,33 @@
+; Aerotech F62T (Blue Thunder)\r
+;\r
+; AeroTech RMS-29/60 Easy Access Reloadable Motor Hardware.\r
+;\r
+; RASP.ENG file made from manufacturers catalog data.\r
+;\r
+; File produced May, 17 2004.\r
+;\r
+; The file was produced by scaling 16 data points off\r
+; the thrust curves in the manufacturers catalog.\r
+;\r
+; The F62T cannot be found on thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+F62T 29 99 6-8-9-10-11-13-14-16-18 0.025 0.109 AT\r
+0.0046 053.6364\r
+0.0416 055.2727\r
+0.0909 058.3636\r
+0.1356 061.6364\r
+0.1649 064.9091\r
+0.1864 067.6364\r
+0.5085 067.6364\r
+0.5701 064.7273\r
+0.6687 060.0000\r
+0.7427 055.0909\r
+0.7982 049.6364\r
+0.9029 048.7273\r
+0.9492 024.7273\r
+0.9661 020.1818\r
+0.9985 000.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_F72.eng b/datafiles/thrustcurves/AeroTech_F72.eng
new file mode 100644 (file)
index 0000000..25d9203
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Aerotech F72 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F72 24 124 5-10-15 0.0368 0.0742 AeroTech\r
+0.012 62.586\r
+0.017 84.986\r
+0.02 98.78\r
+0.03 94.748\r
+0.05 90.152\r
+0.069 82.688\r
+0.089 85.556\r
+0.104 80.39\r
+0.136 83.255\r
+0.146 80.96\r
+0.176 82.688\r
+0.198 78.672\r
+0.213 80.96\r
+0.253 80.39\r
+0.315 80.96\r
+0.38 79.821\r
+0.429 79.241\r
+0.489 78.092\r
+0.523 78.672\r
+0.536 75.225\r
+0.675 73.496\r
+0.699 67.182\r
+0.719 68.331\r
+0.747 64.884\r
+0.769 66.033\r
+0.858 60.867\r
+0.923 52.824\r
+0.98 40.195\r
+1.012 29.864\r
+1.034 20.092\r
+1.089 11.48\r
+1.21 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G101.eng b/datafiles/thrustcurves/AeroTech_G101.eng
new file mode 100644 (file)
index 0000000..9b306f1
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+G101T  29.0 114.30 5-8-12 0.04600 0.13600 AT\r
+   0.01      35.29 \r
+   0.02      63.97 \r
+   0.02      78.09 \r
+   0.03      84.71 \r
+   0.04      89.96 \r
+   0.06      93.96 \r
+   0.12      97.26 \r
+   0.17      99.14 \r
+   0.21     101.73 \r
+   0.27     104.09 \r
+   0.31     104.56 \r
+   0.36     103.62 \r
+   0.40     103.38 \r
+   0.45     101.03 \r
+   0.51      99.27 \r
+   0.53      94.41 \r
+   0.56      93.09 \r
+   0.64      87.35 \r
+   0.76      78.53 \r
+   0.80      75.88 \r
+   0.88      68.82 \r
+   0.89      65.73 \r
+   0.90      61.32 \r
+   0.91      55.15 \r
+   0.93      35.73 \r
+   0.94      32.65 \r
+   0.95      22.50 \r
+   0.98      10.59 \r
+   1.00       5.29 \r
+   1.05       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_G104.eng b/datafiles/thrustcurves/AeroTech_G104.eng
new file mode 100644 (file)
index 0000000..8045a05
--- /dev/null
@@ -0,0 +1,45 @@
+;\r
+; Aerotech G104T (Blue Thunder)\r
+;\r
+; AeroTech RMS-29/100 EZ Access Reloadable Motors.\r
+;\r
+; File produced 28 Feb 2005.\r
+;\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the manufacturers catalog sheet.\r
+;\r
+; The motor is not yet on www.thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia  Len  Delay Prop Gross Mfg\r
+G104T 29 124 6-8-9-10-11-13-14-16-18 0.0408 0.136 AT\r
+0.0067 125.3426\r
+0.0471 123.5424\r
+0.0856 121.9671\r
+0.1019 121.4046\r
+0.1462 121.1795\r
+0.1837 120.8420\r
+0.2029 120.5044\r
+0.2385 118.8167\r
+0.2644 117.2415\r
+0.2798 116.9039\r
+0.3279 116.6789\r
+0.3923 116.6789\r
+0.4298 116.1163\r
+0.4615 114.0910\r
+0.5067 110.1530\r
+0.5404 104.6397\r
+0.5760 096.2010\r
+0.6067 089.5626\r
+0.6817 078.8736\r
+0.7692 067.9595\r
+0.7865 064.9216\r
+0.7990 062.5588\r
+0.8058 058.0582\r
+0.8192 050.5196\r
+0.8385 039.0430\r
+0.8625 027.5664\r
+0.8769 016.6523\r
+0.9019 000.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_G12.eng b/datafiles/thrustcurves/AeroTech_G12.eng
new file mode 100644 (file)
index 0000000..aac501c
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Aerotech G12RC RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G12RC 32 107 100 0.0511 0.131 AT\r
+0.03 18.549\r
+0.117 19.96\r
+0.239 20.64\r
+0.362 20.111\r
+0.519 18.982\r
+0.694 17.138\r
+0.886 15.02\r
+1.131 13.186\r
+1.375 11.915\r
+1.689 11.069\r
+2.021 10.363\r
+2.422 10.232\r
+3.172 9.677\r
+4.114 9.267\r
+5.039 8.857\r
+6.137 8.733\r
+7.132 8.607\r
+7.795 8.335\r
+7.952 8.196\r
+8.074 8.055\r
+8.179 6.924\r
+8.319 4.661\r
+8.476 1.973\r
+8.55 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G25.eng b/datafiles/thrustcurves/AeroTech_G25.eng
new file mode 100644 (file)
index 0000000..02d639e
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Aerotech G25 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G25 29 124 5-10-15 0.0625 0.1197 AeroTech\r
+0.035 30.499\r
+0.047 36.712\r
+0.059 41.18\r
+0.13 40.669\r
+0.177 38.969\r
+0.295 38.969\r
+0.343 40.947\r
+0.413 40.38\r
+0.437 38.69\r
+0.484 39.824\r
+0.532 37.845\r
+0.65 37.557\r
+0.721 38.969\r
+0.803 38.69\r
+0.85 37.279\r
+0.98 39.535\r
+1.063 36.434\r
+1.098 38.124\r
+1.252 37.845\r
+1.37 37.279\r
+1.583 37\r
+1.819 35.3\r
+1.984 33.61\r
+2.185 31.344\r
+2.315 28.809\r
+2.622 24.286\r
+3.024 18.917\r
+3.39 13.838\r
+3.839 7.624\r
+4.323 4.518\r
+4.783 2.541\r
+5.3 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G33.eng b/datafiles/thrustcurves/AeroTech_G33.eng
new file mode 100644 (file)
index 0000000..b0fd50e
--- /dev/null
@@ -0,0 +1,40 @@
+;\r
+;Aerotech G33 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G33 29 124 5-7 0.0722 0.1593 AT\r
+0.027 22.642\r
+0.061 42.201\r
+0.117 47.354\r
+0.243 46.678\r
+0.34 46.339\r
+0.438 47.384\r
+0.48 50.92\r
+0.508 46.359\r
+0.543 47.732\r
+0.662 45.693\r
+0.851 42.28\r
+1.039 41.266\r
+1.116 42.987\r
+1.193 39.226\r
+1.221 42.31\r
+1.312 38.888\r
+1.326 40.609\r
+1.479 38.221\r
+1.675 35.157\r
+1.843 32.77\r
+1.878 36.888\r
+1.899 32.093\r
+1.997 30.382\r
+2.13 26.622\r
+2.263 23.547\r
+2.444 19.11\r
+2.591 13.977\r
+2.752 8.502\r
+2.892 4.743\r
+3.053 2.014\r
+3.27 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G339.eng b/datafiles/thrustcurves/AeroTech_G339.eng
new file mode 100644 (file)
index 0000000..5bf8159
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+; 38-120\r
+;  Created from TRA Certification Record issued 23 Nov 2006\r
+;  Bill Wagstaff - 04/30/07\r
+G339N  38 97  0  0.049  0.190  AT\r
+0.009 371\r
+0.05 375\r
+0.10 375\r
+0.15 364\r
+0.20 349\r
+0.25 310\r
+0.30 264\r
+0.324 257\r
+0.342 39\r
+0.359 0\r
+;\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_G35.eng b/datafiles/thrustcurves/AeroTech_G35.eng
new file mode 100644 (file)
index 0000000..91dac9a
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;\r
+G35EJ 29 98 4-7 0.05 0.1005 AeroTech\r
+0.01 39.14\r
+0.02 76.22\r
+0.05 64.46\r
+0.13 57.54\r
+0.21 57.53\r
+0.24 64.43\r
+0.25 57.06\r
+0.35 56.12\r
+0.43 55.2\r
+0.48 57.49\r
+0.51 52.41\r
+0.55 53.33\r
+0.76 50.54\r
+0.91 50.06\r
+1.11 44.96\r
+1.32 41.24\r
+1.55 35.68\r
+1.6 36.13\r
+1.63 33.36\r
+1.67 34.28\r
+1.8 30.12\r
+2 25.02\r
+2.14 21.32\r
+2.23 19.46\r
+2.3 15.77\r
+2.41 9.76\r
+2.53 6.52\r
+2.65 3.74\r
+2.74 1.88\r
+2.91 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G38.eng b/datafiles/thrustcurves/AeroTech_G38.eng
new file mode 100644 (file)
index 0000000..18fc774
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;Aerotech G38FJ RASP.ENG file made by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Thrust curve supplied by Aerotech for the molded case G38FJ motors.\r
+G38FJ 29 124 4-7 0.0597 0.1264 Aerotech\r
+0.024 52.928\r
+0.171 48.424\r
+0.497 45.045\r
+1 42.23\r
+1.279 39.978\r
+1.498 36.599\r
+1.783 30.406\r
+2.011 23.086\r
+2.272 10.135\r
+2.467 3.941\r
+2.64 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G40.eng b/datafiles/thrustcurves/AeroTech_G40.eng
new file mode 100644 (file)
index 0000000..cd2f4c3
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;Aerotech G40W RASP.ENG file made by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Thrust curve supplied by Aerotech for the molded case G40W motors.\r
+G40W 29 124 4-7-10 0.0538 0.123 AeroTech\r
+0.024 74.325\r
+0.057 67.005\r
+0.252 65.879\r
+0.5 63.063\r
+0.765 60.248\r
+1 54.054\r
+1.25 47.298\r
+1.502 36.599\r
+1.751 25.338\r
+1.999 12.951\r
+2.121 3.941\r
+2.3 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G53.eng b/datafiles/thrustcurves/AeroTech_G53.eng
new file mode 100644 (file)
index 0000000..ac94658
--- /dev/null
@@ -0,0 +1,26 @@
+; G53FJ based on Aerotech instruction sheet by C. Kobel 12/9/07\r
+G53FJ 29 124 5-7-10 0.060 0.152 AT\r
+   0.012 44.898\r
+   0.031 71.504\r
+   0.064 80.234\r
+   0.081 83.976\r
+   0.100 86.47\r
+   0.150 84.599\r
+   0.200 81.897\r
+   0.300 78.571\r
+   0.400 76.493\r
+   0.500 73.583\r
+   0.600 70.881\r
+   0.700 67.347\r
+   0.800 63.813\r
+   0.900 60.072\r
+   1.000 54.667\r
+   1.100 47.392\r
+   1.200 39.909\r
+   1.300 32.426\r
+   1.400 25.983\r
+   1.500 20.578\r
+   1.600 10.601\r
+   1.700 3.949\r
+   1.800 1.247\r
+   1.850 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G54.eng b/datafiles/thrustcurves/AeroTech_G54.eng
new file mode 100644 (file)
index 0000000..8b0dd00
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+;Aerotech G54 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G54 29 124 6-10-14 0.046 0.1365 AT\r
+0.018 10.953\r
+0.042 39.215\r
+0.083 66.888\r
+0.14 72.075\r
+0.223 74.958\r
+0.25 76.694\r
+0.282 80.156\r
+0.315 79.577\r
+0.336 79.577\r
+0.354 81.64\r
+0.365 77.841\r
+0.374 80.724\r
+0.389 76.694\r
+0.455 76.116\r
+0.523 74.39\r
+0.639 70.928\r
+0.722 67.467\r
+0.82 64.005\r
+0.897 58.817\r
+0.992 51.894\r
+1.084 43.824\r
+1.197 34.017\r
+1.268 28.251\r
+1.283 29.987\r
+1.295 27.104\r
+1.328 23.642\r
+1.366 16.719\r
+1.399 9.803\r
+1.435 4.612\r
+1.51 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G55.eng b/datafiles/thrustcurves/AeroTech_G55.eng
new file mode 100644 (file)
index 0000000..94f21d7
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Aerotech G55 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G55 24 117 5-10-15 0.0625 0.1148 AeroTech\r
+0.009 81.136\r
+0.014 84.65\r
+0.034 80.557\r
+0.084 77.064\r
+0.13 71.823\r
+0.18 72.422\r
+0.206 68.919\r
+0.342 69.538\r
+0.483 68.989\r
+0.513 66.663\r
+0.543 68.42\r
+0.664 66.114\r
+0.876 66.164\r
+0.901 64.418\r
+0.997 65.026\r
+1.062 66.793\r
+1.088 63.879\r
+1.148 63.889\r
+1.158 66.813\r
+1.173 63.31\r
+1.209 62.741\r
+1.325 61.593\r
+1.395 59.277\r
+1.456 57.541\r
+1.486 58.129\r
+1.587 52.32\r
+1.708 40.094\r
+1.824 26.11\r
+1.95 15.63\r
+2.112 7.498\r
+2.258 3.446\r
+2.44 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G61.eng b/datafiles/thrustcurves/AeroTech_G61.eng
new file mode 100644 (file)
index 0000000..6301a6e
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;G61W Data Entered by Tim Van Milligan\r
+;For RockSim: www.RockSim.com\r
+;Based on TRA Certification Test date: June 13, 2004\r
+;Not Approved by TRA or Aerotech\r
+G61W 38 106.7 6-10-14 0.0613 0.1904 AT\r
+0.008 3.083\r
+0.054 71.348\r
+0.089 72.229\r
+0.174 75.312\r
+0.216 78.394\r
+0.247 79.716\r
+0.502 81.037\r
+0.753 77.073\r
+1.001 72.669\r
+1.132 66.944\r
+1.252 55.933\r
+1.503 38.316\r
+1.754 10.13\r
+1.905 3.523\r
+2.04 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G64.eng b/datafiles/thrustcurves/AeroTech_G64.eng
new file mode 100644 (file)
index 0000000..d3f077a
--- /dev/null
@@ -0,0 +1,38 @@
+;\r
+;Aerotech G64 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G64 29 124 4-8-10 0.0625 0.1512 AT\r
+0.014 54.325\r
+0.032 81.488\r
+0.059 98.31\r
+0.101 85.021\r
+0.165 83.847\r
+0.274 85.614\r
+0.37 87.39\r
+0.476 86.798\r
+0.503 91.516\r
+0.517 85.614\r
+0.585 83.847\r
+0.723 80.896\r
+0.745 82.07\r
+0.773 77.945\r
+0.883 75.576\r
+0.988 74.401\r
+1.093 69.673\r
+1.262 61.412\r
+1.28 61.994\r
+1.326 58.451\r
+1.372 54.907\r
+1.422 47.238\r
+1.505 34.841\r
+1.591 23.027\r
+1.701 13.581\r
+1.829 7.085\r
+1.902 4.133\r
+1.966 1.771\r
+2.09 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G67.eng b/datafiles/thrustcurves/AeroTech_G67.eng
new file mode 100644 (file)
index 0000000..4c6e790
--- /dev/null
@@ -0,0 +1,49 @@
+;\r
+; Aerotech G67R (Redline)\r
+;\r
+; AeroTech RMS-38/120 EZ Access Reloadable Motors (New! Hardware).\r
+; New AeroTech Redline Motor. Just announced on AeroTech's Website!\r
+; File produced 28 Feb 2005.\r
+;\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the manufacturers catalog sheet.\r
+;\r
+; The motor is not yet on www.thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia  Len  Delay Prop Gross Mfg\r
+G67R 38 106 4-6-8-9-10-12-13-15-17 0.0576 0.191 AT\r
+0.0400 004.9200\r
+0.0500 006.5600\r
+0.0600 009.8400\r
+0.0700 016.4100\r
+0.0800 032.8100\r
+0.1000 049.2200\r
+0.1300 068.0800\r
+0.1500 076.2900\r
+0.1800 080.3900\r
+0.2400 082.8500\r
+0.2600 085.3100\r
+0.3100 087.7700\r
+0.5100 089.4100\r
+0.5300 091.0500\r
+0.5600 087.7700\r
+0.6000 086.9500\r
+0.6100 088.5900\r
+0.6700 086.9500\r
+0.6900 085.3100\r
+0.7100 086.9500\r
+0.7200 085.3100\r
+0.7400 086.1300\r
+0.7700 085.3100\r
+0.8100 082.0300\r
+0.9500 078.7500\r
+1.0500 073.8200\r
+1.4300 053.3200\r
+1.5000 052.5000\r
+1.5200 050.8600\r
+1.5400 045.9400\r
+1.6200 011.4800\r
+1.6400 000.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_G69.eng b/datafiles/thrustcurves/AeroTech_G69.eng
new file mode 100644 (file)
index 0000000..19cffae
--- /dev/null
@@ -0,0 +1,28 @@
+; Submitted to ThrustCurve.org by Chris Kobel (4/13/07)\r
+; G69N based on Aerotech instruction sheet by C. Kobel 3/29/07\r
+G69N 38 106 0 0.0622 0.195 AT\r
+   0.020 51.972\r
+   0.050 75.574\r
+   0.100 76.709\r
+   0.200 77.617\r
+   0.300 79.206\r
+   0.400 81.475\r
+   0.500 84.425\r
+   0.600 86.922\r
+   0.700 88.737\r
+   0.800 89.645\r
+   0.900 91.688\r
+   1.000 93.503\r
+   1.100 94.411\r
+   1.200 94.638\r
+   1.300 93.957\r
+   1.350 93.05\r
+   1.400 89.418\r
+   1.500 62.865\r
+   1.600 33.362\r
+   1.650 19.518\r
+   1.700 12.028\r
+   1.750 7.489\r
+   1.800 4.539\r
+   1.900 1.816\r
+   2.000 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G71.eng b/datafiles/thrustcurves/AeroTech_G71.eng
new file mode 100644 (file)
index 0000000..0fc4607
--- /dev/null
@@ -0,0 +1,21 @@
+; G71R based on Aerotech instruction sheet by C. Kobel 3/29/07\r
+G71R 29 124 4-7-10 0.0569 0.147 AT\r
+   0.000 0.389\r
+   0.050 109.714\r
+   0.100 117.884\r
+   0.200 113.216\r
+   0.300 109.714\r
+   0.400 105.045\r
+   0.500 99.21\r
+   0.600 92.207\r
+   0.700 83.258\r
+   0.800 75.477\r
+   0.900 68.085\r
+   1.000 57.97\r
+   1.100 47.465\r
+   1.200 33.848\r
+   1.300 21.009\r
+   1.400 11.283\r
+   1.500 5.447\r
+   1.600 2.334\r
+   1.700 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G71_1.eng b/datafiles/thrustcurves/AeroTech_G71_1.eng
new file mode 100644 (file)
index 0000000..348e7df
--- /dev/null
@@ -0,0 +1,72 @@
+; RMS-29/40-120 Reload, 2 grain design, G71-XR (Redline propellent), with 4, 7,\r
+; 10 second delays\r
+G71-R 29 120 7 0.0569 0.147 AT\r
+   0.0080 46.656\r
+   0.015 74.248\r
+   0.023 85.787\r
+   0.031 100.336\r
+   0.05 110.871\r
+   0.062 116.891\r
+   0.085 120.403\r
+   0.116 119.901\r
+   0.139 118.898\r
+   0.158 116.891\r
+   0.181 115.386\r
+   0.216 114.383\r
+   0.251 113.379\r
+   0.278 111.373\r
+   0.297 111.373\r
+   0.309 114.383\r
+   0.328 112.376\r
+   0.355 109.366\r
+   0.39 107.359\r
+   0.432 104.851\r
+   0.463 103.848\r
+   0.494 100.837\r
+   0.525 98.831\r
+   0.552 95.821\r
+   0.583 94.316\r
+   0.606 92.309\r
+   0.633 89.299\r
+   0.653 87.292\r
+   0.676 85.787\r
+   0.699 81.272\r
+   0.714 84.282\r
+   0.734 81.272\r
+   0.749 88.797\r
+   0.772 80.269\r
+   0.799 76.255\r
+   0.826 73.747\r
+   0.861 70.737\r
+   0.876 73.747\r
+   0.892 69.232\r
+   0.915 69.733\r
+   0.923 65.72\r
+   0.942 63.713\r
+   0.977 60.703\r
+   1.008 58.195\r
+   1.039 54.181\r
+   1.077 50.168\r
+   1.108 46.154\r
+   1.12 50.168\r
+   1.127 46.154\r
+   1.143 43.144\r
+   1.178 37.626\r
+   1.212 32.609\r
+   1.232 30.101\r
+   1.255 26.589\r
+   1.274 23.579\r
+   1.301 20.569\r
+   1.317 18.06\r
+   1.344 15.552\r
+   1.382 11.539\r
+   1.417 10.034\r
+   1.448 7.024\r
+   1.486 6.02\r
+   1.517 3.512\r
+   1.552 3.01\r
+   1.587 2.508\r
+   1.618 1.003\r
+   1.668 0.502\r
+   1.707 0.502\r
+   1.734 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G75.eng b/datafiles/thrustcurves/AeroTech_G75.eng
new file mode 100644 (file)
index 0000000..c85f277
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech G75J\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+G75J 29 194 10 0.112 0.23296 AT\r
+   0.047 65.701\r
+   0.143 68.564\r
+   0.239 72.143\r
+   0.334 73.261\r
+   0.430 73.960\r
+   0.526 75.036\r
+   0.622 75.705\r
+   0.718 75.030\r
+   0.814 77.886\r
+   0.909 76.183\r
+   1.005 76.852\r
+   1.101 75.729\r
+   1.197 78.854\r
+   1.293 78.669\r
+   1.389 76.464\r
+   1.484 76.440\r
+   1.580 74.976\r
+   1.676 72.657\r
+   1.772 69.460\r
+   1.868 62.121\r
+   1.964 39.090\r
+   2.059 19.703\r
+   2.155 7.554\r
+   2.251 2.062\r
+   2.347 0.382\r
+   2.443 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_G75_1.eng b/datafiles/thrustcurves/AeroTech_G75_1.eng
new file mode 100644 (file)
index 0000000..e3e4aa5
--- /dev/null
@@ -0,0 +1,83 @@
+;\r
+; Aerotech G75J (Black Jack)\r
+;\r
+; AeroTech RMS-29/180 Easy Access Reloadable Motor Hardware.\r
+;\r
+; RASP.ENG file made from made from NAR or TMT published data.\r
+;\r
+; File produced May, 17 2004.\r
+;\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data from NAR or TMT files.\r
+;\r
+; The curve drawn with these data points is as accurate as could\r
+; could be made scaling the data from the curve on the TMT html\r
+; page. The file is 63 data points. NOT wRASP v1.6 compatible.\r
+;\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+G75J 29 194 1-3--4-6-7-9-10 0.114 0.236 AT\r
+0.0281 068.8604\r
+0.0380 078.6517\r
+0.0561 075.9230\r
+0.0660 073.0337\r
+0.0776 070.4655\r
+0.1139 069.3419\r
+0.1403 068.6998\r
+0.1667 067.5762\r
+0.1881 070.3050\r
+0.2013 069.0209\r
+0.2294 072.5522\r
+0.2541 076.4045\r
+0.2723 071.4286\r
+0.3102 076.2440\r
+0.3350 071.9101\r
+0.4208 075.6019\r
+0.4604 072.3917\r
+0.5215 079.2937\r
+0.5941 073.3547\r
+0.6436 080.0963\r
+0.7013 073.8363\r
+0.7393 076.4045\r
+0.7541 074.6388\r
+0.7657 077.3676\r
+0.7937 078.6517\r
+0.8036 077.0465\r
+0.8168 080.2568\r
+0.8267 075.2809\r
+0.8383 081.5409\r
+0.8581 075.7624\r
+0.8795 077.8491\r
+0.9340 074.1573\r
+0.9868 079.9358\r
+1.0380 076.7255\r
+1.0561 072.2311\r
+1.0941 078.8122\r
+1.1221 075.6019\r
+1.1502 080.8989\r
+1.1617 076.8860\r
+1.1848 080.0963\r
+1.1997 076.7255\r
+1.2327 078.8122\r
+1.2508 076.4045\r
+1.2871 082.5040\r
+1.3102 077.5281\r
+1.3267 081.8620\r
+1.3564 075.2809\r
+1.3729 080.0963\r
+1.4076 075.7624\r
+1.4884 079.9358\r
+1.5116 073.6758\r
+1.5297 081.0594\r
+1.5512 074.6388\r
+1.5611 080.8989\r
+1.6040 072.8732\r
+1.7211 075.6019\r
+1.7607 068.6998\r
+1.7789 070.1445\r
+1.8119 066.9342\r
+1.8267 070.7865\r
+1.8482 070.7865\r
+2.3878 000.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_G76.eng b/datafiles/thrustcurves/AeroTech_G76.eng
new file mode 100644 (file)
index 0000000..c70eca0
--- /dev/null
@@ -0,0 +1,41 @@
+; Curve fit of AT Instruction sheet by C. Kobel  7/29/08\r
+G76G 29 124 4-7-10 0.06 0.147 AT\r
+   0.025 89.368\r
+   0.042 133.581\r
+   0.052 144.87\r
+   0.067 154.277\r
+   0.098 144.399\r
+   0.117 136.873\r
+   0.150 132.64\r
+   0.196 129.348\r
+   0.255 123.233\r
+   0.299 118.059\r
+   0.349 112.885\r
+   0.399 108.652\r
+   0.449 101.126\r
+   0.486 101.597\r
+   0.511 105.36\r
+   0.516 118.53\r
+   0.543 100.186\r
+   0.601 95.482\r
+   0.656 88.897\r
+   0.720 81.842\r
+   0.737 93.601\r
+   0.754 80.431\r
+   0.797 70.553\r
+   0.856 63.498\r
+   0.898 58.794\r
+   0.948 51.739\r
+   1.000 47.976\r
+   1.063 43.273\r
+   1.102 41.391\r
+   1.152 39.04\r
+   1.200 36.688\r
+   1.301 30.103\r
+   1.347 25.399\r
+   1.401 19.755\r
+   1.499 12.229\r
+   1.547 7.996\r
+   1.599 5.644\r
+   1.699 2.352\r
+   1.750 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G76_1.eng b/datafiles/thrustcurves/AeroTech_G76_1.eng
new file mode 100644 (file)
index 0000000..37b1c40
--- /dev/null
@@ -0,0 +1,34 @@
+; Exported using ThrustCurveTool, www.ThrustGear.com\r
+; NAR S&T Data, contributed by John DeMar\r
+G72 29 124 4-7-10 0.06 0.144 Aerotech\r
+  0.0040     4.2403\r
+  0.0060     9.6831\r
+  0.016      54.397\r
+  0.03       98.17\r
+  0.034      108.509\r
+  0.04       121.159\r
+  0.048      133.047\r
+  0.058      142.348\r
+  0.068      147.209\r
+  0.082      149.393\r
+  0.112      146.89\r
+  0.15       138.783\r
+  0.17       136.039\r
+  0.376      112.222\r
+  0.466      103.079\r
+  0.482      104.438\r
+  0.56       92.6523\r
+  0.606      92.8669\r
+  0.644      84.2058\r
+  0.748      73.9389\r
+  0.76       74.2841\r
+  0.79       69.704\r
+  0.804      70.6\r
+  0.822      66.7591\r
+  0.856      62.6853\r
+  1.154      39.9972\r
+  1.374      19.9105\r
+  1.474      12.5198\r
+  1.574      7.41771\r
+  1.78       1.19026\r
+  2.00       0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G77.eng b/datafiles/thrustcurves/AeroTech_G77.eng
new file mode 100644 (file)
index 0000000..097b721
--- /dev/null
@@ -0,0 +1,40 @@
+;\r
+; Aerotech G77R (Redline)\r
+;\r
+; AeroTech RMS-29/120 EZ Access Reloadable Motors (New! Hardware).\r
+; New AeroTech Redline Motor. Just announced on AeroTech's Website!\r
+; File produced 28 Feb 2005.\r
+;\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the manufacturers catalog sheet.\r
+;\r
+; The motor is not yet on www.thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia  Len  Delay Prop Gross Mfg\r
+G77R 29 150 4-6-8-9-10-12-13-15-17 0.0554 0.155 AT\r
+0.0132 014.8333\r
+0.0243 032.4479\r
+0.0331 046.3542\r
+0.0375 052.8438\r
+0.0463 056.5521\r
+0.0617 059.3333\r
+0.2580 073.2396\r
+0.6548 087.1458\r
+0.8709 089.0000\r
+0.8885 085.2917\r
+1.0252 086.2188\r
+1.0472 084.3646\r
+1.0715 086.2188\r
+1.1002 084.3646\r
+1.1332 085.2917\r
+1.1950 076.9479\r
+1.2104 076.0208\r
+1.2369 065.8229\r
+1.2611 043.5729\r
+1.2898 027.8125\r
+1.3317 012.0521\r
+1.3625 004.6354\r
+1.4000 000.0000\r
diff --git a/datafiles/thrustcurves/AeroTech_G78.eng b/datafiles/thrustcurves/AeroTech_G78.eng
new file mode 100644 (file)
index 0000000..fff4c48
--- /dev/null
@@ -0,0 +1,35 @@
+; @File: G78G_Typ.txt, @Pts-I: 1001, @Pts-O: 31, @Sm: 0, @CO: 5%\r
+; @TI: 109.776, @TIa: 109.5639, @TIe: 0.0%, @ThMax: 102.242, @ThAvg: 79.5671, @Tb: 1.377\r
+; Exported using ThrustCurveTool, www.ThrustGear.com\r
+G78G 29 146 4-7-10, 0.0597 0.125 AT/RCS\r
+0.0040 2.76203\r
+0.0060 34.7707\r
+0.0080 44.326\r
+0.01 41.2789\r
+0.012 28.5933\r
+0.014 27.3926\r
+0.016 38.0574\r
+0.02 47.7036\r
+0.022 48.2012\r
+0.03 56.4344\r
+0.042 61.4034\r
+0.06 65.883\r
+0.212 84.526\r
+0.266 88.9218\r
+0.404 95.5932\r
+0.43 98.9746\r
+0.466 100.5362\r
+0.58 102.242\r
+0.694 101.3187\r
+0.86 95.4526\r
+1.1 90.4186\r
+1.132 82.5618\r
+1.156 72.6383\r
+1.168 64.903\r
+1.194 42.3389\r
+1.216 28.3424\r
+1.226 24.9802\r
+1.2559 17.40051\r
+1.2859 13.04651\r
+1.3819 5.05295\r
+1.4719 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G79.eng b/datafiles/thrustcurves/AeroTech_G79.eng
new file mode 100644 (file)
index 0000000..508cb9d
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;G79W Data Entered by Tim Van Milligan\r
+;For RockSim: www.RockSim.com\r
+;Based on TRA Certification Test date: June 13, 2004\r
+;Not Approved by TRA or Aerotech\r
+G79W 29 149.86 6-10-14 0.0609 0.154 AT\r
+0.015 7.157\r
+0.074 91.937\r
+0.09 91.387\r
+0.114 84.781\r
+0.145 84.23\r
+0.201 89.185\r
+0.291 94.69\r
+0.4 98.544\r
+0.6 99.645\r
+0.708 96.892\r
+0.8 93.038\r
+0.915 85.331\r
+1 77.624\r
+1.085 71.017\r
+1.175 68.265\r
+1.199 44.59\r
+1.28 22.021\r
+1.36 4.955\r
+1.42 0\r
diff --git a/datafiles/thrustcurves/AeroTech_G80.eng b/datafiles/thrustcurves/AeroTech_G80.eng
new file mode 100644 (file)
index 0000000..0df93f6
--- /dev/null
@@ -0,0 +1,37 @@
+; 136 N-sec G80, Certified Nov. 2007. As published by NAR S&T.\r
+;\r
+; @File: NewATG80.txt, @Pts-I: 905, @Pts-O: 31, @Sm: 0, @CO: 5%\r
+; @TI: 133.2377, @TIa: 133.1309, @TIe: 0.0%, @ThMax: 102.2, @ThAvg: 77.9911, @Tb: 1.707\r
+; Exported using ThrustCurveTool, www.ThrustGear.com\r
+G78 29 128 7,10,13 0.0625 0.1282 RCS/Aerotech\r
+0.0060 1.158086\r
+0.0080 7.48984\r
+0.01 33.7575\r
+0.012 64.5955\r
+0.014 62.9316\r
+0.016 58.8272\r
+0.018 74.9118\r
+0.02 85.0062\r
+0.022 91.1072\r
+0.026 93.9913\r
+0.028 98.4284\r
+0.032 97.652\r
+0.038 102.2\r
+0.074 97.3192\r
+0.124 95.4334\r
+0.376 99.3159\r
+0.68 99.4268\r
+0.994 91.6619\r
+1.2459 83.0095\r
+1.2819 77.3522\r
+1.3159 61.9332\r
+1.3599 44.6285\r
+1.4239 29.0986\r
+1.5039 21.2227\r
+1.5979 19.33693\r
+1.6559 16.34188\r
+1.6759 13.90147\r
+1.6779 11.79384\r
+1.7139 5.0938\r
+1.7339 1.388816\r
+1.8079 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G80_1.eng b/datafiles/thrustcurves/AeroTech_G80_1.eng
new file mode 100644 (file)
index 0000000..37179e0
--- /dev/null
@@ -0,0 +1,28 @@
+; AEROTECH G80 RASP.ENG FILE\r
+; Note: this is for the 94 N-sec G80T certified in Sept. 2006\r
+G80 29 124 4-7-10 0.0479 0.1129998 AERO\r
+  0.0060     84.371\r
+  0.018      118.23\r
+  0.027      109.378\r
+  0.037      101.35\r
+  0.042      105.565\r
+  0.059      98.37\r
+  0.113      95.821\r
+  0.185      96.252\r
+  0.277      94.556\r
+  0.404      94.978\r
+  0.526      91.165\r
+  0.671      87.1\r
+  0.792      82.675\r
+  0.885      77.166\r
+  0.943      73.353\r
+  0.971      68.266\r
+  0.997      57.237\r
+  1.03       48.337\r
+  1.059      40.279\r
+  1.085      27.986\r
+  1.112      17.811\r
+  1.144      10.175\r
+  1.168      5.512\r
+  1.182      2.968\r
+  1.21       0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_G80_2.eng b/datafiles/thrustcurves/AeroTech_G80_2.eng
new file mode 100644 (file)
index 0000000..7943632
--- /dev/null
@@ -0,0 +1,34 @@
+; Aerotech G80 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Note: This is for the 116N-sec G80T produced before Sept. 2006\r
+G80 29 124 4-7-10 0.0574 0.1049 A\r
+  0.0060     101.291\r
+  0.013      105.18\r
+  0.031      103.473\r
+  0.038      104.069\r
+  0.067      99.803\r
+  0.103      96.906\r
+  0.181      94.733\r
+  0.271      94.039\r
+  0.303      96.985\r
+  0.367      95.547\r
+  0.428      94.842\r
+  0.456      97.055\r
+  0.463      92.65\r
+  0.51       94.872\r
+  0.596      93.444\r
+  0.606      95.646\r
+  0.624      91.985\r
+  0.635      95.656\r
+  0.646      91.995\r
+  0.696      90.547\r
+  0.846      85.477\r
+  0.96       80.388\r
+  1.071      74.564\r
+  1.207      62.878\r
+  1.296      52.639\r
+  1.35       37.252\r
+  1.382      20.397\r
+  1.418      10.139\r
+  1.457      4.281\r
+  1.5        0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_H112.eng b/datafiles/thrustcurves/AeroTech_H112.eng
new file mode 100644 (file)
index 0000000..0663f32
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H112J\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H112J 38 202 0 0.187712 0.379456 AT\r
+   0.064 85.431\r
+   0.194 101.938\r
+   0.324 101.897\r
+   0.454 102.839\r
+   0.584 104.479\r
+   0.715 103.845\r
+   0.845 103.439\r
+   0.975 104.286\r
+   1.106 104.922\r
+   1.236 104.390\r
+   1.367 102.768\r
+   1.497 102.237\r
+   1.627 100.032\r
+   1.757 98.345\r
+   1.888 94.560\r
+   2.018 89.018\r
+   2.148 82.857\r
+   2.279 77.685\r
+   2.409 72.373\r
+   2.540 67.041\r
+   2.670 59.764\r
+   2.800 37.616\r
+   2.930 14.457\r
+   3.060 4.642\r
+   3.192 1.818\r
+   3.323 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H123.eng b/datafiles/thrustcurves/AeroTech_H123.eng
new file mode 100644 (file)
index 0000000..32cff0f
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H123W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H123W 38 154 0 0.126336 0.278656 AT\r
+   0.047 96.764\r
+   0.143 146.256\r
+   0.239 150.699\r
+   0.334 152.496\r
+   0.430 151.248\r
+   0.526 149.875\r
+   0.622 150.200\r
+   0.718 149.176\r
+   0.814 144.858\r
+   0.909 143.536\r
+   1.005 141.414\r
+   1.101 135.125\r
+   1.198 125.288\r
+   1.295 114.035\r
+   1.391 101.556\r
+   1.486 90.175\r
+   1.582 78.694\r
+   1.678 66.364\r
+   1.774 54.260\r
+   1.870 46.872\r
+   1.966 38.186\r
+   2.061 22.737\r
+   2.157 13.478\r
+   2.253 7.587\r
+   2.350 5.252\r
+   2.447 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H125.eng b/datafiles/thrustcurves/AeroTech_H125.eng
new file mode 100644 (file)
index 0000000..c7951e4
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H125W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H125W 29 330 14 0.18816 0.32256 AT\r
+   0.053 275.994\r
+   0.161 241.473\r
+   0.270 216.283\r
+   0.378 199.482\r
+   0.488 188.758\r
+   0.597 182.349\r
+   0.705 175.862\r
+   0.814 169.365\r
+   0.922 162.281\r
+   1.031 154.276\r
+   1.141 143.915\r
+   1.249 133.480\r
+   1.357 123.007\r
+   1.466 113.058\r
+   1.575 101.801\r
+   1.684 88.423\r
+   1.793 73.530\r
+   1.901 60.425\r
+   2.009 46.643\r
+   2.119 36.785\r
+   2.228 29.546\r
+   2.336 23.641\r
+   2.445 18.794\r
+   2.553 14.728\r
+   2.663 10.970\r
+   2.772 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H128.eng b/datafiles/thrustcurves/AeroTech_H128.eng
new file mode 100644 (file)
index 0000000..8fbfbb9
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H128W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H128W 29 194 14 0.09408 0.2016 AT\r
+   0.024 102.423\r
+   0.074 175.203\r
+   0.125 181.379\r
+   0.176 179.281\r
+   0.226 181.423\r
+   0.277 186.074\r
+   0.328 190.483\r
+   0.378 189.509\r
+   0.429 186.162\r
+   0.480 184.114\r
+   0.530 180.300\r
+   0.581 174.144\r
+   0.632 172.019\r
+   0.683 169.360\r
+   0.734 166.017\r
+   0.784 161.882\r
+   0.835 157.331\r
+   0.886 153.073\r
+   0.936 151.985\r
+   0.988 139.902\r
+   1.039 87.865\r
+   1.089 40.857\r
+   1.140 14.328\r
+   1.191 4.330\r
+   1.242 1.550\r
+   1.293 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H148.eng b/datafiles/thrustcurves/AeroTech_H148.eng
new file mode 100644 (file)
index 0000000..af5e0ad
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech H148R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H148R 38 152 0 0.14784 0.30912 AT\r
+   0.027 77.232\r
+   0.088 174.296\r
+   0.148 185.046\r
+   0.208 190.458\r
+   0.268 192.497\r
+   0.327 191.996\r
+   0.388 188.790\r
+   0.448 187.548\r
+   0.509 182.697\r
+   0.570 178.151\r
+   0.630 172.906\r
+   0.690 169.607\r
+   0.750 164.510\r
+   0.810 158.375\r
+   0.870 153.019\r
+   0.930 146.810\r
+   0.991 139.443\r
+   1.053 132.001\r
+   1.112 123.271\r
+   1.173 112.559\r
+   1.233 104.737\r
+   1.292 97.657\r
+   1.353 94.932\r
+   1.413 60.644\r
+   1.474 13.007\r
+   1.535 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H165.eng b/datafiles/thrustcurves/AeroTech_H165.eng
new file mode 100644 (file)
index 0000000..be08011
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech H165R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H165R 29 194 0 0.0896 0.2016 AT\r
+   0.018 55.047\r
+   0.059 157.258\r
+   0.101 168.509\r
+   0.144 173.219\r
+   0.186 179.237\r
+   0.229 183.947\r
+   0.271 187.872\r
+   0.314 188.134\r
+   0.356 188.919\r
+   0.399 190.488\r
+   0.441 187.349\r
+   0.484 189.180\r
+   0.525 186.547\r
+   0.566 185.517\r
+   0.609 180.807\r
+   0.651 177.667\r
+   0.694 170.602\r
+   0.736 167.201\r
+   0.779 158.828\r
+   0.821 155.688\r
+   0.864 153.333\r
+   0.906 136.325\r
+   0.949 73.526\r
+   0.991 20.671\r
+   1.034 4.448\r
+   1.076 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H180.eng b/datafiles/thrustcurves/AeroTech_H180.eng
new file mode 100644 (file)
index 0000000..7b26127
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H180W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H180W 29 238 6 0.12096 0.2464 AT\r
+   0.024 149.374\r
+   0.075 222.273\r
+   0.127 222.339\r
+   0.178 227.835\r
+   0.229 234.963\r
+   0.281 238.162\r
+   0.333 240.252\r
+   0.384 243.126\r
+   0.435 240.757\r
+   0.487 240.724\r
+   0.539 236.311\r
+   0.590 236.799\r
+   0.642 234.897\r
+   0.694 232.763\r
+   0.745 229.198\r
+   0.796 228.816\r
+   0.848 231.906\r
+   0.899 225.853\r
+   0.950 188.285\r
+   1.002 134.679\r
+   1.054 78.940\r
+   1.105 34.557\r
+   1.156 15.482\r
+   1.208 7.279\r
+   1.260 3.585\r
+   1.313 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H210.eng b/datafiles/thrustcurves/AeroTech_H210.eng
new file mode 100644 (file)
index 0000000..fbbc08e
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech H210R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H210R 29 238 0 0.12096 0.2464 AT\r
+   0.019 105.923\r
+   0.059 211.290\r
+   0.099 219.770\r
+   0.139 229.639\r
+   0.179 235.082\r
+   0.220 241.594\r
+   0.260 242.706\r
+   0.300 245.347\r
+   0.341 249.100\r
+   0.381 253.410\r
+   0.421 258.553\r
+   0.461 260.221\r
+   0.502 257.997\r
+   0.543 259.248\r
+   0.583 256.607\r
+   0.623 252.436\r
+   0.663 245.056\r
+   0.704 219.909\r
+   0.744 209.344\r
+   0.784 200.587\r
+   0.824 193.565\r
+   0.865 184.323\r
+   0.905 153.881\r
+   0.945 58.244\r
+   0.986 13.210\r
+   1.027 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H220.eng b/datafiles/thrustcurves/AeroTech_H220.eng
new file mode 100644 (file)
index 0000000..6c34838
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;\r
+H220T 29 239 6-10-14 0.1064 0.2386 AT\r
+0 314.1\r
+0.1 236.61\r
+0.2 269.23\r
+0.3 261.06\r
+0.4 252.9\r
+0.72 252.9\r
+0.8 112.58\r
+0.9 9.78\r
+0.96 0\r
diff --git a/datafiles/thrustcurves/AeroTech_H238.eng b/datafiles/thrustcurves/AeroTech_H238.eng
new file mode 100644 (file)
index 0000000..66810d4
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H238T\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H238T 29 194 6 0.08064 0.18816 AT\r
+   0.019 173.100\r
+   0.059 206.876\r
+   0.100 211.036\r
+   0.141 214.353\r
+   0.181 217.113\r
+   0.222 219.343\r
+   0.263 221.034\r
+   0.303 224.951\r
+   0.344 229.324\r
+   0.384 229.308\r
+   0.425 228.558\r
+   0.466 226.573\r
+   0.506 222.365\r
+   0.547 219.205\r
+   0.588 217.750\r
+   0.628 213.350\r
+   0.669 208.070\r
+   0.709 200.966\r
+   0.750 196.988\r
+   0.791 151.387\r
+   0.831 96.273\r
+   0.872 59.475\r
+   0.912 18.621\r
+   0.953 7.986\r
+   0.995 3.697\r
+   1.036 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H242.eng b/datafiles/thrustcurves/AeroTech_H242.eng
new file mode 100644 (file)
index 0000000..ca4affa
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H242T\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H242T 38 152 10 0.11648 0.2688 AT\r
+   0.030 164.060\r
+   0.093 197.516\r
+   0.155 204.324\r
+   0.218 208.970\r
+   0.280 211.481\r
+   0.343 211.261\r
+   0.405 209.291\r
+   0.468 208.438\r
+   0.531 206.707\r
+   0.595 203.967\r
+   0.657 198.175\r
+   0.720 192.137\r
+   0.782 186.840\r
+   0.845 180.802\r
+   0.907 174.635\r
+   0.970 165.581\r
+   1.033 159.726\r
+   1.097 151.690\r
+   1.159 144.167\r
+   1.222 138.550\r
+   1.284 119.114\r
+   1.347 69.055\r
+   1.409 21.396\r
+   1.472 3.473\r
+   1.535 0.594\r
+   1.599 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H242_1.eng b/datafiles/thrustcurves/AeroTech_H242_1.eng
new file mode 100644 (file)
index 0000000..a59878e
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H242T\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H242T 38 154 0 0.114688 0.264768 AT\r
+   0.025 207.058\r
+   0.077 237.941\r
+   0.129 240.171\r
+   0.181 241.906\r
+   0.234 246.425\r
+   0.287 245.971\r
+   0.340 247.210\r
+   0.392 246.516\r
+   0.445 245.710\r
+   0.498 244.881\r
+   0.550 242.997\r
+   0.602 240.518\r
+   0.655 235.271\r
+   0.708 229.464\r
+   0.760 222.871\r
+   0.813 216.278\r
+   0.866 206.959\r
+   0.919 195.458\r
+   0.971 184.255\r
+   1.023 174.490\r
+   1.076 170.067\r
+   1.129 99.588\r
+   1.181 25.281\r
+   1.233 12.839\r
+   1.286 7.769\r
+   1.340 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H250.eng b/datafiles/thrustcurves/AeroTech_H250.eng
new file mode 100644 (file)
index 0000000..24c0e99
--- /dev/null
@@ -0,0 +1,26 @@
+;I don't know that \r
+;these ejection \r
+;delays are correct.  \r
+;This was made \r
+;using the Aerotech \r
+;test thrust curves.  \r
+;By Tobin Yehle, \r
+;11/11/07.\r
+H250G 29 228.93 0-6-10-14 0.1163 0.256 Aerotech \r
+0.00250627 88.6915\r
+0.0125313 177.383\r
+0.0300752 279.719\r
+0.0726817 311.103\r
+0.145363 320.654\r
+0.24812 311.103\r
+0.308271 297.458\r
+0.398496 282.448\r
+0.45614 270.168\r
+0.593985 238.785\r
+0.691729 221.047\r
+0.799499 218.318\r
+0.83208 210.131\r
+0.844612 189.663\r
+0.907268 13.6449\r
+0.92 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_H268.eng b/datafiles/thrustcurves/AeroTech_H268.eng
new file mode 100644 (file)
index 0000000..a200864
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech H268R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H268R 29 333 0 0.18368 0.3584 AT\r
+   0.022 268.095\r
+   0.069 332.446\r
+   0.116 312.429\r
+   0.164 306.810\r
+   0.211 305.757\r
+   0.259 306.576\r
+   0.306 312.546\r
+   0.354 319.687\r
+   0.401 321.234\r
+   0.448 320.974\r
+   0.495 321.208\r
+   0.542 321.794\r
+   0.590 323.315\r
+   0.638 322.847\r
+   0.685 307.044\r
+   0.732 291.593\r
+   0.779 277.713\r
+   0.826 267.127\r
+   0.874 257.529\r
+   0.921 252.846\r
+   0.969 222.645\r
+   1.016 159.668\r
+   1.064 108.747\r
+   1.111 52.091\r
+   1.159 15.569\r
+   1.207 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H45.eng b/datafiles/thrustcurves/AeroTech_H45.eng
new file mode 100644 (file)
index 0000000..2b98b87
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H45W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H45W 38 194 0 0.193984 0.294784 AT\r
+   0.141 62.554\r
+   0.424 63.504\r
+   0.707 65.913\r
+   0.992 68.370\r
+   1.276 69.315\r
+   1.559 68.523\r
+   1.843 67.231\r
+   2.127 65.705\r
+   2.411 63.154\r
+   2.695 59.210\r
+   2.979 55.600\r
+   3.264 50.790\r
+   3.547 45.237\r
+   3.830 39.835\r
+   4.115 34.562\r
+   4.399 29.213\r
+   4.682 24.720\r
+   4.967 20.616\r
+   5.251 17.475\r
+   5.534 14.498\r
+   5.818 12.697\r
+   6.102 10.792\r
+   6.386 9.229\r
+   6.670 7.754\r
+   6.954 6.075\r
+   7.239 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H55.eng b/datafiles/thrustcurves/AeroTech_H55.eng
new file mode 100644 (file)
index 0000000..579bd6a
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H55W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H55W 29 191 0 0.09856 0.18816 AT\r
+   0.052 92.752\r
+   0.159 98.019\r
+   0.268 95.821\r
+   0.375 96.162\r
+   0.482 97.146\r
+   0.591 96.927\r
+   0.699 95.915\r
+   0.806 94.447\r
+   0.914 92.001\r
+   1.022 88.756\r
+   1.129 86.970\r
+   1.236 84.072\r
+   1.345 80.172\r
+   1.453 74.343\r
+   1.560 64.990\r
+   1.668 46.380\r
+   1.776 32.835\r
+   1.883 25.734\r
+   1.991 19.920\r
+   2.099 16.229\r
+   2.207 13.059\r
+   2.315 10.451\r
+   2.422 7.700\r
+   2.530 5.696\r
+   2.639 3.979\r
+   2.747 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H669.eng b/datafiles/thrustcurves/AeroTech_H669.eng
new file mode 100644 (file)
index 0000000..2bddd5e
--- /dev/null
@@ -0,0 +1,42 @@
+;\r
+; 38-240\r
+;  Greg Gardner - 09/15/06\r
+H669N  38 152  0  0.096  0.252  AT\r
+0.003  141\r
+0.006  523\r
+0.009  934\r
+0.012  1178\r
+0.016  926\r
+0.019  684\r
+0.022  487\r
+0.025  415\r
+0.028  622\r
+0.031  801\r
+0.0325  906\r
+0.034  866\r
+0.037  755\r
+0.04   737\r
+0.043  666\r
+0.047  737\r
+0.0485  802\r
+0.05   755\r
+0.053  791\r
+0.056  765\r
+0.059  755\r
+0.062  747\r
+0.069  737\r
+0.075  761\r
+0.082  755\r
+0.088  729\r
+0.093  741\r
+0.1    751\r
+0.2    703\r
+0.25   640\r
+0.3    586\r
+0.306  584\r
+0.309  576\r
+0.312  506\r
+0.318  292\r
+0.325  93\r
+0.329  0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_H70.eng b/datafiles/thrustcurves/AeroTech_H70.eng
new file mode 100644 (file)
index 0000000..2b714e9
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H70W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H70W 29 229 0 0.11648 0.224 AT\r
+   0.055 114.847\r
+   0.169 131.427\r
+   0.283 126.879\r
+   0.397 127.136\r
+   0.510 127.254\r
+   0.625 125.894\r
+   0.739 124.917\r
+   0.852 122.031\r
+   0.967 119.032\r
+   1.080 115.071\r
+   1.194 108.446\r
+   1.308 102.273\r
+   1.422 96.098\r
+   1.535 86.953\r
+   1.650 75.702\r
+   1.764 62.402\r
+   1.877 48.132\r
+   1.992 36.862\r
+   2.105 28.065\r
+   2.219 21.592\r
+   2.333 16.894\r
+   2.447 12.686\r
+   2.560 9.681\r
+   2.675 6.818\r
+   2.790 4.488\r
+   2.904 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H73.eng b/datafiles/thrustcurves/AeroTech_H73.eng
new file mode 100644 (file)
index 0000000..fba0687
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H73J\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H73J 38 152 6 0.14784 0.30912 AT\r
+   0.056 49.252\r
+   0.172 82.004\r
+   0.287 82.130\r
+   0.403 84.596\r
+   0.520 86.883\r
+   0.635 88.888\r
+   0.751 89.652\r
+   0.867 91.342\r
+   0.982 92.980\r
+   1.099 94.571\r
+   1.215 94.641\r
+   1.330 93.549\r
+   1.446 91.447\r
+   1.561 88.189\r
+   1.678 82.436\r
+   1.794 77.397\r
+   1.909 70.772\r
+   2.025 61.173\r
+   2.141 51.161\r
+   2.257 38.540\r
+   2.373 21.562\r
+   2.489 12.213\r
+   2.604 7.327\r
+   2.720 3.706\r
+   2.836 1.777\r
+   2.953 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H97.eng b/datafiles/thrustcurves/AeroTech_H97.eng
new file mode 100644 (file)
index 0000000..3504d33
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech H97J\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H97J 29 238 6 0.1344 0.27776 AT\r
+   0.045 89.405\r
+   0.136 100.289\r
+   0.228 100.463\r
+   0.320 102.019\r
+   0.411 102.813\r
+   0.503 103.550\r
+   0.595 101.701\r
+   0.686 103.056\r
+   0.778 103.331\r
+   0.870 102.613\r
+   0.961 103.394\r
+   1.053 100.963\r
+   1.145 101.226\r
+   1.236 99.864\r
+   1.328 98.420\r
+   1.420 96.827\r
+   1.511 95.034\r
+   1.603 93.241\r
+   1.695 93.485\r
+   1.786 88.068\r
+   1.878 64.358\r
+   1.970 30.264\r
+   2.061 8.691\r
+   2.153 1.399\r
+   2.245 0.525\r
+   2.336 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_H999.eng b/datafiles/thrustcurves/AeroTech_H999.eng
new file mode 100644 (file)
index 0000000..15d7e2b
--- /dev/null
@@ -0,0 +1,42 @@
+;\r
+; 38-360\r
+;  Greg Gardner - 09/15/06\r
+H999N 38 203 0 0.144 0.331 AT\r
+0.003  204\r
+0.006  757\r
+0.009  1357\r
+0.012  1710\r
+0.016  1345\r
+0.019  995\r
+0.022  710\r
+0.025  606\r
+0.028  905\r
+0.031  1165\r
+0.0325  1311\r
+0.034  1258\r
+0.037  1098\r
+0.04   1072\r
+0.043  969\r
+0.047  1072\r
+0.0485  1166\r
+0.05   1098\r
+0.053  1160\r
+0.056  1117\r
+0.059  1103\r
+0.062  1093\r
+0.069  1076\r
+0.075  1110\r
+0.082  1105\r
+0.088  1065\r
+0.093  1082\r
+0.1    1092\r
+0.2    1022\r
+0.25   931\r
+0.3    853\r
+0.306  850\r
+0.309  838\r
+0.312  735\r
+0.318  435\r
+0.325  161\r
+0.329  0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_I115.eng b/datafiles/thrustcurves/AeroTech_I115.eng
new file mode 100644 (file)
index 0000000..08157b0
--- /dev/null
@@ -0,0 +1,26 @@
+; Aerotech I115W from TRA Cert Data\r
+I115W 54 156 6-10-14-P 0.229 0.58 AT\r
+   0.034 12.095\r
+   0.177 105.225\r
+   0.206 113.087\r
+   1.017 163.281\r
+   1.166 161.466\r
+   1.257 162.676\r
+   1.343 166.909\r
+   1.417 160.862\r
+   1.514 162.676\r
+   1.617 163.885\r
+   1.686 160.257\r
+   1.977 142.719\r
+   2.497 106.435\r
+   2.68 91.316\r
+   2.994 72.569\r
+   3.103 67.126\r
+   3.189 65.917\r
+   3.24 59.265\r
+   3.291 42.937\r
+   3.331 30.237\r
+   3.377 20.561\r
+   3.429 12.7\r
+   3.491 7.257\r
+   3.514 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I117.eng b/datafiles/thrustcurves/AeroTech_I117.eng
new file mode 100644 (file)
index 0000000..2106850
--- /dev/null
@@ -0,0 +1,33 @@
+; Aerotech I117FJ from TRA Cert Data\r
+I117FJ 54 156 6-10-14-P 0.253 0.58 AT\r
+   0.014 65.9\r
+   0.021 94.456\r
+   0.055 120.816\r
+   0.089 107.636\r
+   0.179 107.636\r
+   0.344 124.111\r
+   0.385 133.996\r
+   0.447 123.013\r
+   0.509 138.389\r
+   0.564 131.799\r
+   0.646 146.077\r
+   0.708 144.979\r
+   0.736 127.406\r
+   0.75 149.372\r
+   0.798 141.684\r
+   0.825 164.749\r
+   0.866 152.667\r
+   1.004 158.159\r
+   1.141 155.962\r
+   1.224 154.864\r
+   1.492 155.962\r
+   2.304 121.914\r
+   2.407 118.619\r
+   2.482 117.521\r
+   2.544 128.504\r
+   2.599 112.029\r
+   2.654 75.784\r
+   2.695 45.031\r
+   2.75 16.475\r
+   2.771 8.787\r
+   2.806 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I1299.eng b/datafiles/thrustcurves/AeroTech_I1299.eng
new file mode 100644 (file)
index 0000000..f698f30
--- /dev/null
@@ -0,0 +1,24 @@
+;Entered by Jim Yehle\r
+;from TRA cert document\r
+I1299N 38 249 1000 0.192 0.422 AT-RMS \r
+0 15.7171\r
+0.00361 222.5\r
+0.0115 1112\r
+0.0134228 1237.11\r
+0.02 1287\r
+0.04 1359\r
+0.1 1451\r
+0.12 1470\r
+0.18 1491\r
+0.2 1483\r
+0.22 1462\r
+0.24 1399\r
+0.28 1208\r
+0.294743 1131.63\r
+0.3 1065\r
+0.304251 974.46\r
+0.32 305\r
+0.330537 55.0098\r
+0.333893 11.7878\r
+0.34 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_I132.eng b/datafiles/thrustcurves/AeroTech_I132.eng
new file mode 100644 (file)
index 0000000..453304c
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I132W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I132W 38 335 0 0.365568 0.512064 AT\r
+   0.096 204.011\r
+   0.290 174.236\r
+   0.484 168.865\r
+   0.679 170.783\r
+   0.874 173.028\r
+   1.069 174.287\r
+   1.264 174.647\r
+   1.458 174.364\r
+   1.652 174.645\r
+   1.847 173.002\r
+   2.042 169.209\r
+   2.236 164.309\r
+   2.431 157.149\r
+   2.626 149.580\r
+   2.821 138.360\r
+   3.016 124.171\r
+   3.210 107.626\r
+   3.404 89.785\r
+   3.599 71.747\r
+   3.794 55.124\r
+   3.989 42.264\r
+   4.183 31.373\r
+   4.378 21.980\r
+   4.573 14.389\r
+   4.768 8.794\r
+   4.962 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I154.eng b/datafiles/thrustcurves/AeroTech_I154.eng
new file mode 100644 (file)
index 0000000..9624eaa
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I154J\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I154J 38 250 0 0.25088 0.491904 AT\r
+   0.066 120.409\r
+   0.199 150.638\r
+   0.332 151.666\r
+   0.466 156.806\r
+   0.599 150.331\r
+   0.732 150.602\r
+   0.866 145.101\r
+   0.999 144.469\r
+   1.133 145.159\r
+   1.268 145.912\r
+   1.401 141.710\r
+   1.534 142.828\r
+   1.668 141.187\r
+   1.801 140.970\r
+   1.934 137.832\r
+   2.068 128.417\r
+   2.202 122.339\r
+   2.336 111.986\r
+   2.470 105.295\r
+   2.603 96.602\r
+   2.736 90.469\r
+   2.870 57.427\r
+   3.003 20.489\r
+   3.136 4.707\r
+   3.271 2.966\r
+   3.405 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I161.eng b/datafiles/thrustcurves/AeroTech_I161.eng
new file mode 100644 (file)
index 0000000..948fbae
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I161W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I161W 38 191 0 0.189952 0.370048 AT\r
+   0.043 178.900\r
+   0.131 206.770\r
+   0.221 206.101\r
+   0.310 205.175\r
+   0.400 206.924\r
+   0.490 210.603\r
+   0.579 210.475\r
+   0.669 211.555\r
+   0.758 212.379\r
+   0.848 212.096\r
+   0.938 209.060\r
+   1.027 202.345\r
+   1.116 192.439\r
+   1.204 179.499\r
+   1.294 162.159\r
+   1.383 148.446\r
+   1.473 135.222\r
+   1.563 120.095\r
+   1.652 104.041\r
+   1.742 87.962\r
+   1.831 74.789\r
+   1.921 54.362\r
+   2.010 23.386\r
+   2.100 7.332\r
+   2.190 5.171\r
+   2.279 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I195.eng b/datafiles/thrustcurves/AeroTech_I195.eng
new file mode 100644 (file)
index 0000000..1033f0b
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I195J\r
+; Copyright Tripoli Motor Testing 1996 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I195J 38 298 10 0.3136 0.59136 AT\r
+   0.050 258.670\r
+   0.152 353.638\r
+   0.254 300.655\r
+   0.356 265.354\r
+   0.458 266.338\r
+   0.560 283.233\r
+   0.662 332.442\r
+   0.765 283.040\r
+   0.867 230.795\r
+   0.969 222.867\r
+   1.071 217.091\r
+   1.173 210.600\r
+   1.275 202.722\r
+   1.377 192.671\r
+   1.479 182.571\r
+   1.581 171.964\r
+   1.683 162.238\r
+   1.785 148.138\r
+   1.888 130.259\r
+   1.990 107.022\r
+   2.092 80.230\r
+   2.194 51.074\r
+   2.296 26.313\r
+   2.398 10.397\r
+   2.500 3.977\r
+   2.602 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I195_1.eng b/datafiles/thrustcurves/AeroTech_I195_1.eng
new file mode 100644 (file)
index 0000000..2e8e003
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I195J\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I195J 38 297 0 0.296576 0.563136 AT\r
+   0.033 190.099\r
+   0.103 354.046\r
+   0.173 393.473\r
+   0.243 414.842\r
+   0.314 379.747\r
+   0.383 364.640\r
+   0.453 364.776\r
+   0.524 357.242\r
+   0.594 355.802\r
+   0.664 355.644\r
+   0.734 353.557\r
+   0.804 339.941\r
+   0.874 309.753\r
+   0.944 275.017\r
+   1.014 243.739\r
+   1.084 218.135\r
+   1.154 197.291\r
+   1.224 173.680\r
+   1.295 147.000\r
+   1.365 116.506\r
+   1.434 83.105\r
+   1.505 51.011\r
+   1.575 26.480\r
+   1.645 13.927\r
+   1.716 7.273\r
+   1.786 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I200.eng b/datafiles/thrustcurves/AeroTech_I200.eng
new file mode 100644 (file)
index 0000000..bd7545f
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I200W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I200W 29 333 0 0.181888 0.357504 AT\r
+   0.033 303.951\r
+   0.103 273.452\r
+   0.174 276.061\r
+   0.245 271.625\r
+   0.316 268.233\r
+   0.386 258.449\r
+   0.457 252.480\r
+   0.528 246.642\r
+   0.599 242.304\r
+   0.670 237.737\r
+   0.741 234.769\r
+   0.811 233.171\r
+   0.882 230.660\r
+   0.953 224.985\r
+   1.024 221.658\r
+   1.095 214.548\r
+   1.166 177.365\r
+   1.236 154.208\r
+   1.307 119.146\r
+   1.378 91.586\r
+   1.449 65.330\r
+   1.520 32.877\r
+   1.591 28.702\r
+   1.661 22.211\r
+   1.732 15.558\r
+   1.803 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I211.eng b/datafiles/thrustcurves/AeroTech_I211.eng
new file mode 100644 (file)
index 0000000..238614c
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I211W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I211W 38 240 0 0.247296 0.466368 AT\r
+   0.044 257.326\r
+   0.134 295.533\r
+   0.226 296.087\r
+   0.318 298.204\r
+   0.408 295.082\r
+   0.499 287.669\r
+   0.591 282.578\r
+   0.682 272.875\r
+   0.773 266.997\r
+   0.864 257.602\r
+   0.955 250.495\r
+   1.047 238.574\r
+   1.138 228.571\r
+   1.228 215.135\r
+   1.320 198.047\r
+   1.411 180.631\r
+   1.502 161.261\r
+   1.593 146.708\r
+   1.684 134.484\r
+   1.776 101.241\r
+   1.867 52.688\r
+   1.957 35.461\r
+   2.049 24.321\r
+   2.141 11.165\r
+   2.232 4.587\r
+   2.324 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I215.eng b/datafiles/thrustcurves/AeroTech_I215.eng
new file mode 100644 (file)
index 0000000..3fa6057
--- /dev/null
@@ -0,0 +1,22 @@
+; Aerotech I215R from TRA Cert Data\r
+I215R 54 156 6-10-14-P 0.20800000000000002 0.527 AT\r
+   0.049 88.39\r
+   0.089 154.683\r
+   0.094 206.914\r
+   0.178 245.083\r
+   0.251 255.127\r
+   0.325 259.145\r
+   0.404 249.1\r
+   0.582 257.136\r
+   0.631 253.118\r
+   0.7 253.118\r
+   0.774 245.083\r
+   1.001 239.056\r
+   1.509 192.852\r
+   1.681 178.79\r
+   1.716 180.799\r
+   1.746 190.843\r
+   1.775 178.79\r
+   1.8 90.399\r
+   1.82 34.151\r
+   1.859 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I218.eng b/datafiles/thrustcurves/AeroTech_I218.eng
new file mode 100644 (file)
index 0000000..f9bf8c9
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech I218R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I218R 38 191 0 0.19264 0.37184 AT\r
+   0.027 136.078\r
+   0.088 275.030\r
+   0.148 280.998\r
+   0.208 284.371\r
+   0.268 284.037\r
+   0.327 279.311\r
+   0.388 277.791\r
+   0.448 276.309\r
+   0.509 269.384\r
+   0.570 266.041\r
+   0.630 261.907\r
+   0.690 256.366\r
+   0.750 250.565\r
+   0.810 242.206\r
+   0.870 234.607\r
+   0.930 225.488\r
+   0.991 216.166\r
+   1.053 205.415\r
+   1.112 193.238\r
+   1.173 177.206\r
+   1.233 161.304\r
+   1.292 139.118\r
+   1.353 96.082\r
+   1.413 38.848\r
+   1.474 5.978\r
+   1.535 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I225.eng b/datafiles/thrustcurves/AeroTech_I225.eng
new file mode 100644 (file)
index 0000000..ca05042
--- /dev/null
@@ -0,0 +1,28 @@
+; AeroTech I225FJ\r
+; Curvefit to instruction sheet on Aerotech website (12/27/06)\r
+; by Chris Kobel\r
+; burn time: 1.8 seconds\r
+; total impulse: 350.5 newton-seconds\r
+; average thrust: 43.8 pounds\r
+I225FJ 38 202 6-10-14 0.2417 0.486 AT\r
+ 0.04   213.6\r
+ 0.10   213.6\r
+ 0.20   218.1\r
+ 0.28   235.9\r
+ 0.30   249.2\r
+ 0.40   262.6\r
+ 0.50   267.0\r
+ 0.60   271.5\r
+ 0.70   275.9\r
+ 0.80   275.9\r
+ 0.87   271.5\r
+ 0.90   258.1\r
+ 1.00   240.3\r
+ 1.10   218.1\r
+ 1.20   200.3\r
+ 1.30   178.0\r
+ 1.40   160.2\r
+ 1.50    97.9\r
+ 1.60    40.1\r
+ 1.70    13.4\r
+ 1.80     0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I229.eng b/datafiles/thrustcurves/AeroTech_I229.eng
new file mode 100644 (file)
index 0000000..5596f76
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+I229T  54.0 156.00 6-10-14 0.20600 0.52000 AT\r
+   0.01      44.73 \r
+   0.02     216.65 \r
+   0.19     244.72 \r
+   0.40     266.64 \r
+   0.49     266.64 \r
+   0.52     273.66 \r
+   0.59     271.90 \r
+   0.75     272.78 \r
+   0.84     268.40 \r
+   0.88     271.90 \r
+   0.97     262.26 \r
+   1.00     265.76 \r
+   1.07     255.24 \r
+   1.21     249.10 \r
+   1.51     219.28 \r
+   1.60     230.68 \r
+   1.63     191.21 \r
+   1.64     132.44 \r
+   1.66      86.83 \r
+   1.70      44.73 \r
+   1.71      21.05 \r
+   1.73       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_I245.eng b/datafiles/thrustcurves/AeroTech_I245.eng
new file mode 100644 (file)
index 0000000..6c0c393
--- /dev/null
@@ -0,0 +1,30 @@
+;Ejection delays may not be corrrect.\r
+;From Aerotech pre-cert data.\r
+;Created 11/11/07 by Jim Yehle.\r
+I245G 38 192.532 0-6-10-14 0.1813 0.365 Aerotech \r
+0.0244989 234.061\r
+0.0550162 257.888\r
+0.0868597 368.567\r
+0.106904 382.335\r
+0.13363 390.808\r
+0.200445 405.635\r
+0.262806 410.931\r
+0.302895 411.99\r
+0.363029 408.813\r
+0.401294 398.43\r
+0.501114 363.271\r
+0.594655 320.907\r
+0.68932 278.355\r
+0.797327 212.879\r
+0.893204 181.477\r
+1.00647 154.187\r
+1.09061 133.72\r
+1.16036 120.737\r
+1.1804 122.856\r
+1.23625 106.43\r
+1.30421 75.0467\r
+1.3608 36.0094\r
+1.40312 19.0638\r
+1.43875 5.2955\r
+1.46325 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_I284.eng b/datafiles/thrustcurves/AeroTech_I284.eng
new file mode 100644 (file)
index 0000000..54ffc69
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I284W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I284W 38 298 10 0.3136 0.55552 AT\r
+   0.033 370.682\r
+   0.103 483.606\r
+   0.174 483.282\r
+   0.245 486.856\r
+   0.316 490.842\r
+   0.386 499.428\r
+   0.457 508.800\r
+   0.528 506.326\r
+   0.599 485.287\r
+   0.670 481.043\r
+   0.741 455.776\r
+   0.811 426.920\r
+   0.882 393.422\r
+   0.953 367.404\r
+   1.024 347.490\r
+   1.095 325.191\r
+   1.166 304.064\r
+   1.236 284.158\r
+   1.307 271.165\r
+   1.378 228.579\r
+   1.449 130.521\r
+   1.520 57.212\r
+   1.591 29.552\r
+   1.661 16.413\r
+   1.732 10.365\r
+   1.803 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I284_1.eng b/datafiles/thrustcurves/AeroTech_I284_1.eng
new file mode 100644 (file)
index 0000000..ee1909e
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I284W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I284W 38 297 0 0.310016 0.555072 AT\r
+   0.041 422.031\r
+   0.125 448.597\r
+   0.210 459.029\r
+   0.295 451.940\r
+   0.379 439.556\r
+   0.465 427.370\r
+   0.549 407.558\r
+   0.633 399.734\r
+   0.719 380.049\r
+   0.803 368.042\r
+   0.887 352.020\r
+   0.973 342.102\r
+   1.057 325.767\r
+   1.142 306.936\r
+   1.227 292.029\r
+   1.311 267.283\r
+   1.396 251.784\r
+   1.481 227.534\r
+   1.566 210.504\r
+   1.650 168.299\r
+   1.735 110.789\r
+   1.820 71.036\r
+   1.904 32.505\r
+   1.990 17.537\r
+   2.075 7.317\r
+   2.160 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I285.eng b/datafiles/thrustcurves/AeroTech_I285.eng
new file mode 100644 (file)
index 0000000..306ebff
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech I285R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I285R 38 250 0 0.25088 0.4928 AT\r
+   0.027 171.405\r
+   0.088 325.573\r
+   0.148 341.697\r
+   0.208 358.916\r
+   0.268 373.706\r
+   0.327 373.966\r
+   0.388 368.442\r
+   0.448 367.497\r
+   0.507 361.900\r
+   0.568 351.928\r
+   0.628 346.109\r
+   0.687 340.993\r
+   0.749 329.382\r
+   0.810 321.625\r
+   0.870 310.856\r
+   0.930 295.955\r
+   0.990 283.704\r
+   1.050 269.655\r
+   1.110 253.419\r
+   1.170 240.222\r
+   1.230 224.116\r
+   1.290 204.118\r
+   1.350 118.730\r
+   1.410 23.483\r
+   1.471 2.046\r
+   1.532 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I300.eng b/datafiles/thrustcurves/AeroTech_I300.eng
new file mode 100644 (file)
index 0000000..b0249ef
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+;\r
+I300T 38 250 6-10-14 0.2216 0.4405 AT\r
+0 473.17\r
+0.1 395.68\r
+0.2 375.31\r
+0.3 367.14\r
+0.4 358.97\r
+0.5 346.72\r
+0.6 338.56\r
+0.7 318.19\r
+0.8 305.94\r
+0.9 295.35\r
+1.07 269.23\r
+1.1 258.01\r
+1.2 246.79\r
+1.3 179.49\r
+1.4 48.95\r
+1.5 13.91\r
+1.6 0\r
diff --git a/datafiles/thrustcurves/AeroTech_I305.eng b/datafiles/thrustcurves/AeroTech_I305.eng
new file mode 100644 (file)
index 0000000..ac446a2
--- /dev/null
@@ -0,0 +1,21 @@
+; I305FJ based on Aerotech instruction sheet by C. Kobel 3/30/07\r
+I305FJ 38 298 6-10-14 0.302 0.581 AT\r
+   0.020 341.398\r
+   0.100 365.497\r
+   0.200 383.571\r
+   0.300 403.653\r
+   0.400 405.662\r
+   0.500 405.662\r
+   0.600 404.657\r
+   0.700 374.534\r
+   0.800 342.402\r
+   0.900 309.267\r
+   1.000 272.115\r
+   1.100 238.979\r
+   1.150 224.921\r
+   1.200 194.798\r
+   1.300 119.489\r
+   1.400 62.255\r
+   1.450 33.136\r
+   1.500 23.095\r
+   1.600 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I357.eng b/datafiles/thrustcurves/AeroTech_I357.eng
new file mode 100644 (file)
index 0000000..0115e4e
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I357T\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I357T 38 203 14 0.1792 0.34944 AT\r
+   0.028 311.629\r
+   0.087 351.768\r
+   0.147 349.074\r
+   0.206 346.175\r
+   0.266 341.229\r
+   0.325 336.857\r
+   0.384 333.748\r
+   0.444 326.960\r
+   0.503 319.679\r
+   0.563 312.533\r
+   0.622 300.790\r
+   0.681 292.787\r
+   0.741 283.766\r
+   0.800 274.578\r
+   0.859 264.915\r
+   0.919 254.273\r
+   0.978 241.755\r
+   1.037 229.020\r
+   1.097 216.238\r
+   1.156 187.776\r
+   1.216 109.940\r
+   1.275 56.459\r
+   1.334 24.476\r
+   1.394 10.977\r
+   1.454 3.450\r
+   1.515 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I364.eng b/datafiles/thrustcurves/AeroTech_I364.eng
new file mode 100644 (file)
index 0000000..c2fffb8
--- /dev/null
@@ -0,0 +1,25 @@
+; AeroTech I364FJ\r
+; Curvefit to instruction sheet on Aerotech website (12/27/06)\r
+; by Chris Kobel\r
+; burn time: 1.7 seconds\r
+; total impulse: 551.2 newton-seconds\r
+; average thrust: 72.9 pounds\r
+I364FJ 38 345 6-10-14 0.3625 0.678 AT\r
+  0.02    356.0\r
+  0.10    373.8\r
+  0.20    387.2\r
+  0.30    400.5\r
+  0.40    400.5\r
+  0.50    409.4\r
+  0.60    413.9\r
+  0.70    409.4\r
+  0.80    382.7\r
+  0.90    373.8\r
+  1.00    351.6\r
+  1.10    333.8\r
+  1.20    320.4\r
+  1.30    311.5\r
+  1.40    244.8\r
+  1.50    178.0\r
+  1.60     80.1\r
+  1.70 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I366.eng b/datafiles/thrustcurves/AeroTech_I366.eng
new file mode 100644 (file)
index 0000000..324ddf1
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech I366R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I366R 38 298 0 0.3136 0.55552 AT\r
+   0.027 323.256\r
+   0.088 485.393\r
+   0.148 483.744\r
+   0.208 479.926\r
+   0.268 473.365\r
+   0.327 466.192\r
+   0.388 457.444\r
+   0.448 448.751\r
+   0.509 441.477\r
+   0.570 430.236\r
+   0.630 421.524\r
+   0.690 411.757\r
+   0.750 398.876\r
+   0.810 387.496\r
+   0.870 375.430\r
+   0.930 361.325\r
+   0.991 345.057\r
+   1.053 330.392\r
+   1.112 312.636\r
+   1.173 293.508\r
+   1.233 275.085\r
+   1.292 262.408\r
+   1.353 230.881\r
+   1.413 118.008\r
+   1.474 23.611\r
+   1.535 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I435.eng b/datafiles/thrustcurves/AeroTech_I435.eng
new file mode 100644 (file)
index 0000000..7ae4a54
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I435T\r
+; Copyright Tripoli Motor Testing 1996 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I435T 38 298 6 0.28672 0.52864 AT\r
+   0.026 684.626\r
+   0.080 702.334\r
+   0.134 655.130\r
+   0.190 638.942\r
+   0.245 624.098\r
+   0.299 611.802\r
+   0.354 602.601\r
+   0.409 590.237\r
+   0.464 575.712\r
+   0.519 563.654\r
+   0.574 548.912\r
+   0.628 527.885\r
+   0.683 504.211\r
+   0.739 480.412\r
+   0.793 459.219\r
+   0.848 436.771\r
+   0.903 414.493\r
+   0.957 392.151\r
+   1.012 366.634\r
+   1.068 299.670\r
+   1.122 182.639\r
+   1.177 106.457\r
+   1.232 55.447\r
+   1.286 23.628\r
+   1.342 11.052\r
+   1.397 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I435_1.eng b/datafiles/thrustcurves/AeroTech_I435_1.eng
new file mode 100644 (file)
index 0000000..54b43b1
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I435T\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I435T 38 297 0 0.26656 0.513408 AT\r
+   0.024 808.049\r
+   0.074 749.691\r
+   0.124 709.215\r
+   0.174 656.216\r
+   0.224 636.578\r
+   0.274 621.839\r
+   0.324 592.267\r
+   0.374 584.551\r
+   0.424 573.277\r
+   0.474 547.725\r
+   0.524 539.962\r
+   0.574 525.268\r
+   0.624 500.456\r
+   0.674 484.978\r
+   0.724 464.323\r
+   0.774 442.837\r
+   0.824 424.540\r
+   0.874 405.872\r
+   0.924 393.443\r
+   0.974 317.157\r
+   1.024 217.630\r
+   1.074 126.188\r
+   1.124 74.391\r
+   1.174 30.034\r
+   1.224 9.380\r
+   1.274 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_I599.eng b/datafiles/thrustcurves/AeroTech_I599.eng
new file mode 100644 (file)
index 0000000..f306c52
--- /dev/null
@@ -0,0 +1,37 @@
+; Aerotech I599N from TRA Cert Data\r
+I599N 54 156 100 0.195 0.5200512 AT\r
+   0.0070 179.424\r
+   0.01 495.908\r
+   0.012 578.144\r
+   0.014 647.92\r
+   0.017 797.44\r
+   0.024 593.096\r
+   0.032 647.92\r
+   0.045 677.824\r
+   0.051 702.744\r
+   0.055 677.824\r
+   0.062 720.188\r
+   0.076 707.728\r
+   0.12 752.584\r
+   0.202 757.568\r
+   0.225 755.076\r
+   0.241 735.14\r
+   0.25 720.188\r
+   0.263 730.156\r
+   0.329 722.68\r
+   0.399 697.76\r
+   0.45 667.856\r
+   0.501 618.016\r
+   0.536 585.62\r
+   0.55 580.636\r
+   0.564 585.62\r
+   0.578 632.968\r
+   0.581 555.716\r
+   0.584 426.132\r
+   0.589 299.04\r
+   0.595 176.932\r
+   0.598 112.14\r
+   0.604 57.316\r
+   0.608 24.92\r
+   0.619 12.46\r
+   0.623 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_I600.eng b/datafiles/thrustcurves/AeroTech_I600.eng
new file mode 100644 (file)
index 0000000..8499b0c
--- /dev/null
@@ -0,0 +1,23 @@
+;\r
+;I600R Data Entered by Tim Van Milligan\r
+;For RockSim: www.RockSim.com\r
+;Based on Aerotech's Reload Kit Instruction Sheet.\r
+;Not Officially Approved by TRA or Aerotech\r
+I600R 38 344.68 6-10-14 0.3237 0.617 AT\r
+0.005 40.438\r
+0.046 817.754\r
+0.059 813.261\r
+0.1 772.822\r
+0.2 736.877\r
+0.4 696.439\r
+0.5 669.48\r
+0.6 620.055\r
+0.796 539.178\r
+0.894 485.261\r
+0.951 453.809\r
+0.964 435.836\r
+1 274.082\r
+1.052 152.767\r
+1.106 62.904\r
+1.144 13.48\r
+1.18 0\r
diff --git a/datafiles/thrustcurves/AeroTech_I65.eng b/datafiles/thrustcurves/AeroTech_I65.eng
new file mode 100644 (file)
index 0000000..3413084
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech I65W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I65W 54 235 0 0.41216 0.7616 AT\r
+   0.180 125.414\r
+   0.544 139.304\r
+   0.908 145.369\r
+   1.273 148.283\r
+   1.638 146.745\r
+   2.002 139.049\r
+   2.367 131.200\r
+   2.731 123.276\r
+   3.096 113.454\r
+   3.460 102.368\r
+   3.825 90.210\r
+   4.190 78.084\r
+   4.554 66.812\r
+   4.919 55.780\r
+   5.283 47.281\r
+   5.648 39.154\r
+   6.012 32.528\r
+   6.377 27.069\r
+   6.742 22.099\r
+   7.106 18.095\r
+   7.471 14.819\r
+   7.835 12.097\r
+   8.200 9.763\r
+   8.565 7.875\r
+   8.929 5.999\r
+   9.294 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J125.eng b/datafiles/thrustcurves/AeroTech_J125.eng
new file mode 100644 (file)
index 0000000..fc4fac3
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J125\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J125 54 368 0 0.63392 1.288 AT\r
+   0.174 223.931\r
+   0.525 254.842\r
+   0.877 275.347\r
+   1.229 285.163\r
+   1.581 280.333\r
+   1.933 264.476\r
+   2.285 244.373\r
+   2.638 223.774\r
+   2.990 204.720\r
+   3.342 185.434\r
+   3.694 166.807\r
+   4.046 147.653\r
+   4.398 127.914\r
+   4.750 108.483\r
+   5.102 92.582\r
+   5.454 77.817\r
+   5.806 63.844\r
+   6.158 53.017\r
+   6.510 44.507\r
+   6.862 37.543\r
+   7.215 32.205\r
+   7.567 27.212\r
+   7.919 22.847\r
+   8.271 18.596\r
+   8.623 14.790\r
+   8.975 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J1299.eng b/datafiles/thrustcurves/AeroTech_J1299.eng
new file mode 100644 (file)
index 0000000..cf0b523
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+; AT 54-852\r
+;  Greg Gardner - 09/15/06\r
+J1299N  54  230  0 0.3716  0.834  AT\r
+0.01  548\r
+0.02  1152\r
+0.03  1232\r
+0.04  1277\r
+0.05  1272\r
+0.06  1288\r
+0.07  1333\r
+0.08  1347\r
+0.09  1378\r
+0.10  1383\r
+0.12  1405\r
+0.14  1410\r
+0.16  1440\r
+0.18  1444\r
+0.20  1446\r
+0.25  1449\r
+0.30  1452\r
+0.35  1448\r
+0.40  1440\r
+0.45  1405\r
+0.50  1320\r
+0.55  1248\r
+0.57  1224\r
+0.59  1210\r
+0.60  1180\r
+0.61  1188\r
+0.615 1195\r
+0.62  1188\r
+0.63  510\r
+0.64  220\r
+0.65  96\r
+0.66  46\r
+0.67  26\r
+0.678 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_J135.eng b/datafiles/thrustcurves/AeroTech_J135.eng
new file mode 100644 (file)
index 0000000..290b5e1
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J135W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J135W 54 368 0 0.62272 1.14106 AT\r
+   0.147 226.295\r
+   0.444 243.688\r
+   0.742 250.916\r
+   1.040 257.345\r
+   1.338 259.308\r
+   1.635 253.727\r
+   1.933 246.071\r
+   2.231 235.780\r
+   2.529 221.775\r
+   2.827 205.143\r
+   3.125 183.570\r
+   3.423 161.103\r
+   3.720 140.983\r
+   4.017 122.984\r
+   4.315 106.605\r
+   4.612 91.959\r
+   4.910 77.693\r
+   5.208 65.304\r
+   5.506 54.347\r
+   5.804 44.246\r
+   6.102 35.395\r
+   6.400 27.716\r
+   6.698 21.121\r
+   6.996 14.939\r
+   7.294 9.737\r
+   7.592 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J145.eng b/datafiles/thrustcurves/AeroTech_J145.eng
new file mode 100644 (file)
index 0000000..190eb13
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J145H\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J145H 54 709 0 0.410816 1.79738 AT\r
+   0.113 253.118\r
+   0.340 293.672\r
+   0.567 300.149\r
+   0.794 289.519\r
+   1.021 253.366\r
+   1.248 251.809\r
+   1.476 246.042\r
+   1.704 236.553\r
+   1.931 229.907\r
+   2.158 222.550\r
+   2.385 211.120\r
+   2.612 201.066\r
+   2.841 191.143\r
+   3.069 139.197\r
+   3.296 79.889\r
+   3.523 63.900\r
+   3.750 51.048\r
+   3.977 40.565\r
+   4.205 31.710\r
+   4.433 24.429\r
+   4.660 19.950\r
+   4.887 15.256\r
+   5.115 12.412\r
+   5.342 10.212\r
+   5.570 9.135\r
+   5.798 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J180.eng b/datafiles/thrustcurves/AeroTech_J180.eng
new file mode 100644 (file)
index 0000000..362f73b
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J180T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J180T 54 230 0 0.429184 0.809088 AT\r
+   0.093 301.634\r
+   0.281 313.236\r
+   0.470 313.710\r
+   0.658 308.334\r
+   0.847 300.100\r
+   1.035 290.743\r
+   1.224 278.867\r
+   1.412 263.823\r
+   1.601 245.974\r
+   1.790 226.651\r
+   1.978 207.345\r
+   2.167 187.053\r
+   2.355 168.339\r
+   2.544 149.993\r
+   2.732 133.094\r
+   2.921 116.330\r
+   3.109 100.088\r
+   3.298 84.507\r
+   3.486 70.453\r
+   3.675 57.263\r
+   3.864 44.453\r
+   4.052 33.340\r
+   4.241 24.654\r
+   4.429 17.964\r
+   4.619 12.391\r
+   4.808 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J1999.eng b/datafiles/thrustcurves/AeroTech_J1999.eng
new file mode 100644 (file)
index 0000000..b577746
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+; AT 54-1280\r
+;  Greg Gardner - 09/15/06\r
+J1999N  54  314  0  0.5574  1.111  AT\r
+0.01  830\r
+0.02  1716\r
+0.03  1787\r
+0.04  1873\r
+0.05  1896\r
+0.06  1918\r
+0.07  1984\r
+0.08  2007\r
+0.09  2051\r
+0.10  2058\r
+0.12  2090\r
+0.14  2098\r
+0.16  2135\r
+0.18  2138\r
+0.20  2142\r
+0.25  2146\r
+0.30  2150\r
+0.35  2146\r
+0.40  2138\r
+0.45  2096\r
+0.50  1974\r
+0.55  1864\r
+0.57  1829\r
+0.59  1815\r
+0.60  1762\r
+0.61  1673\r
+0.62  1085\r
+0.63  490\r
+0.64  190\r
+0.65  81\r
+0.66  31\r
+0.67  0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_J210.eng b/datafiles/thrustcurves/AeroTech_J210.eng
new file mode 100644 (file)
index 0000000..2182361
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+J210H 54 609.6 100 0.471 1.497 Aerotech\r
+0.00772798 651.819\r
+0.0695518 528.502\r
+0.200927 488.864\r
+0.502318 409.589\r
+0.996909 374.355\r
+1.4915 312.697\r
+1.59196 286.272\r
+2.00927 167.359\r
+2.43431 88.0836\r
+2.50386 101.296\r
+2.55023 74.8711\r
+3.02164 57.2543\r
+4 0\r
diff --git a/datafiles/thrustcurves/AeroTech_J250.eng b/datafiles/thrustcurves/AeroTech_J250.eng
new file mode 100644 (file)
index 0000000..d39891c
--- /dev/null
@@ -0,0 +1,24 @@
+; Aerotech J250FJ from TRA Cert Data\r
+J250FJ 54 241 6-10-14-18 0.511 0.92 AT\r
+   0.011 132.176\r
+   0.021 263.335\r
+   0.084 236.899\r
+   0.168 252.151\r
+   0.294 238.933\r
+   0.494 261.301\r
+   0.715 285.703\r
+   0.993 295.87\r
+   1.177 306.038\r
+   1.267 305.021\r
+   1.498 301.971\r
+   1.64 292.82\r
+   2.002 253.167\r
+   2.344 224.699\r
+   2.391 225.715\r
+   2.502 233.849\r
+   2.57 185.046\r
+   2.659 116.925\r
+   2.685 76.255\r
+   2.738 32.536\r
+   2.77 17.285\r
+   2.796 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_J260.eng b/datafiles/thrustcurves/AeroTech_J260.eng
new file mode 100644 (file)
index 0000000..5f07d7f
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;\r
+J260HW 54 708.66 100 0.558 1.574 AT\r
+0.00772798 598.969\r
+0.0386399 475.651\r
+0.108192 506.481\r
+0.463679 493.268\r
+0.780526 475.651\r
+1.01236 427.205\r
+2.00155 330.314\r
+2.48841 193.784\r
+2.99073 114.509\r
+4.01082 57.2543\r
+4.5 0\r
diff --git a/datafiles/thrustcurves/AeroTech_J275.eng b/datafiles/thrustcurves/AeroTech_J275.eng
new file mode 100644 (file)
index 0000000..11aaa2b
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J275W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J275W 54 230 0 0.468608 0.864192 AT\r
+   0.075 239.740\r
+   0.227 289.133\r
+   0.380 299.773\r
+   0.533 312.721\r
+   0.686 323.878\r
+   0.840 332.165\r
+   0.992 336.422\r
+   1.145 335.110\r
+   1.298 329.538\r
+   1.451 325.343\r
+   1.604 309.980\r
+   1.756 292.901\r
+   1.909 275.732\r
+   2.063 257.341\r
+   2.216 234.891\r
+   2.369 213.102\r
+   2.521 182.501\r
+   2.674 167.853\r
+   2.827 153.041\r
+   2.980 138.115\r
+   3.133 105.605\r
+   3.285 67.369\r
+   3.439 29.239\r
+   3.592 14.599\r
+   3.745 6.662\r
+   3.898 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J315.eng b/datafiles/thrustcurves/AeroTech_J315.eng
new file mode 100644 (file)
index 0000000..5947864
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech J315R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J315R 54 243 0 0.42112 0.8512 AT\r
+   0.051 189.719\r
+   0.154 337.529\r
+   0.259 354.534\r
+   0.363 364.111\r
+   0.468 371.479\r
+   0.572 373.222\r
+   0.676 376.062\r
+   0.780 372.962\r
+   0.884 368.988\r
+   0.989 366.978\r
+   1.093 358.752\r
+   1.197 351.302\r
+   1.301 339.336\r
+   1.406 325.202\r
+   1.510 311.322\r
+   1.614 300.496\r
+   1.718 288.598\r
+   1.822 278.279\r
+   1.927 270.538\r
+   2.031 262.127\r
+   2.136 245.027\r
+   2.239 236.238\r
+   2.344 188.308\r
+   2.448 63.668\r
+   2.552 18.746\r
+   2.657 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J350.eng b/datafiles/thrustcurves/AeroTech_J350.eng
new file mode 100644 (file)
index 0000000..59076b3
--- /dev/null
@@ -0,0 +1,21 @@
+J350W-L 38 337 P 0.361 0.651 AT\r
+   0.041 841.443\r
+   0.051 767.077\r
+   0.088 698.219\r
+   0.173 644.51\r
+   0.256 621.098\r
+   0.298 564.635\r
+   0.547 543.977\r
+   0.783 487.514\r
+   0.989 418.656\r
+   1.16 359.438\r
+   1.192 340.158\r
+   1.213 320.878\r
+   1.287 216.214\r
+   1.319 179.031\r
+   1.342 126.699\r
+   1.386 84.007\r
+   1.427 53.709\r
+   1.48 45.446\r
+   1.591 20.657\r
+   1.695 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_J350_1.eng b/datafiles/thrustcurves/AeroTech_J350_1.eng
new file mode 100644 (file)
index 0000000..9fed7db
--- /dev/null
@@ -0,0 +1,31 @@
+; AeroTech J350W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J350W 38 337 0 0.375872 0.650944 AT\r
+   0.038 706.781\r
+   0.115 669.055\r
+   0.192 602.539\r
+   0.270 565.084\r
+   0.348 539.143\r
+   0.425 514.910\r
+   0.503 483.098\r
+   0.581 449.128\r
+   0.658 437.256\r
+   0.736 424.199\r
+   0.815 414.461\r
+   0.892 402.956\r
+   0.970 393.604\r
+   1.048 377.837\r
+   1.125 359.785\r
+   1.203 341.916\r
+   1.281 324.721\r
+   1.358 305.935\r
+   1.436 264.279\r
+   1.515 175.471\r
+   1.592 110.912\r
+   1.670 77.100\r
+   1.748 55.472\r
+   1.825 39.990\r
+   1.903 26.276\r
+   1.981 0.000\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_J390.eng b/datafiles/thrustcurves/AeroTech_J390.eng
new file mode 100644 (file)
index 0000000..7dee40e
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+J390HW-TURBO 54 708.66 100 0.69 1.74 AT\r
+0.015456 440.418\r
+0.100464 550.523\r
+0.193199 546.118\r
+0.301391 656.223\r
+0.502318 647.414\r
+0.973725 581.352\r
+1.48377 471.247\r
+1.98609 378.759\r
+2.17929 334.718\r
+2.30294 255.442\r
+2.49614 158.55\r
+3.01391 57.2543\r
+3.5 0\r
diff --git a/datafiles/thrustcurves/AeroTech_J415.eng b/datafiles/thrustcurves/AeroTech_J415.eng
new file mode 100644 (file)
index 0000000..07aafc4
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J415W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J415W 54 314 0 0.686336 1.15718 AT\r
+   0.065 431.300\r
+   0.196 452.427\r
+   0.327 489.904\r
+   0.458 513.542\r
+   0.591 523.192\r
+   0.723 531.440\r
+   0.854 542.165\r
+   0.985 542.731\r
+   1.118 549.788\r
+   1.250 553.889\r
+   1.381 537.331\r
+   1.512 512.126\r
+   1.645 517.338\r
+   1.777 498.098\r
+   1.908 473.365\r
+   2.040 444.157\r
+   2.172 413.187\r
+   2.304 384.854\r
+   2.435 360.556\r
+   2.567 297.571\r
+   2.699 178.288\r
+   2.831 89.889\r
+   2.962 43.066\r
+   3.094 19.126\r
+   3.226 8.995\r
+   3.358 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J420.eng b/datafiles/thrustcurves/AeroTech_J420.eng
new file mode 100644 (file)
index 0000000..10da3b6
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech J420R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J420R 38 337 0 0.37632 0.6496 AT\r
+   0.031 61.083\r
+   0.095 563.470\r
+   0.160 525.283\r
+   0.224 521.242\r
+   0.288 527.371\r
+   0.352 537.088\r
+   0.418 535.138\r
+   0.481 534.623\r
+   0.545 530.245\r
+   0.610 526.447\r
+   0.674 517.203\r
+   0.738 510.279\r
+   0.802 500.887\r
+   0.868 479.450\r
+   0.931 460.675\r
+   0.995 438.594\r
+   1.060 409.647\r
+   1.124 383.454\r
+   1.188 361.024\r
+   1.252 339.741\r
+   1.318 319.194\r
+   1.381 296.714\r
+   1.445 195.191\r
+   1.510 61.984\r
+   1.575 7.220\r
+   1.640 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J460.eng b/datafiles/thrustcurves/AeroTech_J460.eng
new file mode 100644 (file)
index 0000000..55b2719
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J460T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J460T 54 230 0 0.413504 0.801024 AT\r
+   0.041 500.927\r
+   0.125 509.423\r
+   0.209 516.357\r
+   0.294 527.752\r
+   0.379 535.135\r
+   0.464 541.858\r
+   0.548 545.793\r
+   0.633 545.678\r
+   0.718 544.832\r
+   0.802 540.278\r
+   0.887 533.698\r
+   0.972 526.340\r
+   1.056 511.003\r
+   1.141 492.475\r
+   1.225 474.977\r
+   1.310 457.021\r
+   1.395 437.203\r
+   1.479 418.093\r
+   1.565 403.240\r
+   1.649 339.173\r
+   1.733 203.861\r
+   1.819 102.620\r
+   1.903 49.295\r
+   1.987 9.538\r
+   2.073 2.155\r
+   2.158 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J500.eng b/datafiles/thrustcurves/AeroTech_J500.eng
new file mode 100644 (file)
index 0000000..4358a4b
--- /dev/null
@@ -0,0 +1,21 @@
+;Delays are speculation.\r
+;Taken from Aerotech curves, not cert docs.\r
+;Jim Yehle 15 Nov 07\r
+J500G 38 335.407 0-6-10-14 0.3626 0.654 Aerotech \r
+0.0134378 40.2458\r
+0.0335946 724.425\r
+0.0403135 781.616\r
+0.0604703 787.971\r
+0.0895857 711.716\r
+0.134378 686.297\r
+0.394177 637.578\r
+0.575588 588.86\r
+0.606943 622.751\r
+0.633819 620.633\r
+1.20045 360.094\r
+1.24076 345.267\r
+1.31019 182.165\r
+1.38186 65.6642\r
+1.43337 23.3002\r
+1.45 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_J540.eng b/datafiles/thrustcurves/AeroTech_J540.eng
new file mode 100644 (file)
index 0000000..58cd385
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech J540R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J540R 54 314 0 0.61376 1.08416 AT\r
+   0.044 498.757\r
+   0.134 639.617\r
+   0.224 649.317\r
+   0.314 657.966\r
+   0.404 664.020\r
+   0.494 666.924\r
+   0.584 663.699\r
+   0.675 658.398\r
+   0.765 651.232\r
+   0.855 638.505\r
+   0.945 626.396\r
+   1.035 612.557\r
+   1.126 590.090\r
+   1.216 562.391\r
+   1.306 536.875\r
+   1.396 511.607\r
+   1.486 490.354\r
+   1.576 468.978\r
+   1.667 451.342\r
+   1.758 430.180\r
+   1.847 414.549\r
+   1.937 398.116\r
+   2.027 305.877\r
+   2.118 55.541\r
+   2.208 1.523\r
+   2.299 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J570.eng b/datafiles/thrustcurves/AeroTech_J570.eng
new file mode 100644 (file)
index 0000000..aa796a9
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J570W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J570W 38 479 0 0.547904 0.886144 AT\r
+   0.039 1149.795\r
+   0.119 1042.846\r
+   0.199 960.891\r
+   0.279 900.020\r
+   0.360 837.772\r
+   0.441 792.834\r
+   0.521 735.510\r
+   0.602 685.857\r
+   0.682 649.599\r
+   0.762 608.757\r
+   0.844 597.350\r
+   0.924 568.934\r
+   1.004 548.552\r
+   1.084 505.080\r
+   1.165 484.626\r
+   1.246 452.328\r
+   1.326 362.439\r
+   1.406 297.973\r
+   1.487 262.381\r
+   1.568 195.696\r
+   1.648 156.733\r
+   1.729 124.649\r
+   1.809 113.749\r
+   1.890 69.812\r
+   1.971 46.023\r
+   2.052 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J575.eng b/datafiles/thrustcurves/AeroTech_J575.eng
new file mode 100644 (file)
index 0000000..e059cc3
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+J575FJ 38 478.79 6-10-14 0.576 0.91424 Aerotech \r
+0.0156556 656.682\r
+0.0195695 840.689\r
+0.037182 840.689\r
+0.0606654 839.001\r
+0.101761 839.001\r
+0.162427 839.001\r
+0.228963 839.001\r
+0.315068 839.001\r
+0.399217 837.312\r
+0.459883 837.312\r
+0.547945 822.119\r
+0.60274 801.862\r
+0.700587 742.777\r
+0.802348 685.381\r
+0.841487 646.554\r
+0.902153 573.964\r
+0.949791 483.69\r
+1 319.581\r
+1.05365 220.66\r
+1.12916 153.62\r
+1.19961 99.5997\r
+1.27593 43.8914\r
+1.34 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_J800.eng b/datafiles/thrustcurves/AeroTech_J800.eng
new file mode 100644 (file)
index 0000000..9e6bed3
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J800T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J800T 54 314 0 0.613312 1.08595 AT\r
+   0.040 841.341\r
+   0.121 818.497\r
+   0.203 776.386\r
+   0.285 784.308\r
+   0.367 785.314\r
+   0.449 783.315\r
+   0.531 782.539\r
+   0.612 779.977\r
+   0.695 773.680\r
+   0.777 765.307\r
+   0.858 755.517\r
+   0.941 744.777\r
+   1.023 733.131\r
+   1.105 719.947\r
+   1.187 702.235\r
+   1.269 685.369\r
+   1.351 668.265\r
+   1.433 650.327\r
+   1.515 630.472\r
+   1.597 615.483\r
+   1.679 470.262\r
+   1.760 256.617\r
+   1.843 108.716\r
+   1.925 15.005\r
+   2.007 1.249\r
+   2.090 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_J825.eng b/datafiles/thrustcurves/AeroTech_J825.eng
new file mode 100644 (file)
index 0000000..ef08bdf
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;Aerotech J825R\r
+J825R  38  479 10 0.53 .88  AT\r
+0.0 11.504\r
+0.048913 1069.87\r
+0.100155 977.842\r
+0.118789 1035.36\r
+0.652174 897.314\r
+0.801242 839.794\r
+0.899068 782.274\r
+0.999224 586.705\r
+1.09938 103.536\r
+1.14363 23.008\r
+1.18 0.0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_J90.eng b/datafiles/thrustcurves/AeroTech_J90.eng
new file mode 100644 (file)
index 0000000..ee8767e
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech J90W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J90W 54 243 0 0.427392 0.852544 AT\r
+   0.143 116.187\r
+   0.430 165.444\r
+   0.718 176.536\r
+   1.005 184.645\r
+   1.293 187.242\r
+   1.580 183.651\r
+   1.868 175.492\r
+   2.155 167.687\r
+   2.443 156.858\r
+   2.730 143.514\r
+   3.018 128.856\r
+   3.305 110.879\r
+   3.593 94.003\r
+   3.880 79.657\r
+   4.168 67.472\r
+   4.455 57.268\r
+   4.743 48.008\r
+   5.030 40.523\r
+   5.318 33.901\r
+   5.605 28.248\r
+   5.893 23.334\r
+   6.180 19.275\r
+   6.468 15.923\r
+   6.755 12.727\r
+   7.044 9.903\r
+   7.332 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K1050.eng b/datafiles/thrustcurves/AeroTech_K1050.eng
new file mode 100644 (file)
index 0000000..2b394c5
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K1050W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K1050W 54 676 0 1.34714 2.12845 AT\r
+   0.049 1305.649\r
+   0.149 1270.386\r
+   0.249 1288.922\r
+   0.349 1327.059\r
+   0.449 1345.719\r
+   0.549 1359.794\r
+   0.649 1364.452\r
+   0.749 1365.493\r
+   0.849 1377.189\r
+   0.949 1379.519\r
+   1.049 1346.586\r
+   1.149 1286.742\r
+   1.249 1232.101\r
+   1.349 1186.480\r
+   1.449 1156.521\r
+   1.549 1120.045\r
+   1.649 1098.708\r
+   1.749 1070.186\r
+   1.849 889.885\r
+   1.949 646.691\r
+   2.049 441.213\r
+   2.149 302.245\r
+   2.249 155.001\r
+   2.349 52.187\r
+   2.449 43.415\r
+   2.549 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K1100.eng b/datafiles/thrustcurves/AeroTech_K1100.eng
new file mode 100644 (file)
index 0000000..0a0920b
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech K1100T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K1100T 54 398 0 0.7616 1.32518 AT\r
+   0.034 1234.653\r
+   0.105 1233.429\r
+   0.176 1192.393\r
+   0.247 1163.041\r
+   0.318 1147.963\r
+   0.389 1146.319\r
+   0.460 1140.958\r
+   0.532 1132.640\r
+   0.603 1123.824\r
+   0.674 1108.921\r
+   0.745 1090.974\r
+   0.816 1073.937\r
+   0.887 1049.133\r
+   0.959 1021.216\r
+   1.030 994.559\r
+   1.101 966.571\r
+   1.172 940.194\r
+   1.243 909.792\r
+   1.315 880.264\r
+   1.386 844.477\r
+   1.457 643.599\r
+   1.528 401.861\r
+   1.599 145.498\r
+   1.670 28.372\r
+   1.742 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K1275.eng b/datafiles/thrustcurves/AeroTech_K1275.eng
new file mode 100644 (file)
index 0000000..490f512
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech K1275R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K1275R 54 569 0 1.29024 2.03392 AT\r
+   0.039 1282.616\r
+   0.119 1557.989\r
+   0.199 1540.196\r
+   0.279 1526.782\r
+   0.359 1500.693\r
+   0.440 1456.584\r
+   0.520 1425.794\r
+   0.600 1390.416\r
+   0.680 1355.109\r
+   0.761 1323.311\r
+   0.841 1282.825\r
+   0.921 1247.795\r
+   1.001 1194.417\r
+   1.081 1150.977\r
+   1.162 1108.223\r
+   1.242 1068.754\r
+   1.322 1036.922\r
+   1.403 997.444\r
+   1.482 964.569\r
+   1.563 933.305\r
+   1.644 889.992\r
+   1.724 599.467\r
+   1.804 134.559\r
+   1.884 5.630\r
+   1.964 0.205\r
+   2.045 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K1499.eng b/datafiles/thrustcurves/AeroTech_K1499.eng
new file mode 100644 (file)
index 0000000..49ebe4c
--- /dev/null
@@ -0,0 +1,13 @@
+;Entered by Jim Yehle\r
+;from TRA cert document\r
+K1499N 75 260 1000 0.604 1.741 AT-RMS \r
+0.01 1450\r
+0.2 1720.12\r
+0.35 1700\r
+0.5 1600\r
+0.6 1575\r
+0.7 1500\r
+0.82 1400\r
+0.84 250\r
+0.88 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_K185.eng b/datafiles/thrustcurves/AeroTech_K185.eng
new file mode 100644 (file)
index 0000000..08793ec
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K185W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K185W 54 437 0 0.827008 1.43405 AT\r
+   0.150 279.128\r
+   0.452 308.220\r
+   0.754 328.435\r
+   1.056 338.929\r
+   1.359 339.677\r
+   1.663 333.166\r
+   1.965 321.891\r
+   2.267 309.687\r
+   2.570 293.260\r
+   2.873 271.536\r
+   3.175 247.174\r
+   3.477 216.883\r
+   3.780 186.951\r
+   4.083 161.096\r
+   4.385 138.113\r
+   4.688 117.749\r
+   4.991 99.372\r
+   5.294 82.759\r
+   5.596 68.426\r
+   5.898 55.126\r
+   6.201 44.162\r
+   6.504 34.209\r
+   6.806 25.064\r
+   7.108 16.880\r
+   7.411 9.200\r
+   7.715 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K1999.eng b/datafiles/thrustcurves/AeroTech_K1999.eng
new file mode 100644 (file)
index 0000000..5136824
--- /dev/null
@@ -0,0 +1,21 @@
+; AeroTech K1999N\r
+; Curvefit to instruction sheet on Aerotech website (1/29/07)\r
+; by Chris Kobel\r
+; burn time: 1.4 seconds\r
+; total impulse: 2522 newton-seconds\r
+; average thrust: 405 pounds\r
+K1999N 98 289 6-10-14-18 1.195 2.989 AT\r
+  0.02 1557.5\r
+  0.08 1780.0\r
+  0.10 1913.5\r
+  0.12 1869.0\r
+  0.18 2002.5\r
+  1.08 2002.5\r
+  1.10 1958.0\r
+  1.20 1780.0\r
+  1.25 1557.5\r
+  1.27 1335.0\r
+  1.31 890.0\r
+  1.33 667.5\r
+  1.35 222.5\r
+  1.40 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_K250.eng b/datafiles/thrustcurves/AeroTech_K250.eng
new file mode 100644 (file)
index 0000000..bcc73d2
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K250W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K250W 54 673 0 1.52902 2.21133 AT\r
+   0.199 365.330\r
+   0.599 403.324\r
+   0.999 418.669\r
+   1.400 409.813\r
+   1.801 408.949\r
+   2.201 412.146\r
+   2.602 411.952\r
+   3.003 409.488\r
+   3.403 393.214\r
+   3.804 373.599\r
+   4.205 348.913\r
+   4.605 328.463\r
+   5.006 307.163\r
+   5.407 281.467\r
+   5.807 249.011\r
+   6.208 217.159\r
+   6.609 185.908\r
+   7.009 149.190\r
+   7.410 119.808\r
+   7.811 92.096\r
+   8.211 69.726\r
+   8.613 52.613\r
+   9.014 35.876\r
+   9.414 16.727\r
+   9.815 4.086\r
+   10.216 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K270.eng b/datafiles/thrustcurves/AeroTech_K270.eng
new file mode 100644 (file)
index 0000000..2cc9069
--- /dev/null
@@ -0,0 +1,35 @@
+; Aerotech K270W-P Moon Burner from TRA Certification Data\r
+K270W 54 579 P 1.188 2.1 AT\r
+   0.046 177.061\r
+   0.062 292.932\r
+   0.092 425.727\r
+   0.154 414.01\r
+   0.277 389.273\r
+   0.446 377.556\r
+   0.585 381.462\r
+   0.738 372.349\r
+   1.0 377.556\r
+   1.154 376.254\r
+   1.231 378.858\r
+   1.308 395.783\r
+   1.4 380.16\r
+   1.569 399.689\r
+   1.615 381.462\r
+   1.846 381.462\r
+   2.369 368.443\r
+   2.415 381.462\r
+   2.554 360.631\r
+   3.015 350.216\r
+   3.354 328.083\r
+   3.723 300.743\r
+   4.0 273.403\r
+   4.6 225.232\r
+   5.262 175.759\r
+   5.677 144.513\r
+   6.0 124.984\r
+   6.538 89.832\r
+   7.015 66.398\r
+   8.0 22.133\r
+   8.323 10.415\r
+   8.508 5.208\r
+   8.692 0.0\r
diff --git a/datafiles/thrustcurves/AeroTech_K458.eng b/datafiles/thrustcurves/AeroTech_K458.eng
new file mode 100644 (file)
index 0000000..ffcf9af
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K458W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K458W 98 275 0 1.42778 3.16378 AT\r
+   0.133 294.911\r
+   0.403 404.808\r
+   0.674 462.021\r
+   0.944 515.863\r
+   1.214 555.072\r
+   1.484 583.153\r
+   1.755 600.299\r
+   2.025 610.254\r
+   2.295 618.543\r
+   2.566 623.155\r
+   2.835 618.885\r
+   3.105 589.082\r
+   3.376 546.307\r
+   3.647 505.042\r
+   3.917 451.412\r
+   4.186 391.651\r
+   4.457 338.409\r
+   4.727 288.429\r
+   4.997 245.814\r
+   5.268 208.209\r
+   5.539 178.153\r
+   5.808 149.825\r
+   6.078 62.931\r
+   6.349 8.427\r
+   6.620 2.562\r
+   6.891 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K485.eng b/datafiles/thrustcurves/AeroTech_K485.eng
new file mode 100644 (file)
index 0000000..3c808ec
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K485HW\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K485HW 54 699 0 0.910784 2.22029 AT\r
+   0.075 454.453\r
+   0.227 568.735\r
+   0.380 831.332\r
+   0.533 825.584\r
+   0.686 795.935\r
+   0.840 759.473\r
+   0.992 727.238\r
+   1.145 680.051\r
+   1.298 653.091\r
+   1.451 627.316\r
+   1.604 601.548\r
+   1.756 576.270\r
+   1.909 542.033\r
+   2.063 479.078\r
+   2.216 394.184\r
+   2.369 346.719\r
+   2.521 307.435\r
+   2.674 276.291\r
+   2.827 216.608\r
+   2.980 146.021\r
+   3.133 106.838\r
+   3.285 81.226\r
+   3.439 52.105\r
+   3.592 37.385\r
+   3.745 29.462\r
+   3.898 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K550.eng b/datafiles/thrustcurves/AeroTech_K550.eng
new file mode 100644 (file)
index 0000000..cd8254b
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K550W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K550W 54 410 0 0.919744 1.48736 AT\r
+   0.065 604.264\r
+   0.196 642.625\r
+   0.327 682.197\r
+   0.458 732.995\r
+   0.591 758.236\r
+   0.723 780.289\r
+   0.854 794.452\r
+   0.985 797.939\r
+   1.117 797.601\r
+   1.249 773.842\r
+   1.381 711.608\r
+   1.512 646.522\r
+   1.644 590.724\r
+   1.775 537.505\r
+   1.907 491.012\r
+   2.040 445.836\r
+   2.171 401.461\r
+   2.302 364.291\r
+   2.433 319.614\r
+   2.566 255.577\r
+   2.698 172.573\r
+   2.829 103.501\r
+   2.960 51.795\r
+   3.092 26.814\r
+   3.224 15.203\r
+   3.356 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K560.eng b/datafiles/thrustcurves/AeroTech_K560.eng
new file mode 100644 (file)
index 0000000..bc7912e
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K560W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K560W 75 396 0 1.40806 2.71354 AT\r
+   0.096 552.123\r
+   0.290 645.403\r
+   0.484 681.109\r
+   0.679 716.167\r
+   0.874 742.678\r
+   1.069 764.778\r
+   1.264 775.710\r
+   1.458 785.859\r
+   1.653 789.305\r
+   1.848 789.077\r
+   2.043 744.622\r
+   2.237 676.886\r
+   2.432 614.711\r
+   2.627 557.908\r
+   2.822 503.641\r
+   3.017 455.504\r
+   3.211 412.045\r
+   3.406 372.963\r
+   3.601 335.987\r
+   3.796 307.346\r
+   3.991 279.856\r
+   4.185 223.491\r
+   4.380 70.441\r
+   4.575 10.028\r
+   4.770 2.445\r
+   4.965 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K650.eng b/datafiles/thrustcurves/AeroTech_K650.eng
new file mode 100644 (file)
index 0000000..ac0c45c
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K650T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K650T 98 289 0 1.27008 2.9353 AT\r
+   0.079 514.338\r
+   0.240 594.264\r
+   0.401 618.849\r
+   0.563 641.658\r
+   0.723 665.057\r
+   0.884 686.488\r
+   1.046 704.685\r
+   1.206 720.215\r
+   1.368 730.072\r
+   1.529 736.891\r
+   1.690 743.109\r
+   1.851 747.503\r
+   2.013 747.557\r
+   2.174 744.081\r
+   2.335 732.294\r
+   2.496 710.412\r
+   2.657 682.670\r
+   2.819 653.246\r
+   2.979 627.020\r
+   3.141 595.456\r
+   3.302 563.844\r
+   3.463 551.080\r
+   3.624 236.059\r
+   3.785 1.383\r
+   3.947 1.234\r
+   4.108 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K680.eng b/datafiles/thrustcurves/AeroTech_K680.eng
new file mode 100644 (file)
index 0000000..c92ed34
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;Aerotech K680R RASP engine file\r
+;Data Entered by Tim Van Milligan\r
+;Source: TRA Certification paperwork, and\r
+;Aerotech's instruction sheet: RMS 98/2560-10240 REDLINE.\r
+K680R 98 289 100 1.316 3.035 AT\r
+0.085 629.798\r
+0.494 717.881\r
+0.996 797.157\r
+1.29 819.178\r
+1.506 819.178\r
+2.001 775.136\r
+2.519 673.84\r
+2.99 563.735\r
+3.137 541.714\r
+3.176 532.906\r
+3.238 563.735\r
+3.276 563.735\r
+3.408 52.85\r
+3.431 22.02\r
+3.49 0\r
diff --git a/datafiles/thrustcurves/AeroTech_K695.eng b/datafiles/thrustcurves/AeroTech_K695.eng
new file mode 100644 (file)
index 0000000..5932925
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech K695R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K695R 54 410 0 0.9184 1.48736 AT\r
+   0.044 618.611\r
+   0.134 727.840\r
+   0.224 751.996\r
+   0.314 812.480\r
+   0.404 900.125\r
+   0.495 884.763\r
+   0.585 873.457\r
+   0.675 864.561\r
+   0.765 849.672\r
+   0.856 838.886\r
+   0.946 822.550\r
+   1.036 806.240\r
+   1.126 781.342\r
+   1.216 753.973\r
+   1.307 728.472\r
+   1.398 697.629\r
+   1.487 672.979\r
+   1.578 646.660\r
+   1.667 620.897\r
+   1.758 595.574\r
+   1.849 571.720\r
+   1.939 546.822\r
+   2.029 272.824\r
+   2.119 57.950\r
+   2.209 4.509\r
+   2.300 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K700.eng b/datafiles/thrustcurves/AeroTech_K700.eng
new file mode 100644 (file)
index 0000000..4155a3b
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech K700W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K700W 54 568 0 1.29158 2.03526 AT\r
+   0.069 1005.472\r
+   0.209 1018.916\r
+   0.350 1026.610\r
+   0.491 1028.637\r
+   0.632 1029.337\r
+   0.773 1004.203\r
+   0.914 970.694\r
+   1.055 946.516\r
+   1.196 918.437\r
+   1.336 873.783\r
+   1.478 821.276\r
+   1.619 773.270\r
+   1.759 735.553\r
+   1.900 692.732\r
+   2.041 658.984\r
+   2.182 626.737\r
+   2.323 591.431\r
+   2.464 508.666\r
+   2.605 420.175\r
+   2.746 328.309\r
+   2.886 202.409\r
+   3.028 121.672\r
+   3.169 80.453\r
+   3.309 50.873\r
+   3.451 31.548\r
+   3.593 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K780.eng b/datafiles/thrustcurves/AeroTech_K780.eng
new file mode 100644 (file)
index 0000000..cfda61d
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech K780R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K780R 75 289 0 1.26784 2.9344 AT\r
+   0.053 383.290\r
+   0.173 718.241\r
+   0.292 849.343\r
+   0.413 885.503\r
+   0.533 903.243\r
+   0.652 924.403\r
+   0.772 938.825\r
+   0.892 938.623\r
+   1.013 947.130\r
+   1.133 953.578\r
+   1.253 944.001\r
+   1.373 935.448\r
+   1.495 929.447\r
+   1.617 920.379\r
+   1.737 897.293\r
+   1.857 888.917\r
+   1.977 861.127\r
+   2.098 840.971\r
+   2.217 812.360\r
+   2.337 779.614\r
+   2.457 747.866\r
+   2.578 726.819\r
+   2.697 729.258\r
+   2.817 279.891\r
+   2.940 10.969\r
+   3.063 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_K828.eng b/datafiles/thrustcurves/AeroTech_K828.eng
new file mode 100644 (file)
index 0000000..2c073f6
--- /dev/null
@@ -0,0 +1,30 @@
+;\r
+K828FJ  54.0 579.00 6-10-14-18 1.45000 2.25500 AT\r
+   0.01    1112.06 \r
+   0.02    1238.60 \r
+   0.04    1303.79 \r
+   0.06    1135.06 \r
+   0.08    1077.54 \r
+   0.13    1031.53 \r
+   0.20    1016.19 \r
+   0.50     993.18 \r
+   0.65    1004.68 \r
+   1.00     985.51 \r
+   1.08     974.01 \r
+   1.19     974.01 \r
+   1.42     954.83 \r
+   1.51     935.66 \r
+   1.69     912.65 \r
+   1.75     885.81 \r
+   1.83     893.48 \r
+   1.89     843.63 \r
+   1.95     774.60 \r
+   2.00     667.23 \r
+   2.15     444.82 \r
+   2.20     364.29 \r
+   2.23     260.76 \r
+   2.27     184.06 \r
+   2.33     111.21 \r
+   2.39      49.85 \r
+   2.50       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_L1120.eng b/datafiles/thrustcurves/AeroTech_L1120.eng
new file mode 100644 (file)
index 0000000..639635a
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech L1120W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1120W 75 665 0 2.75699 4.65786 AT\r
+   0.097 1377.215\r
+   0.293 1442.670\r
+   0.489 1496.986\r
+   0.685 1537.057\r
+   0.882 1554.962\r
+   1.078 1554.131\r
+   1.275 1547.973\r
+   1.472 1533.465\r
+   1.668 1510.342\r
+   1.865 1472.279\r
+   2.061 1362.534\r
+   2.257 1245.425\r
+   2.454 1148.864\r
+   2.651 1062.680\r
+   2.847 984.952\r
+   3.044 916.169\r
+   3.241 831.929\r
+   3.436 766.450\r
+   3.633 698.978\r
+   3.830 562.966\r
+   4.026 384.579\r
+   4.223 227.654\r
+   4.420 105.078\r
+   4.616 56.339\r
+   4.813 21.712\r
+   5.009 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_L1150.eng b/datafiles/thrustcurves/AeroTech_L1150.eng
new file mode 100644 (file)
index 0000000..32059ad
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech L1150\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1150 75 531 0 2.06528 3.6736 AT\r
+   0.053 935.855\r
+   0.175 1292.642\r
+   0.300 1260.926\r
+   0.425 1241.482\r
+   0.550 1257.058\r
+   0.675 1272.287\r
+   0.800 1287.605\r
+   0.925 1301.012\r
+   1.048 1309.708\r
+   1.170 1308.417\r
+   1.295 1304.830\r
+   1.420 1285.265\r
+   1.545 1267.657\r
+   1.670 1255.624\r
+   1.795 1227.212\r
+   1.920 1202.443\r
+   2.043 1182.617\r
+   2.165 1150.712\r
+   2.290 1117.909\r
+   2.415 1081.739\r
+   2.540 1037.547\r
+   2.665 1007.091\r
+   2.790 1008.911\r
+   2.915 643.124\r
+   3.040 64.371\r
+   3.165 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_L1300.eng b/datafiles/thrustcurves/AeroTech_L1300.eng
new file mode 100644 (file)
index 0000000..ec9fc02
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;\r
+L1300R 98 443 100 2.508 4.884 AT\r
+0.0231839 1299.23\r
+0.502318 1332.26\r
+0.996909 1497.42\r
+1.49923 1552.47\r
+1.99382 1508.43\r
+2.49614 1354.29\r
+2.99845 1101.05\r
+3.12983 1090.03\r
+3.21484 1145.09\r
+3.3694 176.167\r
+3.5 0\r
diff --git a/datafiles/thrustcurves/AeroTech_L1420.eng b/datafiles/thrustcurves/AeroTech_L1420.eng
new file mode 100644 (file)
index 0000000..022b1ef
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;\r
+L1420R 75 443 100 2.56 4.562 AT\r
+0.0386399 1332.26\r
+0.123648 1563.48\r
+0.502318 1519.44\r
+0.996909 1574.49\r
+1.49923 1662.58\r
+2.00155 1574.49\r
+2.48068 1409.34\r
+2.92117 1299.23\r
+2.99073 1167.11\r
+3.11437 187.178\r
+3.24 0\r
diff --git a/datafiles/thrustcurves/AeroTech_L1500.eng b/datafiles/thrustcurves/AeroTech_L1500.eng
new file mode 100644 (file)
index 0000000..ffb1057
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech L1500T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1500T 98 443 0 2.464 4.6592 AT\r
+   0.073 1320.328\r
+   0.222 1454.823\r
+   0.372 1508.992\r
+   0.522 1556.781\r
+   0.672 1602.407\r
+   0.822 1642.004\r
+   0.971 1670.099\r
+   1.120 1694.804\r
+   1.270 1701.295\r
+   1.420 1704.286\r
+   1.570 1701.008\r
+   1.720 1694.550\r
+   1.869 1683.861\r
+   2.018 1659.694\r
+   2.168 1620.161\r
+   2.318 1570.033\r
+   2.468 1517.933\r
+   2.618 1463.319\r
+   2.767 1400.991\r
+   2.916 1331.420\r
+   3.066 1279.479\r
+   3.216 1108.987\r
+   3.366 217.788\r
+   3.516 10.579\r
+   3.666 3.245\r
+   3.816 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_L850.eng b/datafiles/thrustcurves/AeroTech_L850.eng
new file mode 100644 (file)
index 0000000..fee1d2a
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech L850W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L850W 75 531 0 2.06528 3.67315 AT\r
+   0.091 1015.926\r
+   0.274 1064.942\r
+   0.458 1101.366\r
+   0.643 1143.358\r
+   0.827 1170.928\r
+   1.011 1184.795\r
+   1.196 1178.044\r
+   1.380 1177.598\r
+   1.564 1174.910\r
+   1.748 1170.021\r
+   1.932 1113.716\r
+   2.117 1042.586\r
+   2.301 972.795\r
+   2.485 908.071\r
+   2.670 844.471\r
+   2.854 773.595\r
+   3.039 714.046\r
+   3.222 649.095\r
+   3.406 597.341\r
+   3.591 557.444\r
+   3.775 422.233\r
+   3.959 200.739\r
+   4.144 79.411\r
+   4.328 43.959\r
+   4.513 14.862\r
+   4.697 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_L952.eng b/datafiles/thrustcurves/AeroTech_L952.eng
new file mode 100644 (file)
index 0000000..9afeb32
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech L952W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L952W 98 427 0 2.73011 5.01222 AT\r
+   0.141 679.073\r
+   0.425 801.562\r
+   0.709 848.474\r
+   0.994 913.345\r
+   1.278 981.614\r
+   1.562 1043.690\r
+   1.847 1088.114\r
+   2.131 1112.556\r
+   2.416 1121.541\r
+   2.700 1118.573\r
+   2.984 1100.665\r
+   3.269 1039.140\r
+   3.553 965.784\r
+   3.837 876.793\r
+   4.122 780.693\r
+   4.406 693.903\r
+   4.691 608.030\r
+   4.975 528.335\r
+   5.259 463.528\r
+   5.544 405.769\r
+   5.828 358.367\r
+   6.112 279.009\r
+   6.397 99.897\r
+   6.681 20.108\r
+   6.967 3.317\r
+   7.252 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M1297.eng b/datafiles/thrustcurves/AeroTech_M1297.eng
new file mode 100644 (file)
index 0000000..8745297
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+; Aerotech M1297W\r
+;  Greg Gardner - 12/20/04\r
+M1297W  75  665  0  2.722  4.637  AT\r
+0.10  1433.4\r
+0.15  1789.3\r
+0.20  1922.8\r
+0.25  1869.4\r
+0.30  1856.0\r
+0.35  1833.8\r
+0.40  1767.0\r
+0.50  1722.6\r
+0.60  1709.2\r
+0.90  1700.3\r
+1.00  1688.1\r
+1.50  1678.7\r
+1.75  1634.6\r
+1.85  1622.3\r
+1.95  1572.8\r
+2.00  1554.0\r
+2.50  1346.5\r
+3.00  1136.0\r
+3.20  1053.3\r
+3.25  1044.1\r
+3.35  1032.0\r
+3.38  1020.0\r
+3.40  937.0\r
+3.50  738.0\r
+3.60  545.0\r
+3.75  393.0\r
+4.00  226.0\r
+4.25  94.0\r
+4.35  45.0\r
+4.40  0.0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_M1315.eng b/datafiles/thrustcurves/AeroTech_M1315.eng
new file mode 100644 (file)
index 0000000..cb47989
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech M1315W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1315W 75 801 0 3.4496 5.6448 AT\r
+   0.116 1728.683\r
+   0.349 1673.336\r
+   0.582 1686.810\r
+   0.816 1696.068\r
+   1.049 1663.167\r
+   1.282 1631.243\r
+   1.516 1620.471\r
+   1.749 1619.702\r
+   1.982 1621.042\r
+   2.216 1615.320\r
+   2.449 1567.089\r
+   2.682 1493.722\r
+   2.916 1420.079\r
+   3.149 1358.660\r
+   3.382 1292.507\r
+   3.616 1224.806\r
+   3.849 1171.995\r
+   4.082 928.809\r
+   4.316 577.949\r
+   4.549 395.445\r
+   4.782 314.006\r
+   5.016 228.273\r
+   5.249 159.803\r
+   5.482 118.348\r
+   5.716 109.782\r
+   5.949 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M1419.eng b/datafiles/thrustcurves/AeroTech_M1419.eng
new file mode 100644 (file)
index 0000000..89319c7
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech M1419W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1419W 98 579 0 4.032 6.91622 AT\r
+   0.154 1154.896\r
+   0.465 1241.151\r
+   0.776 1300.224\r
+   1.087 1358.364\r
+   1.399 1411.033\r
+   1.710 1461.033\r
+   2.022 1485.747\r
+   2.333 1503.653\r
+   2.644 1513.113\r
+   2.955 1511.947\r
+   3.267 1492.438\r
+   3.578 1418.368\r
+   3.890 1326.608\r
+   4.201 1219.222\r
+   4.513 1087.648\r
+   4.824 937.068\r
+   5.135 810.066\r
+   5.446 709.130\r
+   5.757 624.701\r
+   6.069 557.223\r
+   6.380 437.806\r
+   6.692 252.076\r
+   7.003 107.741\r
+   7.315 19.973\r
+   7.626 0.515\r
+   7.937 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M1550.eng b/datafiles/thrustcurves/AeroTech_M1550.eng
new file mode 100644 (file)
index 0000000..75211cc
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech M1550R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1550R 75 800 0 3.4496 5.6448 AT\r
+   0.069 1720.759\r
+   0.212 2125.329\r
+   0.358 1995.947\r
+   0.501 1908.442\r
+   0.645 1868.713\r
+   0.790 1835.504\r
+   0.935 1808.662\r
+   1.079 1796.300\r
+   1.222 1785.423\r
+   1.368 1773.153\r
+   1.511 1746.590\r
+   1.655 1715.709\r
+   1.800 1689.633\r
+   1.945 1660.720\r
+   2.089 1633.277\r
+   2.232 1606.038\r
+   2.378 1570.222\r
+   2.521 1534.714\r
+   2.665 1503.345\r
+   2.810 1461.317\r
+   2.955 1427.572\r
+   3.099 1393.229\r
+   3.242 939.955\r
+   3.388 268.504\r
+   3.532 4.985\r
+   3.677 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M1600.eng b/datafiles/thrustcurves/AeroTech_M1600.eng
new file mode 100644 (file)
index 0000000..f145847
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech M1600R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1600R 98 579 0 4.032 6.91712 AT\r
+   0.088 1370.361\r
+   0.268 1626.628\r
+   0.448 1672.654\r
+   0.628 1720.596\r
+   0.808 1763.287\r
+   0.987 1801.282\r
+   1.167 1829.825\r
+   1.348 1845.146\r
+   1.529 1856.370\r
+   1.710 1850.089\r
+   1.890 1847.370\r
+   2.070 1829.454\r
+   2.250 1810.982\r
+   2.430 1784.910\r
+   2.610 1754.267\r
+   2.790 1726.898\r
+   2.971 1689.288\r
+   3.152 1641.579\r
+   3.332 1581.589\r
+   3.513 1511.036\r
+   3.692 1431.400\r
+   3.872 1361.032\r
+   4.053 1234.566\r
+   4.232 621.206\r
+   4.414 42.471\r
+   4.595 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M1850.eng b/datafiles/thrustcurves/AeroTech_M1850.eng
new file mode 100644 (file)
index 0000000..6ee44ba
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;75-7680 case\r
+;  Greg Gardner - 10/25/07\r
+M1850W  75 935  0  3.979  6.871  AT\r
+0.1 2411\r
+0.2 2135\r
+0.3 2015\r
+0.4 2000\r
+0.5 2055\r
+1.0 2098\r
+1.5 1860\r
+2.0 1788\r
+2.5 1659\r
+3.0 1468\r
+3.25 1423\r
+3.35 1334\r
+3.5 1201\r
+3.75 934\r
+3.8 930\r
+4.0 881\r
+4.25 600\r
+4.5 468\r
+4.75 400\r
+5.0 290\r
+5.5 85\r
+6.0 23\r
+6.5 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_M1939.eng b/datafiles/thrustcurves/AeroTech_M1939.eng
new file mode 100644 (file)
index 0000000..c012908
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech M1939W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1939W 98 732 0 5.656 8.98822 AT\r
+   0.134 1905.185\r
+   0.406 2021.155\r
+   0.679 2095.900\r
+   0.952 2158.087\r
+   1.225 2198.211\r
+   1.498 2219.694\r
+   1.770 2228.643\r
+   2.042 2229.881\r
+   2.315 2225.641\r
+   2.587 2211.713\r
+   2.860 2164.724\r
+   3.133 2047.014\r
+   3.405 1916.238\r
+   3.677 1805.664\r
+   3.950 1658.489\r
+   4.223 1497.704\r
+   4.496 1339.452\r
+   4.769 1213.061\r
+   5.041 1102.130\r
+   5.313 966.508\r
+   5.585 670.253\r
+   5.858 443.975\r
+   6.131 155.355\r
+   6.404 41.358\r
+   6.677 5.775\r
+   6.950 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M2000.eng b/datafiles/thrustcurves/AeroTech_M2000.eng
new file mode 100644 (file)
index 0000000..e2bf3ec
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech M2000R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M2000R 98 732 0 5.65824 8.98688 AT\r
+   0.091 1530.959\r
+   0.279 2186.270\r
+   0.466 2166.698\r
+   0.655 2187.237\r
+   0.844 2219.069\r
+   1.031 2248.071\r
+   1.220 2273.743\r
+   1.409 2298.306\r
+   1.596 2309.753\r
+   1.785 2315.708\r
+   1.974 2316.158\r
+   2.161 2306.313\r
+   2.350 2282.230\r
+   2.539 2252.104\r
+   2.726 2209.638\r
+   2.915 2168.800\r
+   3.104 2117.175\r
+   3.291 2067.533\r
+   3.480 2004.508\r
+   3.669 1934.442\r
+   3.856 1831.480\r
+   4.045 1745.634\r
+   4.234 1504.269\r
+   4.421 649.796\r
+   4.610 58.178\r
+   4.799 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M2400.eng b/datafiles/thrustcurves/AeroTech_M2400.eng
new file mode 100644 (file)
index 0000000..7487ded
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech M2400T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M2400T 98 597 0 3.65254 6.4512 AT\r
+   0.070 2441.945\r
+   0.211 2495.460\r
+   0.353 2556.133\r
+   0.495 2601.596\r
+   0.636 2637.660\r
+   0.778 2660.804\r
+   0.920 2676.486\r
+   1.061 2687.081\r
+   1.203 2695.807\r
+   1.345 2694.493\r
+   1.486 2684.268\r
+   1.628 2667.289\r
+   1.771 2629.961\r
+   1.914 2578.923\r
+   2.055 2522.074\r
+   2.197 2461.704\r
+   2.339 2393.518\r
+   2.480 2303.939\r
+   2.622 2201.610\r
+   2.764 2097.461\r
+   2.905 2010.409\r
+   3.047 1275.776\r
+   3.189 418.836\r
+   3.330 17.586\r
+   3.473 3.669\r
+   3.616 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M2500.eng b/datafiles/thrustcurves/AeroTech_M2500.eng
new file mode 100644 (file)
index 0000000..8847c58
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech M2500T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M2500T 98 751 0 4.6592 8.064 AT\r
+   0.082 2651.855\r
+   0.249 2780.285\r
+   0.416 2820.733\r
+   0.583 2843.010\r
+   0.751 2847.765\r
+   0.918 2851.215\r
+   1.084 2854.737\r
+   1.252 2861.690\r
+   1.420 2858.088\r
+   1.586 2851.086\r
+   1.754 2844.622\r
+   1.922 2830.855\r
+   2.089 2804.711\r
+   2.255 2765.796\r
+   2.423 2710.509\r
+   2.591 2648.262\r
+   2.757 2586.910\r
+   2.925 2520.794\r
+   3.093 2462.217\r
+   3.259 2419.937\r
+   3.426 1894.936\r
+   3.594 808.043\r
+   3.761 282.403\r
+   3.928 97.876\r
+   4.096 24.492\r
+   4.264 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_M650.eng b/datafiles/thrustcurves/AeroTech_M650.eng
new file mode 100644 (file)
index 0000000..38c027e
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;75-6400 case\r
+;  Greg Gardner - 10/25/07\r
+M650W  75 801  0  3.351  5.125  AT\r
+0.08 1240\r
+0.12 1328\r
+0.25 1230\r
+0.5 1142\r
+1.0 1071\r
+1.5 1048\r
+2.0 1018\r
+2.5 982\r
+3.0 950\r
+3.5 853\r
+4.0 781\r
+5.0 595\r
+6.0 443\r
+7.0 297\r
+8.0 155\r
+9.0 88\r
+10.0 32\r
+10.5 12\r
+11.0 4\r
+11.5 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_M750.eng b/datafiles/thrustcurves/AeroTech_M750.eng
new file mode 100644 (file)
index 0000000..903d0a1
--- /dev/null
@@ -0,0 +1,24 @@
+;\r
+;98-10240 case\r
+;  Greg Gardner - 10/25/07\r
+M750W  98 732  0  5.3  8.776  AT\r
+0.1 1032\r
+0.2 992\r
+0.3 974\r
+0.48 966\r
+1.0 1055\r
+1.5 1152\r
+2.0 1192\r
+2.5 1218\r
+4.0 1103\r
+6.0 818\r
+8.0 561\r
+10.0 318\r
+11.0 216\r
+12.0 125\r
+13.0 76\r
+14.0 47\r
+15.0 23\r
+15.5 9\r
+16.0 0\r
+;\r
diff --git a/datafiles/thrustcurves/AeroTech_M845.eng b/datafiles/thrustcurves/AeroTech_M845.eng
new file mode 100644 (file)
index 0000000..a7e8c69
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+M845HW 98 795.02 100 3.569 6.833 AT\r
+0.015456 1332.26\r
+0.0463679 1706.62\r
+0.0772798 1178.12\r
+0.185471 1310.24\r
+0.973725 1222.16\r
+1.51468 1200.14\r
+1.97836 1123.07\r
+3.97218 1057\r
+4.20402 880.836\r
+6.01236 627.596\r
+6.495 418.397\r
+7.017 99.0941\r
+7.5 0\r
diff --git a/datafiles/thrustcurves/AeroTech_N2000.eng b/datafiles/thrustcurves/AeroTech_N2000.eng
new file mode 100644 (file)
index 0000000..52788cf
--- /dev/null
@@ -0,0 +1,30 @@
+; AeroTech N2000W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+N2000W 98 1046 0 7.66707 12.2828 AT\r
+   0.146 2775.075\r
+   0.446 2831.810\r
+   0.746 2834.354\r
+   1.046 2829.564\r
+   1.346 2777.650\r
+   1.646 2688.252\r
+   1.950 2597.973\r
+   2.254 2501.043\r
+   2.554 2415.747\r
+   2.854 2343.624\r
+   3.154 2262.579\r
+   3.454 2178.182\r
+   3.758 2104.164\r
+   4.062 2024.475\r
+   4.362 1935.616\r
+   4.663 1839.781\r
+   4.962 1756.910\r
+   5.262 1351.806\r
+   5.567 954.556\r
+   5.871 681.831\r
+   6.171 475.910\r
+   6.471 361.124\r
+   6.771 194.633\r
+   7.071 44.938\r
+   7.375 6.030\r
+   7.679 0.000\r
diff --git a/datafiles/thrustcurves/AeroTech_N4800.eng b/datafiles/thrustcurves/AeroTech_N4800.eng
new file mode 100644 (file)
index 0000000..b9db511
--- /dev/null
@@ -0,0 +1,29 @@
+; AeroTech N4800T\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+N4800T 98 1194 0 9.7664 14.784 AT\r
+   0.098 4752.717\r
+   0.301 6007.533\r
+   0.506 5594.225\r
+   0.710 5270.361\r
+   0.914 5150.120\r
+   1.119 5108.054\r
+   1.324 5086.206\r
+   1.528 5031.651\r
+   1.731 4941.811\r
+   1.936 4800.400\r
+   2.140 4664.876\r
+   2.344 4527.840\r
+   2.549 4401.003\r
+   2.754 4263.565\r
+   2.958 4120.406\r
+   3.161 3971.136\r
+   3.366 3876.421\r
+   3.570 3916.232\r
+   3.774 3913.510\r
+   3.979 3312.758\r
+   4.184 1649.267\r
+   4.388 523.361\r
+   4.591 327.209\r
+   4.796 251.041\r
+   5.001 128.177\r
+   5.206 0.000\r
diff --git a/datafiles/thrustcurves/Apogee_1/2A2.eng b/datafiles/thrustcurves/Apogee_1/2A2.eng
new file mode 100644 (file)
index 0000000..7471c9d
--- /dev/null
@@ -0,0 +1,39 @@
+;\r
+;Apogee 1/2A2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/2A2 11 57 2-4-6 0.0015 0.0044 Apogee \r
+0.007 0.19\r
+0.045 1.494\r
+0.078 3.152\r
+0.088 3.805\r
+0.093 3.805\r
+0.1 3.97\r
+0.105 3.696\r
+0.11 3.071\r
+0.117 2.554\r
+0.123 2.582\r
+0.132 2.31\r
+0.163 2.146\r
+0.2 1.984\r
+0.242 1.902\r
+0.253 2.01\r
+0.275 1.929\r
+0.342 1.929\r
+0.403 1.929\r
+0.41 1.848\r
+0.42 1.902\r
+0.467 1.902\r
+0.528 1.929\r
+0.565 1.929\r
+0.58 1.902\r
+0.593 1.848\r
+0.603 1.657\r
+0.61 1.141\r
+0.615 0.597\r
+0.622 0.244\r
+0.63 0\r
diff --git a/datafiles/thrustcurves/Apogee_1/4A2.eng b/datafiles/thrustcurves/Apogee_1/4A2.eng
new file mode 100644 (file)
index 0000000..3c66820
--- /dev/null
@@ -0,0 +1,30 @@
+;Apogee 1/4A2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/4A2 11 38 2-4 0.0008 0.0036 Apogee \r
+0.007 0.162\r
+0.023 0.65\r
+0.041 1.463\r
+0.058 2.519\r
+0.074 3.738\r
+0.079 3.9\r
+0.088 4.915\r
+0.097 5.119\r
+0.106 5.4\r
+0.11 5.119\r
+0.118 3.981\r
+0.125 3.656\r
+0.132 3.453\r
+0.136 3.209\r
+0.151 3.169\r
+0.156 2.966\r
+0.168 2.884\r
+0.18 2.397\r
+0.194 1.625\r
+0.207 1.056\r
+0.218 0.406\r
+0.23 0\r
diff --git a/datafiles/thrustcurves/Apogee_A2.eng b/datafiles/thrustcurves/Apogee_A2.eng
new file mode 100644 (file)
index 0000000..00e9585
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;Apogee A2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A2 11 58 0-3-5-7 0.003 0.0067 Apogee \r
+0.014 0.241\r
+0.036 0.895\r
+0.064 2.618\r
+0.1 4.82\r
+0.111 4.133\r
+0.125 2.687\r
+0.139 2.307\r
+0.185 2.031\r
+0.296 1.928\r
+0.481 1.825\r
+0.517 1.722\r
+0.538 1.791\r
+0.649 1.688\r
+0.748 1.757\r
+0.869 1.825\r
+1.04 1.894\r
+1.101 1.894\r
+1.119 1.825\r
+1.144 1.928\r
+1.229 1.859\r
+1.265 1.894\r
+1.283 1.757\r
+1.29 1.412\r
+1.293 0.688\r
+1.3 0.275\r
+1.31 0\r
diff --git a/datafiles/thrustcurves/Apogee_B2.eng b/datafiles/thrustcurves/Apogee_B2.eng
new file mode 100644 (file)
index 0000000..50c2fd7
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;Apogee B2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+B2 11 88 0-3-5-7-9 0.006 0.0106 Apogee \r
+0.057 1.637\r
+0.093 4.091\r
+0.121 5.48\r
+0.143 4.787\r
+0.157 3.478\r
+0.207 2.578\r
+0.328 2.087\r
+0.371 2.087\r
+0.406 1.882\r
+0.641 1.841\r
+0.869 1.841\r
+1.283 1.882\r
+1.361 1.882\r
+1.397 1.718\r
+1.439 1.841\r
+1.532 1.718\r
+1.71 1.841\r
+1.888 1.882\r
+2.095 1.8\r
+2.23 1.8\r
+2.295 1.677\r
+2.423 1.759\r
+2.444 1.637\r
+2.466 0.982\r
+2.494 0.327\r
+2.53 0\r
diff --git a/datafiles/thrustcurves/Apogee_B7.eng b/datafiles/thrustcurves/Apogee_B7.eng
new file mode 100644 (file)
index 0000000..30c41d6
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+;Apogee B7 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+B7 13 50 4-6-8-10 0.0028 0.0091 Apogee \r
+0.007 5.708\r
+0.013 7.211\r
+0.032 6.111\r
+0.045 8.116\r
+0.056 7.717\r
+0.069 9.02\r
+0.078 12.122\r
+0.087 14.76\r
+0.106 13.832\r
+0.117 13.733\r
+0.125 12.636\r
+0.155 12.438\r
+0.168 11.836\r
+0.2 11.243\r
+0.209 11.737\r
+0.219 10.739\r
+0.266 9.846\r
+0.29 9.849\r
+0.299 8.949\r
+0.367 7.456\r
+0.393 7.159\r
+0.429 5.761\r
+0.487 4.567\r
+0.571 2.975\r
+0.607 2.178\r
+0.669 1.084\r
+0.708 0.489\r
+0.74 0\r
diff --git a/datafiles/thrustcurves/Apogee_C10.eng b/datafiles/thrustcurves/Apogee_C10.eng
new file mode 100644 (file)
index 0000000..416c051
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Apogee C10 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C10 18 50 4-7-10 0.0049 0.0176 Apogee \r
+0.01 2.712\r
+0.019 5.842\r
+0.029 17.116\r
+0.037 25.72\r
+0.051 22.535\r
+0.07 20.446\r
+0.106 18.983\r
+0.164 17.085\r
+0.188 17.085\r
+0.2 15.824\r
+0.216 16.036\r
+0.255 15.602\r
+0.293 14.35\r
+0.343 13.503\r
+0.394 12.655\r
+0.41 11.605\r
+0.434 11.605\r
+0.521 9.287\r
+0.631 6.34\r
+0.741 4.021\r
+0.851 2.119\r
+0.911 1.48\r
+0.945 1.264\r
+0.96 0\r
diff --git a/datafiles/thrustcurves/Apogee_C4.eng b/datafiles/thrustcurves/Apogee_C4.eng
new file mode 100644 (file)
index 0000000..af54909
--- /dev/null
@@ -0,0 +1,37 @@
+;\r
+;Apogee C4 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C4 18 50 3-5-7 0.0045 0.017 Apogee \r
+0.018 3.23\r
+0.041 6.874\r
+0.147 8.779\r
+0.294 10.683\r
+0.365 11.31\r
+0.388 10.521\r
+0.412 8.779\r
+0.441 7.04\r
+0.465 4.555\r
+0.529 3.479\r
+0.629 2.981\r
+0.653 3.23\r
+0.718 2.816\r
+0.853 2.733\r
+1.065 2.65\r
+1.253 2.567\r
+1.453 2.401\r
+1.694 2.484\r
+1.794 2.484\r
+1.812 2.733\r
+1.841 2.401\r
+1.947 2.401\r
+2.112 2.401\r
+2.235 2.401\r
+2.282 2.236\r
+2.312 1.656\r
+2.329 0.662\r
+2.35 0\r
diff --git a/datafiles/thrustcurves/Apogee_C6.eng b/datafiles/thrustcurves/Apogee_C6.eng
new file mode 100644 (file)
index 0000000..62cfe24
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Apogee C6 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C6 13 83 4-7-10 0.007 0.0151 Apogee \r
+0.008 13.958\r
+0.016 21.1\r
+0.022 15.511\r
+0.03 12.831\r
+0.052 14.8\r
+0.081 15.927\r
+0.092 14.658\r
+0.114 16.069\r
+0.125 14.658\r
+0.136 15.369\r
+0.168 14.8\r
+0.214 13.816\r
+0.225 12.973\r
+0.247 13.958\r
+0.252 12.831\r
+0.285 12.547\r
+0.307 12.405\r
+0.317 12.831\r
+0.328 11.562\r
+0.347 11.988\r
+0.393 11.42\r
+0.442 10.719\r
+0.464 11.136\r
+0.488 9.164\r
+0.545 8.459\r
+0.624 7.754\r
+0.716 6.485\r
+0.838 5.075\r
+0.977 3.102\r
+1.096 1.833\r
+1.207 0.986\r
+1.32 0\r
diff --git a/datafiles/thrustcurves/Apogee_D10.eng b/datafiles/thrustcurves/Apogee_D10.eng
new file mode 100644 (file)
index 0000000..80d4ff9
--- /dev/null
@@ -0,0 +1,41 @@
+;\r
+;Apogee D10 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D10 18 70 3-5-7 0.0098 0.0259 Apogee \r
+0.011 14.506\r
+0.018 25.13\r
+0.032 20.938\r
+0.079 19.065\r
+0.122 21.139\r
+0.136 19.686\r
+0.169 21.139\r
+0.201 20.728\r
+0.223 21.76\r
+0.233 20.938\r
+0.255 21.97\r
+0.276 20.938\r
+0.352 20.728\r
+0.402 20.107\r
+0.42 20.728\r
+0.459 20.107\r
+0.488 20.517\r
+0.556 18.243\r
+0.671 15.959\r
+0.707 14.717\r
+0.729 15.127\r
+0.779 12.853\r
+0.793 13.474\r
+0.836 11.401\r
+0.904 10.158\r
+0.926 10.569\r
+0.99 8.083\r
+1.026 8.498\r
+1.123 6.011\r
+1.231 2.487\r
+1.342 0.829\r
+1.4 0\r
diff --git a/datafiles/thrustcurves/Apogee_D3.eng b/datafiles/thrustcurves/Apogee_D3.eng
new file mode 100644 (file)
index 0000000..5c23b40
--- /dev/null
@@ -0,0 +1,27 @@
+;\r
+;Apogee D3 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D3 18 77 3-5-7 0.0098 0.0249 Apogee \r
+0.05 6.79\r
+0.168 8.788\r
+0.318 10.46\r
+0.385 10.07\r
+0.402 7.909\r
+0.469 5.432\r
+0.486 3.914\r
+0.687 3.115\r
+1.122 2.876\r
+2.06 2.636\r
+3.349 2.397\r
+4.639 2.156\r
+5.727 1.997\r
+6.163 1.837\r
+6.263 3.994\r
+6.347 2.317\r
+6.364 0.719\r
+6.39 0\r
diff --git a/datafiles/thrustcurves/Apogee_E6.eng b/datafiles/thrustcurves/Apogee_E6.eng
new file mode 100644 (file)
index 0000000..75d54c7
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;Aerotech E6 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+E6 24 70 2-4-6-8-100 0.0215 0.0463 Apogee \r
+0.056 18.59\r
+0.112 20.12\r
+0.168 17.575\r
+0.307 14.38\r
+0.531 10.45\r
+0.894 7.696\r
+1.146 6.244\r
+1.691 5.808\r
+2.836 5.663\r
+3.898 5.517\r
+4.275 5.227\r
+4.415 4.937\r
+5.058 5.082\r
+5.519 5.227\r
+5.603 6.679\r
+5.729 3.921\r
+5.882 2.323\r
+5.966 1.016\r
+6.06 0\r
diff --git a/datafiles/thrustcurves/Apogee_F10.eng b/datafiles/thrustcurves/Apogee_F10.eng
new file mode 100644 (file)
index 0000000..66c2154
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;Aerotech F10 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F10 29 93 4-6-8 0.0407 0.0841 Apogee \r
+0.015 28.22\r
+0.077 26.082\r
+0.201 24.934\r
+0.31 22.806\r
+0.464 20.183\r
+0.573 17.886\r
+0.789 16.075\r
+1.068 13.946\r
+1.393 12.63\r
+1.718 11.155\r
+2.166 9.844\r
+2.677 9.515\r
+3.311 9.187\r
+3.683 8.859\r
+3.791 9.679\r
+4.101 9.679\r
+4.658 9.515\r
+5.168 9.023\r
+5.725 9.023\r
+6.112 8.531\r
+6.329 8.859\r
+6.499 7.546\r
+6.685 5.742\r
+6.778 4.921\r
+6.917 2.625\r
+7.025 1.312\r
+7.13 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_G60.eng b/datafiles/thrustcurves/Cesaroni_G60.eng
new file mode 100644 (file)
index 0000000..d4218fa
--- /dev/null
@@ -0,0 +1,30 @@
+; Cesaroni G60\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+G60 38 125 0 0.077056 0.2016 CSR\r
+   0.043 65.216\r
+   0.130 74.906\r
+   0.218 84.596\r
+   0.305 83.963\r
+   0.393 81.982\r
+   0.480 81.956\r
+   0.568 81.138\r
+   0.655 80.530\r
+   0.743 79.923\r
+   0.830 78.867\r
+   0.918 76.675\r
+   1.005 75.118\r
+   1.094 73.732\r
+   1.182 71.315\r
+   1.270 68.781\r
+   1.357 66.853\r
+   1.445 65.111\r
+   1.532 63.526\r
+   1.620 61.229\r
+   1.707 59.249\r
+   1.795 57.110\r
+   1.882 51.671\r
+   1.970 18.562\r
+   2.057 2.667\r
+   2.146 1.470\r
+   2.234 0.000\r
diff --git a/datafiles/thrustcurves/Cesaroni_G69.eng b/datafiles/thrustcurves/Cesaroni_G69.eng
new file mode 100644 (file)
index 0000000..c5d5add
--- /dev/null
@@ -0,0 +1,25 @@
+; Pro38 G69\r
+G69 38 127 5-7-9-12 0.066500 0.2045 Pro38\r
+   0.079 79.935\r
+   0.103 72.367\r
+   0.136 82.989\r
+   0.217 85.910\r
+   0.247 82.193\r
+   0.311 84.317\r
+   0.352 80.201\r
+   0.387 82.856\r
+   0.840 72.499\r
+   0.944 72.234\r
+   0.978 69.047\r
+   1.017 71.835\r
+   1.082 67.852\r
+   1.227 66.657\r
+   1.237 65.329\r
+   1.493 62.010\r
+   1.530 59.354\r
+   1.591 60.151\r
+   1.714 56.167\r
+   1.769 54.574\r
+   1.848 46.607\r
+   1.887 27.884\r
+   1.958 00.000  \r
diff --git a/datafiles/thrustcurves/Cesaroni_G69_1.eng b/datafiles/thrustcurves/Cesaroni_G69_1.eng
new file mode 100644 (file)
index 0000000..06b0f47
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+133G69  38.0 127.00 3-5-7-9-12 0.08400 0.20510 CTI\r
+   0.06      80.77 \r
+   0.20      84.50 \r
+   0.41      82.74 \r
+   0.59      80.11 \r
+   0.81      76.82 \r
+   1.00      72.87 \r
+   1.20      68.92 \r
+   1.40      64.31 \r
+   1.61      59.48 \r
+   1.82      50.92 \r
+   1.90      21.40 \r
+   1.93       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_G79.eng b/datafiles/thrustcurves/Cesaroni_G79.eng
new file mode 100644 (file)
index 0000000..dbbb08d
--- /dev/null
@@ -0,0 +1,30 @@
+;\r
+; Pro38 G79SS\r
+G79SS 38 127 6-8-10-13 0.069000 0.2070 Pro38\r
+0.042 67.279\r
+0.050 72.145\r
+0.065 76.176\r
+0.072 76.176\r
+0.082 74.647\r
+0.094 68.252\r
+0.109 66.167\r
+0.122 65.611\r
+0.433 81.041\r
+0.633 88.130\r
+0.643 87.574\r
+0.684 89.659\r
+0.723 89.798\r
+0.834 92.162\r
+0.939 93.135\r
+1.000 93.969\r
+1.151 91.884\r
+1.160 90.772\r
+1.185 91.189\r
+1.303 86.879\r
+1.499 77.149\r
+1.518 75.064\r
+1.540 66.584\r
+1.587 23.631\r
+1.607 10.982\r
+1.629  4.865\r
+1.631  0.000\r
diff --git a/datafiles/thrustcurves/Cesaroni_G79_1.eng b/datafiles/thrustcurves/Cesaroni_G79_1.eng
new file mode 100644 (file)
index 0000000..21566ab
--- /dev/null
@@ -0,0 +1,27 @@
+;\r
+;\r
+G79SS  38.0 127.00 13-10-8-6-4 0.08500 0.22600 CTI\r
+   0.00       9.07 \r
+   0.03      54.45 \r
+   0.07      76.33 \r
+   0.09      70.95 \r
+   0.11      65.92 \r
+   0.17      68.59 \r
+   0.20      70.64 \r
+   0.30      74.89 \r
+   0.40      80.39 \r
+   0.50      83.76 \r
+   0.60      86.45 \r
+   0.70      88.65 \r
+   0.81      91.40 \r
+   0.90      93.06 \r
+   1.00      93.99 \r
+   1.10      95.81 \r
+   1.20      90.70 \r
+   1.30      86.92 \r
+   1.40      81.98 \r
+   1.50      76.54 \r
+   1.55      58.92 \r
+   1.60      16.41 \r
+   1.63       5.16 \r
+   1.63       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_H120.eng b/datafiles/thrustcurves/Cesaroni_H120.eng
new file mode 100644 (file)
index 0000000..6556837
--- /dev/null
@@ -0,0 +1,35 @@
+;Pro-38 Red Lightning 2 Grain reload\r
+H120-14A 38 186 14-11-9-7-5 0.1366 0.295 CTI \r
+0.016 53.023\r
+0.029 107.113\r
+0.036 124.55\r
+0.049 129.532\r
+0.062 117.789\r
+0.072 98.217\r
+0.131 123.838\r
+0.199 136.649\r
+0.258 144.122\r
+0.313 147.681\r
+0.369 146.257\r
+0.441 145.19\r
+0.558 143.411\r
+0.683 141.631\r
+0.777 140.92\r
+0.859 139.14\r
+0.98 136.293\r
+1.097 133.091\r
+1.251 128.82\r
+1.434 122.771\r
+1.558 118.856\r
+1.639 117.077\r
+1.731 117.077\r
+1.884 117.077\r
+1.927 105.334\r
+1.959 88.964\r
+1.995 68.325\r
+2.031 41.991\r
+2.083 18.505\r
+2.142 6.761\r
+2.181 2.135\r
+2.24 0\r
+;\r
diff --git a/datafiles/thrustcurves/Cesaroni_H143.eng b/datafiles/thrustcurves/Cesaroni_H143.eng
new file mode 100644 (file)
index 0000000..4edc131
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+H143SS  38.0 186.00 4-6-8-10-13 0.16540 0.34700 CTI\r
+   0.06     114.68 \r
+   0.19     134.25 \r
+   0.40     149.61 \r
+   0.60     158.10 \r
+   0.80     163.77 \r
+   1.00     167.00 \r
+   1.21     160.93 \r
+   1.40     148.80 \r
+   1.60     128.59 \r
+   1.63     117.26 \r
+   1.65     106.35 \r
+   1.70      35.63 \r
+   1.73       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_H153.eng b/datafiles/thrustcurves/Cesaroni_H153.eng
new file mode 100644 (file)
index 0000000..a95f09b
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+244H153  38.0 186.00 4-6-8-10-13 0.14390 0.30390 CTI\r
+   0.13     149.36 \r
+   0.17     173.70 \r
+   0.23     171.77 \r
+   0.39     179.91 \r
+   0.60     188.30 \r
+   0.81     180.40 \r
+   1.01     168.25 \r
+   1.18     160.91 \r
+   1.29     149.64 \r
+   1.41     136.95 \r
+   1.60     105.37 \r
+   1.69      23.58 \r
+   1.75       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_H565.eng b/datafiles/thrustcurves/Cesaroni_H565.eng
new file mode 100644 (file)
index 0000000..71609ac
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;\r
+H565 38 245 5-7-9-11-14 0.1742 0.3622 Cesaroni\r
+0.01 106.91\r
+0.02 479.76\r
+0.0347144 528.926\r
+0.0515118 553.719\r
+0.0761478 578.512\r
+0.100784 586.777\r
+0.150056 611.57\r
+0.2 607.1\r
+0.3 614.73\r
+0.4 616.58\r
+0.5 622.37\r
+0.51 645.28\r
+0.52 628.99\r
+0.53 535.19\r
+0.54 327.35\r
+0.55 147.98\r
+0.56 60.36\r
+0.57 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_I170.eng b/datafiles/thrustcurves/Cesaroni_I170.eng
new file mode 100644 (file)
index 0000000..0a59f76
--- /dev/null
@@ -0,0 +1,30 @@
+; Cesaroni I170\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I170 38 242 0 0.209216 0.404992 CSR\r
+   0.044 133.251\r
+   0.134 230.114\r
+   0.226 224.136\r
+   0.318 223.695\r
+   0.409 223.888\r
+   0.501 225.265\r
+   0.593 226.670\r
+   0.684 229.397\r
+   0.775 231.998\r
+   0.866 233.034\r
+   0.957 233.144\r
+   1.049 232.703\r
+   1.141 231.574\r
+   1.232 227.745\r
+   1.324 225.844\r
+   1.416 221.160\r
+   1.506 217.396\r
+   1.597 211.711\r
+   1.689 205.678\r
+   1.780 198.020\r
+   1.872 125.016\r
+   1.964 61.847\r
+   2.055 23.279\r
+   2.147 2.066\r
+   2.239 0.799\r
+   2.330 0.000\r
diff --git a/datafiles/thrustcurves/Cesaroni_I205.eng b/datafiles/thrustcurves/Cesaroni_I205.eng
new file mode 100644 (file)
index 0000000..baeddf0
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+384I205  38.0 245.00 5-7-9-11-14 0.20610 0.40220 CTI\r
+   0.10     181.50 \r
+   0.13     213.30 \r
+   0.20     210.10 \r
+   0.40     226.61 \r
+   0.60     235.80 \r
+   0.80     234.00 \r
+   1.00     232.80 \r
+   1.20     227.70 \r
+   1.40     216.80 \r
+   1.60     197.21 \r
+   1.74     183.13 \r
+   1.80      87.30 \r
+   1.88       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_I212.eng b/datafiles/thrustcurves/Cesaroni_I212.eng
new file mode 100644 (file)
index 0000000..4d4b40f
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+I212SS  38.0 245.00 5-7-9-11-14 0.24810 0.47500 CTI\r
+   0.04     189.66 \r
+   0.20     207.11 \r
+   0.40     222.41 \r
+   0.60     236.62 \r
+   0.80     249.60 \r
+   0.95     255.15 \r
+   1.01     250.22 \r
+   1.21     233.54 \r
+   1.40     208.99 \r
+   1.55     183.99 \r
+   1.60     168.08 \r
+   1.63     134.62 \r
+   1.69      25.86 \r
+   1.71       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_I240.eng b/datafiles/thrustcurves/Cesaroni_I240.eng
new file mode 100644 (file)
index 0000000..ad5799a
--- /dev/null
@@ -0,0 +1,30 @@
+; Cesaroni I240\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I240 38 302 0 0.274624 0.503552 CSR\r
+   0.043 265.317\r
+   0.131 320.903\r
+   0.221 314.148\r
+   0.310 312.413\r
+   0.399 313.564\r
+   0.488 314.335\r
+   0.577 321.117\r
+   0.667 325.923\r
+   0.755 327.040\r
+   0.844 326.831\r
+   0.933 324.348\r
+   1.023 321.063\r
+   1.111 317.446\r
+   1.200 308.301\r
+   1.290 300.612\r
+   1.379 293.536\r
+   1.468 283.358\r
+   1.556 273.832\r
+   1.646 259.708\r
+   1.735 190.662\r
+   1.824 124.130\r
+   1.912 60.875\r
+   2.002 26.967\r
+   2.092 7.636\r
+   2.181 2.296\r
+   2.271 0.000\r
diff --git a/datafiles/thrustcurves/Cesaroni_I285.eng b/datafiles/thrustcurves/Cesaroni_I285.eng
new file mode 100644 (file)
index 0000000..497f350
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+512I285  38.0 303.00 6-8-10-12-15 0.27240 0.50590 CTI\r
+   0.10     350.60 \r
+   0.15     318.73 \r
+   0.20     312.30 \r
+   0.40     322.37 \r
+   0.60     330.57 \r
+   0.80     329.40 \r
+   1.00     319.64 \r
+   1.20     294.14 \r
+   1.40     271.90 \r
+   1.60     239.90 \r
+   1.66     178.10 \r
+   1.80      44.80 \r
+   1.91       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_I287.eng b/datafiles/thrustcurves/Cesaroni_I287.eng
new file mode 100644 (file)
index 0000000..12556e5
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+I287SS  38.0 303.00 6-8-10-12-15 0.33080 0.60500 CTI\r
+   0.05     275.86 \r
+   0.20     292.53 \r
+   0.41     309.20 \r
+   0.61     327.53 \r
+   0.80     341.70 \r
+   0.90     344.20 \r
+   1.01     331.70 \r
+   1.20     311.70 \r
+   1.40     280.03 \r
+   1.53     245.02 \r
+   1.58     176.62 \r
+   1.60     141.76 \r
+   1.64      68.99 \r
+   1.70      17.48 \r
+   1.70       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_I350.eng b/datafiles/thrustcurves/Cesaroni_I350.eng
new file mode 100644 (file)
index 0000000..249406c
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+I350SS  38.0 367.00 7-9-11-13-16 0.41350 0.78200 CTI\r
+   0.05     399.74 \r
+   0.13     390.06 \r
+   0.19     386.19 \r
+   0.40     388.13 \r
+   0.60     388.13 \r
+   0.80     388.13 \r
+   1.00     389.91 \r
+   1.20     387.38 \r
+   1.33     368.77 \r
+   1.44     350.38 \r
+   1.52     320.37 \r
+   1.60     164.79 \r
+   1.68      36.77 \r
+   1.71       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_I360.eng b/datafiles/thrustcurves/Cesaroni_I360.eng
new file mode 100644 (file)
index 0000000..dc8e5ce
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+;\r
+I360 38 367 15 0.3346 0.5963 Cesaroni\r
+0.08 555.5\r
+0.1 489.7\r
+0.13 448\r
+0.2 449\r
+0.4 483.7\r
+0.55 498\r
+0.6 494.9\r
+0.7 481.91\r
+0.8 457.9\r
+1 406.6\r
+1.2 344.4\r
+1.3 309.3\r
+1.4 182.2\r
+1.55 158.9\r
+1.6 101.8\r
+1.7 55.8\r
+1.77 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_I540.eng b/datafiles/thrustcurves/Cesaroni_I540.eng
new file mode 100644 (file)
index 0000000..9c39ad0
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;\r
+I540WT 38 367 7-9-11-13-16 0.3288 0.5982 CTI \r
+0.03 597.86\r
+0.04 611.31\r
+0.06 605.64\r
+0.12 612.36\r
+0.24 624.54\r
+0.36 626\r
+0.48 623.63\r
+0.6 616.42\r
+0.72 598.14\r
+0.84 583.16\r
+0.95 568.92\r
+0.96 558.53\r
+0.98 533.45\r
+1.02 436.53\r
+1.06 303.15\r
+1.09 184.92\r
+1.13 74.27\r
+1.18 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_J210.eng b/datafiles/thrustcurves/Cesaroni_J210.eng
new file mode 100644 (file)
index 0000000..b9bde28
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+J210  54.0 236.00 6-16 0.08270 0.84200 CTI\r
+   0.04     335.00 \r
+   0.16     270.92 \r
+   0.41     269.30 \r
+   0.80     268.49 \r
+   1.18     256.32 \r
+   1.62     236.85 \r
+   2.03     214.14 \r
+   2.38     193.86 \r
+   2.79     174.39 \r
+   3.20     163.85 \r
+   3.60     157.36 \r
+   3.75     135.46 \r
+   3.86      85.17 \r
+   3.99       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J280.eng b/datafiles/thrustcurves/Cesaroni_J280.eng
new file mode 100644 (file)
index 0000000..dd8bf4f
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+J280SS  54.0 236.00 6-16 0.51200 0.95400 CTI\r
+   0.10     259.43 \r
+   0.30     278.91 \r
+   0.60     293.07 \r
+   0.90     306.85 \r
+   1.20     319.19 \r
+   1.50     321.10 \r
+   1.80     310.85 \r
+   2.11     279.89 \r
+   2.35     286.70 \r
+   2.40     269.17 \r
+   2.44     178.24 \r
+   2.49      42.80 \r
+   2.54       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J285.eng b/datafiles/thrustcurves/Cesaroni_J285.eng
new file mode 100644 (file)
index 0000000..a9c783e
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+J285  38.0 367.00 6-8-10-12-15 0.31250 0.59500 CTI\r
+   0.06     351.01 \r
+   0.15     346.01 \r
+   0.25     357.64 \r
+   0.50     363.90 \r
+   0.75     369.26 \r
+   1.03     343.33 \r
+   1.27     337.07 \r
+   1.51     317.40 \r
+   1.75     282.53 \r
+   1.93     127.86 \r
+   2.02      84.94 \r
+   2.25      11.02 \r
+   2.26       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J295.eng b/datafiles/thrustcurves/Cesaroni_J295.eng
new file mode 100644 (file)
index 0000000..922671b
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+J295  54.0 329.00 6-16 0.59400 1.11900 CTI\r
+   0.04     450.52 \r
+   0.28     428.70 \r
+   0.54     423.25 \r
+   1.00     391.61 \r
+   1.48     352.34 \r
+   1.99     304.35 \r
+   2.51     266.17 \r
+   3.00     243.26 \r
+   3.50     216.92 \r
+   3.67     126.54 \r
+   3.82      64.36 \r
+   4.00       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J300.eng b/datafiles/thrustcurves/Cesaroni_J300.eng
new file mode 100644 (file)
index 0000000..601ee22
--- /dev/null
@@ -0,0 +1,30 @@
+; Cesaroni J300\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J300 38 360 0 0.340032 0.606592 CSR\r
+   0.043 357.026\r
+   0.131 436.586\r
+   0.221 407.925\r
+   0.310 399.528\r
+   0.400 400.588\r
+   0.490 406.733\r
+   0.578 414.302\r
+   0.667 417.117\r
+   0.756 418.415\r
+   0.846 421.302\r
+   0.935 422.229\r
+   1.025 415.951\r
+   1.114 406.356\r
+   1.202 395.237\r
+   1.292 381.728\r
+   1.381 369.861\r
+   1.471 355.451\r
+   1.560 331.691\r
+   1.649 246.243\r
+   1.738 161.766\r
+   1.827 109.478\r
+   1.917 71.413\r
+   2.006 37.058\r
+   2.096 13.880\r
+   2.185 5.059\r
+   2.275 0.000\r
diff --git a/datafiles/thrustcurves/Cesaroni_J330.eng b/datafiles/thrustcurves/Cesaroni_J330.eng
new file mode 100644 (file)
index 0000000..c1e8e5e
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+J330  38.0 421.00 7-9-11-13-16 0.37500 0.70200 CTI\r
+   0.05     459.32 \r
+   0.16     448.20 \r
+   0.27     440.41 \r
+   0.51     437.08 \r
+   0.75     427.07 \r
+   1.02     412.61 \r
+   1.26     387.03 \r
+   1.50     360.34 \r
+   1.69     321.41 \r
+   1.79     300.28 \r
+   1.91     126.79 \r
+   1.99     107.88 \r
+   2.23      22.56 \r
+   2.26       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J360.eng b/datafiles/thrustcurves/Cesaroni_J360.eng
new file mode 100644 (file)
index 0000000..acc545d
--- /dev/null
@@ -0,0 +1,30 @@
+; Cesaroni J360\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J360 38 419 0 0.409024 0.709184 CSR\r
+   0.041 618.905\r
+   0.124 616.584\r
+   0.207 563.785\r
+   0.291 557.730\r
+   0.374 558.409\r
+   0.457 562.088\r
+   0.541 561.267\r
+   0.624 563.219\r
+   0.708 565.328\r
+   0.793 566.558\r
+   0.876 549.383\r
+   0.959 529.633\r
+   1.043 511.099\r
+   1.126 483.285\r
+   1.209 445.397\r
+   1.293 421.658\r
+   1.377 378.330\r
+   1.461 261.647\r
+   1.545 197.445\r
+   1.628 146.570\r
+   1.711 101.807\r
+   1.795 78.039\r
+   1.878 47.847\r
+   1.961 31.861\r
+   2.046 9.220\r
+   2.130 0.000\r
diff --git a/datafiles/thrustcurves/Cesaroni_J380.eng b/datafiles/thrustcurves/Cesaroni_J380.eng
new file mode 100644 (file)
index 0000000..ebd8eff
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+J380SS  54.0 320.00 6-16 0.76900 1.29330 CTI\r
+   0.05     368.48 \r
+   0.30     348.31 \r
+   0.60     378.83 \r
+   0.90     400.93 \r
+   1.20     419.52 \r
+   1.50     433.09 \r
+   1.80     434.60 \r
+   2.10     408.81 \r
+   2.40     369.92 \r
+   2.56     410.58 \r
+   2.59     297.80 \r
+   2.71      45.25 \r
+   2.73       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J400.eng b/datafiles/thrustcurves/Cesaroni_J400.eng
new file mode 100644 (file)
index 0000000..ab756c1
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+J400SS  38.0 421.00 7-9-11-13-16 0.48960 0.70200 CTI\r
+   0.05     451.79 \r
+   0.20     461.14 \r
+   0.31     465.81 \r
+   0.44     463.47 \r
+   0.60     477.48 \r
+   0.80     482.15 \r
+   1.00     461.31 \r
+   1.20     433.12 \r
+   1.35     402.76 \r
+   1.40     382.92 \r
+   1.47     321.04 \r
+   1.55     258.00 \r
+   1.60     178.62 \r
+   1.73      14.58 \r
+   1.75       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_J410.eng b/datafiles/thrustcurves/Cesaroni_J410.eng
new file mode 100644 (file)
index 0000000..e176b03
--- /dev/null
@@ -0,0 +1,23 @@
+; Pro38 Red Lightning 6G.\r
+J410-16A 38 421 16 0.4098 0.735 CTI\r
+   0.023 375.45\r
+   0.029 446.221\r
+   0.042 510.996\r
+   0.11 499.0\r
+   0.22 495.402\r
+   0.442 491.803\r
+   0.675 475.01\r
+   0.901 464.214\r
+   1.092 448.62\r
+   1.221 437.825\r
+   1.34 429.428\r
+   1.492 419.832\r
+   1.553 389.844\r
+   1.592 349.06\r
+   1.65 273.491\r
+   1.689 196.721\r
+   1.75 122.351\r
+   1.809 64.774\r
+   1.889 28.788\r
+   1.941 13.195\r
+   1.989 0.0\r
diff --git a/datafiles/thrustcurves/Cesaroni_K445.eng b/datafiles/thrustcurves/Cesaroni_K445.eng
new file mode 100644 (file)
index 0000000..366ef5b
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+K445  54.0 404.00 7-17 0.79200 1.39800 CTI\r
+   0.05     664.83 \r
+   0.19     640.68 \r
+   0.48     622.98 \r
+   1.00     576.29 \r
+   1.51     515.12 \r
+   2.00     442.68 \r
+   2.50     392.26 \r
+   3.02     350.93 \r
+   3.13     339.66 \r
+   3.31     210.88 \r
+   3.47      78.88 \r
+   3.67       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_K510.eng b/datafiles/thrustcurves/Cesaroni_K510.eng
new file mode 100644 (file)
index 0000000..c673b79
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;   Cesaroni Pro75 2486K510 \r
+;         'Classic Propellant'\r
+;\r
+;      RockSim file by Kathy Miller\r
+;          wRasp Adaptation by Len Lekx\r
+;\r
+K510   75      350     0       1.19    2.59    CTI\r
+0.10     645.25 \r
+0.30     689.75 \r
+0.50     658.60 \r
+1.00     636.35 \r
+1.60     600.75 \r
+2.00     565.15 \r
+2.40     534.00 \r
+2.50     525.10 \r
+3.00     471.70 \r
+3.50     422.75 \r
+3.70     400.50 \r
+4.00     391.60 \r
+4.40     382.70 \r
+4.50     378.25 \r
+4.60     333.75 \r
+4.70      66.75 \r
+4.84       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_K510_1.eng b/datafiles/thrustcurves/Cesaroni_K510_1.eng
new file mode 100644 (file)
index 0000000..ca5e64d
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;\r
+K510  75.0 350.00 0 1.19700 2.59000 CTI\r
+   0.04     394.38 \r
+   0.07     617.68 \r
+   0.10     645.17 \r
+   0.21     658.16 \r
+   0.35     669.23 \r
+   0.53     667.72 \r
+   0.82     661.58 \r
+   1.18     626.92 \r
+   1.72     588.46 \r
+   2.15     557.69 \r
+   2.39     542.31 \r
+   2.90     492.86 \r
+   3.07     470.31 \r
+   3.56     426.81 \r
+   3.98     398.96 \r
+   4.32     393.98 \r
+   4.48     380.63 \r
+   4.60     364.22 \r
+   4.65     290.91 \r
+   4.80      91.23 \r
+   4.84      45.82 \r
+   4.84       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_K530.eng b/datafiles/thrustcurves/Cesaroni_K530.eng
new file mode 100644 (file)
index 0000000..df2132f
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+K530SS  54.0 404.00 6-16 1.02500 1.63980 CTI\r
+   0.05     533.59 \r
+   0.09     503.98 \r
+   0.29     514.09 \r
+   0.60     534.31 \r
+   0.90     557.41 \r
+   1.20     577.63 \r
+   1.50     587.74 \r
+   1.80     596.40 \r
+   2.10     535.44 \r
+   2.31     502.54 \r
+   2.47     551.63 \r
+   2.56     393.94 \r
+   2.60     274.37 \r
+   2.64     137.19 \r
+   2.67       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_K570.eng b/datafiles/thrustcurves/Cesaroni_K570.eng
new file mode 100644 (file)
index 0000000..0b41307
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+K570  54.0 488.00 7-17 0.99000 1.68500 CTI\r
+   0.04     892.67 \r
+   0.50     797.99 \r
+   1.00     738.68 \r
+   1.50     659.37 \r
+   2.00     585.96 \r
+   2.50     512.88 \r
+   2.97     417.16 \r
+   3.20     224.79 \r
+   3.47      67.00 \r
+   3.59       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_K575.eng b/datafiles/thrustcurves/Cesaroni_K575.eng
new file mode 100644 (file)
index 0000000..9beb7b4
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;\r
+K575SS 75 395 1000 1.803 3.143 Cesaroni\r
+0 16\r
+0.11 664.5\r
+0.43 620.2\r
+0.87 629\r
+1.3 637.92\r
+1.73 637.92\r
+2.17 629\r
+2.6 615.77\r
+3.03 553.75\r
+3.47 518.31\r
+3.9 438.57\r
+4.18 79.74\r
+4.33 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_K650.eng b/datafiles/thrustcurves/Cesaroni_K650.eng
new file mode 100644 (file)
index 0000000..8d9f29d
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+K650SS  54.0 488.00 6-16 1.28100 1.98990 CTI\r
+   0.04     664.52 \r
+   0.12     645.90 \r
+   0.31     642.24 \r
+   0.60     664.78 \r
+   0.91     684.59 \r
+   1.22     712.82 \r
+   1.50     723.41 \r
+   1.80     728.70 \r
+   2.10     664.52 \r
+   2.40     614.68 \r
+   2.51     680.53 \r
+   2.55     534.62 \r
+   2.61     268.19 \r
+   2.66       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_K660.eng b/datafiles/thrustcurves/Cesaroni_K660.eng
new file mode 100644 (file)
index 0000000..41caa36
--- /dev/null
@@ -0,0 +1,19 @@
+;\r
+;\r
+K660  54.0 572.00 7-17 1.17700 1.94900 CTI\r
+   0.07    1078.90 \r
+   0.23    1006.47 \r
+   0.40     966.76 \r
+   0.80     897.52 \r
+   1.20     842.72 \r
+   1.60     794.15 \r
+   2.01     744.52 \r
+   2.40     692.27 \r
+   2.54     671.37 \r
+   2.68     439.08 \r
+   2.80     400.68 \r
+   3.01     386.90 \r
+   3.20     234.31 \r
+   3.45     106.65 \r
+   3.60      44.03 \r
+   3.69       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_L1090.eng b/datafiles/thrustcurves/Cesaroni_L1090.eng
new file mode 100644 (file)
index 0000000..391d406
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+L1090SS 75 665 1000 3.491 5.461 Cesaroni\r
+0 487.3\r
+0.11 1639.1\r
+0.22 1484.05\r
+0.44 1417.6\r
+0.87 1373.3\r
+1.31 1329\r
+1.74 1306.85\r
+2.18 1262.55\r
+2.61 1218.25\r
+3.05 1151.8\r
+3.21 775.25\r
+3.48 598.05\r
+3.92 553.75\r
+4.13 221.5\r
+4.35 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_L1115.eng b/datafiles/thrustcurves/Cesaroni_L1115.eng
new file mode 100644 (file)
index 0000000..1914f38
--- /dev/null
@@ -0,0 +1,23 @@
+;\r
+;   Cesaroni Pro75 5015L1115\r
+;         'Classic Propellant'\r
+;\r
+;      RockSim file by Kathy Miller\r
+;          wRasp Adaptation by Len Lekx\r
+;\r
+L1115  75      621     0       2.39    4.40    CTI\r
+0.10    1468.85 \r
+0.30    1490.75 \r
+0.80    1401.75 \r
+1.00    1437.35 \r
+1.50    1335.00 \r
+2.00    1268.25 \r
+2.20    1246.00 \r
+2.50    1112.50 \r
+3.00    1090.25 \r
+3.30     979.00 \r
+3.80     979.00 \r
+4.00     623.00 \r
+4.20     311.50 \r
+4.40      35.00 \r
+4.48       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_L1115_1.eng b/datafiles/thrustcurves/Cesaroni_L1115_1.eng
new file mode 100644 (file)
index 0000000..b02a524
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;\r
+L1115  75.0 621.00 0 2.39400 4.40400 CTI\r
+   0.01      45.46 \r
+   0.01     522.52 \r
+   0.01     984.04 \r
+   0.04    1256.10 \r
+   0.05    1389.85 \r
+   0.08    1713.25 \r
+   0.24    1515.65 \r
+   0.30    1474.74 \r
+   0.40    1443.28 \r
+   0.42    1446.25 \r
+   0.50    1430.02 \r
+   0.76    1392.85 \r
+   1.00    1361.70 \r
+   1.28    1339.45 \r
+   1.84    1259.35 \r
+   2.25    1201.50 \r
+   3.00    1076.11 \r
+   3.50     990.86 \r
+   3.92     832.85 \r
+   4.00     607.78 \r
+   4.10     434.99 \r
+   4.22     288.73 \r
+   4.38     156.49 \r
+   4.48      86.39 \r
+   5.00       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_L610.eng b/datafiles/thrustcurves/Cesaroni_L610.eng
new file mode 100644 (file)
index 0000000..5cd2843
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;\r
+L610 98 394 0 2.415 4.975 CTI \r
+0.06 262.5\r
+0.12 667.2\r
+0.25 929.7\r
+0.39 871.21\r
+0.65 849.83\r
+1.05 823.1\r
+1.5 785.69\r
+2 747.3\r
+2.5 707.3\r
+3 667.2\r
+3.48 641.38\r
+4 593.28\r
+4.47 561.21\r
+5 523.79\r
+5.44 502.41\r
+5.68 491.72\r
+6 475.69\r
+6.5 459.66\r
+7.01 443.62\r
+7.5 413.7\r
+8 284.7\r
+8.12 53.3\r
+8.13 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_L730.eng b/datafiles/thrustcurves/Cesaroni_L730.eng
new file mode 100644 (file)
index 0000000..33f1245
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;\r
+L730  54.0 649.00 0 1.35100 2.24700 CTI\r
+   0.00      81.36 \r
+   0.01    1079.71 \r
+   0.02    1216.59 \r
+   0.04    1154.68 \r
+   0.20    1127.51 \r
+   0.45    1055.11 \r
+   0.60    1028.17 \r
+   0.75     995.24 \r
+   1.00     959.33 \r
+   1.50     898.71 \r
+   2.00     830.70 \r
+   2.50     730.76 \r
+   2.60     592.55 \r
+   2.70     510.96 \r
+   2.90     487.88 \r
+   3.00     405.72 \r
+   3.10     299.80 \r
+   3.20     296.09 \r
+   3.30     251.85 \r
+   3.40     171.70 \r
+   3.50     165.26 \r
+   3.60     139.38 \r
+   3.65     117.77 \r
+   3.77      45.38 \r
+   3.77       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_L800.eng b/datafiles/thrustcurves/Cesaroni_L800.eng
new file mode 100644 (file)
index 0000000..9795533
--- /dev/null
@@ -0,0 +1,24 @@
+;\r
+;   Cesaroni Pro75 3757L800\r
+;         'Classic Propellant'\r
+;\r
+;      RockSim file by Kathy Miller\r
+;          wRasp Adaptation by Len Lekx\r
+;\r
+L800   75      486     0       1.79    3.51    CTI\r
+0.10    1023.50 \r
+0.20    1005.70 \r
+0.30    1023.50 \r
+0.50    1014.60 \r
+1.00    1010.15 \r
+1.50    1001.25 \r
+2.00     956.75 \r
+2.40     890.00 \r
+2.50     845.50 \r
+3.00     756.50 \r
+3.50     689.75 \r
+3.70     667.50 \r
+3.90     654.15 \r
+4.00     623.00 \r
+4.60     111.25 \r
+4.67       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_L800_1.eng b/datafiles/thrustcurves/Cesaroni_L800_1.eng
new file mode 100644 (file)
index 0000000..0ed5737
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;\r
+L800  75.0 486.00 0 1.79500 3.51100 CTI\r
+   0.00      27.28 \r
+   0.01     402.41 \r
+   0.01    1285.54 \r
+   0.12    1056.51 \r
+   0.26    1041.73 \r
+   0.71    1026.95 \r
+   1.28     998.38 \r
+   2.05     901.36 \r
+   2.41     849.64 \r
+   2.83     763.51 \r
+   3.25     707.06 \r
+   3.65     655.14 \r
+   3.80     651.74 \r
+   4.00     624.07 \r
+   4.10     601.34 \r
+   4.19     536.17 \r
+   4.31     415.67 \r
+   4.41     270.17 \r
+   4.52     140.20 \r
+   4.60      76.92 \r
+   4.65      54.94 \r
+   4.67      40.16 \r
+   5.00       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_L890.eng b/datafiles/thrustcurves/Cesaroni_L890.eng
new file mode 100644 (file)
index 0000000..5289d09
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+L890SS 75 530 1000 2.671 4.346 Cesaroni\r
+0 20\r
+0.05 1151.8\r
+0.41 1054.34\r
+0.83 1045.48\r
+1.24 1036.62\r
+1.65 1027.76\r
+2.07 1018.9\r
+2.89 886\r
+3.31 775.25\r
+3.72 664.5\r
+3.98 177.2\r
+4.13 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_M1060.eng b/datafiles/thrustcurves/Cesaroni_M1060.eng
new file mode 100644 (file)
index 0000000..b6d6762
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;\r
+M1060 98 548 0 3.622 6.673 CTI \r
+0.07 131\r
+0.1 594\r
+0.2 1453\r
+0.238 1494\r
+0.378 1450\r
+0.378 1425\r
+0.5 1423\r
+1 1462\r
+1.5 1456\r
+2 1430\r
+2.5 1376\r
+3 1280\r
+3.5 1190\r
+4 1051\r
+4.5 976\r
+5 883\r
+5.5 835\r
+6 793\r
+6.5 321\r
+7 13\r
+7.229 7\r
+7.23 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_M1400.eng b/datafiles/thrustcurves/Cesaroni_M1400.eng
new file mode 100644 (file)
index 0000000..b6a75d0
--- /dev/null
@@ -0,0 +1,22 @@
+;\r
+;   Cesaroni Pro75 6251M1400\r
+;         'Classic Propellant'\r
+;\r
+;      RockSim file by Kathy Miller\r
+;          wRasp Adaptation by Len Lekx\r
+;\r
+M1400  75      7570    0       2.99    5.30    CTI\r
+0.10    1993.60 \r
+0.50    1891.25 \r
+1.10    1780.00 \r
+1.50    1691.00 \r
+2.00    1602.00 \r
+2.30    1557.50 \r
+2.50    1513.00 \r
+3.00    1335.00 \r
+3.50    1223.75 \r
+3.70    1112.00 \r
+3.90     667.50 \r
+4.00     534.00 \r
+4.40     222.50 \r
+4.47       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_M1400_1.eng b/datafiles/thrustcurves/Cesaroni_M1400_1.eng
new file mode 100644 (file)
index 0000000..de7f4c8
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;\r
+M1400  75.0 757.00 0 2.99200 5.30200 CTI\r
+   0.02     991.61 \r
+   0.07    1939.66 \r
+   0.11    2291.75 \r
+   0.14    1976.39 \r
+   0.19    1962.48 \r
+   0.29    1936.13 \r
+   0.52    1881.02 \r
+   0.75    1833.40 \r
+   1.00    1778.08 \r
+   1.25    1738.57 \r
+   1.70    1654.82 \r
+   2.40    1502.39 \r
+   2.85    1389.48 \r
+   3.25    1283.00 \r
+   3.40    1232.23 \r
+   3.53    1199.64 \r
+   3.65    1083.69 \r
+   3.70     909.39 \r
+   3.90     641.50 \r
+   4.00     502.82 \r
+   4.03     463.03 \r
+   4.22     336.09 \r
+   4.43     138.68 \r
+   4.47      93.21 \r
+   5.00       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_M1450.eng b/datafiles/thrustcurves/Cesaroni_M1450.eng
new file mode 100644 (file)
index 0000000..e43f4d5
--- /dev/null
@@ -0,0 +1,24 @@
+;\r
+;\r
+M1450 98 702 0 4.83 8.578 CTI \r
+0.01 60\r
+0.06 524\r
+0.1 2164\r
+0.151 2416\r
+0.25 2162\r
+0.5 2037\r
+0.75 2022\r
+1 2009\r
+1.5 2006\r
+2 1968\r
+2.5 1895\r
+3 1770\r
+3.5 1673\r
+4 1517\r
+4.5 1337\r
+5 1166\r
+5.5 954\r
+5.8 687\r
+6.2 360\r
+6.86 79\r
+6.87 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_M2505.eng b/datafiles/thrustcurves/Cesaroni_M2505.eng
new file mode 100644 (file)
index 0000000..5d64794
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+M2505  98.0 548.00 0 3.42300 6.25800 CTI\r
+   0.12    2600.00 \r
+   0.21    2482.00 \r
+   0.60    2715.00 \r
+   0.90    2876.00 \r
+   1.20    2938.00 \r
+   1.50    2889.00 \r
+   1.80    2785.00 \r
+   2.10    2573.00 \r
+   2.40    2349.00 \r
+   2.70    2182.00 \r
+   3.00      85.00 \r
+   3.00       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_M520.eng b/datafiles/thrustcurves/Cesaroni_M520.eng
new file mode 100644 (file)
index 0000000..6b8e663
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;\r
+M520  98.0 548.00 0 3.71300 6.69300 CTI\r
+   0.01    1077.00 \r
+   0.25    1062.83 \r
+   0.38    1065.66 \r
+   0.50     971.00 \r
+   0.71     938.12 \r
+   0.93     915.45 \r
+   1.23     878.61 \r
+   2.07     906.95 \r
+   2.61     901.28 \r
+   3.03     892.78 \r
+   3.50     872.94 \r
+   3.93     836.09 \r
+   4.96     756.73 \r
+   6.08     657.54 \r
+   7.05     549.84 \r
+   7.79     461.98 \r
+   8.39     391.12 \r
+   9.06     323.10 \r
+  10.01     243.74 \r
+  11.01     172.89 \r
+  12.00     116.20 \r
+  13.95       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_M795.eng b/datafiles/thrustcurves/Cesaroni_M795.eng
new file mode 100644 (file)
index 0000000..3a11fac
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;\r
+M795 98 702 0 4.892 8.492 CTI \r
+0.15 612.314\r
+0.21 1532.76\r
+0.245 1722\r
+0.43 1717.66\r
+0.5 1542.85\r
+0.62 1430.02\r
+0.8 1389.71\r
+1 1374.27\r
+1.5 1338.9\r
+2 1305.38\r
+3 1271.81\r
+4 1204\r
+5 1078\r
+6 928\r
+7 743\r
+8 563\r
+9 424.898\r
+10 299.697\r
+11 196.164\r
+12 116.759\r
+12.7 65.434\r
+12.76 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_N1100.eng b/datafiles/thrustcurves/Cesaroni_N1100.eng
new file mode 100644 (file)
index 0000000..119bc59
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+N1100 98 1010 0 4.517 11.644 CTI \r
+0.16 2624\r
+0.33 2708\r
+0.91 2055\r
+1.22 1896\r
+2.44 1793\r
+3.66 1625\r
+4.88 1402\r
+6.12 1158\r
+7.41 854\r
+9.77 494\r
+12.18 111.2\r
+12.19 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_N2500.eng b/datafiles/thrustcurves/Cesaroni_N2500.eng
new file mode 100644 (file)
index 0000000..59c1425
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;\r
+N2500  98.0 1010.00 0 6.77800 11.66800 CTI\r
+   0.02     773.70 \r
+   0.05    3356.60 \r
+   0.06    3657.80 \r
+   0.10    3546.80 \r
+   0.25    3403.80 \r
+   0.40    3309.20 \r
+   0.80    3262.50 \r
+   1.00    3206.10 \r
+   1.50    3088.50 \r
+   2.00    2940.40 \r
+   2.50    2792.60 \r
+   3.00    2598.40 \r
+   3.50    2402.50 \r
+   4.00    2227.00 \r
+   4.25    2152.50 \r
+   4.40    2102.50 \r
+   4.50    2007.00 \r
+   4.60    1683.80 \r
+   4.75    1269.50 \r
+   5.00     767.30 \r
+   5.41     341.30 \r
+   5.42       0.00 \r
diff --git a/datafiles/thrustcurves/Cesaroni_O5100.eng b/datafiles/thrustcurves/Cesaroni_O5100.eng
new file mode 100644 (file)
index 0000000..6ed9444
--- /dev/null
@@ -0,0 +1,44 @@
+;\r
+;Cesaroni Technologies Inc Motor Data File\r
+;Composed by Carl Tulanko for 150mm "O" CAR Certed Motor\r
+;24-Jun-2003 using CTI Cert graph to chart points\r
+O5100 150 803 1000 13.245 23.577 Cesaroni\r
+0.01 815.07\r
+0.02 1407.85\r
+0.03 2334.11\r
+0.04 3260.42\r
+0.05 4001.47\r
+0.07 4927.78\r
+0.07 5483.57\r
+0.09 5817.04\r
+0.13 6057.88\r
+0.2 6206.09\r
+0.3 6298.72\r
+0.43 6280.19\r
+0.6 6261.67\r
+0.78 6298.72\r
+0.97 6354.3\r
+1.05 6428.4\r
+1.12 6391.35\r
+1.34 6465.46\r
+1.49 6502.51\r
+1.75 6539.56\r
+1.88 6558.09\r
+2.16 6521.03\r
+2.36 6465.46\r
+2.58 6372.82\r
+2.96 6113.46\r
+3.56 5557.67\r
+4.13 4909.25\r
+4.72 4260.83\r
+4.83 4149.68\r
+4.93 3038.1\r
+5 2612\r
+5.1 2111.79\r
+5.23 1741.29\r
+5.32 1537.53\r
+5.52 1222.61\r
+5.8 907.69\r
+5.85 666.88\r
+5.89 333.44\r
+5.9 0\r
diff --git a/datafiles/thrustcurves/Cesaroni_O5800.eng b/datafiles/thrustcurves/Cesaroni_O5800.eng
new file mode 100644 (file)
index 0000000..d1f8e40
--- /dev/null
@@ -0,0 +1,33 @@
+; Pro 150 O5800 White Thunder\r
+O5800 150 754 P 13.950000000000001 26.368000000000002 CTI\r
+   0.069 6337.621\r
+   0.103 5700.965\r
+   0.218 5874.598\r
+   0.378 6135.048\r
+   0.561 6337.621\r
+   0.745 6221.865\r
+   0.985 6221.865\r
+   1.18 6192.926\r
+   1.455 6308.682\r
+   1.753 6366.559\r
+   1.994 6337.621\r
+   2.269 6395.498\r
+   2.509 6308.682\r
+   2.83 6192.926\r
+   3.14 6048.232\r
+   3.426 5874.598\r
+   3.69 5729.904\r
+   3.965 5585.209\r
+   4.263 5382.637\r
+   4.572 5295.82\r
+   4.939 5180.064\r
+   5.053 5035.37\r
+   5.11 4717.042\r
+   5.133 4225.08\r
+   5.145 3675.241\r
+   5.156 3038.585\r
+   5.179 2344.051\r
+   5.214 1475.884\r
+   5.259 607.717\r
+   5.294 57.878\r
+   5.295 0.0\r
diff --git a/datafiles/thrustcurves/Cesaroni_O8000.eng b/datafiles/thrustcurves/Cesaroni_O8000.eng
new file mode 100644 (file)
index 0000000..6031e41
--- /dev/null
@@ -0,0 +1,34 @@
+; Pro 150 O8000 White Thunder\r
+O8000 150 957 P 18.61 32.672000000000004 CTI\r
+   0.045 3964.63\r
+   0.046 6742.765\r
+   0.047 8623.794\r
+   0.125 7929.26\r
+   0.239 8160.772\r
+   0.364 8392.283\r
+   0.489 8508.039\r
+   0.614 8536.977\r
+   0.773 8392.283\r
+   0.989 8421.222\r
+   1.273 8479.1\r
+   1.602 8623.794\r
+   2.011 8565.916\r
+   2.33 8565.916\r
+   2.682 8479.1\r
+   3.102 8276.527\r
+   3.568 8045.016\r
+   3.886 7900.322\r
+   4.239 7668.81\r
+   4.591 7524.116\r
+   4.739 7524.116\r
+   4.909 7263.666\r
+   4.955 7003.215\r
+   4.977 6540.193\r
+   4.989 5845.659\r
+   5.0 5006.431\r
+   5.023 4051.447\r
+   5.034 3067.524\r
+   5.045 1996.785\r
+   5.08 1012.862\r
+   5.114 318.328\r
+   5.17 0.0\r
diff --git a/datafiles/thrustcurves/Contrail_G100.eng b/datafiles/thrustcurves/Contrail_G100.eng
new file mode 100644 (file)
index 0000000..27625ae
--- /dev/null
@@ -0,0 +1,7 @@
+;\r
+G100 38 406 0 0.093 0.511 Contrail_Rockets \r
+0 182.756\r
+0.199105 177.584\r
+0.606264 132.757\r
+0.986577 53.4476\r
+1.43 0\r
diff --git a/datafiles/thrustcurves/Contrail_G123.eng b/datafiles/thrustcurves/Contrail_G123.eng
new file mode 100644 (file)
index 0000000..33a0249
--- /dev/null
@@ -0,0 +1,9 @@
+;\r
+;\r
+G123 38 406 0 0.083 0.511 Contrail_Rockets \r
+0.00223714 217.239\r
+0.00671141 399.995\r
+0.0201342 220.687\r
+0.914989 72.4129\r
+0.955257 37.9306\r
+1.15 0\r
diff --git a/datafiles/thrustcurves/Contrail_G130.eng b/datafiles/thrustcurves/Contrail_G130.eng
new file mode 100644 (file)
index 0000000..5b4f418
--- /dev/null
@@ -0,0 +1,8 @@
+;\r
+;\r
+G130 38 406 0 0.093 0.516 Contrail_Rockets \r
+0 662.061\r
+0.0145414 448.27\r
+0.0234899 241.376\r
+0.642058 41.3788\r
+0.86 0\r
diff --git a/datafiles/thrustcurves/Contrail_G234.eng b/datafiles/thrustcurves/Contrail_G234.eng
new file mode 100644 (file)
index 0000000..e363596
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;G-234-HP Reload\r
+;38mm/16 Inch Hardware\r
+;Fast Nozzle\r
+G234 38 406.4 0 0.498 0.544 Contrail_Rockets \r
+0.00169492 245.419\r
+0.0973154 540.63\r
+0.183445 526.943\r
+0.202461 191.616\r
+0.237136 143.712\r
+0.260626 136.868\r
+0.533 0\r
diff --git a/datafiles/thrustcurves/Contrail_G300.eng b/datafiles/thrustcurves/Contrail_G300.eng
new file mode 100644 (file)
index 0000000..b11822e
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;G-300 PVC Motor for 38mm/16 Inch Case.\r
+;Motor uses Fast Nozzle\r
+;90cc's of Nitrous Oxide Used\r
+G300 38 406.4 0 0.023 0.544 Contrail_Rockets \r
+0.00111857 602.221\r
+0.0497763 814.367\r
+0.100671 670.655\r
+0.114094 266.893\r
+0.158837 239.52\r
+0.25 0\r
diff --git a/datafiles/thrustcurves/Contrail_H121.eng b/datafiles/thrustcurves/Contrail_H121.eng
new file mode 100644 (file)
index 0000000..2d1b380
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;\r
+H121 38 516 0 0.11 0.612 Contrail_Rockets \r
+0.00223714 251.721\r
+0.0402685 265.514\r
+0.0738255 203.446\r
+0.400447 179.308\r
+0.60179 134.481\r
+1.08949 127.585\r
+1.40268 93.1023\r
+1.61969 37.9306\r
+1.85 0\r
diff --git a/datafiles/thrustcurves/Contrail_H141.eng b/datafiles/thrustcurves/Contrail_H141.eng
new file mode 100644 (file)
index 0000000..cabed3b
--- /dev/null
@@ -0,0 +1,9 @@
+;\r
+;\r
+H141 38 516 0 0.125 0.612 Contrail_Rockets \r
+0.00223714 265.514\r
+0.111857 262.066\r
+1.20134 106.895\r
+1.25951 55.1717\r
+1.3557 27.5859\r
+1.7 0\r
diff --git a/datafiles/thrustcurves/Contrail_H211.eng b/datafiles/thrustcurves/Contrail_H211.eng
new file mode 100644 (file)
index 0000000..966d41f
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;\r
+H211 38 516 0 0.125 0.612 Contrail_Rockets \r
+0.00111857 531.028\r
+0.0190157 634.475\r
+0.0223714 593.096\r
+0.033557 544.821\r
+0.296421 317.238\r
+0.313199 186.205\r
+0.743848 96.5506\r
+0.97 0\r
diff --git a/datafiles/thrustcurves/Contrail_H222.eng b/datafiles/thrustcurves/Contrail_H222.eng
new file mode 100644 (file)
index 0000000..d708758
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;H-222-HP Reload\r
+;38mm/16 inch Case Used\r
+;Medium Nozzle Used For Reload\r
+;140cc of Nitrous Oxide Used\r
+H222 38 406.4 0 0.022 0.52 Contrail_Rockets \r
+0 684.342\r
+0.0302013 656.968\r
+0.0525727 574.847\r
+0.0581655 349.014\r
+0.346756 260.05\r
+0.364653 191.616\r
+0.7 0\r
diff --git a/datafiles/thrustcurves/Contrail_H246.eng b/datafiles/thrustcurves/Contrail_H246.eng
new file mode 100644 (file)
index 0000000..0bd0b63
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;H-246 HP Reload\r
+;38mm/20 Inch Case Used\r
+;Medium Nozzle Used\r
+;185cc Nitrous Oxide Used\r
+H246 38 508 0 0.022 0.598 Contrail_Rockets \r
+0.00111857 609.064\r
+0.0123043 499.57\r
+0.502237 253.206\r
+0.514541 157.399\r
+0.9 0\r
diff --git a/datafiles/thrustcurves/Contrail_H277.eng b/datafiles/thrustcurves/Contrail_H277.eng
new file mode 100644 (file)
index 0000000..6032648
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;\r
+H277 38 719 0 0.11 0.71 Contrail_Rockets \r
+0 765.508\r
+0.0738255 703.44\r
+0.118568 337.927\r
+0.917226 179.308\r
+0.957494 75.8612\r
+0.995526 41.3788\r
+1.02908 48.2753\r
+1.15 0\r
diff --git a/datafiles/thrustcurves/Contrail_H300.eng b/datafiles/thrustcurves/Contrail_H300.eng
new file mode 100644 (file)
index 0000000..05f3910
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+H300 38 516 0 0.11 0.612 Contrail_Rockets \r
+0 558.614\r
+0.115213 717.233\r
+0.12528 268.962\r
+0.214765 248.273\r
+0.286353 241.376\r
+0.334452 227.583\r
+0.62 0\r
diff --git a/datafiles/thrustcurves/Contrail_H303.eng b/datafiles/thrustcurves/Contrail_H303.eng
new file mode 100644 (file)
index 0000000..e1cc505
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;H-303-PVC Hybrid Motor\r
+;Uses Fast Nozzle\r
+;38mm/20 Inch Hardware\r
+;Uses 185cc Nitrous Oxide\r
+H303 38 508 0 0.023 0.589 Contrail_Rockets \r
+0 663.812\r
+0.0447427 780.15\r
+0.108501 704.872\r
+0.111857 342.171\r
+0.176734 328.484\r
+0.196868 307.954\r
+0.6 0\r
diff --git a/datafiles/thrustcurves/Contrail_H340.eng b/datafiles/thrustcurves/Contrail_H340.eng
new file mode 100644 (file)
index 0000000..ab350da
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+H340 38 711.2 0 0.024 0.816 Contrail_Rockets \r
+0 920.322\r
+0.0847458 715.806\r
+0.101695 345.121\r
+0.683051 332.338\r
+0.740678 255.645\r
+0.766102 153.387\r
+0.95 0\r
diff --git a/datafiles/thrustcurves/Contrail_I155.eng b/datafiles/thrustcurves/Contrail_I155.eng
new file mode 100644 (file)
index 0000000..934a4a3
--- /dev/null
@@ -0,0 +1,8 @@
+;\r
+;\r
+I155 38 711.2 0 0.045 0.725 Contrail_Rockets \r
+0.0111857 222.411\r
+2.71253 150.555\r
+2.82998 82.121\r
+2.96421 58.1691\r
+3.5 0\r
diff --git a/datafiles/thrustcurves/Contrail_I210.eng b/datafiles/thrustcurves/Contrail_I210.eng
new file mode 100644 (file)
index 0000000..17ea573
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+I210 38 922 0 0.125 0.87 Contrail_Rockets \r
+0 468.96\r
+0.464206 386.202\r
+0.497763 206.894\r
+2.25391 110.343\r
+2.34899 41.3788\r
+2.40492 13.7929\r
+2.72 0\r
diff --git a/datafiles/thrustcurves/Contrail_I221.eng b/datafiles/thrustcurves/Contrail_I221.eng
new file mode 100644 (file)
index 0000000..63fa12d
--- /dev/null
@@ -0,0 +1,9 @@
+;\r
+;\r
+I221 38 719 0 0.125 0.71 Contrail_Rockets \r
+0 482.753\r
+0.503356 358.616\r
+0.519016 179.308\r
+1.49217 103.447\r
+1.53691 27.5859\r
+1.74 0\r
diff --git a/datafiles/thrustcurves/Contrail_I290.eng b/datafiles/thrustcurves/Contrail_I290.eng
new file mode 100644 (file)
index 0000000..36c1a7a
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+I290 38 914.4 0 0.068 0.884 Contrail_Rockets \r
+0 521.516\r
+0.0847458 337.451\r
+0.138983 357.903\r
+0.19661 398.806\r
+0.308475 490.838\r
+0.40339 449.935\r
+0.589831 357.903\r
+0.762712 419.258\r
+0.932203 265.871\r
+1.08814 163.613\r
+1.24068 81.8064\r
+1.5 0\r
diff --git a/datafiles/thrustcurves/Contrail_I307.eng b/datafiles/thrustcurves/Contrail_I307.eng
new file mode 100644 (file)
index 0000000..eeabc88
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;\r
+I307 38 922 0 0.11 0.81 Contrail_Rockets \r
+0.00223714 551.717\r
+0.199105 717.233\r
+0.210291 386.202\r
+0.756152 620.682\r
+0.834452 455.167\r
+0.941834 310.341\r
+1.09172 199.998\r
+1.22371 117.24\r
+1.85 0\r
diff --git a/datafiles/thrustcurves/Contrail_I333.eng b/datafiles/thrustcurves/Contrail_I333.eng
new file mode 100644 (file)
index 0000000..3e7333b
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;I-333-PVC Reload\r
+;38mm/36 Inch Hardware\r
+;Uses Fast Nozzle\r
+;460cc Nitrous Oxide\r
+I333 38 914.4 0 0.068 0.929 Contrail_Rockets \r
+0.00894855 855.427\r
+0.0290828 881.09\r
+0.0536913 504.702\r
+0.604027 342.171\r
+0.796421 461.931\r
+1.7 0\r
diff --git a/datafiles/thrustcurves/Contrail_I400.eng b/datafiles/thrustcurves/Contrail_I400.eng
new file mode 100644 (file)
index 0000000..6b34950
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;I-400-HP\r
+;38mm/36 Inch Hardware\r
+;Uses Fast/X-Fast Nozzle\r
+;460cc Nitrous Oxide\r
+I400 38 914.4 0 0.086 0.925 Contrail_Rockets \r
+0.00447427 667.233\r
+0.0782998 898.199\r
+0.116331 598.799\r
+0.297539 521.811\r
+0.420582 410.605\r
+0.559284 487.594\r
+0.738255 367.834\r
+1 0\r
diff --git a/datafiles/thrustcurves/Contrail_I500.eng b/datafiles/thrustcurves/Contrail_I500.eng
new file mode 100644 (file)
index 0000000..31cfb23
--- /dev/null
@@ -0,0 +1,9 @@
+;\r
+;\r
+I500 38 719 0 0.748 0.8 Contrail_Rockets \r
+0.00111857 1155.16\r
+0.0201342 706.888\r
+0.0313199 999.988\r
+0.574944 103.447\r
+0.623043 120.688\r
+0.7 0\r
diff --git a/datafiles/thrustcurves/Contrail_I727.eng b/datafiles/thrustcurves/Contrail_I727.eng
new file mode 100644 (file)
index 0000000..31a5ddc
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;\r
+I727 38 914.4 0 0.022 0.929 Contrail_Rockets \r
+0.00847458 1278.22\r
+0.0355932 1661.69\r
+0.0983051 1508.31\r
+0.144068 1482.74\r
+0.171186 1175.97\r
+0.218644 1022.58\r
+0.422034 792.499\r
+0.75 0\r
diff --git a/datafiles/thrustcurves/Contrail_I747.eng b/datafiles/thrustcurves/Contrail_I747.eng
new file mode 100644 (file)
index 0000000..76de396
--- /dev/null
@@ -0,0 +1,5 @@
+;\r
+;\r
+I747 38 711.2 0 0.068 0.839 Contrail_Rockets \r
+0 1917.34\r
+0.45 0\r
diff --git a/datafiles/thrustcurves/Contrail_J150.eng b/datafiles/thrustcurves/Contrail_J150.eng
new file mode 100644 (file)
index 0000000..9bd4ade
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;J-150-HP\r
+;38mm/36 Inch\r
+;550cc\r
+;Slow Nozzle\r
+J150 38 914.4 0 0.091 0.839 Contrail_Rockets \r
+0 266.893\r
+2.00224 184.772\r
+2.75727 150.555\r
+3.00895 92.3861\r
+4.1 0\r
diff --git a/datafiles/thrustcurves/Contrail_J222.eng b/datafiles/thrustcurves/Contrail_J222.eng
new file mode 100644 (file)
index 0000000..2a3ceb7
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;J-222-HP Reload\r
+;Medium Nozzle\r
+;38mm/48 Inch Hardware\r
+;830cc\r
+J222 38 1219.2 0 0.091 1.043 Contrail_Rockets \r
+0.00559284 547.473\r
+0.167785 355.858\r
+2.86353 191.616\r
+2.95861 143.712\r
+3.08725 130.025\r
+3.46756 95.8079\r
+4.3 0\r
diff --git a/datafiles/thrustcurves/Contrail_J234.eng b/datafiles/thrustcurves/Contrail_J234.eng
new file mode 100644 (file)
index 0000000..574cbd3
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;J-234-BG Reload\r
+;Slow Nozzle\r
+;54mm/36 Inch Hardware\r
+J234 54 914.4 0 0.177 1.764 Contrail_Rockets \r
+0.00559284 229.255\r
+0.503356 349.014\r
+3.47875 208.724\r
+3.62416 116.338\r
+3.75839 78.6993\r
+4.3 0\r
diff --git a/datafiles/thrustcurves/Contrail_J242.eng b/datafiles/thrustcurves/Contrail_J242.eng
new file mode 100644 (file)
index 0000000..a0c271f
--- /dev/null
@@ -0,0 +1,8 @@
+;\r
+;\r
+J242 38 1227 0 0.11 1.065 Contrail_Rockets \r
+0.0111857 448.27\r
+1.73937 268.962\r
+1.76174 165.515\r
+2.97539 48.2753\r
+3.1 0\r
diff --git a/datafiles/thrustcurves/Contrail_J245.eng b/datafiles/thrustcurves/Contrail_J245.eng
new file mode 100644 (file)
index 0000000..9fbd976
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;J-245-BG Reload\r
+;Slow Nozzle\r
+;54mm/28 Inch Hardware\r
+J245 54 711.2 0 0.1 1.55 Contrail_Rockets \r
+0 444.822\r
+0.139821 355.858\r
+1.05145 307.954\r
+2.06376 184.772\r
+2.15884 102.651\r
+2.28188 68.4342\r
+2.62 0\r
diff --git a/datafiles/thrustcurves/Contrail_J246.eng b/datafiles/thrustcurves/Contrail_J246.eng
new file mode 100644 (file)
index 0000000..3dfff09
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;J-246-HP Reload\r
+;38mm/36 Inch Hardware\r
+;550cc\r
+;Medium Nozzle\r
+J246 38 914.4 0 0.068 0.861 Contrail_Rockets \r
+0.0167785 492.726\r
+0.0279642 328.484\r
+0.134228 526.943\r
+0.341163 403.762\r
+0.520134 349.014\r
+2.00224 191.616\r
+2.12528 116.338\r
+2.8 0\r
diff --git a/datafiles/thrustcurves/Contrail_J272.eng b/datafiles/thrustcurves/Contrail_J272.eng
new file mode 100644 (file)
index 0000000..314da9a
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+J272 54 914.4 0 0.114 1.746 Contrail_Rockets \r
+0.00847458 398.806\r
+0.169492 572.645\r
+0.533898 460.161\r
+0.872881 388.58\r
+1.05932 357.903\r
+2.91525 204.516\r
+3.19492 71.5806\r
+3.51695 40.9032\r
+3.63559 51.129\r
+3.86 0\r
diff --git a/datafiles/thrustcurves/Contrail_J292.eng b/datafiles/thrustcurves/Contrail_J292.eng
new file mode 100644 (file)
index 0000000..3b4b919
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+J292 54 711.2 0 0.136 1.542 Contrail_Rockets \r
+0.00847458 552.193\r
+0.262712 480.612\r
+0.423729 419.258\r
+0.762712 337.451\r
+1.97458 245.419\r
+2.07627 143.161\r
+2.53 0\r
diff --git a/datafiles/thrustcurves/Contrail_J333.eng b/datafiles/thrustcurves/Contrail_J333.eng
new file mode 100644 (file)
index 0000000..4a0dba8
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+J333 38 1227 0 0.11 1.064 Contrail_Rockets \r
+0 717.233\r
+0.204139 799.99\r
+0.752237 448.27\r
+0.763423 268.962\r
+2.16443 62.0682\r
+2.23714 27.5859\r
+2.4 0\r
diff --git a/datafiles/thrustcurves/Contrail_J345.eng b/datafiles/thrustcurves/Contrail_J345.eng
new file mode 100644 (file)
index 0000000..72f66c6
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;J-345-PVC\r
+;38mm/48 Inch Hardware\r
+;735cc\r
+;Fast Nozzle\r
+J345 38 1219.2 0 0.098 1.118 Contrail_Rockets \r
+0.00559284 881.09\r
+0.0782998 667.233\r
+1.21924 376.388\r
+1.26398 359.279\r
+2.7 0\r
diff --git a/datafiles/thrustcurves/Contrail_J355.eng b/datafiles/thrustcurves/Contrail_J355.eng
new file mode 100644 (file)
index 0000000..99af48c
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+J355 54 711.2 0 0.09 1.564 Contrail_Rockets \r
+0 562.419\r
+0.176271 501.064\r
+0.2 286.322\r
+0.433898 286.322\r
+0.688136 337.451\r
+0.701695 501.064\r
+0.80678 490.838\r
+1.00339 521.516\r
+1.21695 419.258\r
+1.31186 429.484\r
+1.37627 460.161\r
+1.49831 429.484\r
+1.54576 224.968\r
+1.64068 122.71\r
+1.91 0\r
diff --git a/datafiles/thrustcurves/Contrail_J358.eng b/datafiles/thrustcurves/Contrail_J358.eng
new file mode 100644 (file)
index 0000000..9f61faa
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;\r
+J358 54 914.4 0 0.111 1.743 Contrail_Rockets \r
+0.00847458 726.032\r
+0.0932203 726.032\r
+0.110169 501.064\r
+0.483051 480.612\r
+0.550847 398.806\r
+2.23729 286.322\r
+2.32203 153.387\r
+2.44915 112.484\r
+2.69 0\r
diff --git a/datafiles/thrustcurves/Contrail_J416.eng b/datafiles/thrustcurves/Contrail_J416.eng
new file mode 100644 (file)
index 0000000..07e4551
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+J416 54 914.4 0 0.158 1.7 Contrail_Rockets \r
+0 787.386\r
+0.0762712 777.161\r
+0.211864 572.645\r
+0.432203 531.741\r
+0.864407 511.29\r
+1.26271 480.612\r
+1.82203 470.387\r
+2.00847 347.677\r
+2.13559 276.097\r
+2.24576 184.064\r
+2.40678 81.8064\r
+2.75 0\r
diff --git a/datafiles/thrustcurves/Contrail_J555.eng b/datafiles/thrustcurves/Contrail_J555.eng
new file mode 100644 (file)
index 0000000..a1b444e
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+J555 38 1227 0 0.166 1.132 Contrail_Rockets \r
+0 931.023\r
+0.0581655 1344.81\r
+0.277405 810.335\r
+1.17226 241.376\r
+1.2774 68.9647\r
+1.31767 51.7235\r
+1.6 0\r
diff --git a/datafiles/thrustcurves/Contrail_J642.eng b/datafiles/thrustcurves/Contrail_J642.eng
new file mode 100644 (file)
index 0000000..229af7e
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;\r
+J642 54 914.4 0 0.159 1.791 Contrail_Rockets \r
+0.00677966 1482.74\r
+0.0779661 997.015\r
+0.471186 1303.79\r
+0.542373 818.064\r
+0.633898 741.37\r
+0.742373 587.983\r
+1.25085 485.725\r
+1.29831 332.338\r
+1.39661 178.951\r
+1.47458 51.129\r
+1.72 0\r
diff --git a/datafiles/thrustcurves/Contrail_J800.eng b/datafiles/thrustcurves/Contrail_J800.eng
new file mode 100644 (file)
index 0000000..c33d77b
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;J-800-HP\r
+;38mm/48 Inch\r
+;685cc\r
+;XXF Nozzle (Short Nozzle)\r
+J800 38 1219.2 0 0.105 1.148 Contrail_Rockets \r
+0.00223714 1830.61\r
+0.52349 889.644\r
+0.639821 650.125\r
+0.740492 444.822\r
+0.823266 273.737\r
+0.90604 153.977\r
+0.997763 136.868\r
+1.2 0\r
diff --git a/datafiles/thrustcurves/Contrail_K234.eng b/datafiles/thrustcurves/Contrail_K234.eng
new file mode 100644 (file)
index 0000000..62ce6e1
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;K-234-BG Reload\r
+;Slow Nozzle\r
+;54mm/48 Inch Hardware\r
+K234 54 1219.2 0 0.385 2.063 Contrail_Rockets \r
+0 92.3861\r
+0.234899 396.918\r
+0.973154 338.749\r
+5.97315 171.085\r
+6.05145 106.073\r
+6.19687 78.6993\r
+6.37584 54.7473\r
+7.05 0\r
diff --git a/datafiles/thrustcurves/Contrail_K265.eng b/datafiles/thrustcurves/Contrail_K265.eng
new file mode 100644 (file)
index 0000000..14fe5a0
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;\r
+K265 54 1219.2 0 0.271 2.085 Contrail_Rockets \r
+0 470.387\r
+2.44068 347.677\r
+3.91525 224.968\r
+4.77966 173.839\r
+5.13559 112.484\r
+5.33898 51.129\r
+6.26 0\r
diff --git a/datafiles/thrustcurves/Contrail_K300.eng b/datafiles/thrustcurves/Contrail_K300.eng
new file mode 100644 (file)
index 0000000..385b78c
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;K-300-BS\r
+;75mm/40 Inch Hardware\r
+;2050cc\r
+;Slow Nozzle\r
+K300 75 1016 0 0.181 4.059 Contrail_Rockets \r
+0 431.135\r
+0.324385 526.943\r
+0.98434 479.039\r
+1.1745 369.545\r
+5 280.58\r
+5.19016 171.085\r
+5.35794 102.651\r
+5.6264 54.7473\r
+5.79418 27.3737\r
+6.5 0\r
diff --git a/datafiles/thrustcurves/Contrail_K321.eng b/datafiles/thrustcurves/Contrail_K321.eng
new file mode 100644 (file)
index 0000000..42c6698
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;K-321-BG Reload\r
+;54mm/48 Inch Hardware\r
+;Medium Nozzle\r
+K321 54 1219.2 0 0.183 2.043 Contrail_Rockets \r
+0.00559284 218.989\r
+0.218121 410.605\r
+0.973154 718.559\r
+0.989933 732.246\r
+1.05705 444.822\r
+1.4877 403.762\r
+3.97092 232.676\r
+4.11633 88.9644\r
+4.23378 54.7473\r
+4.34564 54.7473\r
+4.9 0\r
diff --git a/datafiles/thrustcurves/Contrail_K404.eng b/datafiles/thrustcurves/Contrail_K404.eng
new file mode 100644 (file)
index 0000000..ad9e933
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;K-404-Sparky\r
+;75mm/40 Inch Hardware\r
+;2050cc\r
+;Slow Nozzle\r
+K404 75 1016 0 0.318 4.15 Contrail_Rockets \r
+0.0111857 670.655\r
+4.63087 335.328\r
+4.80984 205.303\r
+4.9217 130.025\r
+5.0783 82.121\r
+5.26846 41.0605\r
+6.4 0\r
diff --git a/datafiles/thrustcurves/Contrail_K456.eng b/datafiles/thrustcurves/Contrail_K456.eng
new file mode 100644 (file)
index 0000000..72e556e
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;\r
+K456 75 813 0 0.58 3.704 Contrail_Rockets \r
+0.00559284 681.026\r
+0.212528 896.541\r
+0.503356 775.853\r
+1.36465 577.579\r
+1.52685 525.856\r
+2.51119 370.685\r
+2.66779 129.309\r
+3.7 0\r
diff --git a/datafiles/thrustcurves/Contrail_K630.eng b/datafiles/thrustcurves/Contrail_K630.eng
new file mode 100644 (file)
index 0000000..1da3faf
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;K-630-Sparky Reload\r
+;75mm/41 Inch Hardare\r
+;1400cc\r
+;Medium Nozzle\r
+K630 75 1041.4 0 0.075 3.55 Contrail_Rockets \r
+0.00559284 307.954\r
+0.0978747 573.136\r
+0.500559 889.644\r
+1.75336 667.233\r
+1.85403 410.605\r
+1.93792 239.52\r
+2.04978 128.314\r
+2.2 0\r
diff --git a/datafiles/thrustcurves/Contrail_K678.eng b/datafiles/thrustcurves/Contrail_K678.eng
new file mode 100644 (file)
index 0000000..16febf9
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;K-678-Sparky\r
+;75mm/40 Inch Hardware\r
+;2050cc\r
+;Medium Nozzle\r
+K678 75 1016 0 0.827 4.05 Contrail_Rockets \r
+0.00559284 1163.38\r
+2.21477 444.822\r
+2.32103 256.628\r
+2.38814 102.651\r
+2.8 0\r
diff --git a/datafiles/thrustcurves/Contrail_K707.eng b/datafiles/thrustcurves/Contrail_K707.eng
new file mode 100644 (file)
index 0000000..3d6ec1e
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+K707 75 813 0 0.145 3.674 Contrail_Rockets \r
+0.0466102 281.209\r
+0.122881 1278.22\r
+0.165254 894.757\r
+0.495763 1431.61\r
+0.618644 1150.4\r
+0.694915 945.886\r
+0.834746 920.322\r
+1.01271 664.677\r
+1.50847 536.854\r
+1.62288 281.209\r
+1.72881 127.822\r
+2 0\r
diff --git a/datafiles/thrustcurves/Contrail_K777.eng b/datafiles/thrustcurves/Contrail_K777.eng
new file mode 100644 (file)
index 0000000..befb614
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+K777 75 1016 0 0.645 4.05 Contrail_Rockets \r
+0 931.023\r
+0.0950783 965.506\r
+0.111857 1793.08\r
+0.167785 1741.36\r
+0.206935 1344.81\r
+0.727069 1137.92\r
+1.00112 810.335\r
+1.97427 413.788\r
+2.04698 172.412\r
+2.6 0\r
diff --git a/datafiles/thrustcurves/Contrail_L1222.eng b/datafiles/thrustcurves/Contrail_L1222.eng
new file mode 100644 (file)
index 0000000..f11e7a1
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;Contrail Rockets LLC Hybrid Rocket Motor (L1222)\r
+;75mm-3200cc Motor System\r
+;Sparky Hybrid Fuel\r
+;Data Input By Tom R. Sanders of Contrail Rockets\r
+L1222 75 1339.85 0 3.9 4.989 Contrail_Rockets \r
+0 455\r
+0.25 455\r
+0.5 2725\r
+0.75 1816\r
+2.75 680\r
+3.1 0\r
diff --git a/datafiles/thrustcurves/Contrail_L2525.eng b/datafiles/thrustcurves/Contrail_L2525.eng
new file mode 100644 (file)
index 0000000..032e487
--- /dev/null
@@ -0,0 +1,6 @@
+;\r
+;\r
+L2525 75 1492.25 0 3.5 5.579 Contrail_Rockets \r
+0 4200\r
+0.754759 3294.57\r
+1.9 0\r
diff --git a/datafiles/thrustcurves/Contrail_L369.eng b/datafiles/thrustcurves/Contrail_L369.eng
new file mode 100644 (file)
index 0000000..7c1d69d
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;L-369-Sparky\r
+;Slow nozzle\r
+;75mm/54 Inch Hardware\r
+;3200cc\r
+L369 75 1371.6 0 0.514 4.8 Contrail_Rockets \r
+0.0223714 540.63\r
+1.45414 533.787\r
+8.92617 260.05\r
+9.08277 130.025\r
+9.28412 68.4342\r
+10.6 0\r
diff --git a/datafiles/thrustcurves/Contrail_L800.eng b/datafiles/thrustcurves/Contrail_L800.eng
new file mode 100644 (file)
index 0000000..dadaba2
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;L-800-Sparky\r
+;75mm/54 Inch Hardware\r
+;3200cc\r
+;Medium Nozzle\r
+L800 75 1371.6 0 0.988 4.726 Contrail_Rockets \r
+0.00559284 1351.58\r
+0.167785 1129.16\r
+0.329978 1266.03\r
+0.553691 1248.92\r
+0.665548 1129.16\r
+3.48434 821.21\r
+3.5962 496.148\r
+3.69687 273.737\r
+3.83669 153.977\r
+4.6 0\r
diff --git a/datafiles/thrustcurves/Contrail_M1575.eng b/datafiles/thrustcurves/Contrail_M1575.eng
new file mode 100644 (file)
index 0000000..3b4e0b8
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;M-1575-Black Gold Reload\r
+;5300cc\r
+;98mm/60 Inch Hardware\r
+M1575 98 1524 0 0.726 10.863 Contrail_Rockets \r
+0.139821 2429.41\r
+0.503356 2976.89\r
+2.95302 1402.9\r
+3.06488 923.861\r
+3.21029 376.388\r
+3.31096 205.303\r
+4.2 0\r
diff --git a/datafiles/thrustcurves/Contrail_M2700.eng b/datafiles/thrustcurves/Contrail_M2700.eng
new file mode 100644 (file)
index 0000000..34bc383
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+M2700 98 1524 0 0.412 10.432 Contrail_Rockets \r
+0.00847458 2965.48\r
+0.0508475 3272.26\r
+0.105932 5930.96\r
+0.347458 5828.7\r
+0.504237 6442.25\r
+0.512712 5726.45\r
+0.601695 5112.9\r
+0.745763 3681.29\r
+0.902542 3067.74\r
+1.06356 2454.19\r
+1.18644 1942.9\r
+1.34322 1738.39\r
+1.75 715.806\r
+1.95763 102.258\r
+2.3 0\r
diff --git a/datafiles/thrustcurves/Contrail_M2800.eng b/datafiles/thrustcurves/Contrail_M2800.eng
new file mode 100644 (file)
index 0000000..a1b26b2
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;M-2800-Black Gold Reload\r
+;5300cc\r
+;98mm/60 inch Hardware\r
+M2800 98 1524 0 0.476 10.704 Contrail_Rockets \r
+0.00838926 2395.2\r
+0.251678 2805.8\r
+0.545302 3695.45\r
+0.75783 5611.6\r
+0.911633 4311.35\r
+1.1745 3558.58\r
+1.4094 2258.33\r
+1.70861 1505.55\r
+2.3 0\r
diff --git a/datafiles/thrustcurves/Contrail_M711.eng b/datafiles/thrustcurves/Contrail_M711.eng
new file mode 100644 (file)
index 0000000..9570b1f
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;Contrail Rockets LLC Hybrid Rocket Motor. (M-711)\r
+;75-3200cc Hardware Set\r
+;Black Smoke Fuel\r
+M711BS 75 1340 0 4.2 4.9 Contrail_Rockets \r
+0 1140\r
+1.46697 1069.77\r
+4 680\r
+6.47256 589.147\r
+6.67413 279.07\r
+7.22 0\r
diff --git a/datafiles/thrustcurves/Contrail_O6300.eng b/datafiles/thrustcurves/Contrail_O6300.eng
new file mode 100644 (file)
index 0000000..3d0e4fd
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+O6300 152 1828.8 0 3.175 28.576 Contrail_Rockets \r
+0.0338983 12271\r
+0.728814 9714.51\r
+1.65254 9203.22\r
+2.37288 8947.57\r
+2.51695 6646.77\r
+2.78814 4601.61\r
+2.99153 4857.25\r
+3.27966 3579.03\r
+3.61017 1278.22\r
+4.29 0\r
diff --git a/datafiles/thrustcurves/Ellis_G20.eng b/datafiles/thrustcurves/Ellis_G20.eng
new file mode 100644 (file)
index 0000000..c8fc648
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+G20 29 149 3 0.0729 0.1179 Ellis_Mountain \r
+0.0463679 46.6843\r
+0.278207 30.3888\r
+0.479134 26.8655\r
+1.00464 24.6634\r
+3.47759 22.0209\r
+4.32767 13.653\r
+5.11592 3.08293\r
+5.47 0\r
diff --git a/datafiles/thrustcurves/Ellis_G35.eng b/datafiles/thrustcurves/Ellis_G35.eng
new file mode 100644 (file)
index 0000000..2c6da54
--- /dev/null
@@ -0,0 +1,10 @@
+;\r
+;Ellis Mountain G35 Single Use Motor\r
+G35EM 29 165 6-10 0.082 0.135 Ellis_Mountain \r
+0.01 51.12\r
+0.04 57.55\r
+0.08 43.78\r
+2.73 28.16\r
+3.28 28.16\r
+3.78 6.73\r
+4 0\r
diff --git a/datafiles/thrustcurves/Ellis_G37.eng b/datafiles/thrustcurves/Ellis_G37.eng
new file mode 100644 (file)
index 0000000..1df8f92
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;\r
+G37 24 181 6-10-100 0.068 0.1133 Ellis_Mountain \r
+0.0231839 69.586\r
+0.162287 55.9331\r
+0.332303 48.0056\r
+0.502318 44.9226\r
+0.996909 40.9589\r
+1.49923 38.7568\r
+2.00155 34.3526\r
+2.49614 28.1868\r
+2.75116 18.4976\r
+2.99845 5.28502\r
+3.1 0\r
diff --git a/datafiles/thrustcurves/Ellis_H275.eng b/datafiles/thrustcurves/Ellis_H275.eng
new file mode 100644 (file)
index 0000000..1d83b44
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;\r
+H275 29 275 10 0.142 0.255 Ellis_Mountain \r
+0.0123648 792.752\r
+0.015456 356.739\r
+0.197836 312.697\r
+0.797527 268.655\r
+0.911901 255.442\r
+0.992272 123.317\r
+1.04173 39.6376\r
+1.1 0\r
diff --git a/datafiles/thrustcurves/Ellis_H48.eng b/datafiles/thrustcurves/Ellis_H48.eng
new file mode 100644 (file)
index 0000000..a6d69fb
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+;Ellis Mountain Rocket Works\r
+;H48 Single Use motor\r
+H48 38 200 8-100 0.154 0.292 Ellis_Mountain \r
+0.05 101.5\r
+0.1 101.5\r
+0.21 92.18\r
+0.46 86.48\r
+0.74 83.38\r
+1 80\r
+1.49 74.57\r
+1.99 68.36\r
+2.48 63.18\r
+2.99 56.45\r
+3.2 34.18\r
+3.5 18\r
+3.69 13.46\r
+4 11\r
+4.36 7.77\r
+4.4 0\r
diff --git a/datafiles/thrustcurves/Ellis_H50.eng b/datafiles/thrustcurves/Ellis_H50.eng
new file mode 100644 (file)
index 0000000..717bcd0
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;Ellis Mountain Rocket Works\r
+;H50 Single Use motor\r
+H50 29 279 6-10 0.163 0.3 Ellis_Mountain \r
+0.01 63.67\r
+0.17 108.9\r
+0.27 94.9\r
+0.47 81.43\r
+0.79 71.02\r
+1.27 64.9\r
+1.97 60.61\r
+2.56 56.94\r
+3.01 52.04\r
+3.52 45.31\r
+3.97 34.9\r
+4.49 18.37\r
+4.97 4.9\r
+5.28 0\r
diff --git a/datafiles/thrustcurves/Ellis_I130.eng b/datafiles/thrustcurves/Ellis_I130.eng
new file mode 100644 (file)
index 0000000..005f74f
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;\r
+I130 38 330 100 0.308 0.625 Ellis_Mountain \r
+0.015456 266.453\r
+0.0540958 160.753\r
+0.502318 169.561\r
+2.23338 180.571\r
+2.48841 149.742\r
+2.99073 136.53\r
+3.49304 77.0732\r
+4.01082 26.4251\r
+4.43 0\r
diff --git a/datafiles/thrustcurves/Ellis_I134.eng b/datafiles/thrustcurves/Ellis_I134.eng
new file mode 100644 (file)
index 0000000..95a6c9a
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;Ellis Mountain Rocket Works\r
+;I134 38mm Single Use motor\r
+I134 38 355 15 0.2807 0.5812 Ellis_Mountain \r
+0.1 268.8\r
+0.2 138\r
+1 116\r
+2 102\r
+3 85\r
+4 67\r
+4.65 16.46\r
+4.82 6.86\r
+5.07 6.86\r
+5.15 0\r
diff --git a/datafiles/thrustcurves/Ellis_I150.eng b/datafiles/thrustcurves/Ellis_I150.eng
new file mode 100644 (file)
index 0000000..936cd57
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain I150\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I150 38 229 0 0.172032 0.425152 EM\r
+   0.050 101.298\r
+   0.152 159.193\r
+   0.255 169.686\r
+   0.358 179.603\r
+   0.460 188.152\r
+   0.564 193.364\r
+   0.667 204.520\r
+   0.769 212.046\r
+   0.872 212.937\r
+   0.975 208.076\r
+   1.077 196.555\r
+   1.180 191.025\r
+   1.283 186.106\r
+   1.386 181.835\r
+   1.490 177.947\r
+   1.592 175.877\r
+   1.695 173.744\r
+   1.798 170.664\r
+   1.900 161.823\r
+   2.003 149.111\r
+   2.106 124.923\r
+   2.208 68.392\r
+   2.311 20.122\r
+   2.415 7.794\r
+   2.518 4.464\r
+   2.621 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_I160.eng b/datafiles/thrustcurves/Ellis_I160.eng
new file mode 100644 (file)
index 0000000..659ce05
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain I160\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I160 38 280 0 0.235648 0.528192 EM\r
+   0.068 169.405\r
+   0.206 199.425\r
+   0.346 205.072\r
+   0.485 206.075\r
+   0.624 205.840\r
+   0.763 204.052\r
+   0.902 200.850\r
+   1.042 200.885\r
+   1.180 203.053\r
+   1.319 204.157\r
+   1.458 206.392\r
+   1.598 210.051\r
+   1.736 212.769\r
+   1.875 211.177\r
+   2.015 207.500\r
+   2.154 189.766\r
+   2.293 136.149\r
+   2.431 52.306\r
+   2.571 42.841\r
+   2.710 41.803\r
+   2.849 33.042\r
+   2.987 24.614\r
+   3.127 17.154\r
+   3.267 7.477\r
+   3.406 1.777\r
+   3.546 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_I230.eng b/datafiles/thrustcurves/Ellis_I230.eng
new file mode 100644 (file)
index 0000000..2d12b49
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain I230\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I230 38 331 0 0.282688 0.620928 EM\r
+   0.058 292.627\r
+   0.178 317.660\r
+   0.298 309.874\r
+   0.418 305.243\r
+   0.537 299.679\r
+   0.657 298.170\r
+   0.777 294.591\r
+   0.897 293.800\r
+   1.018 289.736\r
+   1.138 288.222\r
+   1.257 284.614\r
+   1.377 281.149\r
+   1.497 274.879\r
+   1.617 269.775\r
+   1.736 258.925\r
+   1.856 242.249\r
+   1.976 207.607\r
+   2.097 136.698\r
+   2.217 86.506\r
+   2.336 74.324\r
+   2.456 51.246\r
+   2.576 45.546\r
+   2.696 27.050\r
+   2.816 6.382\r
+   2.936 1.423\r
+   3.057 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_I69.eng b/datafiles/thrustcurves/Ellis_I69.eng
new file mode 100644 (file)
index 0000000..c0d6005
--- /dev/null
@@ -0,0 +1,19 @@
+;\r
+;Ellis Mountain Rocket Works\r
+;I69 38mm Single Use motor\r
+I69 29 406 10 0.236 0.4 Ellis_Mountain \r
+0.05 78.67\r
+0.1 149.7\r
+0.25 133.5\r
+0.49 111.51\r
+0.75 100\r
+1.07 93.18\r
+1.48 87.83\r
+2 82.49\r
+2.5 78\r
+2.99 73.32\r
+3.5 64.5\r
+3.99 48.88\r
+4.5 29.79\r
+4.99 9.17\r
+5.28 0\r
diff --git a/datafiles/thrustcurves/Ellis_J110.eng b/datafiles/thrustcurves/Ellis_J110.eng
new file mode 100644 (file)
index 0000000..394d41d
--- /dev/null
@@ -0,0 +1,11 @@
+;\r
+;\r
+J110 54 276.2 100 0.45359 0.8754 Ellis_Mountain \r
+0.108192 193.784\r
+0.386399 147.54\r
+1.00464 139.833\r
+4.034 116.711\r
+5.00773 94.6899\r
+6.01236 67.1637\r
+6.53787 37.4355\r
+6.8 0\r
diff --git a/datafiles/thrustcurves/Ellis_J148.eng b/datafiles/thrustcurves/Ellis_J148.eng
new file mode 100644 (file)
index 0000000..2b37673
--- /dev/null
@@ -0,0 +1,12 @@
+;\r
+;\r
+J148 54 355.6 14 0.67 1.179 Ellis_Mountain \r
+0.139104 218.007\r
+0.231839 183.875\r
+0.448223 171.763\r
+1.00464 170.662\r
+2.10201 170.662\r
+5.02318 147.54\r
+5.31685 133.226\r
+5.67233 49.547\r
+6.1 0\r
diff --git a/datafiles/thrustcurves/Ellis_J228.eng b/datafiles/thrustcurves/Ellis_J228.eng
new file mode 100644 (file)
index 0000000..54560e8
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+J228 38 562 6 0.27 0.8391 Ellis_Mountain \r
+0.0309119 665.031\r
+0.0927357 444.822\r
+0.262751 356.739\r
+0.664606 343.526\r
+0.989181 317.101\r
+1.96291 259.847\r
+2.99845 193.784\r
+4.01855 118.913\r
+4.99227 35.2334\r
+5.2 0\r
diff --git a/datafiles/thrustcurves/Ellis_J270.eng b/datafiles/thrustcurves/Ellis_J270.eng
new file mode 100644 (file)
index 0000000..02d8cc5
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain J270\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J270 38 384 0 0.341824 0.711872 EM\r
+   0.057 357.607\r
+   0.175 386.516\r
+   0.294 368.069\r
+   0.412 360.627\r
+   0.530 356.068\r
+   0.648 353.900\r
+   0.767 351.910\r
+   0.885 349.900\r
+   1.003 348.675\r
+   1.121 347.552\r
+   1.240 343.075\r
+   1.358 338.000\r
+   1.476 330.566\r
+   1.594 315.474\r
+   1.712 293.325\r
+   1.831 266.102\r
+   1.949 184.040\r
+   2.067 131.638\r
+   2.185 109.171\r
+   2.304 89.570\r
+   2.422 74.945\r
+   2.540 55.700\r
+   2.658 31.860\r
+   2.777 17.751\r
+   2.896 10.109\r
+   3.015 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_J330.eng b/datafiles/thrustcurves/Ellis_J330.eng
new file mode 100644 (file)
index 0000000..dae2cea
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain J330\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J330 38 433 0 0.407232 0.820736 EM\r
+   0.055 482.013\r
+   0.169 515.156\r
+   0.283 509.959\r
+   0.398 511.485\r
+   0.512 509.155\r
+   0.626 503.627\r
+   0.740 495.461\r
+   0.854 486.118\r
+   0.969 477.786\r
+   1.083 472.073\r
+   1.197 455.861\r
+   1.310 433.714\r
+   1.425 407.542\r
+   1.540 367.945\r
+   1.654 271.221\r
+   1.768 203.711\r
+   1.881 152.800\r
+   1.996 106.108\r
+   2.110 91.404\r
+   2.225 72.286\r
+   2.339 63.983\r
+   2.452 61.809\r
+   2.567 42.010\r
+   2.681 16.437\r
+   2.796 4.496\r
+   2.910 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_K475.eng b/datafiles/thrustcurves/Ellis_K475.eng
new file mode 100644 (file)
index 0000000..3fb8edb
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+K475 54 663.6 14 1.035 2.168 Ellis_Mountain \r
+0.0463679 797.157\r
+0.15456 616.585\r
+0.278207 585.756\r
+0.479134 568.139\r
+2.92117 576.948\r
+3.29212 568.139\r
+4.00309 303.888\r
+4.51314 224.613\r
+5.02318 74.8711\r
+5.5 0\r
diff --git a/datafiles/thrustcurves/Ellis_L330.eng b/datafiles/thrustcurves/Ellis_L330.eng
new file mode 100644 (file)
index 0000000..4a67154
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain L330\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L330 76 381 0 1.46944 2.67008 EM\r
+   0.194 298.963\r
+   0.584 378.807\r
+   0.975 376.204\r
+   1.366 382.475\r
+   1.757 391.163\r
+   2.148 399.442\r
+   2.539 406.048\r
+   2.930 407.731\r
+   3.321 405.666\r
+   3.711 400.636\r
+   4.103 393.384\r
+   4.494 384.520\r
+   4.884 377.009\r
+   5.275 368.385\r
+   5.666 359.041\r
+   6.057 350.117\r
+   6.448 341.587\r
+   6.839 337.109\r
+   7.230 300.039\r
+   7.621 194.602\r
+   8.011 123.445\r
+   8.403 66.942\r
+   8.794 32.233\r
+   9.184 8.248\r
+   9.576 1.563\r
+   9.968 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_L600.eng b/datafiles/thrustcurves/Ellis_L600.eng
new file mode 100644 (file)
index 0000000..3aa297f
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain L600\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L600 76 584 0 2.4407 4.11981 EM\r
+   0.186 829.668\r
+   0.561 773.861\r
+   0.936 767.837\r
+   1.313 755.034\r
+   1.689 736.454\r
+   2.064 722.717\r
+   2.440 706.215\r
+   2.816 688.253\r
+   3.191 673.457\r
+   3.567 660.981\r
+   3.943 648.124\r
+   4.318 634.689\r
+   4.694 622.058\r
+   5.070 607.970\r
+   5.445 594.926\r
+   5.821 583.003\r
+   6.197 573.084\r
+   6.572 553.530\r
+   6.948 399.379\r
+   7.324 270.410\r
+   7.699 211.401\r
+   8.075 144.237\r
+   8.451 74.227\r
+   8.826 19.378\r
+   9.202 4.274\r
+   9.578 0.000\r
diff --git a/datafiles/thrustcurves/Ellis_M1000.eng b/datafiles/thrustcurves/Ellis_M1000.eng
new file mode 100644 (file)
index 0000000..a19dda8
--- /dev/null
@@ -0,0 +1,30 @@
+; Ellis Mountain M1000\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1000 76 787 0 3.47514 5.5776 EM\r
+   0.159 1897.088\r
+   0.481 1606.200\r
+   0.803 1441.676\r
+   1.125 1360.014\r
+   1.447 1299.506\r
+   1.769 1259.449\r
+   2.091 1231.131\r
+   2.413 1202.529\r
+   2.735 1179.968\r
+   3.057 1154.573\r
+   3.379 1108.815\r
+   3.701 1075.453\r
+   4.023 1045.316\r
+   4.345 1010.304\r
+   4.667 951.184\r
+   4.989 860.548\r
+   5.310 727.369\r
+   5.633 595.659\r
+   5.955 518.911\r
+   6.277 439.902\r
+   6.599 347.743\r
+   6.921 239.388\r
+   7.243 144.608\r
+   7.565 75.112\r
+   7.887 33.539\r
+   8.210 0.000\r
diff --git a/datafiles/thrustcurves/Estes_1/2A3.eng b/datafiles/thrustcurves/Estes_1/2A3.eng
new file mode 100644 (file)
index 0000000..e947f91
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;Estes 1/2A3T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/2A3T 13 45 2-4 0.002 0.0066 Estes \r
+0.024 0.501\r
+0.042 1.454\r
+0.064 3.009\r
+0.076 4.062\r
+0.088 4.914\r
+0.093 5.065\r
+0.103 6.068\r
+0.112 6.87\r
+0.117 7.021\r
+0.126 7.62\r
+0.137 7.472\r
+0.146 6.87\r
+0.153 6.118\r
+0.159 5.065\r
+0.166 4.363\r
+0.179 3.66\r
+0.197 2.908\r
+0.222 2.256\r
+0.25 2.156\r
+0.277 2.106\r
+0.294 2.056\r
+0.304 2.156\r
+0.316 1.955\r
+0.326 1.554\r
+0.339 1.053\r
+0.35 0.651\r
+0.36 0\r
diff --git a/datafiles/thrustcurves/Estes_1/2A6.eng b/datafiles/thrustcurves/Estes_1/2A6.eng
new file mode 100644 (file)
index 0000000..716ba46
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+;Estes 1/2A6 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/2A6 18 70 2 0.0026 0.0138 Estes \r
+0.031 0.404\r
+0.064 1.258\r
+0.096 2.263\r
+0.124 3.467\r
+0.149 4.72\r
+0.172 6.023\r
+0.196 7.027\r
+0.21 7.528\r
+0.225 7.86\r
+0.235 7.482\r
+0.244 6.683\r
+0.254 5.685\r
+0.263 4.487\r
+0.269 4.087\r
+0.279 3.039\r
+0.29 1.79\r
+0.297 1.042\r
+0.306 0.593\r
+0.314 0.344\r
+0.33 0\r
diff --git a/datafiles/thrustcurves/Estes_1/4A3.eng b/datafiles/thrustcurves/Estes_1/4A3.eng
new file mode 100644 (file)
index 0000000..e74ab24
--- /dev/null
@@ -0,0 +1,34 @@
+;Estes 1/4A3T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/4A3T 13 45 3 0.00083 0.0061 Estes \r
+0.016 0.243\r
+0.044 1.164\r
+0.08 2.698\r
+0.088 2.851\r
+0.096 3.312\r
+0.105 3.804\r
+0.116 4.325\r
+0.129 4.754\r
+0.131 4.754\r
+0.135 4.95\r
+0.139 4.815\r
+0.143 4.814\r
+0.149 4.66\r
+0.157 4.289\r
+0.173 3.548\r
+0.187 2.808\r
+0.194 2.592\r
+0.197 2.13\r
+0.202 1.913\r
+0.206 1.512\r
+0.213 1.389\r
+0.218 1.112\r
+0.227 0.802\r
+0.236 0.493\r
+0.241 0.277\r
+0.25 0\r
diff --git a/datafiles/thrustcurves/Estes_A10.eng b/datafiles/thrustcurves/Estes_A10.eng
new file mode 100644 (file)
index 0000000..064f78c
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+;Estes A10T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A10T 13 45 3-100 0.0038 0.00525 Estes \r
+0.026 0.478\r
+0.055 1.919\r
+0.093 4.513\r
+0.124 8.165\r
+0.146 10.956\r
+0.166 12.64\r
+0.179 11.046\r
+0.194 7.966\r
+0.203 6.042\r
+0.209 3.154\r
+0.225 1.421\r
+0.26 1.225\r
+0.333 1.41\r
+0.456 1.206\r
+0.575 1.195\r
+0.663 1.282\r
+0.76 1.273\r
+0.811 1.268\r
+0.828 0.689\r
+0.85 0\r
diff --git a/datafiles/thrustcurves/Estes_A3.eng b/datafiles/thrustcurves/Estes_A3.eng
new file mode 100644 (file)
index 0000000..46d406d
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Estes A3T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A3T 13 45 4 0.0033 0.0085 Estes \r
+0.024 0.195\r
+0.048 0.899\r
+0.086 2.658\r
+0.11 4.183\r
+0.14 5.83\r
+0.159 5.395\r
+0.18 4.301\r
+0.199 3.635\r
+0.215 2.736\r
+0.234 2.267\r
+0.258 2.15\r
+0.315 2.072\r
+0.441 1.993\r
+0.554 2.033\r
+0.605 2.072\r
+0.673 1.954\r
+0.764 1.954\r
+0.874 2.072\r
+0.931 2.15\r
+0.953 2.072\r
+0.966 1.719\r
+0.977 1.173\r
+0.993 0.547\r
+1.01 0\r
diff --git a/datafiles/thrustcurves/Estes_A8.eng b/datafiles/thrustcurves/Estes_A8.eng
new file mode 100644 (file)
index 0000000..22ab109
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;Estes A8 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A8 18 70 3-5 0.0033 0.01635 Estes \r
+0.041 0.512\r
+0.084 2.115\r
+0.127 4.358\r
+0.166 6.794\r
+0.192 8.588\r
+0.206 9.294\r
+0.226 9.73\r
+0.236 8.845\r
+0.247 7.179\r
+0.261 5.063\r
+0.277 3.717\r
+0.306 3.205\r
+0.351 2.884\r
+0.405 2.499\r
+0.467 2.371\r
+0.532 2.307\r
+0.589 2.371\r
+0.632 2.371\r
+0.652 2.243\r
+0.668 1.794\r
+0.684 1.153\r
+0.703 0.448\r
+0.73 0\r
diff --git a/datafiles/thrustcurves/Estes_B4.eng b/datafiles/thrustcurves/Estes_B4.eng
new file mode 100644 (file)
index 0000000..64b0998
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+;Estes B4 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+B4 18 70 2-4 0.006 0.0189 Estes \r
+0.02 0.418\r
+0.04 1.673\r
+0.065 4.076\r
+0.085 6.69\r
+0.105 9.304\r
+0.119 11.496\r
+0.136 12.75\r
+0.153 11.916\r
+0.173 10.666\r
+0.187 9.304\r
+0.198 7.214\r
+0.207 5.645\r
+0.226 4.809\r
+0.258 4.182\r
+0.326 3.763\r
+0.422 3.554\r
+0.549 3.345\r
+0.665 3.345\r
+0.776 3.345\r
+0.863 3.345\r
+0.94 3.449\r
+0.991 3.449\r
+1.002 2.404\r
+1.01 1.254\r
+1.03 0\r
diff --git a/datafiles/thrustcurves/Estes_B6.eng b/datafiles/thrustcurves/Estes_B6.eng
new file mode 100644 (file)
index 0000000..b720401
--- /dev/null
@@ -0,0 +1,26 @@
+; Estes B6-0 from NAR data by Mark Koelsch\r
+B6-0 18 70 0 0.0056 0.0156 E\r
+   0.036 1.364\r
+   0.064 2.727\r
+   0.082 4.215\r
+   0.111 6.694\r
+   0.135 9.05\r
+   0.146 9.545\r
+   0.172 11.901\r
+   0.181 12.149\r
+   0.191 11.901\r
+   0.211 9.174\r
+   0.239 7.314\r
+   0.264 6.074\r
+   0.275 5.95\r
+   0.333 5.207\r
+   0.394 4.835\r
+   0.445 4.835\r
+   0.556 4.339\r
+   0.667 4.587\r
+   0.723 4.339\r
+   0.78 4.339\r
+   0.793 4.091\r
+   0.812 2.603\r
+   0.833 1.24\r
+   0.857 0.0\r
diff --git a/datafiles/thrustcurves/Estes_C11.eng b/datafiles/thrustcurves/Estes_C11.eng
new file mode 100644 (file)
index 0000000..7e9a028
--- /dev/null
@@ -0,0 +1,36 @@
+;\r
+;ESTES C11 RASP.ENG file made from NAR published data\r
+;File produced JANUARY 1, 2002\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C11 24 70 0-3-5-7 0.012 0.0353 Estes \r
+0.034 1.692\r
+0.066 3.782\r
+0.107 7.566\r
+0.145 10.946\r
+0.183 14.832\r
+0.214 17.618\r
+0.226 18.213\r
+0.256 20.107\r
+0.281 21.208\r
+0.298 21.73\r
+0.306 20.206\r
+0.323 17.321\r
+0.337 14.931\r
+0.358 13.236\r
+0.385 11.947\r
+0.413 11.65\r
+0.468 10.946\r
+0.539 10.45\r
+0.619 10.648\r
+0.683 10.648\r
+0.715 10.648\r
+0.726 10.053\r
+0.74 8.163\r
+0.758 5.773\r
+0.778 3.185\r
+0.795 1.394\r
+0.81 0\r
diff --git a/datafiles/thrustcurves/Estes_C5.eng b/datafiles/thrustcurves/Estes_C5.eng
new file mode 100644 (file)
index 0000000..a5ff563
--- /dev/null
@@ -0,0 +1,27 @@
+;\r
+;Estes C5 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C5 18 70 3 0.0113 0.0248 Estes \r
+0.042 2.195\r
+0.107 9.118\r
+0.159 16.213\r
+0.21 21.85\r
+0.233 18.407\r
+0.27 13.677\r
+0.289 9.793\r
+0.303 7.092\r
+0.326 5.065\r
+0.401 4.39\r
+0.55 3.883\r
+0.802 3.714\r
+1.026 3.883\r
+1.291 3.883\r
+1.524 4.221\r
+1.683 4.221\r
+1.702 2.195\r
+1.73 0\r
diff --git a/datafiles/thrustcurves/Estes_C6.eng b/datafiles/thrustcurves/Estes_C6.eng
new file mode 100644 (file)
index 0000000..f7cf3a3
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;Estes C6 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C6 18 70 0-3-5-7 0.0108 0.0231 Estes \r
+0.031 0.946\r
+0.092 4.826\r
+0.139 9.936\r
+0.192 14.09\r
+0.209 11.446\r
+0.231 7.381\r
+0.248 6.151\r
+0.292 5.489\r
+0.37 4.921\r
+0.475 4.448\r
+0.671 4.258\r
+0.702 4.542\r
+0.723 4.164\r
+0.85 4.448\r
+1.063 4.353\r
+1.211 4.353\r
+1.242 4.069\r
+1.303 4.258\r
+1.468 4.353\r
+1.656 4.448\r
+1.821 4.448\r
+1.834 2.933\r
+1.847 1.325\r
+1.86 0\r
diff --git a/datafiles/thrustcurves/Estes_D11.eng b/datafiles/thrustcurves/Estes_D11.eng
new file mode 100644 (file)
index 0000000..ca69c1a
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;Estes D11 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D11 24 70 100 0.0245 0.0448 Estes \r
+0.033 2.393\r
+0.084 5.783\r
+0.144 12.17\r
+0.214 20.757\r
+0.261 24.35\r
+0.289 26.01\r
+0.311 23.334\r
+0.325 18.532\r
+0.338 14.536\r
+0.356 12.331\r
+0.398 10.72\r
+0.48 9.303\r
+0.618 8.676\r
+0.761 8.247\r
+0.955 8.209\r
+1.222 7.955\r
+1.402 8.319\r
+1.54 8.291\r
+1.701 8.459\r
+1.784 8.442\r
+1.803 6.239\r
+1.834 3.033\r
+1.86 0\r
diff --git a/datafiles/thrustcurves/Estes_D12.eng b/datafiles/thrustcurves/Estes_D12.eng
new file mode 100644 (file)
index 0000000..0d93929
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+;Estes D12 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D12 24 70 0-3-5-7 0.0211 0.0426 Estes \r
+0.049 2.569\r
+0.116 9.369\r
+0.184 17.275\r
+0.237 24.258\r
+0.282 29.73\r
+0.297 27.01\r
+0.311 22.589\r
+0.322 17.99\r
+0.348 14.126\r
+0.386 12.099\r
+0.442 10.808\r
+0.546 9.876\r
+0.718 9.306\r
+0.879 9.105\r
+1.066 8.901\r
+1.257 8.698\r
+1.436 8.31\r
+1.59 8.294\r
+1.612 4.613\r
+1.65 0\r
diff --git a/datafiles/thrustcurves/Estes_E9.eng b/datafiles/thrustcurves/Estes_E9.eng
new file mode 100644 (file)
index 0000000..c7e56a7
--- /dev/null
@@ -0,0 +1,17 @@
+; Estes E9-0 by Mark Koelsch from NAR data\r
+E9-0 24 95 0 0.0358 0.056799999999999996 E\r
+   0.046 1.913\r
+   0.235 16.696\r
+   0.273 18.435\r
+   0.326 14.957\r
+   0.38 12.174\r
+   0.44 10.435\r
+   0.835 9.043\r
+   1.093 8.87\r
+   1.496 8.696\r
+   1.997 8.696\r
+   2.498 8.696\r
+   3.014 9.217\r
+   3.037 5.043\r
+   3.067 1.217\r
+   3.09 0.0\r
diff --git a/datafiles/thrustcurves/FALSE-apogee.eng b/datafiles/thrustcurves/FALSE-apogee.eng
new file mode 100644 (file)
index 0000000..52aba5c
--- /dev/null
@@ -0,0 +1,18 @@
+; False data to test 1/2/4A-motors
+1/2A3 11 58 0-3-5-7 0.003 0.0067 Apogee 
+0.014 0.241
+0.036 0.895
+0.064 2.618
+0.1 4.82
+0.111 4.133
+0.125 2.687
+0.139 2.307
+; More false data
+1/4A5 11 58 0-3-5-7 0.003 0.0067 Apogee 
+0.014 0.241
+0.036 0.895
+0.064 2.618
+0.1 4.82
+0.111 4.133
+0.125 2.687
+0.139 2.307
diff --git a/datafiles/thrustcurves/GR_K555.eng b/datafiles/thrustcurves/GR_K555.eng
new file mode 100644 (file)
index 0000000..c399712
--- /dev/null
@@ -0,0 +1,26 @@
+;The K555GT "Green Tornado" motor is a green flame, low smoke propellant.\r
+;This reload produces a 9% "K" motor with 1397 N-seconds of total impulse,\r
+;maximum thrust of 645.3 Newtons, and an average thrust of 556 Newtons,\r
+;for a 2.51 second burn time.\r
+K555GT 54 430 1000 0.78 1.52 Gorilla_Motors \r
+0.025 267\r
+0.05 338.2\r
+0.1 471.7\r
+0.12 498.4\r
+0.15 511.75\r
+0.18 522.875\r
+0.2 534\r
+0.7 631.9\r
+0.75 636.35\r
+0.9 645.25\r
+1.15 636.35\r
+1.57 623\r
+1.87 614.1\r
+2.17 600.75\r
+2.25 578.5\r
+2.27 480.6\r
+2.3 356\r
+2.35 178\r
+2.45 66.75\r
+2.51 0\r
+;\r
diff --git a/datafiles/thrustcurves/Hypertek_I130.eng b/datafiles/thrustcurves/Hypertek_I130.eng
new file mode 100644 (file)
index 0000000..7749161
--- /dev/null
@@ -0,0 +1,17 @@
+;hand entered from Cesaroni (Mike Dennett) curve data\r
+;Andrew MacMillen NAR 77472 2/5/02\r
+;NOTE: NOT CTI OR TMT APPROVED\r
+;Hypertek 300CC098J\r
+I130 54 521 100 0.298 1.049 HyperTek \r
+0.05 200\r
+0.1 223\r
+0.5 205\r
+1 187\r
+1.5 169\r
+2 151\r
+2.25 143\r
+2.4 89\r
+2.5 71\r
+3 40\r
+3.5 18\r
+4 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I136.eng b/datafiles/thrustcurves/Hypertek_I136.eng
new file mode 100644 (file)
index 0000000..1cfe087
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;Hypertek I136 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Mike Dennett at Hypertek\r
+I136 54 546 100 0.283 1.001 Hypertek \r
+0.155 256.236\r
+0.5 232.756\r
+1 212.85\r
+1.5 196.005\r
+2 174.976\r
+2.21 163.338\r
+2.4 100.912\r
+2.5 83.813\r
+3 42.468\r
+3.5 19.754\r
+3.7 15.262\r
+3.8 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I145.eng b/datafiles/thrustcurves/Hypertek_I145.eng
new file mode 100644 (file)
index 0000000..633d719
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+;Hypertek I145 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Tripoli Certification - test date 9/8/01\r
+;Not endorsed by TRA or Hypertek\r
+I145 54 546 100 0.311 1.068 Hypertek \r
+0.057 256.195\r
+0.204 253.38\r
+0.497 236.488\r
+1.002 208.334\r
+1.205 199.888\r
+1.376 211.15\r
+1.482 197.073\r
+2.003 177.366\r
+2.125 168.92\r
+2.5 90.091\r
+2.997 45.045\r
+3.282 16.892\r
+3.7 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I205.eng b/datafiles/thrustcurves/Hypertek_I205.eng
new file mode 100644 (file)
index 0000000..c9dbe18
--- /dev/null
@@ -0,0 +1,16 @@
+;\r
+;hand entered from Cesaroni (Mike Dennett) curve data\r
+;Andrew MacMillen NAR 77472 2/5/02\r
+;NOTE: NOT CTI OR TMT APPROVED\r
+;Hypertek 300CC125J\r
+I205 54 521 100 0.298 1.049 HyperTek \r
+0.05 312\r
+0.1 347\r
+0.5 312\r
+1 258\r
+1.35 223\r
+1.6 125\r
+1.75 80\r
+2 45\r
+2.25 22\r
+2.75 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I222.eng b/datafiles/thrustcurves/Hypertek_I222.eng
new file mode 100644 (file)
index 0000000..fe386c6
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+;Hypertek I222 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Mike Dennett at Hypertek\r
+I222 54 546 100 0.28 1.013 Hypertek \r
+0.037 394.146\r
+0.065 439.192\r
+0.12 450.547\r
+0.24 436.734\r
+0.5 411.158\r
+0.66 392.487\r
+1 338.349\r
+1.348 292.639\r
+1.432 259.337\r
+1.5 193.668\r
+1.67 117.056\r
+2 57.357\r
+2.3 22.358\r
+2.4 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I225.eng b/datafiles/thrustcurves/Hypertek_I225.eng
new file mode 100644 (file)
index 0000000..7303e82
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;Hypertek I225 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Tripoli Certification - test date 9/8/01\r
+;Not endorsed by TRA or Hypertek\r
+I225 54 546 100 0.298 1.067 Hypertek \r
+0.012 309.686\r
+0.037 343.47\r
+0.106 351.916\r
+0.244 354.732\r
+0.497 337.84\r
+0.749 320.948\r
+0.998 298.425\r
+1.254 273.087\r
+1.433 239.303\r
+1.502 194.258\r
+1.755 101.352\r
+1.999 53.491\r
+2.211 11.261\r
+2.37 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I260.eng b/datafiles/thrustcurves/Hypertek_I260.eng
new file mode 100644 (file)
index 0000000..1d8c7a1
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;Hypertek I260 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Mike Dennett at Hypertek\r
+I260 54 614 100 0.409 1.296 Hypertek \r
+0.03 339.01\r
+0.041 425.115\r
+0.12 413.854\r
+0.216 394.146\r
+0.354 391.331\r
+0.497 368.808\r
+0.749 346.286\r
+1.002 306.871\r
+1.36 264.641\r
+1.454 228.042\r
+1.502 180.181\r
+1.686 109.798\r
+2.003 50.676\r
+2.2 27.483\r
+2.3 0\r
diff --git a/datafiles/thrustcurves/Hypertek_I310.eng b/datafiles/thrustcurves/Hypertek_I310.eng
new file mode 100644 (file)
index 0000000..4796d1a
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek I310\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I310 54 645 0 0.40096 1.30502 HT\r
+   0.042 465.886\r
+   0.128 450.815\r
+   0.216 438.421\r
+   0.303 443.241\r
+   0.391 433.808\r
+   0.478 415.992\r
+   0.566 406.746\r
+   0.653 380.383\r
+   0.741 385.170\r
+   0.828 372.458\r
+   0.916 358.282\r
+   1.003 348.621\r
+   1.091 337.887\r
+   1.178 333.898\r
+   1.266 303.469\r
+   1.353 301.589\r
+   1.441 268.788\r
+   1.528 222.719\r
+   1.616 155.314\r
+   1.703 112.163\r
+   1.791 80.510\r
+   1.878 58.562\r
+   1.966 41.774\r
+   2.053 30.243\r
+   2.141 21.270\r
+   2.228 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J115.eng b/datafiles/thrustcurves/Hypertek_J115.eng
new file mode 100644 (file)
index 0000000..8efaca4
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J115 (440CC076J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J115 54 614 0 0.411264 1.28218 HT\r
+   0.129 218.303\r
+   0.391 230.563\r
+   0.653 216.171\r
+   0.916 165.676\r
+   1.178 158.834\r
+   1.441 161.888\r
+   1.703 157.955\r
+   1.966 152.977\r
+   2.228 148.337\r
+   2.491 141.919\r
+   2.753 136.970\r
+   3.016 129.152\r
+   3.278 121.815\r
+   3.541 111.971\r
+   3.803 79.163\r
+   4.066 53.433\r
+   4.328 42.975\r
+   4.591 38.391\r
+   4.853 33.418\r
+   5.116 28.709\r
+   5.378 23.886\r
+   5.641 19.658\r
+   5.903 15.894\r
+   6.166 11.955\r
+   6.428 9.151\r
+   6.691 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J120.eng b/datafiles/thrustcurves/Hypertek_J120.eng
new file mode 100644 (file)
index 0000000..7bf3181
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J120 (440CC076JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J120 54 614 0 0.442176 1.29338 HT\r
+   0.134 232.707\r
+   0.405 264.084\r
+   0.676 230.699\r
+   0.948 185.971\r
+   1.220 174.226\r
+   1.491 173.853\r
+   1.763 165.828\r
+   2.034 158.016\r
+   2.305 152.389\r
+   2.577 143.399\r
+   2.849 135.969\r
+   3.120 129.537\r
+   3.392 124.822\r
+   3.664 118.872\r
+   3.934 109.922\r
+   4.206 69.777\r
+   4.478 47.837\r
+   4.749 40.178\r
+   5.021 35.768\r
+   5.293 31.265\r
+   5.564 26.359\r
+   5.835 21.215\r
+   6.107 17.175\r
+   6.378 12.931\r
+   6.650 9.463\r
+   6.922 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J150.eng b/datafiles/thrustcurves/Hypertek_J150.eng
new file mode 100644 (file)
index 0000000..b29e8ba
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J150\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J150 54 645 0 0.428288 1.30592 HT\r
+   0.111 177.498\r
+   0.336 193.696\r
+   0.561 200.136\r
+   0.786 204.034\r
+   1.011 200.531\r
+   1.236 197.233\r
+   1.461 192.706\r
+   1.686 189.854\r
+   1.911 185.892\r
+   2.136 183.117\r
+   2.361 179.325\r
+   2.586 174.178\r
+   2.813 171.123\r
+   3.039 164.933\r
+   3.264 160.032\r
+   3.489 154.604\r
+   3.714 148.653\r
+   3.939 92.092\r
+   4.164 55.325\r
+   4.389 42.913\r
+   4.614 32.903\r
+   4.839 24.742\r
+   5.064 16.445\r
+   5.289 8.527\r
+   5.515 4.923\r
+   5.741 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J170.eng b/datafiles/thrustcurves/Hypertek_J170.eng
new file mode 100644 (file)
index 0000000..c6c056c
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J170 (440CC098J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J170 54 614 0 0.4032 1.28218 HT\r
+   0.092 315.198\r
+   0.278 351.486\r
+   0.466 314.152\r
+   0.653 255.278\r
+   0.841 235.396\r
+   1.027 234.785\r
+   1.214 230.871\r
+   1.401 223.051\r
+   1.589 217.688\r
+   1.776 209.940\r
+   1.962 203.806\r
+   2.149 197.520\r
+   2.336 191.243\r
+   2.524 178.598\r
+   2.711 129.785\r
+   2.898 82.459\r
+   3.084 71.693\r
+   3.272 64.633\r
+   3.459 54.015\r
+   3.647 45.022\r
+   3.833 36.373\r
+   4.020 28.397\r
+   4.207 21.518\r
+   4.395 16.072\r
+   4.582 11.712\r
+   4.770 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J190.eng b/datafiles/thrustcurves/Hypertek_J190.eng
new file mode 100644 (file)
index 0000000..78b4ef1
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J190 (440CC098JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J190 54 614 0 0.439488 1.29338 HT\r
+   0.095 338.583\r
+   0.287 384.416\r
+   0.481 341.472\r
+   0.674 279.175\r
+   0.867 267.528\r
+   1.060 256.325\r
+   1.254 250.108\r
+   1.447 244.404\r
+   1.640 238.846\r
+   1.833 236.505\r
+   2.026 232.026\r
+   2.219 223.962\r
+   2.413 213.236\r
+   2.606 201.661\r
+   2.799 150.523\r
+   2.992 101.327\r
+   3.185 84.001\r
+   3.378 73.902\r
+   3.571 60.222\r
+   3.765 49.208\r
+   3.958 39.096\r
+   4.151 29.873\r
+   4.344 22.600\r
+   4.538 16.842\r
+   4.731 11.964\r
+   4.925 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J220.eng b/datafiles/thrustcurves/Hypertek_J220.eng
new file mode 100644 (file)
index 0000000..350cceb
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J220\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J220 54 645 0 0.417984 1.30502 HT\r
+   0.071 277.975\r
+   0.215 271.735\r
+   0.358 289.851\r
+   0.502 298.227\r
+   0.647 293.803\r
+   0.792 290.609\r
+   0.935 283.211\r
+   1.079 276.013\r
+   1.223 271.808\r
+   1.368 269.774\r
+   1.513 262.986\r
+   1.656 257.451\r
+   1.800 253.286\r
+   1.944 245.781\r
+   2.089 239.739\r
+   2.233 230.852\r
+   2.377 220.234\r
+   2.521 159.239\r
+   2.665 97.180\r
+   2.809 73.147\r
+   2.954 58.766\r
+   3.098 48.973\r
+   3.242 37.549\r
+   3.385 27.410\r
+   3.530 19.267\r
+   3.675 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J250.eng b/datafiles/thrustcurves/Hypertek_J250.eng
new file mode 100644 (file)
index 0000000..b9e947b
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J250 (440CC125J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J250 54 614 0 0.40768 1.29248 HT\r
+   0.064 409.711\r
+   0.194 453.238\r
+   0.324 416.509\r
+   0.454 383.773\r
+   0.584 359.202\r
+   0.715 343.963\r
+   0.845 336.331\r
+   0.975 328.849\r
+   1.105 318.614\r
+   1.235 309.097\r
+   1.366 306.155\r
+   1.496 290.597\r
+   1.626 283.180\r
+   1.756 261.190\r
+   1.886 200.168\r
+   2.017 143.646\r
+   2.147 126.521\r
+   2.277 113.229\r
+   2.407 91.310\r
+   2.538 71.216\r
+   2.668 54.183\r
+   2.798 40.347\r
+   2.928 29.057\r
+   3.058 20.139\r
+   3.190 13.793\r
+   3.321 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J250_1.eng b/datafiles/thrustcurves/Hypertek_J250_1.eng
new file mode 100644 (file)
index 0000000..b7637a5
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J250\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J250 54 645 0 0.404992 1.30637 HT\r
+   0.055 356.092\r
+   0.168 316.638\r
+   0.281 357.597\r
+   0.395 351.765\r
+   0.508 354.216\r
+   0.622 354.162\r
+   0.735 338.625\r
+   0.849 332.051\r
+   0.963 323.651\r
+   1.076 315.678\r
+   1.190 305.773\r
+   1.303 298.769\r
+   1.417 288.922\r
+   1.530 293.337\r
+   1.644 276.552\r
+   1.757 269.543\r
+   1.871 223.360\r
+   1.984 131.511\r
+   2.098 98.246\r
+   2.211 76.331\r
+   2.325 60.095\r
+   2.439 47.691\r
+   2.552 36.215\r
+   2.666 26.693\r
+   2.779 20.007\r
+   2.893 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J270.eng b/datafiles/thrustcurves/Hypertek_J270.eng
new file mode 100644 (file)
index 0000000..86dae58
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J270 (440CC125JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J270 54 614 0 0.419776 1.29606 HT\r
+   0.064 438.643\r
+   0.193 498.603\r
+   0.322 468.869\r
+   0.451 438.261\r
+   0.581 412.686\r
+   0.711 390.684\r
+   0.841 376.193\r
+   0.970 362.205\r
+   1.100 347.649\r
+   1.230 333.459\r
+   1.359 324.401\r
+   1.489 311.483\r
+   1.619 298.076\r
+   1.749 278.397\r
+   1.878 220.239\r
+   2.007 150.276\r
+   2.137 125.603\r
+   2.268 121.989\r
+   2.397 91.398\r
+   2.526 71.671\r
+   2.656 55.779\r
+   2.786 41.822\r
+   2.916 30.460\r
+   3.045 22.243\r
+   3.175 16.420\r
+   3.305 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J295.eng b/datafiles/thrustcurves/Hypertek_J295.eng
new file mode 100644 (file)
index 0000000..f9a28fe
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;Hypertek J295 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Tripoli Certification - test date 9/8/01\r
+;Not endorsed by TRA or Hypertek\r
+J295 54 614 100 0.409 1.31 Hypertek \r
+0.004 467.345\r
+0.244 461.714\r
+0.501 416.669\r
+1.002 377.254\r
+1.254 343.47\r
+1.364 315.317\r
+1.502 219.596\r
+1.751 112.613\r
+2.003 50.676\r
+2.2 0\r
diff --git a/datafiles/thrustcurves/Hypertek_J317.eng b/datafiles/thrustcurves/Hypertek_J317.eng
new file mode 100644 (file)
index 0000000..668b42b
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J317O (835CC172J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J317O 81 552 0 0.704256 1.7575 HT\r
+   0.071 438.483\r
+   0.215 471.024\r
+   0.358 459.716\r
+   0.502 447.348\r
+   0.647 431.653\r
+   0.792 418.545\r
+   0.935 407.806\r
+   1.079 400.212\r
+   1.223 395.752\r
+   1.368 382.516\r
+   1.513 372.890\r
+   1.656 368.033\r
+   1.800 349.298\r
+   1.944 336.071\r
+   2.089 324.486\r
+   2.233 301.205\r
+   2.377 233.601\r
+   2.521 176.972\r
+   2.665 132.539\r
+   2.809 96.229\r
+   2.954 69.718\r
+   3.098 49.457\r
+   3.242 33.983\r
+   3.385 23.063\r
+   3.530 16.524\r
+   3.675 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J330.eng b/datafiles/thrustcurves/Hypertek_J330.eng
new file mode 100644 (file)
index 0000000..cebede1
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek J330O (835CC172JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J330O 81 552 0 0.727104 1.77722 HT\r
+   0.068 453.500\r
+   0.206 485.741\r
+   0.345 476.873\r
+   0.483 463.622\r
+   0.623 439.951\r
+   0.761 423.260\r
+   0.900 426.386\r
+   1.040 415.245\r
+   1.178 443.371\r
+   1.317 431.352\r
+   1.456 407.015\r
+   1.595 392.143\r
+   1.733 390.332\r
+   1.872 360.092\r
+   2.010 334.240\r
+   2.150 307.215\r
+   2.289 225.611\r
+   2.427 169.224\r
+   2.567 126.562\r
+   2.705 93.167\r
+   2.844 68.293\r
+   2.983 48.099\r
+   3.122 32.856\r
+   3.260 21.857\r
+   3.400 15.193\r
+   3.540 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_J330_1.eng b/datafiles/thrustcurves/Hypertek_J330_1.eng
new file mode 100644 (file)
index 0000000..ab68026
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek J330 (835/54-172-J)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J330 54 787 0 0.73024 1.59936 HT\r
+   0.068 453.500\r
+   0.206 485.741\r
+   0.345 476.873\r
+   0.483 463.622\r
+   0.623 439.951\r
+   0.761 423.260\r
+   0.900 426.386\r
+   1.040 415.245\r
+   1.178 443.371\r
+   1.317 431.352\r
+   1.456 407.015\r
+   1.595 392.143\r
+   1.733 390.332\r
+   1.872 360.092\r
+   2.010 334.240\r
+   2.150 307.215\r
+   2.289 225.611\r
+   2.427 169.224\r
+   2.567 126.562\r
+   2.705 93.167\r
+   2.844 68.293\r
+   2.983 48.099\r
+   3.122 32.856\r
+   3.260 21.857\r
+   3.400 15.193\r
+   3.540 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_K240.eng b/datafiles/thrustcurves/Hypertek_K240.eng
new file mode 100644 (file)
index 0000000..90b8310
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek K240\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K240 81 552 0 0.789376 1.80723 HT\r
+   0.131 278.007\r
+   0.396 329.552\r
+   0.660 338.024\r
+   0.925 334.092\r
+   1.191 326.199\r
+   1.456 319.745\r
+   1.721 315.195\r
+   1.985 311.182\r
+   2.250 302.916\r
+   2.516 305.943\r
+   2.781 289.975\r
+   3.046 281.781\r
+   3.310 273.330\r
+   3.575 268.852\r
+   3.841 255.702\r
+   4.106 251.068\r
+   4.371 234.820\r
+   4.635 159.972\r
+   4.900 96.543\r
+   5.166 73.367\r
+   5.431 55.477\r
+   5.696 40.928\r
+   5.960 29.542\r
+   6.225 21.250\r
+   6.491 14.787\r
+   6.756 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L200.eng b/datafiles/thrustcurves/Hypertek_L200.eng
new file mode 100644 (file)
index 0000000..e9b9594
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L200 (1685CC098L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L200 111 724 0 1.59398 3.89491 HT\r
+   0.292 310.935\r
+   0.877 311.934\r
+   1.464 284.574\r
+   2.050 259.236\r
+   2.636 245.149\r
+   3.223 240.798\r
+   3.809 246.021\r
+   4.396 251.509\r
+   4.981 255.559\r
+   5.568 250.045\r
+   6.154 242.343\r
+   6.741 236.221\r
+   7.327 230.527\r
+   7.914 224.062\r
+   8.500 218.240\r
+   9.086 212.215\r
+   9.673 189.706\r
+   10.258 94.608\r
+   10.845 67.128\r
+   11.431 53.350\r
+   12.018 41.550\r
+   12.604 31.112\r
+   13.191 22.445\r
+   13.777 16.763\r
+   14.364 10.892\r
+   14.950 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L225.eng b/datafiles/thrustcurves/Hypertek_L225.eng
new file mode 100644 (file)
index 0000000..6d6fdfc
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L225 (1685CC098LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L225 111 724 0 1.66118 3.94822 HT\r
+   0.271 380.609\r
+   0.815 364.078\r
+   1.359 347.671\r
+   1.904 311.980\r
+   2.449 292.842\r
+   2.994 284.620\r
+   3.539 284.123\r
+   4.083 295.754\r
+   4.628 286.654\r
+   5.173 271.822\r
+   5.718 258.225\r
+   6.263 249.357\r
+   6.807 240.396\r
+   7.352 232.666\r
+   7.897 226.844\r
+   8.442 217.601\r
+   8.986 208.639\r
+   9.531 122.739\r
+   10.076 74.667\r
+   10.621 60.930\r
+   11.166 48.898\r
+   11.710 38.996\r
+   12.255 29.719\r
+   12.800 21.925\r
+   13.345 16.360\r
+   13.890 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L350.eng b/datafiles/thrustcurves/Hypertek_L350.eng
new file mode 100644 (file)
index 0000000..e529c7a
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L350 (1685CC125L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L350 111 724 0 1.6025 3.90342 HT\r
+   0.188 592.182\r
+   0.566 598.015\r
+   0.945 479.774\r
+   1.324 427.223\r
+   1.702 406.561\r
+   2.080 395.128\r
+   2.459 393.279\r
+   2.839 410.729\r
+   3.217 517.335\r
+   3.595 506.496\r
+   3.974 439.490\r
+   4.353 391.732\r
+   4.731 463.388\r
+   5.109 446.876\r
+   5.489 436.237\r
+   5.868 387.456\r
+   6.246 247.342\r
+   6.624 147.845\r
+   7.003 117.459\r
+   7.382 93.081\r
+   7.760 73.034\r
+   8.139 55.605\r
+   8.518 40.854\r
+   8.897 29.575\r
+   9.276 21.627\r
+   9.655 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L355.eng b/datafiles/thrustcurves/Hypertek_L355.eng
new file mode 100644 (file)
index 0000000..d58dc13
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L355 (1685CC125LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L355 111 724 0 1.61952 3.95405 HT\r
+   0.185 638.681\r
+   0.559 623.792\r
+   0.933 538.081\r
+   1.307 493.173\r
+   1.682 465.144\r
+   2.056 432.702\r
+   2.430 410.644\r
+   2.804 388.862\r
+   3.178 395.452\r
+   3.553 384.851\r
+   3.927 370.103\r
+   4.301 356.484\r
+   4.675 346.195\r
+   5.049 342.684\r
+   5.424 325.430\r
+   5.798 317.798\r
+   6.172 272.453\r
+   6.546 156.214\r
+   6.920 117.579\r
+   7.295 93.203\r
+   7.669 73.056\r
+   8.043 56.997\r
+   8.417 42.994\r
+   8.791 30.338\r
+   9.166 21.725\r
+   9.541 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L475.eng b/datafiles/thrustcurves/Hypertek_L475.eng
new file mode 100644 (file)
index 0000000..46bd6e3
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L475 (1685CC172L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L475 111 724 0 1.52992 3.89805 HT\r
+   0.129 640.755\r
+   0.391 638.069\r
+   0.652 619.018\r
+   0.914 609.661\r
+   1.176 597.937\r
+   1.437 596.251\r
+   1.699 594.039\r
+   1.961 572.245\r
+   2.223 578.304\r
+   2.484 589.224\r
+   2.747 578.752\r
+   3.008 612.426\r
+   3.270 646.188\r
+   3.531 674.617\r
+   3.793 652.574\r
+   4.055 555.384\r
+   4.317 299.749\r
+   4.578 220.284\r
+   4.841 170.007\r
+   5.102 127.011\r
+   5.364 94.998\r
+   5.626 70.208\r
+   5.888 49.458\r
+   6.149 32.102\r
+   6.411 18.382\r
+   6.674 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L535.eng b/datafiles/thrustcurves/Hypertek_L535.eng
new file mode 100644 (file)
index 0000000..9b2449a
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L535 (1685CC172LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L535 111 724 0 1.59667 3.94822 HT\r
+   0.119 838.518\r
+   0.358 768.667\r
+   0.598 727.551\r
+   0.838 745.172\r
+   1.077 712.851\r
+   1.317 694.613\r
+   1.556 672.145\r
+   1.796 656.726\r
+   2.035 685.070\r
+   2.275 808.735\r
+   2.515 798.521\r
+   2.754 755.277\r
+   2.995 726.985\r
+   3.235 699.331\r
+   3.475 670.049\r
+   3.715 515.315\r
+   3.954 293.947\r
+   4.194 227.905\r
+   4.433 180.167\r
+   4.673 140.714\r
+   4.912 107.564\r
+   5.152 80.907\r
+   5.392 58.898\r
+   5.631 39.782\r
+   5.872 24.901\r
+   6.113 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L540.eng b/datafiles/thrustcurves/Hypertek_L540.eng
new file mode 100644 (file)
index 0000000..ea371f7
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L540O (2800CC172L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L540O 111 876 0 2.5303 5.656 HT\r
+   0.191 685.548\r
+   0.574 665.171\r
+   0.957 635.467\r
+   1.341 634.122\r
+   1.725 656.225\r
+   2.109 706.931\r
+   2.493 696.526\r
+   2.876 777.726\r
+   3.260 775.919\r
+   3.645 781.611\r
+   4.028 712.736\r
+   4.411 695.555\r
+   4.796 701.251\r
+   5.180 645.985\r
+   5.564 607.757\r
+   5.947 546.408\r
+   6.331 387.372\r
+   6.716 234.214\r
+   7.099 181.086\r
+   7.482 139.253\r
+   7.867 106.109\r
+   8.251 78.293\r
+   8.634 54.851\r
+   9.018 36.384\r
+   9.402 20.375\r
+   9.786 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L540_1.eng b/datafiles/thrustcurves/Hypertek_L540_1.eng
new file mode 100644 (file)
index 0000000..daf00e1
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek L540 (2800/75-172-L)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L540 75 1387 0 2.52224 5.05792 HT\r
+   0.191 685.548\r
+   0.574 665.171\r
+   0.957 635.467\r
+   1.341 634.122\r
+   1.725 656.225\r
+   2.109 706.931\r
+   2.493 696.526\r
+   2.876 777.726\r
+   3.260 775.919\r
+   3.645 781.611\r
+   4.028 712.736\r
+   4.411 695.555\r
+   4.796 701.251\r
+   5.180 645.985\r
+   5.564 607.757\r
+   5.947 546.408\r
+   6.331 387.372\r
+   6.716 234.214\r
+   7.099 181.086\r
+   7.482 139.253\r
+   7.867 106.109\r
+   8.251 78.293\r
+   8.634 54.851\r
+   9.018 36.384\r
+   9.402 20.375\r
+   9.786 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L550.eng b/datafiles/thrustcurves/Hypertek_L550.eng
new file mode 100644 (file)
index 0000000..0f3f217
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L550 (1685CCRGL)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L550 111 724 0 1.53261 3.89805 HT\r
+   0.124 816.849\r
+   0.375 796.043\r
+   0.626 781.861\r
+   0.877 767.440\r
+   1.129 759.627\r
+   1.380 735.948\r
+   1.631 714.454\r
+   1.883 701.582\r
+   2.134 674.667\r
+   2.385 656.493\r
+   2.637 636.076\r
+   2.889 612.409\r
+   3.140 587.801\r
+   3.391 567.170\r
+   3.642 559.971\r
+   3.894 534.157\r
+   4.145 444.562\r
+   4.396 280.510\r
+   4.648 216.702\r
+   4.899 163.136\r
+   5.150 120.571\r
+   5.402 86.544\r
+   5.653 59.990\r
+   5.904 39.527\r
+   6.156 25.914\r
+   6.408 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L570.eng b/datafiles/thrustcurves/Hypertek_L570.eng
new file mode 100644 (file)
index 0000000..b6bbdb8
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L570O (2800CC172LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L570O 111 876 0 2.57734 5.70618 HT\r
+   0.181 793.916\r
+   0.547 800.029\r
+   0.914 811.765\r
+   1.280 761.283\r
+   1.647 725.674\r
+   2.014 733.246\r
+   2.380 783.159\r
+   2.747 795.348\r
+   3.112 823.178\r
+   3.478 831.812\r
+   3.845 805.614\r
+   4.211 780.534\r
+   4.578 741.917\r
+   4.945 628.980\r
+   5.311 547.886\r
+   5.678 537.830\r
+   6.044 330.850\r
+   6.409 230.792\r
+   6.776 180.510\r
+   7.143 140.226\r
+   7.509 108.348\r
+   7.876 81.342\r
+   8.243 59.608\r
+   8.609 41.592\r
+   8.976 25.536\r
+   9.343 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L570_1.eng b/datafiles/thrustcurves/Hypertek_L570_1.eng
new file mode 100644 (file)
index 0000000..f239320
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek L570 (2800/75-172-L-FX)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L570 75 1387 0 2.57152 5.10272 HT\r
+   0.181 793.916\r
+   0.547 800.029\r
+   0.914 811.765\r
+   1.280 761.283\r
+   1.647 725.674\r
+   2.014 733.246\r
+   2.380 783.159\r
+   2.747 795.348\r
+   3.112 823.178\r
+   3.478 831.812\r
+   3.845 805.614\r
+   4.211 780.534\r
+   4.578 741.917\r
+   4.945 628.980\r
+   5.311 547.886\r
+   5.678 537.830\r
+   6.044 330.850\r
+   6.409 230.792\r
+   6.776 180.510\r
+   7.143 140.226\r
+   7.509 108.348\r
+   7.876 81.342\r
+   8.243 59.608\r
+   8.609 41.592\r
+   8.976 25.536\r
+   9.343 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L575.eng b/datafiles/thrustcurves/Hypertek_L575.eng
new file mode 100644 (file)
index 0000000..fb23337
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L575O (2800CCRGL)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L575O 111 876 0 2.52134 5.65286 HT\r
+   0.190 705.343\r
+   0.572 723.437\r
+   0.955 738.414\r
+   1.337 749.383\r
+   1.720 735.169\r
+   2.103 725.088\r
+   2.486 733.751\r
+   2.869 700.454\r
+   3.251 690.771\r
+   3.634 682.897\r
+   4.017 674.825\r
+   4.399 687.463\r
+   4.782 675.411\r
+   5.166 645.685\r
+   5.548 643.612\r
+   5.930 634.693\r
+   6.314 559.731\r
+   6.696 304.009\r
+   7.078 229.423\r
+   7.461 167.643\r
+   7.845 121.036\r
+   8.227 83.673\r
+   8.609 54.311\r
+   8.993 33.029\r
+   9.376 19.886\r
+   9.759 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L575_1.eng b/datafiles/thrustcurves/Hypertek_L575_1.eng
new file mode 100644 (file)
index 0000000..e724caa
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek L575 (2800/75-RG-L)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L575 75 1387 0 2.51328 5.06688 HT\r
+   0.190 705.343\r
+   0.572 723.437\r
+   0.955 738.414\r
+   1.337 749.383\r
+   1.720 735.169\r
+   2.103 725.088\r
+   2.486 733.751\r
+   2.869 700.454\r
+   3.251 690.771\r
+   3.634 682.897\r
+   4.017 674.825\r
+   4.399 687.463\r
+   4.782 675.411\r
+   5.166 645.685\r
+   5.548 643.612\r
+   5.930 634.693\r
+   6.314 559.731\r
+   6.696 304.009\r
+   7.078 229.423\r
+   7.461 167.643\r
+   7.845 121.036\r
+   8.227 83.673\r
+   8.609 54.311\r
+   8.993 33.029\r
+   9.376 19.886\r
+   9.759 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L610.eng b/datafiles/thrustcurves/Hypertek_L610.eng
new file mode 100644 (file)
index 0000000..d7ed8e4
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L610 (1685CCRGLFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L610 111 724 0 1.57696 3.95091 HT\r
+   0.110 850.823\r
+   0.333 837.700\r
+   0.556 775.061\r
+   0.779 739.656\r
+   1.002 807.273\r
+   1.225 809.462\r
+   1.448 801.752\r
+   1.671 789.534\r
+   1.894 763.842\r
+   2.117 858.087\r
+   2.340 890.644\r
+   2.563 837.593\r
+   2.785 749.631\r
+   3.008 648.961\r
+   3.231 643.064\r
+   3.454 637.413\r
+   3.677 431.325\r
+   3.900 276.205\r
+   4.123 220.930\r
+   4.346 166.632\r
+   4.569 124.031\r
+   4.792 89.721\r
+   5.015 64.295\r
+   5.237 45.360\r
+   5.461 30.400\r
+   5.685 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L625.eng b/datafiles/thrustcurves/Hypertek_L625.eng
new file mode 100644 (file)
index 0000000..0cab431
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek L625O (2800CCRGLFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L625O 111 876 0 2.56614 5.70618 HT\r
+   0.169 855.024\r
+   0.509 896.169\r
+   0.851 905.926\r
+   1.193 902.118\r
+   1.534 853.016\r
+   1.876 953.210\r
+   2.218 904.246\r
+   2.559 856.525\r
+   2.901 743.522\r
+   3.243 758.646\r
+   3.584 751.619\r
+   3.926 745.737\r
+   4.267 751.907\r
+   4.607 728.305\r
+   4.949 691.703\r
+   5.291 658.843\r
+   5.632 504.450\r
+   5.974 279.745\r
+   6.316 216.661\r
+   6.657 164.957\r
+   6.999 123.426\r
+   7.341 89.428\r
+   7.682 62.955\r
+   8.024 43.519\r
+   8.366 27.956\r
+   8.707 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L625_1.eng b/datafiles/thrustcurves/Hypertek_L625_1.eng
new file mode 100644 (file)
index 0000000..d89ebe5
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek L625 (2800/75-RG-L-FX)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L625 75 1387 0 2.56256 5.11616 HT\r
+   0.169 855.024\r
+   0.509 896.169\r
+   0.851 905.926\r
+   1.193 902.118\r
+   1.534 853.016\r
+   1.876 953.210\r
+   2.218 904.246\r
+   2.559 856.525\r
+   2.901 743.522\r
+   3.243 758.646\r
+   3.584 751.619\r
+   3.926 745.737\r
+   4.267 751.907\r
+   4.607 728.305\r
+   4.949 691.703\r
+   5.291 658.843\r
+   5.632 504.450\r
+   5.974 279.745\r
+   6.316 216.661\r
+   6.657 164.957\r
+   6.999 123.426\r
+   7.341 89.428\r
+   7.682 62.955\r
+   8.024 43.519\r
+   8.366 27.956\r
+   8.707 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_L740.eng b/datafiles/thrustcurves/Hypertek_L740.eng
new file mode 100644 (file)
index 0000000..c4bca42
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;\r
+L740 75 1422.4 100 2.667 6.416 HyperTek \r
+0.02 767.76\r
+0.05 1084.9\r
+0.07 1166.87\r
+0.09 1198.96\r
+0.12 1183.37\r
+0.38 1088.96\r
+0.63 1139.56\r
+0.89 1130.73\r
+1.15 1109\r
+1.4 1096.6\r
+1.66 1048.51\r
+1.92 1026.67\r
+2.18 980.7\r
+2.43 949.74\r
+2.69 909.63\r
+2.95 893.62\r
+3.21 866.64\r
+3.46 825.26\r
+3.72 820.05\r
+3.98 789.78\r
+4.24 874.08\r
+4.49 804.36\r
+4.75 738.04\r
+5.01 383.77\r
+5.38 255.83\r
+5.75 183.27\r
+6.13 130.48\r
+6.5 94.03\r
+6.8 0\r
diff --git a/datafiles/thrustcurves/Hypertek_L970.eng b/datafiles/thrustcurves/Hypertek_L970.eng
new file mode 100644 (file)
index 0000000..282a8e5
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;\r
+L970 75 1422.4 100 2.532 6.323 HyperTek \r
+0.01 534.31\r
+0.02 1007.71\r
+0.03 1320.67\r
+0.04 1463.43\r
+0.05 1482.44\r
+0.25 1315.87\r
+0.44 1362.79\r
+0.64 1441.44\r
+0.84 1452.58\r
+1.04 1418.39\r
+1.23 1403.65\r
+1.43 1337.52\r
+1.63 1311.22\r
+1.83 1257.09\r
+2.02 1279.26\r
+2.22 1229.81\r
+2.42 1174.23\r
+2.62 1162.77\r
+2.81 1122.04\r
+3.02 1108.55\r
+3.21 1058.31\r
+3.41 981.23\r
+3.61 959.35\r
+3.8 778.83\r
+4.09 437.55\r
+4.38 294.3\r
+4.66 194.71\r
+4.94 129.33\r
+5.23 86.31\r
+5.23 0\r
diff --git a/datafiles/thrustcurves/Hypertek_M1000.eng b/datafiles/thrustcurves/Hypertek_M1000.eng
new file mode 100644 (file)
index 0000000..7e661db
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek M1000O (4630CCRGM)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1000O 111 1147 0 4.17446 8.90714 HT\r
+   0.197 1368.441\r
+   0.593 1448.113\r
+   0.989 1482.178\r
+   1.384 1431.137\r
+   1.780 1410.278\r
+   2.176 1399.905\r
+   2.573 1365.973\r
+   2.970 1338.653\r
+   3.366 1295.695\r
+   3.761 1280.192\r
+   4.157 1235.621\r
+   4.553 1212.944\r
+   4.950 1196.996\r
+   5.347 1172.466\r
+   5.743 1129.416\r
+   6.139 1051.999\r
+   6.534 635.308\r
+   6.930 474.427\r
+   7.327 359.958\r
+   7.724 272.962\r
+   8.120 205.206\r
+   8.516 149.099\r
+   8.911 103.639\r
+   9.307 70.124\r
+   9.704 48.706\r
+   10.101 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_M1000_1.eng b/datafiles/thrustcurves/Hypertek_M1000_1.eng
new file mode 100644 (file)
index 0000000..b1ae51b
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek M1000 (4630/98-RG-M)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1000 98 1405 0 4.17536 8.72704 HT\r
+   0.197 1368.441\r
+   0.593 1448.113\r
+   0.989 1482.178\r
+   1.384 1431.137\r
+   1.780 1410.278\r
+   2.176 1399.905\r
+   2.573 1365.973\r
+   2.970 1338.653\r
+   3.366 1295.695\r
+   3.761 1280.192\r
+   4.157 1235.621\r
+   4.553 1212.944\r
+   4.950 1196.996\r
+   5.347 1172.466\r
+   5.743 1129.416\r
+   6.139 1051.999\r
+   6.534 635.308\r
+   6.930 474.427\r
+   7.327 359.958\r
+   7.724 272.962\r
+   8.120 205.206\r
+   8.516 149.099\r
+   8.911 103.639\r
+   9.307 70.124\r
+   9.704 48.706\r
+   10.101 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_M1001.eng b/datafiles/thrustcurves/Hypertek_M1001.eng
new file mode 100644 (file)
index 0000000..43c01a1
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;\r
+M1001 98 1493.5 100 5.161 10.092 HyperTek \r
+0.04 1394.15\r
+0.08 1440.23\r
+0.12 1322.3\r
+0.16 1328.89\r
+0.2 1340.76\r
+0.57 1411.23\r
+0.95 1420.87\r
+1.32 1415.98\r
+1.69 1404.61\r
+2.07 1384.44\r
+2.44 1370.95\r
+2.82 1354.19\r
+3.19 1318.56\r
+3.57 1326.82\r
+3.94 1338.4\r
+4.32 1247.32\r
+4.69 1287.12\r
+5.07 1220.48\r
+5.44 1123.54\r
+5.82 1075.29\r
+6.19 1078.11\r
+6.56 996.41\r
+6.94 953.17\r
+7.31 676.72\r
+7.83 419.5\r
+8.35 285.78\r
+8.87 192.18\r
+9.39 128.7\r
+9.87 0\r
diff --git a/datafiles/thrustcurves/Hypertek_M1010.eng b/datafiles/thrustcurves/Hypertek_M1010.eng
new file mode 100644 (file)
index 0000000..c01545b
--- /dev/null
@@ -0,0 +1,30 @@
+; HyperTek M1010O (4630CCRGMFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1010O 111 1147 0 4.27571 8.99987 HT\r
+   0.199 1473.475\r
+   0.599 1472.868\r
+   0.999 1463.740\r
+   1.399 1380.769\r
+   1.799 1408.210\r
+   2.199 1383.970\r
+   2.599 1332.771\r
+   2.999 1356.808\r
+   3.399 1339.075\r
+   3.799 1306.425\r
+   4.199 1266.222\r
+   4.599 1223.656\r
+   4.999 1190.978\r
+   5.399 1145.190\r
+   5.799 1103.440\r
+   6.199 1060.790\r
+   6.599 696.828\r
+   6.999 487.469\r
+   7.399 377.853\r
+   7.799 288.215\r
+   8.199 221.430\r
+   8.599 166.674\r
+   8.999 123.710\r
+   9.399 90.168\r
+   9.800 64.566\r
+   10.201 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_M1010_1.eng b/datafiles/thrustcurves/Hypertek_M1010_1.eng
new file mode 100644 (file)
index 0000000..465642f
--- /dev/null
@@ -0,0 +1,29 @@
+; HyperTek M1010 (4630/98-RG-M-FX)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1010 98 1405 0 4.23808 8.82112 HT\r
+   0.199 1473.475\r
+   0.599 1472.868\r
+   0.999 1463.740\r
+   1.399 1380.769\r
+   1.799 1408.210\r
+   2.199 1383.970\r
+   2.599 1332.771\r
+   2.999 1356.808\r
+   3.399 1339.075\r
+   3.799 1306.425\r
+   4.199 1266.222\r
+   4.599 1223.656\r
+   4.999 1190.978\r
+   5.399 1145.190\r
+   5.799 1103.440\r
+   6.199 1060.790\r
+   6.599 696.828\r
+   6.999 487.469\r
+   7.399 377.853\r
+   7.799 288.215\r
+   8.199 221.430\r
+   8.599 166.674\r
+   8.999 123.710\r
+   9.399 90.168\r
+   9.800 64.566\r
+   10.201 0.000\r
diff --git a/datafiles/thrustcurves/Hypertek_M1015.eng b/datafiles/thrustcurves/Hypertek_M1015.eng
new file mode 100644 (file)
index 0000000..5356c39
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;\r
+M1015 98 1150.6 100 3.25 7.158 HyperTek \r
+0.04 1515.05\r
+0.08 1634.37\r
+0.12 1566.94\r
+0.16 1507.27\r
+0.2 1476.97\r
+0.41 1418.21\r
+0.63 1420.43\r
+0.85 1436.22\r
+1.06 1405.26\r
+1.28 1371.12\r
+1.49 1355\r
+1.71 1320.03\r
+1.93 1286.83\r
+2.14 1268.82\r
+2.36 1267.85\r
+2.58 1281.29\r
+2.79 1248.5\r
+3.01 1268.31\r
+3.23 1273.39\r
+3.44 1298.31\r
+3.66 1213.66\r
+3.87 1167.24\r
+4.09 1134.88\r
+4.31 1121.39\r
+4.69 544.64\r
+5.07 363.55\r
+5.46 234.8\r
+5.84 149.45\r
+6.22 94.73\r
+6.23 0\r
diff --git a/datafiles/thrustcurves/Hypertek_M1040.eng b/datafiles/thrustcurves/Hypertek_M1040.eng
new file mode 100644 (file)
index 0000000..c2472ae
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;\r
+M1040 98 1493.5 100 5.293 10.181 HyperTek \r
+0.02 1288.12\r
+0.05 1545.28\r
+0.07 1583.82\r
+0.09 1530.98\r
+0.12 1497.57\r
+0.53 1421.58\r
+0.95 1430.43\r
+1.37 1412.05\r
+1.78 1398.74\r
+2.2 1371.89\r
+2.61 1382.63\r
+3.03 1406.27\r
+3.44 1478.77\r
+3.86 1420.86\r
+4.27 1377.59\r
+4.69 1333.74\r
+5.1 1289.09\r
+5.52 1274.62\r
+5.93 1176.1\r
+6.35 1152.46\r
+6.77 891.06\r
+7.18 582.18\r
+7.6 429.01\r
+8.01 320.71\r
+8.43 238.67\r
+8.84 175.92\r
+9.25 128.91\r
+9.66 92.08\r
+9.7 0\r
diff --git a/datafiles/thrustcurves/Hypertek_M740.eng b/datafiles/thrustcurves/Hypertek_M740.eng
new file mode 100644 (file)
index 0000000..82a9354
--- /dev/null
@@ -0,0 +1,33 @@
+;\r
+;\r
+M740 75 1422.4 100 2.589 6.322 HyperTek \r
+0.04 979.34\r
+0.08 1135.85\r
+0.12 1065.56\r
+0.16 1026.83\r
+0.2 1022.88\r
+0.44 982.86\r
+0.68 1065.61\r
+0.91 1100.92\r
+1.15 1067.05\r
+1.39 1072.92\r
+1.63 1013.29\r
+1.87 1016.51\r
+2.11 1012.36\r
+2.35 1007.02\r
+2.58 968.14\r
+2.82 948.67\r
+3.06 944\r
+3.3 905.49\r
+3.54 899.55\r
+3.77 866.23\r
+4.02 847.5\r
+4.25 822.68\r
+4.49 813.98\r
+4.73 789.25\r
+4.97 752.47\r
+5.21 344.76\r
+5.45 271.93\r
+5.84 195.15\r
+6.46 108.81\r
+6.97 0\r
diff --git a/datafiles/thrustcurves/Hypertek_M956.eng b/datafiles/thrustcurves/Hypertek_M956.eng
new file mode 100644 (file)
index 0000000..cd5ab55
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;\r
+M956 98 1150.6 100 3.162 7.061 HyperTek \r
+0.04 1325.32\r
+0.08 1335.19\r
+0.12 1273.52\r
+0.16 1245.8\r
+0.2 1261.01\r
+0.46 1283.75\r
+0.71 1339.94\r
+0.97 1333.16\r
+1.23 1322.79\r
+1.49 1330.11\r
+1.75 1294.72\r
+2.01 1271.81\r
+2.27 1246.37\r
+2.52 1233.02\r
+2.78 1214.19\r
+3.04 1199.4\r
+3.3 1152.16\r
+3.56 1128.04\r
+3.81 1119.3\r
+4.08 1098.79\r
+4.33 1054.12\r
+4.59 1031.85\r
+4.85 964.95\r
+5.11 548.37\r
+5.45 373.37\r
+5.79 248.05\r
+6.14 160.88\r
+6.48 109.1\r
+6.7 0\r
diff --git a/datafiles/thrustcurves/Hypertek_M960.eng b/datafiles/thrustcurves/Hypertek_M960.eng
new file mode 100644 (file)
index 0000000..28e272d
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+;\r
+M960 75 1422.4 100 2.629 6.414 HyperTek \r
+0.01 508.27\r
+0.02 1058.4\r
+0.03 1393.68\r
+0.04 1534.58\r
+0.05 1585.27\r
+0.25 1375.21\r
+0.44 1354.52\r
+0.64 1365.14\r
+0.84 1385.36\r
+1.04 1418.42\r
+1.23 1358.94\r
+1.43 1347.56\r
+1.63 1291.46\r
+1.83 1241.32\r
+2.02 1235.83\r
+2.22 1221.66\r
+2.42 1181.59\r
+2.62 1150.77\r
+2.81 1105.59\r
+3.02 1040.1\r
+3.21 982.54\r
+3.41 999.65\r
+3.61 943.72\r
+3.8 871.92\r
+4.11 526.67\r
+4.42 330.98\r
+4.72 212.71\r
+5.03 135.8\r
+5.33 0\r
diff --git a/datafiles/thrustcurves/KBA_I170.eng b/datafiles/thrustcurves/KBA_I170.eng
new file mode 100644 (file)
index 0000000..6e9c0a4
--- /dev/null
@@ -0,0 +1,26 @@
+;Data entered by Tim Van Milligan\r
+;Based on TRA Certification 6-19-2002\r
+;And Instructions provided by Aerotech.\r
+I170S 38 258 14 0.1819 0.52 Kosdon-by-Aerotech \r
+0.019 194.885\r
+0.131 190.481\r
+0.255 191.582\r
+0.513 199.289\r
+0.641 204.794\r
+0.753 206.996\r
+0.88 209.199\r
+1 208.098\r
+1.051 208.098\r
+1.147 206.996\r
+1.24 201.491\r
+1.391 198.188\r
+1.537 190.481\r
+1.707 181.672\r
+1.746 178.369\r
+1.781 173.96\r
+1.808 168.46\r
+1.854 132.12\r
+1.939 53.951\r
+2.005 22.02\r
+2.059 9.909\r
+2.13 0\r
diff --git a/datafiles/thrustcurves/KBA_I280.eng b/datafiles/thrustcurves/KBA_I280.eng
new file mode 100644 (file)
index 0000000..4da3761
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+I280F 38 258 14 100.182 0.52 Kosdon-by-AeroTech \r
+0.009 253.24\r
+0.055 255.442\r
+0.219 277.463\r
+0.482 301.686\r
+0.67 323.707\r
+0.735 330.314\r
+0.797 323.707\r
+1.001 297.282\r
+1.162 266.453\r
+1.205 259.847\r
+1.236 237.826\r
+1.363 50.6481\r
+1.428 26.4251\r
+1.5 0\r
diff --git a/datafiles/thrustcurves/KBA_I301.eng b/datafiles/thrustcurves/KBA_I301.eng
new file mode 100644 (file)
index 0000000..b189e7b
--- /dev/null
@@ -0,0 +1,25 @@
+; KBA I301W\r
+I301W 38 369.6 18 0.295031 0.724 KBA\r
+   0.0080 266.093\r
+   0.014 327.114\r
+   0.03 354.124\r
+   0.058 350.122\r
+   0.107 335.117\r
+   0.133 326.114\r
+   0.189 326.114\r
+   0.217 333.116\r
+   0.237 383.134\r
+   0.253 402.14\r
+   0.287 395.138\r
+   0.33 381.133\r
+   0.72 381.133\r
+   1.035 341.119\r
+   1.437 317.111\r
+   1.57 262.092\r
+   1.698 130.045\r
+   1.789 83.029\r
+   1.833 74.026\r
+   1.867 53.019\r
+   1.893 23.008\r
+   1.916 13.005\r
+   1.952 0.0\r
diff --git a/datafiles/thrustcurves/KBA_I310.eng b/datafiles/thrustcurves/KBA_I310.eng
new file mode 100644 (file)
index 0000000..23f3c5c
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+;Kosdon by AeroTech I310S\r
+;Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+;provided by ThrustCurve.org (www.thrustcurve.org)\r
+I310S 38 368 6-0 0.312256 0.713216 Kosdon-by-AeroTech \r
+0.045 334.66\r
+0.136 314.409\r
+0.228 322.556\r
+0.32 326.871\r
+0.411 331.851\r
+0.503 335.911\r
+0.595 336.933\r
+0.686 340.151\r
+0.778 342.066\r
+0.87 344.722\r
+0.961 348.578\r
+1.053 349.548\r
+1.146 351.943\r
+1.239 347.939\r
+1.33 345.079\r
+1.422 337.035\r
+1.514 333.332\r
+1.605 323.832\r
+1.697 289\r
+1.789 215.097\r
+1.88 136.596\r
+1.972 83.863\r
+2.064 37.922\r
+2.155 20.736\r
+2.248 5.943\r
+2.341 0\r
diff --git a/datafiles/thrustcurves/KBA_I370.eng b/datafiles/thrustcurves/KBA_I370.eng
new file mode 100644 (file)
index 0000000..cb88f11
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+;Kosdon by AeroTech I370F\r
+;Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+;provided by ThrustCurve.org (www.thrustcurve.org)\r
+I370F 38 368 100 0.312256 0.705152 Kosdon-by-AeroTech \r
+0.035 373.074\r
+0.109 389.927\r
+0.184 401.07\r
+0.259 416.613\r
+0.334 429.598\r
+0.409 438.025\r
+0.484 443.83\r
+0.559 447.326\r
+0.634 446.764\r
+0.709 447.263\r
+0.784 444.735\r
+0.859 441.302\r
+0.933 435.676\r
+1.007 425.29\r
+1.082 414.897\r
+1.157 404.222\r
+1.232 395.358\r
+1.307 382.062\r
+1.382 334.152\r
+1.457 275.974\r
+1.532 179.654\r
+1.607 83.023\r
+1.682 39.608\r
+1.757 16.105\r
+1.832 4.151\r
+1.907 0\r
diff --git a/datafiles/thrustcurves/KBA_I450.eng b/datafiles/thrustcurves/KBA_I450.eng
new file mode 100644 (file)
index 0000000..b6d9ced
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+I450F 38 370 14 0.3032 0.73 Kosdon-by-AeroTech \r
+0.012 634.202\r
+0.037 550.523\r
+0.108 519.693\r
+0.241 510.885\r
+0.639 550.523\r
+0.729 554.927\r
+0.809 546.118\r
+0.939 497.672\r
+1.072 471.247\r
+1.128 440.418\r
+1.165 387.568\r
+1.211 206.996\r
+1.295 88.0836\r
+1.36 26.4251\r
+1.41 0\r
diff --git a/datafiles/thrustcurves/KBA_I550.eng b/datafiles/thrustcurves/KBA_I550.eng
new file mode 100644 (file)
index 0000000..488b200
--- /dev/null
@@ -0,0 +1,28 @@
+; KBA I550R\r
+I550R 38 369.6 20 0.295 0.713 KBA\r
+   0.016 156.054\r
+   0.028 278.097\r
+   0.04 427.149\r
+   0.054 550.192\r
+   0.08 542.189\r
+   0.245 588.205\r
+   0.332 611.213\r
+   0.424 631.22\r
+   0.496 638.223\r
+   0.613 644.225\r
+   0.71 643.225\r
+   0.758 631.22\r
+   0.846 603.211\r
+   0.894 613.214\r
+   0.915 611.213\r
+   0.939 586.205\r
+   0.949 546.191\r
+   0.959 505.176\r
+   0.969 469.164\r
+   0.983 381.133\r
+   0.999 278.097\r
+   1.011 200.07\r
+   1.029 112.039\r
+   1.053 42.015\r
+   1.069 15.005\r
+   1.089 0.0\r
diff --git a/datafiles/thrustcurves/KBA_J405.eng b/datafiles/thrustcurves/KBA_J405.eng
new file mode 100644 (file)
index 0000000..2a655c9
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+J405S 38 476 14 0.367 0.88 Kosdon-by-AeroTech \r
+0.009 528.502\r
+0.024 488.864\r
+0.046 462.439\r
+0.136 462.439\r
+0.268 458.035\r
+0.986 453.631\r
+1.421 444.822\r
+1.523 255.442\r
+1.697 92.4878\r
+1.93 0\r
diff --git a/datafiles/thrustcurves/KBA_J605.eng b/datafiles/thrustcurves/KBA_J605.eng
new file mode 100644 (file)
index 0000000..9716ec2
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+J605F 38 476 14 0.367 0.88 Kosdon-by-AeroTech \r
+0.024 886.341\r
+0.037 704.669\r
+0.077 660.627\r
+0.438 704.669\r
+0.506 715.679\r
+0.59 710.174\r
+0.853 655.122\r
+0.973 594.564\r
+1.041 412.892\r
+1.091 324.808\r
+1.177 132.125\r
+1.3 0\r
diff --git a/datafiles/thrustcurves/KBA_K1750.eng b/datafiles/thrustcurves/KBA_K1750.eng
new file mode 100644 (file)
index 0000000..826fc73
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+K1750R  54.0 728.00 0 1.25300 2.56000 KBA\r
+   0.02    1309.09 \r
+   0.03    1679.77 \r
+   0.05    1736.54 \r
+   0.11    1689.79 \r
+   0.26    1799.99 \r
+   0.40    1913.54 \r
+   0.46    1896.84 \r
+   0.68    2023.74 \r
+   0.90    2133.94 \r
+   0.95    2097.21 \r
+   1.00    2050.46 \r
+   1.05    1920.21 \r
+   1.10    1793.31 \r
+   1.16    1676.43 \r
+   1.21    1719.85 \r
+   1.25    1526.15 \r
+   1.27    1302.41 \r
+   1.32     874.95 \r
+   1.35     454.17 \r
+   1.36     317.25 \r
+   1.37     200.37 \r
+   1.40      90.17 \r
+   1.46       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/KBA_K400.eng b/datafiles/thrustcurves/KBA_K400.eng
new file mode 100644 (file)
index 0000000..cb90df9
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+;\r
+K400S 54 403 6-10-14 0.713216 1.50931 Kosdon-by-AeroTech \r
+0.074 465.928\r
+0.225 441.922\r
+0.377 442.414\r
+0.529 445.492\r
+0.681 449.048\r
+0.833 451.88\r
+0.985 454.481\r
+1.138 456.929\r
+1.29 458.237\r
+1.442 457.021\r
+1.594 455.62\r
+1.746 451.772\r
+1.897 446.421\r
+2.048 438.843\r
+2.2 429.377\r
+2.352 419.003\r
+2.504 408.274\r
+2.656 397.608\r
+2.808 388.018\r
+2.96 367.07\r
+3.113 263.666\r
+3.265 114.378\r
+3.417 46.238\r
+3.569 8.62\r
+3.721 2.401\r
+3.873 0\r
diff --git a/datafiles/thrustcurves/KBA_K600.eng b/datafiles/thrustcurves/KBA_K600.eng
new file mode 100644 (file)
index 0000000..a52b6f1
--- /dev/null
@@ -0,0 +1,29 @@
+; Kosdon by AeroTech K600F\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K600F 54 403 0 0.68096 1.41568 KBA\r
+   0.045 639.654\r
+   0.148 711.292\r
+   0.252 695.617\r
+   0.358 696.252\r
+   0.462 701.336\r
+   0.568 703.242\r
+   0.670 705.265\r
+   0.772 704.302\r
+   0.878 702.819\r
+   0.982 701.336\r
+   1.088 696.888\r
+   1.192 689.262\r
+   1.295 681.245\r
+   1.398 668.928\r
+   1.502 653.465\r
+   1.608 637.366\r
+   1.712 619.785\r
+   1.818 599.451\r
+   1.920 586.275\r
+   2.022 510.698\r
+   2.128 334.676\r
+   2.232 125.397\r
+   2.338 37.916\r
+   2.442 17.157\r
+   2.548 4.025\r
+   2.653 0.000\r
diff --git a/datafiles/thrustcurves/KBA_K750.eng b/datafiles/thrustcurves/KBA_K750.eng
new file mode 100644 (file)
index 0000000..2a40ae6
--- /dev/null
@@ -0,0 +1,29 @@
+; Kosdon by Aerotech K750 White Lightning.\r
+K750W 54 728 0 1.315 2.62 KBA\r
+   0.0080 266.075\r
+   0.012 457.102\r
+   0.02 750.467\r
+   0.032 999.485\r
+   0.044 1112.055\r
+   0.06 1180.279\r
+   0.095 1098.41\r
+   0.127 1057.476\r
+   0.163 1040.42\r
+   0.334 1050.653\r
+   0.62 1054.064\r
+   0.998 975.607\r
+   1.324 907.382\r
+   1.69 903.971\r
+   2.06 886.915\r
+   2.184 828.924\r
+   2.299 757.289\r
+   2.394 651.541\r
+   2.502 556.028\r
+   2.609 450.28\r
+   2.784 327.476\r
+   2.999 245.607\r
+   3.039 201.261\r
+   3.134 92.103\r
+   3.206 40.935\r
+   3.337 6.822\r
+   3.468 0.0\r
diff --git a/datafiles/thrustcurves/KBA_L1000.eng b/datafiles/thrustcurves/KBA_L1000.eng
new file mode 100644 (file)
index 0000000..d03b16d
--- /dev/null
@@ -0,0 +1,29 @@
+; Kosdon by AeroTech L1000S\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1000S 54 728 0 1.232 2.32512 KBA\r
+   0.055 795.305\r
+   0.175 981.574\r
+   0.295 989.173\r
+   0.415 1008.634\r
+   0.535 1028.836\r
+   0.655 1048.483\r
+   0.775 1067.573\r
+   0.895 1087.034\r
+   1.015 1108.719\r
+   1.135 1131.516\r
+   1.255 1156.908\r
+   1.375 1177.296\r
+   1.498 1199.596\r
+   1.620 1212.881\r
+   1.740 1227.153\r
+   1.860 1232.342\r
+   1.980 1249.950\r
+   2.100 1026.056\r
+   2.220 737.107\r
+   2.340 565.851\r
+   2.460 313.414\r
+   2.580 89.706\r
+   2.700 20.758\r
+   2.820 8.526\r
+   2.942 5.338\r
+   3.065 0.000\r
diff --git a/datafiles/thrustcurves/KBA_L1400.eng b/datafiles/thrustcurves/KBA_L1400.eng
new file mode 100644 (file)
index 0000000..559ae80
--- /dev/null
@@ -0,0 +1,18 @@
+;\r
+;\r
+L1400F 54 727 100 1.248 2.502 Kosdon-by-AeroTech \r
+0.037 1541.46\r
+0.061 1453.38\r
+0.166 1354.29\r
+1.001 1772.68\r
+1.279 1783.69\r
+1.329 1882.79\r
+1.387 1992.89\r
+1.486 1387.32\r
+1.604 869.826\r
+1.65 748.711\r
+1.666 726.69\r
+1.69 924.878\r
+1.697 594.564\r
+1.758 319.303\r
+1.88 0\r
diff --git a/datafiles/thrustcurves/KBA_M1450.eng b/datafiles/thrustcurves/KBA_M1450.eng
new file mode 100644 (file)
index 0000000..2374eb2
--- /dev/null
@@ -0,0 +1,22 @@
+; KBA M1450W\r
+M1450W 75 1038.9 0 4.15 7.6000000000000005 KBA\r
+   0.035 1842.929\r
+   0.076 2287.088\r
+   0.146 1968.884\r
+   0.215 1882.704\r
+   0.291 1836.299\r
+   0.499 1862.816\r
+   1.005 1935.738\r
+   1.559 1889.333\r
+   2.155 1816.412\r
+   2.862 1750.119\r
+   3.493 1663.939\r
+   3.853 1358.994\r
+   4.221 1060.678\r
+   4.484 788.88\r
+   4.761 523.71\r
+   4.942 258.54\r
+   5.323 258.54\r
+   5.6 172.36\r
+   5.801 119.326\r
+   5.96 0.0\r
diff --git a/datafiles/thrustcurves/Loki_H144.eng b/datafiles/thrustcurves/Loki_H144.eng
new file mode 100644 (file)
index 0000000..2712a80
--- /dev/null
@@ -0,0 +1,25 @@
+;\r
+;\r
+H144 38 178 5-8-10-13-17 0.12 0.335 Loki \r
+0.02 209\r
+0.04 247.6\r
+0.05 241.2\r
+0.1 247.6\r
+0.15 244.4\r
+0.2 237.9\r
+0.25 231.54\r
+0.3 228.3\r
+0.4 215.32\r
+0.45 212.43\r
+0.5 204.48\r
+0.6 194.36\r
+0.7 189.7\r
+0.8 170.4\r
+0.9 154.3\r
+1 127.83\r
+1.1 109.3\r
+1.2 80.4\r
+1.3 64.6\r
+1.4 44.6\r
+1.5 32.1\r
+1.6 0\r
diff --git a/datafiles/thrustcurves/Loki_H500.eng b/datafiles/thrustcurves/Loki_H500.eng
new file mode 100644 (file)
index 0000000..20c571c
--- /dev/null
@@ -0,0 +1,13 @@
+;\r
+;\r
+H500 38 292 5-7-9-12-15 0.16 0.454 Loki \r
+0.001 189.286\r
+0.0116009 534.733\r
+0.099768 539.465\r
+0.199536 544.197\r
+0.302784 553.662\r
+0.402552 548.93\r
+0.50464 544.197\r
+0.584687 435.358\r
+0.61949 9.4643\r
+0.62 0\r
diff --git a/datafiles/thrustcurves/Loki_I405.eng b/datafiles/thrustcurves/Loki_I405.eng
new file mode 100644 (file)
index 0000000..87015bc
--- /dev/null
@@ -0,0 +1,23 @@
+;\r
+;\r
+I405 38 292 5-8-10-13-17 0.24 0.54 Loki \r
+0.01 151.1\r
+0.03 781.4\r
+0.05 800.7\r
+0.06 755.7\r
+0.09 724.3\r
+0.12 697.7\r
+0.15 701\r
+0.17 675.3\r
+0.2 643.1\r
+0.3 607.7\r
+0.4 569.2\r
+0.5 517.7\r
+0.6 472.7\r
+0.7 392.3\r
+0.8 318.3\r
+0.9 241.2\r
+1 151.1\r
+1.1 93.3\r
+1.15 40\r
+1.2 0\r
diff --git a/datafiles/thrustcurves/Loki_J525.eng b/datafiles/thrustcurves/Loki_J525.eng
new file mode 100644 (file)
index 0000000..2aae0e3
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+;\r
+J525 54 327 0 0.59 1.264 Loki \r
+0.01 210.9\r
+0.03 499.3\r
+0.05 628.5\r
+0.1 594\r
+0.13 568.2\r
+0.15 559.6\r
+0.2 555.3\r
+0.3 572.5\r
+0.4 589.7\r
+0.5 606.9\r
+0.6 624.2\r
+0.7 637.1\r
+0.8 645.7\r
+0.9 650\r
+1 658.6\r
+1.1 637.1\r
+1.2 628.5\r
+1.3 615.5\r
+1.41 586.27\r
+1.52 561.52\r
+1.67 536.78\r
+1.78 517.74\r
+1.85 485.38\r
+1.92 91.37\r
+2 0\r
diff --git a/datafiles/thrustcurves/Loki_J528.eng b/datafiles/thrustcurves/Loki_J528.eng
new file mode 100644 (file)
index 0000000..00424fe
--- /dev/null
@@ -0,0 +1,27 @@
+;\r
+J528 38 406 5-8-10-13-17 0.372 0.752 Loki \r
+0.01 704.2\r
+0.02 1019\r
+0.03 983.9\r
+0.05 881.1\r
+0.1 797.5\r
+0.15 771.7\r
+0.17 765.72\r
+0.21 765.72\r
+0.25 778.2\r
+0.42 789.28\r
+0.51 771.61\r
+0.6 756.89\r
+0.66 751\r
+0.71 762.78\r
+0.76 697.99\r
+0.8 665.59\r
+0.84 612.58\r
+0.92 488.88\r
+0.95 385.81\r
+1.02 282.73\r
+1.06 179.65\r
+1.14 53.01\r
+1.19 35.34\r
+1.23 32.2\r
+1.25 0\r
diff --git a/datafiles/thrustcurves/Loki_K250.eng b/datafiles/thrustcurves/Loki_K250.eng
new file mode 100644 (file)
index 0000000..c873dd4
--- /dev/null
@@ -0,0 +1,24 @@
+;\r
+;\r
+K250 54 498 0 0.952544 1.79169 Loki \r
+0.03 800\r
+0.1 682\r
+0.125 574\r
+0.15 476\r
+0.175 447\r
+0.25 385\r
+0.45 340\r
+0.6 320\r
+1 313\r
+1.5 300\r
+2 297\r
+2.5 303\r
+3 294\r
+3.5 287\r
+4 248\r
+4.5 222\r
+5 187\r
+5.5 147\r
+6 114\r
+6.5 62\r
+7 0\r
diff --git a/datafiles/thrustcurves/Loki_K350.eng b/datafiles/thrustcurves/Loki_K350.eng
new file mode 100644 (file)
index 0000000..b7df74c
--- /dev/null
@@ -0,0 +1,24 @@
+;\r
+;\r
+K350 54 702 0 1.4 2.54012 Loki\r
+0.025 1329\r
+0.0375 1061\r
+0.1 1006\r
+0.15 891\r
+0.2 768\r
+0.4 571\r
+0.5 542\r
+0.75 486\r
+1 486\r
+1.25 477\r
+1.5 481\r
+2.5058 460\r
+3.00464 427\r
+3.5 375\r
+4 333\r
+4.5 297\r
+5 249\r
+5.5 210\r
+6 164\r
+6.5 98\r
+7 0\r
diff --git a/datafiles/thrustcurves/Loki_K960.eng b/datafiles/thrustcurves/Loki_K960.eng
new file mode 100644 (file)
index 0000000..3061322
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;\r
+K960 54 498 0 0.929864 1.74633 Loki \r
+0.03 1210\r
+0.05 1512\r
+0.075 1535\r
+0.1 1502\r
+0.125 1437\r
+0.2 1237\r
+0.3 1175\r
+0.5 1139\r
+0.6 1130\r
+0.7 1156\r
+0.8 1182\r
+0.9 1192\r
+1 1166\r
+1.1 1139\r
+1.2 1101\r
+1.3 1091\r
+1.4 1026\r
+1.5 839\r
+1.6 790\r
+1.7 575\r
+1.8 284\r
+1.9 150\r
+2 0\r
diff --git a/datafiles/thrustcurves/Loki_L1400.eng b/datafiles/thrustcurves/Loki_L1400.eng
new file mode 100644 (file)
index 0000000..4ded67e
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+L1400 54 726 0 1.4 2.54 Loki \r
+0.00580046 1606.3\r
+0.11891 1535.7\r
+0.327726 1535.7\r
+0.49884 1588.65\r
+1.00058 1782.82\r
+1.40661 1906.38\r
+1.49942 1376.83\r
+1.60673 953.19\r
+1.74594 547.202\r
+1.90545 335.382\r
+1.99826 211.82\r
+2 0\r
diff --git a/datafiles/thrustcurves/Loki_L930.eng b/datafiles/thrustcurves/Loki_L930.eng
new file mode 100644 (file)
index 0000000..59dbeb5
--- /dev/null
@@ -0,0 +1,24 @@
+;\r
+;\r
+L930 76 498 0 1.81437 3.53802 Loki\r
+0.025 532\r
+0.05 1123\r
+0.075 1123\r
+0.125 1094\r
+0.2 930\r
+0.5 881\r
+0.6 878\r
+0.75 898\r
+1 921\r
+1.25 940\r
+1.5 1012\r
+1.75 1081\r
+2 1100\r
+2.25 1120\r
+2.5 1051\r
+2.75 980\r
+3 934\r
+3.25 826\r
+3.5 722\r
+3.75 280\r
+4 0\r
diff --git a/datafiles/thrustcurves/Loki_M1882.eng b/datafiles/thrustcurves/Loki_M1882.eng
new file mode 100644 (file)
index 0000000..8d4d687
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+M1882 75 785 0 3.12979 5.53383 Loki \r
+0.01 4.8\r
+0.0174014 2579.22\r
+0.0696056 2392.32\r
+0.232019 2298.87\r
+0.50464 2261.49\r
+0.771462 2298.87\r
+0.986079 2411.01\r
+1.1891 2579.22\r
+1.49652 2597.91\r
+1.72854 2485.77\r
+2.00116 2354.94\r
+2.5 1644.72\r
+2.99884 242.97\r
+3.25 0\r
diff --git a/datafiles/thrustcurves/PML_F50.eng b/datafiles/thrustcurves/PML_F50.eng
new file mode 100644 (file)
index 0000000..6970c3e
--- /dev/null
@@ -0,0 +1,20 @@
+;\r
+F50T  29.0  98.00 4-6-9 0.03790 0.08490 AT\r
+   0.01      37.97 \r
+   0.02      56.27 \r
+   0.03      65.08 \r
+   0.12      71.86 \r
+   0.23      75.25 \r
+   0.33      77.29 \r
+   0.35      77.70 \r
+   0.45      75.25 \r
+   0.59      71.86 \r
+   0.72      65.76 \r
+   0.83      58.98 \r
+   1.01      43.39 \r
+   1.19      25.76 \r
+   1.25      15.59 \r
+   1.30       8.81 \r
+   1.36       4.75 \r
+   1.42       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/PML_G40.eng b/datafiles/thrustcurves/PML_G40.eng
new file mode 100644 (file)
index 0000000..8bdc364
--- /dev/null
@@ -0,0 +1,22 @@
+; PML G40W is the same as the Aerotech G40W.\r
+G40W 29 124 4-7-10 0.0624 0.115 PML\r
+   0.015 40.0\r
+   0.037 66.479\r
+   0.066 60.845\r
+   0.161 55.775\r
+   0.308 55.775\r
+   0.447 54.085\r
+   0.549 52.394\r
+   0.637 51.268\r
+   0.857 52.958\r
+   1.018 52.394\r
+   1.172 50.141\r
+   1.362 46.761\r
+   1.611 41.69\r
+   1.691 41.127\r
+   1.845 34.366\r
+   1.999 30.423\r
+   2.372 23.099\r
+   2.585 13.521\r
+   2.738 6.761\r
+   3.039 0.0\r
diff --git a/datafiles/thrustcurves/PML_G80.eng b/datafiles/thrustcurves/PML_G80.eng
new file mode 100644 (file)
index 0000000..c30bece
--- /dev/null
@@ -0,0 +1,19 @@
+; PML G80T is the same as the old Aerotech G480T.\r
+G80T 29 124 4-7-10 0.0574 0.106 PML\r
+   0.0070 82.746\r
+   0.018 104.754\r
+   0.051 104.754\r
+   0.095 97.711\r
+   0.245 94.19\r
+   0.458 95.07\r
+   0.6 93.31\r
+   0.86 84.507\r
+   1.003 76.585\r
+   1.139 69.542\r
+   1.252 57.218\r
+   1.303 51.056\r
+   1.34 37.852\r
+   1.38 19.366\r
+   1.424 7.923\r
+   1.461 2.641\r
+   1.497 0.0\r
diff --git a/datafiles/thrustcurves/PP_H70.eng b/datafiles/thrustcurves/PP_H70.eng
new file mode 100644 (file)
index 0000000..192ca4c
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+H70 38 462 100 0.318 0.627 Propulsion-Polymers \r
+0.05 170.9\r
+0.09 122.3\r
+0.37 106.8\r
+0.74 97.9\r
+1.11 93.4\r
+1.48 89\r
+1.84 84.5\r
+2.21 77.1\r
+2.33 74\r
+2.58 41.5\r
+2.95 23.7\r
+3.32 15.6\r
+3.69 0\r
diff --git a/datafiles/thrustcurves/PP_I160.eng b/datafiles/thrustcurves/PP_I160.eng
new file mode 100644 (file)
index 0000000..c56c2fc
--- /dev/null
@@ -0,0 +1,15 @@
+;\r
+;\r
+I160 38 646 20-100 0.31 0.856 Propulsion-Polymers \r
+0.04 298.9\r
+0.08 272.5\r
+0.32 264.7\r
+0.65 241.3\r
+0.97 218\r
+1.29 194.6\r
+1.61 179\r
+2 163.5\r
+2.26 93.4\r
+2.58 62.3\r
+2.9 31.1\r
+3.23 0\r
diff --git a/datafiles/thrustcurves/PP_I80.eng b/datafiles/thrustcurves/PP_I80.eng
new file mode 100644 (file)
index 0000000..f6e0132
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+I80 38 646 100 0.332 0.842 Propulsion-Polymers \r
+0.04 198.7\r
+0.08 158.4\r
+0.57 133.4\r
+0.9 114.5\r
+1.15 110\r
+1.73 108\r
+2.3 106\r
+2.88 104\r
+3.45 96.3\r
+3.75 66.7\r
+4.03 50\r
+4.6 35.6\r
+5.18 22.2\r
+5.75 0\r
diff --git a/datafiles/thrustcurves/PP_J140.eng b/datafiles/thrustcurves/PP_J140.eng
new file mode 100644 (file)
index 0000000..3102b99
--- /dev/null
@@ -0,0 +1,17 @@
+; Propulsion Polymers 664NS-J140\r
+; from CAR data sheet\r
+; created by John Coker 5/2006\r
+J140 38 881 P 0.626 1.166 PP\r
+   0.02 249\r
+   0.20 320\r
+   0.25 240\r
+   0.30 236\r
+   0.35 191\r
+   0.45 236\r
+   1.15 223\r
+   1.60 178\r
+   3.05 165\r
+   3.40 102\r
+   4.25 45\r
+   4.95 22\r
+   5.00 0\r
diff --git a/datafiles/thrustcurves/Quest_A6.eng b/datafiles/thrustcurves/Quest_A6.eng
new file mode 100644 (file)
index 0000000..9ce8d42
--- /dev/null
@@ -0,0 +1,8 @@
+;\r
+;\r
+A6Q 18 70 4 0.0035 0.0153 Quest \r
+0.1 4.8\r
+0.2 11.82\r
+0.23 7.9\r
+0.3 4.8\r
+0.41 0\r
diff --git a/datafiles/thrustcurves/Quest_B6.eng b/datafiles/thrustcurves/Quest_B6.eng
new file mode 100644 (file)
index 0000000..97d8266
--- /dev/null
@@ -0,0 +1,14 @@
+;\r
+;\r
+B6Q 18 70 0-2-4 0.0065 0.0162 Quest \r
+0.1 7\r
+0.18 14.38\r
+0.2 10.2\r
+0.24 6.6\r
+0.3 6\r
+0.4 6.1\r
+0.5 6.2\r
+0.6 6.3\r
+0.65 6.6\r
+0.7 3\r
+0.75 0\r
diff --git a/datafiles/thrustcurves/Quest_C6.eng b/datafiles/thrustcurves/Quest_C6.eng
new file mode 100644 (file)
index 0000000..8234f20
--- /dev/null
@@ -0,0 +1,31 @@
+; Quest C6-0 from NAR data\r
+C6-0 18 70 0 0.0083 0.0216 Q\r
+   0.02 0.497\r
+   0.057 2.539\r
+   0.089 5.132\r
+   0.129 7.947\r
+   0.159 9.437\r
+   0.171 21.247\r
+   0.181 23.234\r
+   0.194 22.958\r
+   0.204 22.185\r
+   0.218 19.592\r
+   0.233 17.881\r
+   0.258 10.486\r
+   0.308 2.428\r
+   0.338 2.539\r
+   0.385 2.98\r
+   0.412 3.091\r
+   0.442 3.422\r
+   0.459 2.98\r
+   0.536 3.256\r
+   0.732 3.311\r
+   0.747 2.483\r
+   0.78 2.98\r
+   1.323 3.587\r
+   1.365 2.815\r
+   1.887 3.808\r
+   1.974 3.256\r
+   2.1 3.532\r
+   2.227 3.201\r
+   2.247 0.0\r
diff --git a/datafiles/thrustcurves/Quest_D5.eng b/datafiles/thrustcurves/Quest_D5.eng
new file mode 100644 (file)
index 0000000..74c5a20
--- /dev/null
@@ -0,0 +1,24 @@
+; Quest D5-0 by Mark Koelsch from NAR data\r
+D5-0 20 88 0 0.025 0.0384 Q\r
+   0.096 1.241\r
+   0.252 5.897\r
+   0.304 8.586\r
+   0.357 10.552\r
+   0.391 11.483\r
+   0.435 9.828\r
+   0.557 6.103\r
+   0.583 5.172\r
+   0.67 5.172\r
+   1.078 4.966\r
+   1.2 4.345\r
+   1.73 4.759\r
+   1.8 4.759\r
+   1.887 4.138\r
+   2.391 5.069\r
+   2.626 4.966\r
+   3.009 5.379\r
+   3.357 5.276\r
+   3.661 5.69\r
+   3.835 3.103\r
+   3.887 1.655\r
+   3.983 0.0\r
diff --git a/datafiles/thrustcurves/RATT_H70.eng b/datafiles/thrustcurves/RATT_H70.eng
new file mode 100644 (file)
index 0000000..536d3f0
--- /dev/null
@@ -0,0 +1,30 @@
+; RATT Works H70H\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H70H 29 457 0 0.106176 0.348992 RTW\r
+   0.051 124.137\r
+   0.156 134.902\r
+   0.261 127.047\r
+   0.367 115.870\r
+   0.473 110.286\r
+   0.578 108.630\r
+   0.683 105.741\r
+   0.789 104.108\r
+   0.894 101.515\r
+   1.000 98.471\r
+   1.105 93.809\r
+   1.210 90.543\r
+   1.316 85.219\r
+   1.421 74.353\r
+   1.527 57.739\r
+   1.632 44.019\r
+   1.738 34.554\r
+   1.843 27.992\r
+   1.948 22.691\r
+   2.054 19.064\r
+   2.159 15.665\r
+   2.265 12.897\r
+   2.370 11.332\r
+   2.475 10.107\r
+   2.581 8.591\r
+   2.688 0.000\r
diff --git a/datafiles/thrustcurves/RATT_I80.eng b/datafiles/thrustcurves/RATT_I80.eng
new file mode 100644 (file)
index 0000000..a9025a6
--- /dev/null
@@ -0,0 +1,30 @@
+; RATT Works I80H\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I80H 29 730 0 0.21952 0.549248 RTW\r
+   0.093 84.101\r
+   0.280 102.075\r
+   0.469 118.708\r
+   0.657 117.130\r
+   0.846 114.829\r
+   1.034 112.225\r
+   1.222 109.818\r
+   1.410 107.622\r
+   1.599 105.945\r
+   1.787 102.168\r
+   1.976 100.449\r
+   2.164 99.265\r
+   2.352 96.272\r
+   2.541 91.951\r
+   2.729 89.380\r
+   2.918 86.430\r
+   3.105 54.544\r
+   3.294 41.902\r
+   3.482 33.368\r
+   3.671 26.516\r
+   3.859 21.324\r
+   4.047 16.951\r
+   4.235 14.387\r
+   4.424 13.456\r
+   4.612 12.777\r
+   4.801 0.000\r
diff --git a/datafiles/thrustcurves/RATT_I90.eng b/datafiles/thrustcurves/RATT_I90.eng
new file mode 100644 (file)
index 0000000..194c6bf
--- /dev/null
@@ -0,0 +1,30 @@
+; RATT Works I90LH\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I90LH 29 921 0 0.285376 0.684544 RTW\r
+   0.127 137.737\r
+   0.383 121.431\r
+   0.640 117.048\r
+   0.897 121.354\r
+   1.154 118.436\r
+   1.410 116.538\r
+   1.668 114.826\r
+   1.925 112.358\r
+   2.181 110.303\r
+   2.439 107.768\r
+   2.696 105.749\r
+   2.952 104.733\r
+   3.209 102.802\r
+   3.467 95.410\r
+   3.723 68.281\r
+   3.980 55.000\r
+   4.237 43.620\r
+   4.494 33.784\r
+   4.751 31.704\r
+   5.008 27.714\r
+   5.265 24.875\r
+   5.522 22.930\r
+   5.779 21.379\r
+   6.035 20.884\r
+   6.293 18.637\r
+   6.550 0.000\r
diff --git a/datafiles/thrustcurves/RATT_J160.eng b/datafiles/thrustcurves/RATT_J160.eng
new file mode 100644 (file)
index 0000000..e39baf8
--- /dev/null
@@ -0,0 +1,26 @@
+;\r
+;J160 data entered by Tim Van Milligan\r
+;Thrust Curve based on TRA certification dated 10/24/2003.\r
+;Propellant weight based on 80F degree day, 490cc Oxidizer + 4.25g AP weight.\r
+J160 38 1219 100 0.327926 0.544 RATT_Works \r
+0.015456 262.049\r
+0.185471 255.442\r
+0.278207 240.028\r
+0.278207 229.017\r
+0.386399 251.038\r
+0.510046 242.23\r
+0.64915 213.603\r
+0.788253 189.38\r
+1.00464 178.369\r
+1.45286 160.753\r
+2.00927 147.54\r
+2.44204 129.923\r
+2.90572 121.115\r
+2.96754 162.955\r
+3.15301 149.742\r
+3.32303 125.519\r
+3.67852 94.6899\r
+3.97218 74.8711\r
+4.32767 46.2439\r
+4.69861 19.8188\r
+5.1 0\r
diff --git a/datafiles/thrustcurves/RATT_K240.eng b/datafiles/thrustcurves/RATT_K240.eng
new file mode 100644 (file)
index 0000000..e5805bf
--- /dev/null
@@ -0,0 +1,30 @@
+; RATT Works K240H\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K240H 64 908 0 1.29338 2.81434 RTW\r
+   0.164 413.978\r
+   0.493 392.975\r
+   0.822 370.841\r
+   1.151 362.058\r
+   1.480 337.839\r
+   1.809 321.401\r
+   2.139 285.118\r
+   2.468 280.031\r
+   2.798 275.060\r
+   3.128 278.392\r
+   3.457 274.394\r
+   3.786 252.302\r
+   4.116 272.858\r
+   4.445 264.671\r
+   4.774 255.572\r
+   5.103 210.314\r
+   5.433 163.955\r
+   5.764 126.239\r
+   6.093 98.015\r
+   6.422 74.491\r
+   6.751 55.617\r
+   7.080 40.778\r
+   7.409 29.618\r
+   7.739 21.915\r
+   8.069 17.214\r
+   8.399 0.000\r
diff --git a/datafiles/thrustcurves/RATT_L600.eng b/datafiles/thrustcurves/RATT_L600.eng
new file mode 100644 (file)
index 0000000..aa97415
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+;Rattworks L600 Hybrid\r
+;prepared by Andrew MacMillen NAR 77472 2/28/04\r
+;based on TMT thrust data from Paul Holmes & TMT cert doc\r
+;NOX weight calc'd at .7048gm/cc, 75 deg F @ 812 psi\r
+;compiled with RockSim7 EngEdit\r
+;within 3 percent for total impulse\r
+;peak and average thrust are off due to data spike smoothing\r
+;accurate thrust profile\r
+;NOTE: NOT RATT, CAR, TMT OR NAR APPROVED\r
+;Rattworks L600\r
+L600 64 1066 100 1.863 2.25 RATT_Works \r
+0 88.62\r
+0.02 153.82\r
+0.19 158.02\r
+0.35 169.94\r
+0.4 1127.04\r
+0.78 1001.2\r
+2.23 778.11\r
+2.84 736.24\r
+3.07 704\r
+3.62 673.43\r
+3.81 435.29\r
+3.89 350.43\r
+4.14 240.05\r
+4.31 197.19\r
+4.49 165.4\r
+4.66 139.26\r
+4.82 119.21\r
+5.01 99.01\r
+5.34 70.99\r
+5.93 49.74\r
+6.49 38.96\r
+6.98 36.42\r
+7.1 0\r
diff --git a/datafiles/thrustcurves/RATT_M900.eng b/datafiles/thrustcurves/RATT_M900.eng
new file mode 100644 (file)
index 0000000..df948e7
--- /dev/null
@@ -0,0 +1,23 @@
+;\r
+;Rattworks M900 Hybrid\r
+;prepared by Andrew MacMillen NAR 77472 12/23/04\r
+;based on TMT thrust data from Paul Holmes & TMT cert doc\r
+;NOX weight calc'd at .7048gm/cc, 75 deg F @ 812 psi\r
+;compiled with RockSim6 EngEdit\r
+;within 2 percent for total impulse\r
+;peak and average thrust are off due to data spike smoothing\r
+;accurate thrust profile\r
+;NOTE: NOT RATT, CAR, TMT OR NAR APPROVED\r
+;Rattworks M900\r
+M900 69 1828 100 3.288 5.956 RATT_Works \r
+0.04 151.9\r
+0.43 241.76\r
+0.5 1054.61\r
+7.04 718.06\r
+7.22 346.42\r
+7.26 297.14\r
+7.35 241.24\r
+7.48 200.65\r
+7.66 164.8\r
+12.28 49.36\r
+12.3 0\r
diff --git a/datafiles/thrustcurves/RV_F32.eng b/datafiles/thrustcurves/RV_F32.eng
new file mode 100644 (file)
index 0000000..9c1deca
--- /dev/null
@@ -0,0 +1,11 @@
+; Rocketvision F32\r
+; from NAR data sheet updated 11/2000\r
+; created by John Coker 5/2006\r
+F32 24 124 5-10-15 .0377 .0695 RV\r
+   0.01 50\r
+   0.05 56\r
+   0.10 48\r
+   2.00 24\r
+   2.20 19\r
+   2.45  5\r
+   2.72  0\r
diff --git a/datafiles/thrustcurves/RV_F72.eng b/datafiles/thrustcurves/RV_F72.eng
new file mode 100644 (file)
index 0000000..f53740d
--- /dev/null
@@ -0,0 +1,30 @@
+; Same motor as the Aerotech F72T single use from NAR cert data\r
+F72T 24 124 5-10-15 0.0368 0.0746 Rocketvision\r
+   0.0040 37.671\r
+   0.01 78.082\r
+   0.017 97.26\r
+   0.027 91.781\r
+   0.043 89.726\r
+   0.06 80.822\r
+   0.087 84.932\r
+   0.101 78.767\r
+   0.132 81.507\r
+   0.143 78.767\r
+   0.171 81.507\r
+   0.192 78.082\r
+   0.215 80.822\r
+   0.24 78.082\r
+   0.264 81.507\r
+   0.279 78.767\r
+   0.298 80.137\r
+   0.517 76.027\r
+   0.68 70.548\r
+   0.855 58.219\r
+   0.934 49.315\r
+   0.961 43.151\r
+   0.996 31.507\r
+   1.025 21.233\r
+   1.054 14.384\r
+   1.103 7.534\r
+   1.147 3.425\r
+   1.196 0.0\r
diff --git a/datafiles/thrustcurves/RV_G55.eng b/datafiles/thrustcurves/RV_G55.eng
new file mode 100644 (file)
index 0000000..b2366d2
--- /dev/null
@@ -0,0 +1,30 @@
+; Same motor as the Aerotech G55W single use from NAR cert data\r
+G55W 24 177 5-10-15 0.0625 0.115 Rocketvision\r
+   0.0040 74.648\r
+   0.012 85.211\r
+   0.054 81.69\r
+   0.128 73.239\r
+   0.182 73.944\r
+   0.231 69.718\r
+   0.508 69.014\r
+   0.868 67.606\r
+   1.037 65.493\r
+   1.07 67.606\r
+   1.091 64.085\r
+   1.14 63.38\r
+   1.161 66.901\r
+   1.19 62.676\r
+   1.269 62.676\r
+   1.397 59.155\r
+   1.496 57.042\r
+   1.583 52.113\r
+   1.653 45.07\r
+   1.719 38.028\r
+   1.76 31.69\r
+   1.831 24.648\r
+   1.901 19.014\r
+   1.988 12.676\r
+   2.083 9.155\r
+   2.169 5.634\r
+   2.252 3.521\r
+   2.36 0.0\r
diff --git a/datafiles/thrustcurves/Roadrunner_E25.eng b/datafiles/thrustcurves/Roadrunner_E25.eng
new file mode 100644 (file)
index 0000000..5ad7313
--- /dev/null
@@ -0,0 +1,35 @@
+;ROADRUNNER E25R WRASP FILE\r
+E25R 29 76 4-7 0.02 0.078 RR\r
+  0.0        1.15995\r
+  0.0        4.0904\r
+  0.01       13.9194\r
+  0.02       24.481\r
+  0.025      28.327\r
+  0.04       33.028\r
+  0.045      33.639\r
+  0.07       33.516\r
+  0.12       35.287\r
+  0.195      36.569\r
+  0.245      38.278\r
+  0.28       37.668\r
+  0.315      38.657\r
+  0.35       37.729\r
+  0.385      37.973\r
+  0.45       36.691\r
+  0.56       36.569\r
+  0.73       32.295\r
+  0.82       29.487\r
+  0.9        26.068\r
+  0.92       25.824\r
+  0.945      24.176\r
+  0.995      22.772\r
+  1.035      20.024\r
+  1.08       18.5592\r
+  1.165      14.2247\r
+  1.19       13.6142\r
+  1.31       7.2039\r
+  1.395      4.2735\r
+  1.445      3.602\r
+  1.49       2.1978\r
+  1.505      0.79365\r
+  1.506      0\r
diff --git a/datafiles/thrustcurves/Roadrunner_F35.eng b/datafiles/thrustcurves/Roadrunner_F35.eng
new file mode 100644 (file)
index 0000000..d2486dc
--- /dev/null
@@ -0,0 +1,37 @@
+; ROADRUNNER F35 RASP.ENG FILE\r
+; File produced April 5, 2006\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F35 29 112 6-10 0.040 0.111 RR\r
+0.023 33.700\r
+0.040 44.462\r
+0.081 47.206\r
+0.121 48.579\r
+0.166 49.270\r
+0.242 49.550\r
+0.315 51.010\r
+0.411 50.111\r
+0.528 49.710\r
+0.664 48.208\r
+0.791 47.256\r
+0.896 46.986\r
+1.000 45.484\r
+1.097 44.943\r
+1.194 42.338\r
+1.277 40.400\r
+1.323 40.706\r
+1.356 37.691\r
+1.402 35.637\r
+1.451 32.753\r
+1.505 29.467\r
+1.578 25.491\r
+1.675 20.833\r
+1.750 17.137\r
+1.828 13.021\r
+1.907 8.638\r
+1.984 5.075\r
+2.049 2.610\r
+2.130 0.000\r
diff --git a/datafiles/thrustcurves/Roadrunner_F45.eng b/datafiles/thrustcurves/Roadrunner_F45.eng
new file mode 100644 (file)
index 0000000..11833a2
--- /dev/null
@@ -0,0 +1,33 @@
+; ROADRUNNER F45R RASP ENG FILE\r
+F45R 29 93 5-8-14 0.03 0.093 RR\r
+  0.0        4.1971\r
+  0.019      45.500\r
+  0.038      53.070\r
+  0.057      52.402\r
+  0.095      54.741\r
+  0.113      55.298\r
+  0.132      56.744\r
+  0.151      57.190\r
+  0.227      60.419\r
+  0.284      61.754\r
+  0.416      62.422\r
+  0.491      60.753\r
+  0.510      61.420\r
+  0.567      60.307\r
+  0.624      58.414\r
+  0.662      58.080\r
+  0.737      55.298\r
+  0.775      52.959\r
+  0.813      51.513\r
+  0.888      46.169\r
+  0.983      34.701\r
+  1.002      31.807\r
+  1.096      23.902\r
+  1.134      21.453\r
+  1.210      14.106\r
+  1.229      12.992\r
+  1.285      8.4282\r
+  1.342      5.5328\r
+  1.361      5.4218\r
+  1.418      2.9723\r
+  1.420       0.0\r
diff --git a/datafiles/thrustcurves/Roadrunner_F60.eng b/datafiles/thrustcurves/Roadrunner_F60.eng
new file mode 100644 (file)
index 0000000..d8cacf9
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+; ROADRUNNER F60 RASP.ENG FILE\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F60R 29 112 4-7-10 0.038 0.109 RR\r
+0.013 45.860\r
+0.021 63.937\r
+0.029 72.291\r
+0.041 75.214\r
+0.061 74.374\r
+0.087 76.872\r
+0.155 83.122\r
+0.231 86.440\r
+0.309 88.088\r
+0.329 90.070\r
+0.345 88.33\r
+0.395 87.90\r
+0.454 87.208\r
+0.514 87.188\r
+0.616 82.141\r
+0.699 77.105\r
+0.765 70.400\r
+0.807 61.611\r
+0.859 51.983\r
+0.926 42.355\r
+0.978 33.556\r
+1.022 21.430\r
+1.061 13.056\r
+1.101 6.776\r
+1.133 3.423\r
+1.190 0.000\r
diff --git a/datafiles/thrustcurves/Roadrunner_G80.eng b/datafiles/thrustcurves/Roadrunner_G80.eng
new file mode 100644 (file)
index 0000000..4d9abab
--- /dev/null
@@ -0,0 +1,35 @@
+;\r
+; ROADRUNNER G80 RASP.ENG FILE\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+G80R 29 140 4-7-10 0.055 0.133 RR\r
+0.012 63.563\r
+0.028 84.077\r
+0.057 89.563\r
+0.119 96.03\r
+0.206 102.518\r
+0.242 104.42\r
+0.297 106.923\r
+0.356 109.826\r
+0.422 111.829\r
+0.483 111.328\r
+0.558 112.632\r
+0.622 112.750\r
+0.683 112.129\r
+0.739 109.125\r
+0.796 102.017\r
+0.863 90.494\r
+0.901 82.750\r
+0.935 72.41\r
+0.976 59.869\r
+1.018 49.826\r
+1.028 44.321\r
+1.042 39.805\r
+1.073 28.272\r
+1.113 18.231\r
+1.170 11.176\r
+1.218 4.636\r
+1.310 0.000\r
diff --git a/datafiles/thrustcurves/SF_A8.eng b/datafiles/thrustcurves/SF_A8.eng
new file mode 100644 (file)
index 0000000..6c9c908
--- /dev/null
@@ -0,0 +1,15 @@
+; Sachsen Feuerwerk / WECO Feuerwerk A8-3
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+A8 18 70 3 0.00312 0.0153 SF
+   0.065 0.44
+   0.11 1.832
+   0.199 6.412
+   0.298 12.0
+   0.332 7.805
+   0.363 5.716
+   0.438 4.397
+   0.462 4.379
+   0.555 1.227
+   0.62 0.0
diff --git a/datafiles/thrustcurves/SF_B4.eng b/datafiles/thrustcurves/SF_B4.eng
new file mode 100644 (file)
index 0000000..1a0db34
--- /dev/null
@@ -0,0 +1,15 @@
+; Sachsen Feuerwerk / WECO Feuerwerk B4-0, B4-4
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+B4 18 70 0-4 0.00833 0.0195 SF
+   0.088 0.542
+   0.167 3.007
+   0.319 11.678
+   0.373 5.297
+   0.525 4.091
+   0.648 3.461
+   0.8 3.321
+   1.305 3.321
+   1.389 0.455
+   1.443 0.0
diff --git a/datafiles/thrustcurves/SF_C2.eng b/datafiles/thrustcurves/SF_C2.eng
new file mode 100644 (file)
index 0000000..4698c6d
--- /dev/null
@@ -0,0 +1,29 @@
+; Sachsen Feuerwerk / WECO Feuerwerk Held 1000
+; Created by Sampo Niskanen
+; True propellant weight unknown
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+C2 15 95 P 0.012 0.024 SF
+   0.075 3.543
+   0.16 8.231
+   0.184 8.007
+   0.255 3.134
+   0.316 1.765
+   0.444 1.304
+   0.821 1.251
+   0.963 1.04
+   1.538 1.277
+   1.736 1.133
+   2.491 1.317
+   2.704 1.264
+   3.397 1.436
+   3.907 1.436
+   4.157 1.277
+   4.459 1.449
+   4.469 2.292
+   4.53 1.436
+   4.917 1.449
+   4.931 2.292
+   4.969 1.422
+   5.002 1.436
+   5.068 0.0
diff --git a/datafiles/thrustcurves/SF_C6.eng b/datafiles/thrustcurves/SF_C6.eng
new file mode 100644 (file)
index 0000000..3239451
--- /dev/null
@@ -0,0 +1,20 @@
+; Sachsen Feuerwerk / WECO Feuerwerk C6-0, C6-3, C6-5
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+C6 18 70 0-3-5 0.01248 0.022 SF
+   0.096 0.579
+   0.152 2.441
+   0.184 4.372
+   0.312 11.642
+   0.354 11.589
+   0.395 6.269
+   0.441 5.127
+   0.537 4.091
+   0.643 3.529
+   0.983 3.301
+   1.162 3.249
+   1.217 3.02
+   1.882 3.652
+   1.919 1.141
+   1.997 0.0
diff --git a/datafiles/thrustcurves/SF_D7.eng b/datafiles/thrustcurves/SF_D7.eng
new file mode 100644 (file)
index 0000000..9b424fb
--- /dev/null
@@ -0,0 +1,18 @@
+; Sachsen Feuerwerk / WECO Feuerwerk D7-0, D7-3
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+D7 25 70 0-3 0.019 0.043 SF
+   0.079 1.625
+   0.179 6.979
+   0.326 18.992
+   0.355 19.407
+   0.372 20.426
+   0.422 20.331
+   0.48 14.085
+   0.538 11.536
+   0.68 9.815
+   0.96 7.839
+   1.34 8.253
+   1.461 2.167
+   1.582 0.0
diff --git a/datafiles/thrustcurves/SkyR_G125.eng b/datafiles/thrustcurves/SkyR_G125.eng
new file mode 100644 (file)
index 0000000..475ed67
--- /dev/null
@@ -0,0 +1,17 @@
+;\r
+;\r
+;\r
+G125  38.0 408.00 1 0.15800 0.53700 SRS\r
+   0.01     346.87 \r
+   0.04     325.72 \r
+   0.07     324.57 \r
+   0.08     415.57 \r
+   0.09     219.61 \r
+   0.13     194.84 \r
+   0.20     177.77 \r
+   0.40     157.13 \r
+   0.60     131.89 \r
+   0.80      88.31 \r
+   1.00      42.44 \r
+   1.09      14.64 \r
+   1.20       0.00 \r
diff --git a/datafiles/thrustcurves/SkyR_G63.eng b/datafiles/thrustcurves/SkyR_G63.eng
new file mode 100644 (file)
index 0000000..988d871
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+;Sky Ripper Systems 29/75 G63\r
+G63  29 304.80 0 .06500 .23600 SRS\r
+   0.01     121.91 \r
+   0.03     142.66 \r
+   0.07     156.57 \r
+   0.09     133.60 \r
+   0.13     100.62 \r
+   0.18     114.82 \r
+   0.20     105.21 \r
+   0.27     104.91 \r
+   0.35      93.38 \r
+   0.41      83.85 \r
+   0.50      67.95 \r
+   0.65      61.99 \r
+   0.83      61.99 \r
+   0.93      40.14 \r
+   1.09      17.09 \r
+   1.18      13.78 \r
+   1.29       5.56 \r
+   1.30       0.00 \r
diff --git a/datafiles/thrustcurves/SkyR_G69.eng b/datafiles/thrustcurves/SkyR_G69.eng
new file mode 100644 (file)
index 0000000..7c73a6a
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+;\r
+;Sky Ripper Systems 29/125 G69\r
+G69  29 406.40 0 .10700 .33300 SRS\r
+   0.01      99.80 \r
+   0.04     137.51 \r
+   0.07     103.01 \r
+   0.13      94.13 \r
+   0.30      80.09 \r
+   0.49      72.20 \r
+   0.57      70.72 \r
+   0.65      71.96 \r
+   0.74      80.83 \r
+   0.81      81.81 \r
+   0.94      73.43 \r
+   1.01      74.42 \r
+   1.06      85.02 \r
+   1.11      83.78 \r
+   1.19      62.10 \r
+   1.24      60.62 \r
+   1.32      65.30 \r
+   1.37      64.81 \r
+   1.43      52.98 \r
+   1.49      48.55 \r
+   1.55      44.85 \r
+   1.64      28.59 \r
+   1.85      17.74 \r
+   1.99      14.29 \r
+   2.00       0.00 \r
diff --git a/datafiles/thrustcurves/SkyR_H124.eng b/datafiles/thrustcurves/SkyR_H124.eng
new file mode 100644 (file)
index 0000000..88c0bcb
--- /dev/null
@@ -0,0 +1,34 @@
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared for SRS by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS H124 38/220 PVC\r
+H124_pvc  38.0 508.00 0 0.14200 0.66400 SRS\r
+   0.01     198.24 \r
+   0.02     300.84 \r
+   0.03     314.34 \r
+   0.03     316.00 \r
+   0.04     305.39 \r
+   0.08     280.35 \r
+   0.11     221.43 \r
+   0.69     168.38 \r
+   0.78     180.85 \r
+   0.81     158.64 \r
+   0.84     167.59 \r
+   0.89     152.32 \r
+   0.92     120.70 \r
+   0.95     123.55 \r
+   1.06      82.23 \r
+   1.35      48.36 \r
+   1.55      25.63 \r
+   1.60      21.88 \r
+   1.62      23.66 \r
+   1.66      18.58 \r
+   1.67      12.46 \r
+   1.68       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_H155.eng b/datafiles/thrustcurves/SkyR_H155.eng
new file mode 100644 (file)
index 0000000..d77c6a9
--- /dev/null
@@ -0,0 +1,21 @@
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS H155 38/220 PP\r
+H155_pp  38.0 508.00 0 0.14200 0.66400 SRS\r
+   0.03     308.75 \r
+   0.05     362.22 \r
+   0.09     252.41 \r
+   1.02     102.11 \r
+   1.08     119.98 \r
+   1.45      33.45 \r
+   1.66      19.06 \r
+   1.68      12.59 \r
+   1.69       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_H78.eng b/datafiles/thrustcurves/SkyR_H78.eng
new file mode 100644 (file)
index 0000000..3cc0382
--- /dev/null
@@ -0,0 +1,27 @@
+;\r
+;Sky Ripper Systems 29/185 H78\r
+H78  29 520.70 0 .15800 .41800 SRS\r
+   0.01     138.21 \r
+   0.08     150.10 \r
+   0.12     142.67 \r
+   0.15     132.27 \r
+   0.22     130.49 \r
+   0.29      90.30 \r
+   0.34      88.27 \r
+   0.40      86.81 \r
+   0.61      86.52 \r
+   0.72      81.59 \r
+   0.76      71.72 \r
+   0.86      64.46 \r
+   1.01      63.30 \r
+   1.18      62.42 \r
+   1.28      60.39 \r
+   1.50      57.49 \r
+   1.80      58.36 \r
+   1.89      59.23 \r
+   2.01      55.46 \r
+   2.21      36.29 \r
+   2.36      22.94 \r
+   2.54      13.36 \r
+   2.72       9.58 \r
+   2.75       0.00 \r
diff --git a/datafiles/thrustcurves/SkyR_I117.eng b/datafiles/thrustcurves/SkyR_I117.eng
new file mode 100644 (file)
index 0000000..aad7b0d
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS I117 38/580 PVC\r
+I117_pvc  38.0 914.00 0 0.37000 1.13300 SRS\r
+   0.01     203.36 \r
+   0.02     339.95 \r
+   0.02     358.77 \r
+   0.03     367.94 \r
+   0.03     361.47 \r
+   0.06     277.40 \r
+   0.08     277.11 \r
+   0.15     250.99 \r
+   0.27     256.14 \r
+   0.30     248.56 \r
+   0.42     194.78 \r
+   0.47     222.59 \r
+   1.35     174.82 \r
+   1.39     184.03 \r
+   1.79     150.12 \r
+   2.12     156.59 \r
+   3.42      54.16 \r
+   3.80      40.00 \r
+   4.20      17.22 \r
+   4.23       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_I119.eng b/datafiles/thrustcurves/SkyR_I119.eng
new file mode 100644 (file)
index 0000000..7821c00
--- /dev/null
@@ -0,0 +1,31 @@
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS I119 38/400 PVC\r
+I119_pvc  38.0 711.00 0 0.26400 0.96700 SRS\r
+   0.00     192.14 \r
+   0.01     262.20 \r
+   0.02     341.53 \r
+   0.03     338.42 \r
+   0.06     221.25 \r
+   0.07     195.93 \r
+   0.11     189.23 \r
+   0.15     202.20 \r
+   0.18     233.36 \r
+   0.47     230.26 \r
+   0.99     200.90 \r
+   1.18     165.36 \r
+   1.27     172.11 \r
+   1.48     162.61 \r
+   2.18      74.18 \r
+   2.92      24.69 \r
+   2.97      10.98 \r
+   3.10      10.06 \r
+   3.12       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_I147.eng b/datafiles/thrustcurves/SkyR_I147.eng
new file mode 100644 (file)
index 0000000..921ed1a
--- /dev/null
@@ -0,0 +1,28 @@
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS I147 38/400 PP\r
+I147_pp  38.0 711.00 0 0.26400 0.96700 SRS\r
+   0.00     194.47 \r
+   0.02     366.71 \r
+   0.03     390.09 \r
+   0.03     392.30 \r
+   0.05     359.02 \r
+   0.07     217.40 \r
+   0.10     207.68 \r
+   0.38     241.52 \r
+   1.07     183.80 \r
+   2.33     141.34 \r
+   2.51     100.04 \r
+   2.75      71.51 \r
+   3.32      34.49 \r
+   3.89      17.18 \r
+   4.43       7.17 \r
+   4.45       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_J144.eng b/datafiles/thrustcurves/SkyR_J144.eng
new file mode 100644 (file)
index 0000000..c4f4dad
--- /dev/null
@@ -0,0 +1,29 @@
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS J144 38/580 PP\r
+J144_pp  38.0 914.00 0 0.37000 1.13300 SRS\r
+   0.01     301.25 \r
+   0.03     376.44 \r
+   0.03     376.86 \r
+   0.08     247.31 \r
+   0.22     273.45 \r
+   0.45     263.59 \r
+   0.79     231.99 \r
+   1.09     175.69 \r
+   1.21     185.68 \r
+   2.05     149.60 \r
+   2.86     127.64 \r
+   3.42      69.07 \r
+   3.88      42.69 \r
+   4.59      20.49 \r
+   5.20      10.00 \r
+   5.80       6.12 \r
+   5.80       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_J261.eng b/datafiles/thrustcurves/SkyR_J261.eng
new file mode 100644 (file)
index 0000000..ebfa832
--- /dev/null
@@ -0,0 +1,33 @@
+;Sky Ripper Systems 54/830 J261 Gold Insert\r
+J261  54.0 731.50 0 0.56470 1.90250 SRS\r
+   0.02    1303.39 \r
+   0.09     903.88 \r
+   0.21     591.22 \r
+   0.42     349.57 \r
+   0.61     304.62 \r
+   0.81     329.59 \r
+   1.01     277.16 \r
+   1.21     294.64 \r
+   1.39     232.21 \r
+   1.61     197.26 \r
+   1.82     255.99 \r
+   2.00     274.66 \r
+   2.20     269.67 \r
+   2.41     189.77 \r
+   2.61     294.64 \r
+   2.82     212.24 \r
+   3.01     254.69 \r
+   3.21     225.29 \r
+   3.41     194.76 \r
+   3.59     156.32 \r
+   3.74     229.72 \r
+   3.85     237.71 \r
+   3.90     156.32 \r
+   4.00     112.76 \r
+   4.20     140.19 \r
+   4.40     192.26 \r
+   4.48     106.66 \r
+   4.61      54.86 \r
+   4.76      33.52 \r
+   4.95       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_J263.eng b/datafiles/thrustcurves/SkyR_J263.eng
new file mode 100644 (file)
index 0000000..963a225
--- /dev/null
@@ -0,0 +1,26 @@
+;Sky Ripper Systems 54/550 J263 Gold Insert\r
+J263  54.0 606.50 0 0.37420 1.59830 SRS\r
+   0.01    1359.46 \r
+   0.09     794.32 \r
+   0.15     559.93 \r
+   0.21     419.30 \r
+   0.40     296.89 \r
+   0.61     286.48 \r
+   0.81     265.64 \r
+   1.01     244.81 \r
+   1.20     247.41 \r
+   1.40     244.81 \r
+   1.60     231.79 \r
+   1.81     200.53 \r
+   2.01     205.74 \r
+   2.21     205.74 \r
+   2.41     195.32 \r
+   2.60     174.49 \r
+   2.80     166.68 \r
+   2.91     161.47 \r
+   3.00     138.03 \r
+   3.08      98.96 \r
+   3.21      62.50 \r
+   3.41      28.74 \r
+   3.60       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_J337.eng b/datafiles/thrustcurves/SkyR_J337.eng
new file mode 100644 (file)
index 0000000..a60b6e6
--- /dev/null
@@ -0,0 +1,22 @@
+;Sky Ripper Systems 54/830 J337 Black Insert\r
+J337  54.0 731.50 0 0.56470 1.90250 SRS\r
+   0.00       8.88 \r
+   0.01    1521.03 \r
+   0.10     585.29 \r
+   0.20     651.28 \r
+   0.41     589.45 \r
+   0.50     548.23 \r
+   0.60     539.99 \r
+   0.99     461.67 \r
+   1.40     416.33 \r
+   1.60     391.59 \r
+   1.80     387.47 \r
+   1.91     364.90 \r
+   2.01     239.43 \r
+   2.20     171.02 \r
+   2.40     119.72 \r
+   2.60      72.68 \r
+   2.80      42.76 \r
+   3.00      21.68 \r
+   3.10       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_J348.eng b/datafiles/thrustcurves/SkyR_J348.eng
new file mode 100644 (file)
index 0000000..efd3d0f
--- /dev/null
@@ -0,0 +1,19 @@
+;Sky Ripper Systems 54/550 J348 Black Insert\r
+J348  54.0 606.50 0 0.37420 1.59830 SRS\r
+   0.00       8.88 \r
+   0.02     451.80 \r
+   0.13     557.60 \r
+   0.14    1203.84 \r
+   0.20     617.65 \r
+   0.40     549.02 \r
+   0.60     494.69 \r
+   0.79     406.05 \r
+   1.01     354.57 \r
+   1.20     334.56 \r
+   1.31     303.10 \r
+   1.40     237.34 \r
+   1.60     148.69 \r
+   1.80      82.92 \r
+   2.00      37.17 \r
+   2.20       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_K257.eng b/datafiles/thrustcurves/SkyR_K257.eng
new file mode 100644 (file)
index 0000000..a926d86
--- /dev/null
@@ -0,0 +1,34 @@
+;Sky Ripper Systems 54/1130 K257 Gold Insert\r
+K257  54.0 911.40 0 0.76880 2.31070 SRS\r
+   0.07    1133.08 \r
+   0.20     529.98 \r
+   0.40     333.69 \r
+   0.59     363.13 \r
+   0.80     347.18 \r
+   0.98     366.40 \r
+   1.20     356.59 \r
+   1.40     310.79 \r
+   1.60     274.80 \r
+   1.81     337.71 \r
+   2.00     356.59 \r
+   2.20     294.43 \r
+   2.40     314.06 \r
+   2.60     294.43 \r
+   2.81     333.69 \r
+   3.00     340.23 \r
+   3.22     319.78 \r
+   3.41     292.68 \r
+   3.59     249.32 \r
+   3.79     278.07 \r
+   4.00     243.90 \r
+   4.21     281.35 \r
+   4.41     235.54 \r
+   4.60     251.90 \r
+   4.81     211.38 \r
+   5.01     222.22 \r
+   5.20     189.70 \r
+   5.41     153.23 \r
+   5.60      81.40 \r
+   5.80      23.94 \r
+   6.10       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/SkyR_K347.eng b/datafiles/thrustcurves/SkyR_K347.eng
new file mode 100644 (file)
index 0000000..aee6041
--- /dev/null
@@ -0,0 +1,28 @@
+;Sky Ripper Systems 54/1130 K347 Black Insert\r
+K347  54.0 911.40 0 0.76880 2.31070 SRS\r
+   0.00       8.88 \r
+   0.03    1474.51 \r
+   0.11     972.20 \r
+   0.19     737.25 \r
+   0.40     697.22 \r
+   0.61     673.87 \r
+   0.81     653.85 \r
+   1.01     600.40 \r
+   1.20     550.44 \r
+   1.40     530.42 \r
+   1.60     533.76 \r
+   1.80     517.08 \r
+   1.89     396.98 \r
+   1.95     493.19 \r
+   2.06     393.65 \r
+   2.13     443.69 \r
+   2.30     365.38 \r
+   2.30     292.31 \r
+   2.40     257.92 \r
+   2.60     184.84 \r
+   2.79     124.66 \r
+   3.00      81.67 \r
+   3.21      51.58 \r
+   3.41      21.49 \r
+   3.58       0.00 \r
+;\r
diff --git a/datafiles/thrustcurves/WCH_I110.eng b/datafiles/thrustcurves/WCH_I110.eng
new file mode 100644 (file)
index 0000000..6f73ee8
--- /dev/null
@@ -0,0 +1,32 @@
+;\r
+; West Coast Hybrid motor\r
+; prepared for WestCoast/Scott Harrison by\r
+; Andrew MacMillen NAR 77472 9/13/02\r
+;\r
+; based on CAR thrust curves & cert letters\r
+; since the cert letters and test curves don't agree\r
+; the data is hand entered data points from CAR thrust curves\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim5 EngEdit\r
+;\r
+; within 2-3 percent for total impulse, peak and average thrust\r
+; accurate thrust profile\r
+;\r
+; NOTE: NOT WCH, CAR, TMT OR NAR APPROVED\r
+;\r
+; West Coast Hybrids I110\r
+;\r
+I110H  38.0 606.00 0 0.32700 0.82400 WCH\r
+   0.05     240.00 \r
+   0.38     200.00 \r
+   0.76     182.00 \r
+   1.14     160.00 \r
+   1.52     142.00 \r
+   1.90     125.00 \r
+   2.23     113.00 \r
+   2.66      71.00 \r
+   3.04      44.00 \r
+   3.42      29.00 \r
+   3.78      22.00 \r
+   4.00       0.00 \r
+;\r
diff --git a/extra-lib/RXTXcomm.jar b/extra-lib/RXTXcomm.jar
new file mode 100644 (file)
index 0000000..84e5f01
Binary files /dev/null and b/extra-lib/RXTXcomm.jar differ
diff --git a/extra-src/altimeter/Alt15K.java b/extra-src/altimeter/Alt15K.java
new file mode 100644 (file)
index 0000000..0fba135
--- /dev/null
@@ -0,0 +1,562 @@
+package altimeter;
+
+import gnu.io.CommPortIdentifier;
+import gnu.io.PortInUseException;
+import gnu.io.SerialPort;
+import gnu.io.UnsupportedCommOperationException;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.TimeZone;
+
+/**
+ * Class to interface the PerfectFlite Alt15K/WD altimeter.
+ * 
+ * Also includes a main method that retrieves all flight profiles and saves them to files.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class Alt15K {
+       public static final int TIMEOUT = 500;
+       public static final int RWDELAY = 5;
+       
+       private static final boolean DEBUG = false;
+       
+       private static final Charset CHARSET = Charset.forName("ISO-8859-1");
+       
+       private final CommPortIdentifier portID;
+       private SerialPort port = null;
+       private InputStream is = null;
+       private OutputStream os = null;
+       
+       
+
+       @SuppressWarnings("unchecked")
+       public static String[] getNames() {
+               ArrayList<String> list = new ArrayList<String>();;
+               
+               Enumeration pids = CommPortIdentifier.getPortIdentifiers();
+
+               while (pids.hasMoreElements()) {
+                   CommPortIdentifier pid = (CommPortIdentifier) pids.nextElement();
+
+                   if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL)
+                       list.add(pid.getName());
+               }
+               return list.toArray(new String[0]);
+       }
+
+       
+
+       @SuppressWarnings("unchecked")
+       public Alt15K(String name) throws IOException {
+               CommPortIdentifier pID = null;
+               
+               Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
+               while (portIdentifiers.hasMoreElements()) {
+                   CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
+                   
+                   if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
+                      pid.getName().equals(name)) {
+                       pID = pid;
+                       break;
+                   }
+               }
+               
+               if (pID==null) {
+                       throw new IOException("Port '"+name+"' not found.");
+               }
+               this.portID = pID;
+       }
+       
+
+       /**
+        * Get altimeter flight data.  The flight profile is chosen by the parameter n,
+        * 0 = latest flight, 1 = second latest, etc.
+        * 
+        * @param n  Which flight profile to use (0=newest, 1=second newest, etc)
+        * @return   The altimeter flight data
+        * @throws IOException                  in case of IOException
+        * @throws PortInUseException   in case of PortInUseException
+        */
+       public AltData getData(int n) throws IOException, PortInUseException {
+               AltData alt = new AltData();
+               ArrayList<Integer> data = new ArrayList<Integer>();
+               byte[] buf;
+               byte[] buf2 = new byte[0];
+               boolean identical = false;  // Whether identical lines have been read
+               
+               if (DEBUG)
+                       System.out.println("  Retrieving altimeter data n="+n);
+               
+               try {
+                       open();
+
+                       // Get version and position data
+                       byte[] ver = getVersionData();
+                       alt.setVersion(new byte[] { ver[0],ver[1] });
+
+                       // Calculate the position requested
+                       if (n > 2)
+                               n = 2;
+                       int position = ver[2] - n;
+                       while (position < 0)
+                               position += 3;
+
+                       if (DEBUG)
+                               System.out.println("  Requesting data from position "+position);
+                       
+                       // Request the data
+                       write("D");
+                       write((byte)position);
+                       write("PS");
+
+                       sleep();
+
+                       // Read preliminary data
+                       buf = read(4);
+                       int msl_level = combine(buf[0],buf[1]);
+                       int datacount = combine(buf[2],buf[3]);
+
+                       if (DEBUG)
+                               System.out.println("  Preliminary data msl="+msl_level+" count="+datacount);
+                       
+                       alt.setMslLevel(msl_level-6000);
+                       alt.setDataSamples(datacount);
+
+                       if (DEBUG)
+                               System.out.println("  Retrieving "+datacount+" samples");
+
+                       long t = System.currentTimeMillis();
+
+                       int count = 0;
+                       while (count < datacount) {
+                               sleep();
+                               write("G");
+                               sleep();
+                               buf = read(17);
+
+                               if (buf.length == 17) {
+                                       // Checksum = sum of all bytes + 1
+                                       // (signedness does not change the result)
+                                       byte checksum = 1;
+                                       for (int i=0; i<16; i++)
+                                               checksum += buf[i];
+                                       if (checksum != buf[16]) {
+                                               printBytes("ERROR: Checksum fail on data (computed="+checksum+
+                                                               " orig="+buf[16]+")",buf);
+                                               System.out.println("Ignoring error");
+                                       }
+                               } else {
+                                       System.err.println("ERROR:  Only "+buf.length+" bytes read, should be 17");
+                               }
+                               
+                               for (int i=0; i<buf.length-1; i+=2) {
+                                       data.add(combine(buf[i],buf[i+1]));
+                                       count++;
+                               }
+                               
+                               /*
+                                * Check whether the data is identical to the previous data batch.  If reading
+                                * too fast, the data seems to become duplicated in the transfer.  We need to check
+                                * whether this has happened by attempting to read more data than is normally
+                                * available.
+                                */
+                               int c, l=Math.min(buf.length, buf2.length);
+                               for (c=0; c<l; c++) {
+                                       if (buf[c] != buf2[c])
+                                               break;
+                               }
+                               if (c==l && buf.length == buf2.length)
+                                       identical = true;
+                               buf2 = buf.clone();
+                       }
+
+                       if (DEBUG)
+                               System.out.println("  Retrieved "+data.size()+" samples in "+
+                                               (System.currentTimeMillis()-t)+" ms");
+
+
+                       // In case of identical lines, check for more data.  This would mean that the
+                       // transfer was corrupted.
+                       if (identical) {
+                               System.err.println("WARNING:  Duplicate data detected, possible error");
+                       }
+
+                       // Test for more data
+                       if (DEBUG)
+                               System.out.println("  Testing for more data");
+                       sleep();
+                       write("G");
+                       sleep();
+                       buf = read(17);
+                       if (buf.length > 0) {
+                               System.err.println("ERROR: Data available after transfer! (length="+buf.length+")");
+                       }
+
+                       
+                       
+                       
+                       
+                       
+                       // Create an int[] array and set it
+                       int[] d = new int[data.size()];
+                       for (int i=0; i<d.length; i++)
+                               d[i] = data.get(i);
+                       alt.setData(d);
+                       
+               //  Catch all exceptions, close the port and re-throw the exception
+               } catch (PortInUseException e) {
+                       close();
+                       throw e;
+               } catch (IOException e) {
+                       close();
+                       throw e;
+               } catch (UnsupportedCommOperationException e) {
+                       close();
+                       throw new RuntimeException("Required function of RxTx library not supported",e);
+               } catch (RuntimeException e) {
+                       // Catch-all for all other types of exceptions
+                       close();
+                       throw e;
+               }
+
+               close();
+               return alt;
+       }
+       
+
+       
+       
+       private byte[] getVersionData() throws PortInUseException, IOException, 
+                                                                                  UnsupportedCommOperationException {
+               byte[] ver = new byte[3];
+               byte[] buf;
+
+               if (DEBUG)
+                       System.out.println("  Retrieving altimeter version information");
+               
+               // Signal to altimeter we are here
+               write((byte)0);
+               sleep(15);  // Sleep for 15ms, data is incoming at 10 samples/sec
+               
+               // Get altimeter version, skip zeros
+               write("PV");
+               sleep();
+               buf = readSkipZero(2);
+               sleep();
+               if (buf.length != 2) {
+                       close();
+                       throw new IOException("Communication with altimeter failed.");
+               }
+               ver[0] = buf[0];
+               ver[1] = buf[1];
+               
+               // Get position of newest data
+               write("M");
+               sleep();
+               buf = read(1);
+               if (buf.length != 1) {
+                       close();
+                       throw new IOException("Communication with altimeter failed.");
+               }
+               ver[2] = buf[0];
+
+               if (DEBUG)
+                       System.out.println("  Received version info "+ver[0]+"."+ver[1]+", position "+ver[2]);
+               
+               return ver;
+       }
+       
+       
+       /**
+        * Delay the communication by a small delay (RWDELAY ms).
+        */
+       private void sleep() {
+               sleep(RWDELAY);
+       }
+       
+       /**
+        * Sleep for the given amount of milliseconds.
+        */
+       private void sleep(int n) {
+               try {
+                       Thread.sleep(n);
+               } catch (InterruptedException ignore) { }
+       }
+       
+       
+       private void open() 
+       throws PortInUseException, IOException, UnsupportedCommOperationException {
+               if (port != null) {
+                       System.err.println("ERROR: open() called with port="+port);
+                       Thread.dumpStack();
+                       close();
+               }
+               
+               if (DEBUG) {
+                       System.out.println("  Opening port...");
+               }
+
+               port = (SerialPort)portID.open("OpenRocket",1000);
+               
+               port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, 
+                               SerialPort.PARITY_NONE);
+
+               port.setInputBufferSize(1);
+               port.setOutputBufferSize(1);
+
+               port.enableReceiveTimeout(TIMEOUT);
+
+               is = port.getInputStream();
+               os = port.getOutputStream();
+       }
+       
+       
+       private byte[] readSkipZero(int n) throws IOException, UnsupportedCommOperationException {
+               long t = System.currentTimeMillis() + TIMEOUT*2;
+               
+               if (DEBUG) {
+                       System.out.println("    readSkipZero "+n+" bytes");
+               }
+               
+               while (System.currentTimeMillis() < t) {
+                       byte[] buf = read(n);
+                       if (DEBUG)
+                               printBytes("      Received",buf);
+                       
+                       if (buf.length == 0)  // No data available
+                               return buf;
+                       
+                       // Skip zeros
+                       int i;
+                       for (i=0; i<buf.length; i++)
+                               if (buf[i] != 0)
+                                       break;
+                       
+                       if (i==0)   // No zeros to skip
+                               return buf;
+                       
+                       if (i < buf.length) {
+                               // Partially read
+                               int count = buf.length-i;  // No. of data bytes
+                               byte[] array = new byte[n];
+                               System.arraycopy(buf, i, array, 0, count);
+                               buf = read(n-count);
+                               if (DEBUG)
+                                       printBytes("      Received (partial)",buf);
+                               System.arraycopy(buf, 0, array, count, buf.length);
+                               
+                               if (DEBUG)
+                                       printBytes("    Returning",array);
+                               return array;
+                       }
+               }
+               
+               if (DEBUG)
+                       System.out.println("  No data read, returning empty");
+               return new byte[0];  // no data, only zeros
+       }
+       
+
+       private byte[] read(int n) throws IOException, UnsupportedCommOperationException {
+               byte[] bytes = new byte[n];
+               
+               port.enableReceiveThreshold(n);
+               
+               long t = System.currentTimeMillis() + TIMEOUT;
+               int count = 0;
+
+               if (DEBUG)
+                       System.out.println("    Reading "+n+" bytes");
+
+               while (count < n && System.currentTimeMillis() < t) {
+                       byte[] buf = new byte[n-count];
+                       int c = is.read(buf);
+                       System.arraycopy(buf, 0, bytes, count, c);
+                       count += c;
+               }
+               
+               byte[] array = new byte[count];
+               System.arraycopy(bytes, 0, array, 0, count);
+               
+               if (DEBUG)
+                       printBytes("    Returning",array);
+               
+               return array;
+       }
+       
+       private void write(String s) throws IOException {
+               write(s.getBytes(CHARSET));
+       }
+       
+       private void write(byte ... bytes) throws IOException {
+               if (DEBUG)
+                       printBytes("    Writing",bytes);
+               os.write(bytes);
+       }
+       
+       private void close() {
+               if (DEBUG)
+                       System.out.println("  Closing port");
+               
+               SerialPort p = port;
+               port = null;
+               is = null;
+               os = null;
+               if (p != null)
+                       p.close();
+       }
+       
+       
+       
+
+       
+       public static void main(String[] arg) {
+               
+               if (arg.length != 1) {
+                       System.err.println("Usage:  java Alt15K <basename>");
+                       System.err.println("Files will be saved <basename>-old.log, -med and -new");
+                       return;
+               }
+               
+               
+               String device = null;
+               String[] devices = Alt15K.getNames();
+               for (int i=0; i<devices.length; i++) {
+                       if (devices[i].matches(".*USB.*")) {
+                               device = devices[i];
+                               break;
+                       }
+               }
+               if (device == null) {
+                       System.out.println("Device not found.");
+                       return;
+               }
+               
+               
+               System.out.println("Selected device "+device);
+               
+               AltData alt = null;
+               String file;
+               try {
+                       Alt15K p = new Alt15K(device);
+
+                       System.out.println("Retrieving newest data...");
+                       alt = p.getData(0);
+                       System.out.println("Apogee at "+alt.getApogee()+" feet");
+
+                       file = arg[0]+"-new.log";
+                       System.out.println("Saving data to "+file+"...");
+                       savefile(file,alt);
+                       
+                       
+                       System.out.println("Retrieving medium data...");
+                       alt = p.getData(1);
+                       System.out.println("Apogee at "+alt.getApogee()+" feet");
+
+                       file = arg[0]+"-med.log";
+                       System.out.println("Saving data to "+file+"...");
+                       savefile(file,alt);
+                       
+                       
+                       System.out.println("Retrieving oldest data...");
+                       alt = p.getData(2);
+                       System.out.println("Apogee at "+alt.getApogee()+" feet");
+
+                       file = arg[0]+"-old.log";
+                       System.out.println("Saving data to "+file+"...");
+                       savefile(file,alt);
+                       
+               } catch (IOException e) {
+                       e.printStackTrace();
+               } catch (PortInUseException e) {
+                       e.printStackTrace();
+               }
+
+//             System.out.println(alt);
+//             alt.printData();
+               
+       }
+       
+       
+       static private void savefile(String file, AltData data) throws FileNotFoundException {
+               
+               PrintStream output = new PrintStream(file);
+               
+               // WTF is this so difficult?!?
+               DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
+               TimeZone tz=TimeZone.getTimeZone("GMT+3");
+               fmt.setTimeZone(tz);
+
+               output.println("# Alt15K data, file "+file);
+               output.println("# Data retrieved at: "+fmt.format(new Date()));
+               output.println("# Values are in feet above launch level");
+               output.println("# ");
+               output.println("# Apogee = "+data.getApogee());
+               output.println("# MSL level = "+data.getMslLevel());
+               output.println("# Data count = "+data.getDataSamples());
+               
+               byte[] b = data.getVersion();
+               String s="";
+               for (int i=0; i<b.length; i++) {
+                       if (s.equals(""))
+                               s = ""+((int)b[i]);
+                       else 
+                               s = s+"."+((int)b[i]);
+               }
+               output.println("# Altimeter version = " + s);
+               
+               int[] values = data.getData();
+               for (int i=0; i < values.length; i++) {
+                       output.println(""+values[i]);
+               }
+               
+               output.close();
+       }
+       
+       
+       static private void printBytes(String str, byte[] b) {
+               printBytes(str, b,b.length);
+       }
+       
+       static private void printBytes(String str, byte[] b, int n) {
+               String s;
+               s = str+" "+n+" bytes:";
+               for (int i=0; i<n; i++) {
+                       s += " "+unsign(b[i]);
+               }
+               System.out.println(s);
+       }
+       
+       static private int unsign(byte b) {
+               if (b >= 0)
+                       return b;
+               else
+                       return 256 + b;
+       }
+       
+       @SuppressWarnings("unused")
+       static private int combine(int a, int b) {
+               return 256*a + b;
+       }
+       
+       static private int combine(byte a, byte b) {
+               int val = 256*unsign(a)+unsign(b);
+               if (val <= 32767)
+                       return val;
+               else
+                       return val-65536;
+                       
+       }
+       
+}
diff --git a/extra-src/altimeter/AltData.java b/extra-src/altimeter/AltData.java
new file mode 100644 (file)
index 0000000..63314c7
--- /dev/null
@@ -0,0 +1,81 @@
+package altimeter;
+
+public class AltData {
+
+       private int mslLevel = 0;
+       private int samples = 0;
+       private int[] data = null;
+       private byte[] version = null;
+       
+       
+       public void setMslLevel(int msl) {
+               mslLevel = msl;
+       }
+       public int getMslLevel() {
+               return mslLevel;
+       }
+       
+       public void setDataSamples(int s) {
+               samples = s;
+       }
+       public int getDataSamples() {
+               return samples;
+       }
+       
+       public void setVersion(byte[] v) {
+               if (v==null)
+                       version = null;
+               else 
+                       version = v.clone();
+       }
+       public byte[] getVersion() {
+               if (version == null)
+                       return null;
+               return version.clone();         
+       }
+       
+       public void setData(int[] data) {
+               if (data==null)
+                       this.data = null;
+               else 
+                       this.data = data.clone();
+       }
+       public int[] getData() {
+               if (data == null)
+                       return null;
+               return data.clone();
+       }
+
+       public int getApogee() {
+               if (data == null || data.length==0)
+                       return 0;
+               int max = Integer.MIN_VALUE;
+               for (int i=0; i<data.length; i++) {
+                       if (data[i] > max)
+                               max = data[i];
+               }
+               return max;
+       }
+       
+       @Override
+       public String toString() {
+               String s = "AltData(";
+               s += "MSL:"+getMslLevel()+",";
+               s += "Apogee:"+getApogee()+",";
+               s += "Samples:"+getDataSamples();
+               s += ")";
+               return s;
+       }
+       
+       public void printData() {
+               System.out.println(toString()+":");
+               for (int i=0; i<data.length; i+=8) {
+                       String s = "  "+i+":";
+                       for (int j=0; j<8 && (i+j)<data.length; j++) {
+                               s += " "+data[i+j];
+                       }
+                       System.out.println(s);
+               }
+       }
+       
+}
diff --git a/extra-src/altimeter/RotationLogger.java b/extra-src/altimeter/RotationLogger.java
new file mode 100644 (file)
index 0000000..45e7d76
--- /dev/null
@@ -0,0 +1,356 @@
+package altimeter;
+
+import gnu.io.CommPortIdentifier;
+import gnu.io.PortInUseException;
+import gnu.io.SerialPort;
+import gnu.io.UnsupportedCommOperationException;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+/**
+ * Class to interface the PerfectFlite Alt15K/WD altimeter.
+ * 
+ * Also includes a main method that retrieves all flight profiles and saves them to files.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class RotationLogger {
+       private static final boolean DEBUG = false;
+       
+       private static final int BYTES = 65536; 
+       
+       
+       private final CommPortIdentifier portID;
+       private SerialPort port = null;
+       private InputStream is = null;
+       private OutputStream os = null;
+       
+       
+
+       @SuppressWarnings("unchecked")
+       public static String[] getNames() {
+               ArrayList<String> list = new ArrayList<String>();;
+               
+               Enumeration pids = CommPortIdentifier.getPortIdentifiers();
+
+               while (pids.hasMoreElements()) {
+                   CommPortIdentifier pid = (CommPortIdentifier) pids.nextElement();
+
+                   if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL)
+                       list.add(pid.getName());
+               }
+               return list.toArray(new String[0]);
+       }
+
+       
+       
+       
+
+       @SuppressWarnings("unchecked")
+       public RotationLogger(String name) throws IOException {
+               CommPortIdentifier portID = null;
+               
+               Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
+               while (portIdentifiers.hasMoreElements()) {
+                   CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
+                   
+                   if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
+                      pid.getName().equals(name)) {
+                       portID = pid;
+                       break;
+                   }
+               }
+               
+               if (portID==null) {
+                       throw new IOException("Port '"+name+"' not found.");
+               }
+               this.portID = portID;
+       }
+       
+       
+       
+       
+       
+       
+       public void readData() throws IOException, PortInUseException {
+               int c;
+               
+               int[] data = new int[BYTES];
+               
+               FileOutputStream rawdump = null;
+               
+               
+               try {
+                       open();
+
+                       System.err.println("Sending dump mode command...");
+                       
+                       for (int i=0; i<16; i++) {
+                               os.write('D');
+                               try {
+                                       Thread.sleep(10);
+                               } catch (InterruptedException ignore) { }
+                       }
+                       
+                       System.err.println("Waiting for response...");
+                       while (true) {
+                               c = is.read();
+                               if (c == 'K') {
+                                       break;
+                               } else {
+                                       System.err.printf("Received spurious c=%d\n",c);
+                               }
+                       }
+                       
+                       System.err.println("Received response.");
+                       
+
+                       
+                       System.err.println("Opening 'rawdump'...");
+                       rawdump = new FileOutputStream("rawdump");
+                       
+                       
+                       
+                       System.err.println("Performing dump...");
+
+                       os.write('A');
+
+                       byte[] buffer = new byte[1024];
+                       int printCount = 0;
+                       for (int count=0; count < BYTES; ) {
+                               if ((BYTES-count) < buffer.length) {
+                                       buffer = new byte[BYTES-count];
+                               }
+                               
+                               int n = is.read(buffer);
+                               if (n < 0) {
+                                       System.err.println("Error condition, n="+n);
+                                       return;
+                               }
+                       
+                               rawdump.write(buffer, 0, n);
+                               
+                               for (int i=0; i<n; i++) {
+                                       data[count+i] = unsign(buffer[i]);
+                               }
+                               count += n;
+                               if (count - printCount > 1024) {
+                                       System.err.println("Read "+count+" bytes...");
+                                       printCount = count;
+                               }
+                       }
+
+
+                       System.err.println("Verifying checksum...");
+                       int reported = is.read();
+                       
+                       byte computed = 0;
+                       for (int i=0; i < data.length; i++) {
+                               computed += data[i];
+                       }
+                       if (computed == reported) {
+                               System.err.println("Checksum ok ("+computed+")");
+                       } else {
+                               System.err.println("Error in checksum, computed="+computed+
+                                               " reported="+reported);
+                       }
+                       
+                       System.err.println("Communication done.");
+                       
+               } catch (UnsupportedCommOperationException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               } finally {
+                       close();
+                       if (rawdump != null)
+                               rawdump.close();
+               }
+               
+               convertData(data);
+               
+       }
+       
+       
+       
+       ////////////  Data interpretation   //////////////      
+       
+       
+       private static void convertData(int[] data) {
+
+               System.err.println("Converting data...");
+
+               int lastBuffer = data[0xffff];
+               if (lastBuffer < 0 || lastBuffer > 3) {
+                       System.err.println("Illegal last accessed buffer: "+lastBuffer);
+                       return;
+               }
+               System.err.println("Last used buffer: "+lastBuffer);
+               
+               for (int i=4; i>0; i--) {
+                       int n = (lastBuffer + i) % 4;
+                       int bufNumber = 4-i;
+                       
+                       convertBuffer(data, n * (BYTES/4), bufNumber);
+               }
+               
+       }
+       
+       
+       private static void convertBuffer(int[] data, int position, int bufNumber) {
+               int startPosition;
+               
+               startPosition = data[position + 0xfd] << 8 + data[position+0xfe];
+
+               // 50 samples per 128 bytes 
+               int startTime = (startPosition -position) * 50 / 128;
+               
+               System.err.println("  Buffer "+ bufNumber + " (at position "+position+")...");
+               System.err.println("  Start position "+startPosition+" time "+startTime);
+
+               System.out.println("# Buffer "+bufNumber);
+               System.out.println("# Start position t="+startTime);
+               
+               
+               int t = 0;
+               for (int page = 0; page < 128; page++) {
+                       int pageStart = position + page * 128;
+
+                       if (pageStart == startPosition) {
+                               System.out.println("# ---clip---");
+                       }
+
+                       for (int i=0; i<125; i += 5) {
+                               int sample1, sample2;
+                               
+                               int start = pageStart + i;
+//                             System.err.println("page="+page+" i="+i+
+//                                             " position="+position+" pageStart="+pageStart+" start="+start);
+                               
+                               sample1 = (data[start] << 2) + (data[start+1] >> 6);
+                               sample2 = ((data[start+1] & 0x3f) << 4) + (data[start+2] >> 4);
+                               System.out.printf("%d  %4d  %4d %4d\n", bufNumber, t, sample1, sample2);
+                               t++;
+                               
+                               sample1 = ((data[start+2] & 0x0f) << 6) + (data[start+3] >> 2);
+                               sample2 = ((data[start+3] & 3) << 8) + data[start+4];
+                               System.out.printf("%d  %4d  %4d %4d\n", bufNumber, t, sample1, sample2);
+                               t++;
+                       }
+               }
+       }
+
+       
+       
+       private void open() throws PortInUseException, IOException, 
+                       UnsupportedCommOperationException {
+               
+               if (port != null) {
+                       System.err.println("ERROR: open() called with port="+port);
+                       Thread.dumpStack();
+                       close();
+               }
+               
+               if (DEBUG) {
+                       System.err.println("  Opening port...");
+               }
+
+               port = (SerialPort)portID.open("OpenRocket",1000);
+               
+               port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, 
+                               SerialPort.PARITY_NONE);
+
+               port.setInputBufferSize(1);
+               port.setOutputBufferSize(1);
+
+               is = port.getInputStream();
+               os = port.getOutputStream();
+       }
+       
+       
+       private void close() {
+               if (DEBUG)
+                       System.err.println("  Closing port");
+               
+               SerialPort p = port;
+               port = null;
+               is = null;
+               if (p != null)
+                       p.close();
+       }
+       
+       
+       
+       private static int unsign(byte b) {
+               if (b >= 0)
+                       return b;
+               else
+               return 256 + b;
+       }
+       
+       
+
+       
+       public static void main(String[] arg) throws Exception {
+               
+               if (arg.length > 2) {
+                       System.err.println("Illegal arguments.");
+                       return;
+               }
+               if (arg.length == 1) {
+                       FileInputStream is = new FileInputStream(arg[0]);
+                       byte[] buffer = new byte[BYTES];
+                       int n = is.read(buffer);
+                       if (n != BYTES) {
+                               System.err.println("Could read only "+n+" bytes");
+                               return;
+                       }
+                       
+                       int[] data = new int[BYTES];
+                       for (int i=0; i<BYTES; i++) {
+                               data[i] = unsign(buffer[i]);
+                       }
+
+                       int checksum=0;
+                       for (int i=0; i<BYTES; i++) {
+                               checksum += data[i];
+                       }
+                       checksum = checksum%256;
+                       System.err.println("Checksum: "+checksum);
+                       
+                       convertData(data);
+                       return;                 
+               }
+               
+               
+               String device = null;
+               String[] devices = RotationLogger.getNames();
+               for (int i=0; i<devices.length; i++) {
+                       if (devices[i].matches(".*USB.*")) {
+                               device = devices[i];
+                               break;
+                       }
+               }
+               if (device == null) {
+                       System.err.println("Device not found.");
+                       return;
+               }
+               
+               
+               System.err.println("Selected device "+device);
+               
+               
+               RotationLogger p = new RotationLogger(device);
+               
+               p.readData();
+               
+       }
+       
+       
+}
diff --git a/extra-src/altimeter/SerialDownload.java b/extra-src/altimeter/SerialDownload.java
new file mode 100644 (file)
index 0000000..d82e582
--- /dev/null
@@ -0,0 +1,184 @@
+package altimeter;
+
+import gnu.io.CommPortIdentifier;
+import gnu.io.PortInUseException;
+import gnu.io.SerialPort;
+import gnu.io.UnsupportedCommOperationException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+/**
+ * Class to interface the PerfectFlite Alt15K/WD altimeter.
+ * 
+ * Also includes a main method that retrieves all flight profiles and saves them to files.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class SerialDownload {
+       private static final boolean DEBUG = false;
+       
+       private static final int MAGIC = 666;
+       
+       private final CommPortIdentifier portID;
+       private SerialPort port = null;
+       private InputStream is = null;
+       
+       
+
+       @SuppressWarnings("unchecked")
+       public static String[] getNames() {
+               ArrayList<String> list = new ArrayList<String>();;
+               
+               Enumeration pids = CommPortIdentifier.getPortIdentifiers();
+
+               while (pids.hasMoreElements()) {
+                   CommPortIdentifier pid = (CommPortIdentifier) pids.nextElement();
+
+                   if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL)
+                       list.add(pid.getName());
+               }
+               return list.toArray(new String[0]);
+       }
+
+       
+       
+       
+
+       @SuppressWarnings("unchecked")
+       public SerialDownload(String name) throws IOException {
+               CommPortIdentifier portID = null;
+               
+               Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
+               while (portIdentifiers.hasMoreElements()) {
+                   CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
+                   
+                   if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
+                      pid.getName().equals(name)) {
+                       portID = pid;
+                       break;
+                   }
+               }
+               
+               if (portID==null) {
+                       throw new IOException("Port '"+name+"' not found.");
+               }
+               this.portID = portID;
+       }
+       
+       
+       
+       
+       
+       
+       public void readData() throws IOException, PortInUseException {
+               long t0 = -1;
+               long t;
+               
+               int previous = MAGIC;
+               
+               
+               try {
+                       open();
+                       
+                       System.err.println("Ready to read...");
+                       while (true) {
+                               int c = is.read();
+                               t = System.nanoTime();
+                               if (t0 < 0)
+                                       t0 = t;
+                               
+                               System.out.printf("%10.6f %d\n", ((double)t-t0)/1000000000.0, c);
+                               
+                               if (previous == MAGIC) {
+                                       previous = c;
+                               } else {
+                                       System.out.printf("# Altitude: %5d\n", previous*256 + c);
+                                       previous = MAGIC;
+                               }
+                               
+                               if (c < 0)
+                                       break;
+                       }
+                       
+                       
+               } catch (UnsupportedCommOperationException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               } finally {
+                       close();
+               }
+       }
+
+       
+       
+       private void open() throws PortInUseException, IOException, 
+                       UnsupportedCommOperationException {
+               
+               if (port != null) {
+                       System.err.println("ERROR: open() called with port="+port);
+                       Thread.dumpStack();
+                       close();
+               }
+               
+               if (DEBUG) {
+                       System.err.println("  Opening port...");
+               }
+
+               port = (SerialPort)portID.open("OpenRocket",1000);
+               
+               port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, 
+                               SerialPort.PARITY_NONE);
+
+               port.setInputBufferSize(1);
+               port.setOutputBufferSize(1);
+
+               is = port.getInputStream();
+       }
+       
+       
+       private void close() {
+               if (DEBUG)
+                       System.err.println("  Closing port");
+               
+               SerialPort p = port;
+               port = null;
+               is = null;
+               if (p != null)
+                       p.close();
+       }
+       
+       
+       
+
+       
+       public static void main(String[] arg) throws Exception {
+               
+               String device = null;
+               String[] devices = SerialDownload.getNames();
+               for (int i=0; i<devices.length; i++) {
+                       if (devices[i].matches(".*USB.*")) {
+                               device = devices[i];
+                               break;
+                       }
+               }
+               if (device == null) {
+                       System.err.println("Device not found.");
+                       return;
+               }
+               
+               
+               System.err.println("Selected device "+device);
+               
+               
+               SerialDownload p = new SerialDownload(device);
+               
+               p.readData();
+               
+       }
+       
+       
+}
diff --git a/html/contact.html b/html/contact.html
new file mode 100644 (file)
index 0000000..f152043
--- /dev/null
@@ -0,0 +1,106 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;Support and contact information</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, support, bug report, feature request, contact information"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_contact">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>Support and contact information for OpenRocket</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <h2>Mailing lists</h2>
+
+    <p>If you would like to be notified when new versions of
+    OpenRocket are released, you can join the
+    <a href="https://lists.sourceforge.net/lists/listinfo/openrocket-announce"><tt>OpenRocket-announce</tt> mailing list</a>.  The
+    list is moderated and meant only for OpenRocket related
+    announcements.</p>
+
+    <p>When more developers join the project, a development mailing
+    list will be created as well.</p>
+
+    <p><strong>Unsubscribing</strong> from the lists can be performed
+    in the above links as well.  <em>Please do not send unsubscription
+    requests to the list.</em></p>
+
+
+    <h2>Support forums</h2>
+
+    <p>The main support channel for the usage of OpenRocket is the
+    support forums.  This way everybody can benefit from the answers
+    provided.</p>
+
+    <p><em><a href="http://apps.sourceforge.net/phpbb/openrocket/">Go
+    to the support forums &rarr;</a></em></p>
+
+
+    <h2 id="contact">Contact information</h2>
+
+    <p>OpenRocket is developed by Sampo Niskanen.  His contact
+    information can be found below.</p>
+
+    <p><em>If you would like to contribute something to OpenRocket, please
+    contact me!</em></p>
+
+    <p><strong><em>Support requests</em></strong> should be sent to
+    the <a href="http://apps.sourceforge.net/phpbb/openrocket/">support 
+    forums</a>.<br/>
+    <strong><em>Bug reports and feature requests</em></strong> should
+    be <a href="report.html">reported separately</a>.</p>
+
+    <p><strong>Email:</strong>  &nbsp;&nbsp;
+    <em>sam<span>po</span>.<span>niskanen</span><span>@i</span>ki.fi</em></p>
+
+    <p><strong>WWW:</strong>  &nbsp;&nbsp;
+    <a href="http://www.iki.fi/sampo.niskanen/" 
+    title="Home page of Sampo Niskanen"><em>http://www.iki.fi/sampo.niskanen/</em></a></p>
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/documentation.html b/html/documentation.html
new file mode 100644 (file)
index 0000000..a11e8ce
--- /dev/null
@@ -0,0 +1,180 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;Documentation</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, documentation"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_documentation">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>Documentation for OpenRocket</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <h2>User documentation</h2>
+
+    <p>No user's guide currently exists for OpenRocket.  There is a
+    <a href="http://apps.sourceforge.net/mediawiki/openrocket/index.php?title=User%27s_Guide">page on the wiki</a> for creating a User's guide.</p>
+    <p>If you would like to help, please extend it!</p>
+
+
+    <h2>Technical documentation</h2>
+
+    <p>Coming within the next few weeks.</p>
+    <!--
+    <p>OpenRocket was originally written as the Master's thesis of
+    Sampo Niskanen at Helsinki University of Technology.  The thesis
+    currently functions as the technical documentation, and will be
+    updated in the future to account for further enhancements.
+    The thesis is licensed under a 
+    <a rel="license" href="http://creativecommons.org/licenses/by-nd-nc/1.0/fi/deed.en">Creative Commons BY-NC-ND License</a>.</p>
+
+    <p class="right"><a rel="license" href="http://creativecommons.org/licenses/by-nd-nc/1.0/fi/deed.en"><img alt="Creative Commons BY-NC-ND License" src="http://i.creativecommons.org/l/by-nd-nc/1.0/fi/88x31.png" /></a></p>
+
+
+
+    <p><a href="thesis.pdf">Development of an Open Source model
+    rocket simulation software</a> (PDF, 1.3MB)</p>
+
+
+    <p class="quote"><strong>Table of contents:</strong></p>
+    <ol class="toc">
+      <li>1. Introduction</li>
+      <li>2. Basics of model rocket flight</li>
+      <li>3. Aerodynamic properties of model rockets</li>
+      <li>4. Flight simulation</li>
+      <li>5. The OpenRocket simulation software</li>
+      <li>6. Comparison with experimental data</li>
+      <li>7. Conclusion</li>
+    </ol>
+    <ol class="toc">
+      <li>A. Nose cone and transition geometries</li>
+      <li>B. Transonic wave drag of nose cones</li>
+      <li>C. Streamer drag coefficient estimation</li>
+    </ol>
+    -->
+
+    <h2>Resources</h2>
+
+    <p>Below are resources that have been found useful in the analysis
+    of model rockets.  Many useful scientific aerodynamic articles and
+    documents are available at the invaluable
+    <a href="http://ntrs.nasa.gov/">NASA Technical Resources Server
+    (NTRS)</a>.</p>
+
+    <dl>
+      <dt><em>
+      <a href="http://www.apogeerockets.com/Education/downloads/barrowman_report.pdf">The Theoretical Prediction of the Center of
+      Pressure</a></em>, James and Judith Barrowman, 1966.</dt>
+      <dd>The original NARAM R&amp;D report explaining how to
+      calculate the CP position of a rocket.</dd>
+
+      <dt><em>
+      <a href="http://ntrs.nasa.gov/search.jsp?Ntk=all&amp;Ntx=mode+matchall&amp;Ntt=Barrowman+Practical+Calculation+of+the+Aerodynamic+Characteristics+of+Slender+Finned+Vehicles">The Practical Calculation of the Aerodynamic
+      Characteristics of Slender Finned Vehicles</a></em>, James
+      Barrowman, 1967.</dt>
+      <dd>The more in-depth and technical thesis, where Barrowman
+      presents methods for calculating the CP position of a rocket at
+      both subsonic and supersonic velocities and its other
+      aerodynamic properties.  Available on 
+      <a href="http://ntrs.nasa.gov/">NTRS</a>.</dd>
+
+      <dt><em>
+      <a href="http://projetosulfos.if.sc.usp.br/artigos/sentinel39-galejs.pdf">Wind instability&mdash;What Barrowman left out</a></em>,
+      Robert Galejs.</dt>
+      <dd>An extension to the Barrowman method to account for body
+      lift at large angles of attack.</dd>
+
+      <dt><em>Topics in Advanced Model Rocketry</em>, Mandell,
+      Caporaso, Bengen, MIT Press, 1973.</dt>
+      <dd>An excellent theoretical study on the flight of model
+      rockets.  Available as a reprint edition.</dd>
+
+      <dt><em>Fluid-dynamic drag</em>, Sighard Hoerner,
+      published by the author, 1965.</dt>
+      <dd>An excellent resource for all kinds of experimental data
+      regarding drag.  Available as a reprint edition.</dd>
+
+      <dt><em>Tactical missile design</em>, 2nd edition, Eugene
+      L. Fleeman, AIAA, 2006.</dt>
+      <dd>Useful approximation methods for estimating the aerodynamic
+      properties of rockets.</dd>
+
+      <dt><em><a href="http://www.aoe.vt.edu/~mason/Mason_f/CAtxtTop.html">Applied
+      Computational Aerodynamics</a></em>, William Mason.</dt>
+      <dd>An online textbook on computational aerodynamics.</dd>
+
+      <dt><em><a href="http://www.combatindex.com/mil_docs/pdf/hdbk/0700/MIL-HDBK-762.pdf">Design of aerodynamically stabilized free rockets</a></em>,
+      MIL-HDBK-762, US Army Missile Command, 1990.</dt>
+      <dd>Military handbook on the design of rockets, a good resource
+      for aerodynamic estimation methods.</dd>
+
+      <dt><em>
+      <a href="http://www.thrustcurve.org/">ThrustCurve.org</a></em>,
+      John Coker.</dt>
+      <dd>An excellent resource for model rocket motor thrust curves.</dd>
+
+      <dt><em>
+      <a href="http://ntrs.nasa.gov/search.jsp?Ntk=all&amp;Ntx=mode+matchall&amp;Ntt=NASA-TN-D-4013">Static stability investigation of a single-stage
+      sounding rocket at Mach numbers from 0.60 to 1.20</a></em>, James
+      Ferris, NASA-TN-D-4013, 1967.</dt>
+      <dt><em>
+      <a href="http://ntrs.nasa.gov/search.jsp?Ntk=all&amp;Ntx=mode+matchall&amp;Ntt=NASA-TN-D-4014">Static stability investigation of a sounding-rocket
+      vehicle at Mach numbers from 1.50 to 4.63</a></em>, Donald Babb and
+      Dennis Fuller, NASA-TN-D-4014, 1967.</dt>
+      <dd>Experimental data of a wind tunnel investigation of a
+      sounding rocket at subsonic, transonic and supersonic
+      velocities.  Available on
+      <a href="http://ntrs.nasa.gov/">NTRS</a>.</dd>
+
+     
+
+    </dl>
+
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/download.html b/html/download.html
new file mode 100644 (file)
index 0000000..0795dc8
--- /dev/null
@@ -0,0 +1,95 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;Download</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_download">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>Download OpenRocket</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <h2>Binary download</h2>
+
+    <p>The binary download below is the recommended package for general
+    use.  It is pre-packaged with motor thrust curves from
+    <a href="http://www.thrustcurve.org/">thrustcurve.org</a>.</p>
+
+    <p><em>OpenRocket requires <strong>Java version 6</strong> or
+    later.  The Sun JRE is recommended.</em></p>
+
+    <p class="download">
+    <a href="https://sourceforge.net/project/downloading.php?group_id=260357&filename=OpenRocket-0.9.0.jar">Download OpenRocket 0.9.0</a></p>
+
+    <p>OpenRocket is still considered <strong>beta software</strong>.
+    If you encounter any problems, please 
+    <a href="report.html">report them</a> so they can be fixed!</p>
+
+    <p>OpenRocket can be started in graphical environments (such as
+    Windows) by double-clicking the package icon.  No installation is
+    required.  From the command line it can be started by</p>
+    <pre class="quote">$ java -jar OpenRocket-0.9.0.jar</pre>
+
+    <p>Older packages are available from the
+    <a href="https://sourceforge.net/project/showfiles.php?group_id=260357">SourceForge repository</a>.</p>
+
+
+    <h2>Source code</h2>
+
+    <p>The source code for OpenRocket is available from the
+    <a href="http://openrocket.svn.sourceforge.net/viewvc/openrocket/">SourceForge SVN repository</a>.  
+    It can be retrieved simply using the command</p>
+    <pre class="quote">$ svn co https://openrocket.svn.sourceforge.net/svnroot/openrocket openrocket</pre>
+    <p>The above URL may be used to connect to the repository with
+    other Subversion clients as well.</p>
+
+
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/features.html b/html/features.html
new file mode 100644 (file)
index 0000000..19f48a9
--- /dev/null
@@ -0,0 +1,144 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;Features</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, features"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_features">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>Features of OpenRocket</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <h2>Current features</h2>
+
+    <h3>General</h3>
+
+    <ul>
+      <li><strong>Fully cross-platform</strong>, written in Java</li>
+      <li><strong>Fully documented</strong> <a href="documentation.html">simulation
+      methods</a></li>
+      <li><strong>Open Source</strong>, source code available under the
+        <a href="license.html">GNU GPL</a></li>
+    </ul>
+
+    <h3>User interface</h3>
+    <ul>
+      <li><a href="screenshots.html">Easy-to-use user interface</a> for
+        rocket design</li>
+      <li><strong>Zoomable schematic view</strong> of rocket from the side or rear</li>
+      <li>Rocket rotation around center axis</li>
+      <li><strong>Real-time view of CG and CP</strong> position</li>
+      <li><strong>Real-time flight altitude, velocity and
+        acceleration</strong> information from a continuous simulation
+       performed in the background</li> 
+    </ul>
+
+    <h3>Design</h3>
+
+    <ul>
+      <li>A multitude of available components to
+      choose from</li>
+      <li><strong>Trapezoidal</strong>, <strong>elliptical</strong>
+      and <strong>free-form fins</strong> supported</li> 
+      <li>Support for <strong>canted fins</strong> (roll
+      stabilization)</li>
+      <li><strong>Staging</strong> and <strong>clustering</strong> support</li>
+      <li>Automatic calculation of component mass and CG based on
+        shape and density</li>
+      <li>Ability to <strong>override mass and CG</strong> of
+        components or stages separately</li>
+    </ul>
+
+    <h3>Simulation and analysis</h3>
+
+    <ul>
+      <li>Full <strong>six degree of freedom</strong> simulation</li>
+      <li>Rocket stability computed using <strong>extended Barrowman
+        method</strong></li>
+      <li>Realistic wind modeling</li>
+      <li>Analysis of the <strong>effect of separate
+      components</strong> on the stability, drag and roll
+      characteristics of the rocket</li>
+      <li><strong>Fully configurable plotting</strong>, with
+      various preset configurations</li>
+      <li><strong>Simulation listeners</strong> allowing custom-made
+      code to interact with the rocket during flight simulation</li>
+    </ul>
+
+
+    <h2 id="future">Planned future features</h2>
+
+    <p>OpenRocket is under constant work, and anybody can help make
+    OpenRocket an even better simulator!  Here are a few features that
+    have been planned...</p>
+
+    <ul>
+      <li>Aerodynamic computation using 
+      <acronym title="Computational Fluid Dynamics">CFD</acronym>
+      <a href="contact.html" class="help">(help needed!)</a></li>
+      <li>Better support for supersonic simulation
+      <a href="contact.html" class="help">(help needed!)</a></li>
+      <li>3D view of the rocket design 
+      <a href="contact.html" class="help">(help needed!)</a></li>
+      <li>Saving figures and exporting simulation data</li>
+      <li>Importing and plotting actual flight data from altimeters</li>
+      <li>Importing new motor thrust curves</li>
+      <li>Support for ready-made component databases</li>
+      <li>Customized support for hybrid rocket motors and water
+      rockets</li>
+      <li>Rocket flight animation</li>
+      <li>A "wizard" for creating new rocket designs</li>
+      <li>. . .</li>
+    </ul>
+
+    <p>If you want to help make OpenRocket the best rocket simulator,
+    don't hesitate to <a href="contact.html">contact us</a>!</p>
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/index.html b/html/index.html
new file mode 100644 (file)
index 0000000..9963204
--- /dev/null
@@ -0,0 +1,106 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_index">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>OpenRocket &mdash; an Open Source model rocket simulator</h1>
+
+  <div class="menucontainer">
+    <div class="menu">
+      <ul>
+        <li>OpenRocket</li>
+        <li><a href="index.html">Home</a></li>
+        <li><a href="features.html">Features</a></li>
+        <li><a href="screenshots.html">Screenshots</a></li>
+        <li><a href="download.html">Download</a></li>
+        <li><a href="documentation.html">Documentation</a></li>
+        <li><a href="contact.html">
+          Mailing lists<br/>
+          Support forums<br/>
+         Contact info</a></li>
+        <li><a href="report.html">
+          Report a bug<br/>
+         Request a feature</a></li>
+        <li><a href="license.html">License</a></li>
+      </ul>
+      <div class="logo">
+        <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+      </div>
+    </div>
+  </div>
+
+  <div class="content">
+
+    <h2>Introduction</h2>
+
+    <p><strong>OpenRocket</strong> is a Free, fully featured model
+      rocket simulator written in Java.  It can be used to design and
+      simulate rockets before actually building and flying them.
+    </p>
+    <p>OpenRocket features a full six-degree-of-freedom simulation,
+      realistic wind modeling, a multitude of different components
+      including free-form fins and canted fins, clustering and
+      staging.  Read more about its <a href="features.html">features</a>.
+    </p>
+    <p>Best of all, OpenRocket is Open Source&mdash;its source code is
+      freely available to study and extend.  Anybody wishing to
+      contribute to the project can do so according to the
+      <a href="license.html">GNU GPL</a>.  Simply 
+      <a href="download.html">download</a> the source code
+      and start hacking, or <a href="download.html">get the ready
+      package</a> to begin designing and simulating.
+    </p>
+    <p>OpenRocket is still considered to be <strong>beta
+      software</strong>&mdash;there will still be bugs and occasional
+      problems.  If you encounter problems, please
+      <a href="contact.html">report them</a> so they can be fixed.
+    </p>
+
+    <div>
+      <div class="smallshot"><a href="screenshots.html">
+        <img src="shots-small/main.jpg" alt="Main window"/><br/>
+        Main window
+      </a></div>
+      <div class="smallshot"><a href="screenshots.html">
+        <img src="shots-small/dialog-analysis.jpg" alt="Analysis dialog"/><br/>
+        Analysis dialog
+      </a></div>
+      <div class="smallshot last"><a href="screenshots.html">
+        <img src="shots-small/dialog-plot.jpg" alt="Simulation plot"/><br/>
+        Simulation plot
+      </a></div>
+    </div>
+    <div class="clear"></div>
+
+
+    <h2>News</h2>
+
+    <p><strong>24.5.2009:</strong> First version 0.9.0 
+      <a href="download.html">released</a>!</p>
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/layout.css b/html/layout.css
new file mode 100644 (file)
index 0000000..d1e3de1
--- /dev/null
@@ -0,0 +1,230 @@
+
+body {
+  margin: 0;
+  padding: 0;
+}
+
+#iewarn {
+  width: 100%;
+  background-color: #fa0;
+  text-align: center;
+  padding: 1em 2em;
+  border-top: solid 1px black;
+  border-bottom: solid 1px black;
+}
+
+
+h1 {
+  margin: 0.75em 2em 1.25em 2em;
+}
+
+h2 {
+  margin-top: 1.5em;
+  border-bottom: dotted 2px #f99;
+}
+
+a {
+  text-decoration: none;
+  color: #00F;
+}
+a:hover {
+  color: #55c;
+}
+
+
+div.menucontainer {
+  position: relative;
+}
+
+div.menu {
+  position: absolute;
+  left: 1.5em;
+  width: 12em;
+  margin: 0;
+  padding: 0 0;
+  background-color: #ccc;
+}
+div.menu ul {
+  position: relative;
+  left: -2px;
+  top: -2px;
+  right: 2px;
+  bottom: 2px;
+  background-color: #89cbe0;
+  border: solid 1px black;
+  list-style: none;
+  margin: 0;
+  padding: 0 0;
+}
+
+div.menu li {
+  display: block;
+  left: 0;
+  right: 0;
+  margin: 0;
+  text-align: center;
+}
+
+div.menu li:first-child {
+  padding: 0.5em 0;
+  font-size: 160%;
+}
+
+div.menu li+li {
+  border-top: dashed 1px black;
+}
+
+div.menu li a {
+  display: block;
+  left: 0;
+  right: 0;
+  font-style: normal;
+  text-decoration: none;
+  color: #00d;
+  padding: 0.75em 1em;
+  outline: none;
+}
+div.menu li a:focus {
+  background-color: #8fd5eb;
+}
+
+div.menu li a:hover {
+  background-color: #ee9494;
+}
+
+div.menu div.logo {
+  position: absolute;
+  top: 100%;
+  left: -2px;
+  margin-top: 15px;
+  width: 100%;
+}
+
+div.menu div.logo img {
+  display: block;
+  margin: 0 auto;
+}
+
+
+.page_index div.menu a[href="index.html"],
+.page_features div.menu a[href="features.html"],
+.page_screenshots div.menu a[href="screenshots.html"],
+.page_download div.menu a[href="download.html"],
+.page_documentation div.menu a[href="documentation.html"],
+.page_contact div.menu a[href="contact.html"],
+.page_report div.menu a[href="report.html"],
+.page_license div.menu a[href="license.html"] {
+  font-weight: bold;
+  font-size: 110%;
+}
+
+
+.content {
+  margin: 0em 2em 2em 15.5em;
+  min-height: 27em;
+}
+
+img {
+  border: 0px;
+  outline: none;
+  font-size: 70%;
+}
+
+.smallshot {
+  float: left;
+  margin-top:2em;
+  text-align: center;
+  font-style: italic;
+  margin-right: 2em;
+}
+.smallshot.last {
+  margin-right: 0;
+}
+.clear {
+  clear:both;
+}
+
+
+.smallshotconst {
+  float: left;
+  width: 270px;
+  height: 220px;
+  margin: 1em 1em;
+  text-align: center;
+  font-style: italic;
+}
+.smallshotconst em {
+  font-style: normal;
+}
+
+
+
+a.help {
+  margin-left: 1em;
+  font-size: smaller;
+  font-style: italic;
+}
+
+
+pre.quote {
+  margin: 2em;
+  padding: 1em;
+  border: dashed 1px #888;
+  background-color: #ddd;
+}
+
+p.quote {
+  margin: 2em;
+}
+
+hr {
+  margin: 2em 0em;
+}
+
+.right {
+  float: right;
+  margin: 0;
+}
+
+li {
+  margin-top: 0.5em;
+}
+
+
+p.download {
+  margin: 2em;
+}
+p.download a {
+  font-size: 140%;
+  font-style: italic;
+  padding: 0.5em;
+  border: dashed 1px red;
+  background-color: #89cbe0;
+  outline: none;
+}
+p.download a:hover {
+  color: #00F;
+  background-color: #ee9494;
+}
+p.download a:focus {
+  background-color: #8fd5eb;
+}
+
+div.valid {
+  float: right;
+  margin-right: 2em;
+}
+
+
+
+ol.toc {
+  list-style-type: none;
+}
+
+dt+dt {
+  margin-top: 0.5em;
+}
+dd {
+  margin-top: 0.2em;
+  margin-bottom: 1.4em;
+}
diff --git a/html/license.html b/html/license.html
new file mode 100644 (file)
index 0000000..a7cc999
--- /dev/null
@@ -0,0 +1,769 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;License</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, license"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_license">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>OpenRocket license</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <p><em>The license text is available also in the simulator under
+    <strong>Help&nbsp;&rarr;&nbsp;License</strong> and in the file
+    <tt>LICENSE.TXT</tt>.</em></p>
+
+    <hr/>
+
+    <pre>
+OpenRocket - A model rocket simulator
+
+Copyright (C) 2007-2009 Sampo Niskanen
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 3 of the License, or (at
+your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License (below) for more details.
+
+
+Additional permission under GNU GPL version 3 section 7:
+
+The licensors grant additional permission to package this Program, or
+any covered work, along with any non-compilable data files (such as
+thrust curves or component databases) and convey the resulting work.
+
+
+------------------------------------------------------------------------
+
+
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. &lt;http://fsf.org/&gt;
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                      TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+  
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                    END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    &lt;one line to give the program's name and a brief idea of what it does.&gt;
+    Copyright (C) &lt;year&gt;  &lt;name of author&gt;
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    &lt;program&gt;  Copyright (C) &lt;year&gt;  &lt;name of author&gt;
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+&lt;http://www.gnu.org/licenses/&gt;.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+&lt;http://www.gnu.org/philosophy/why-not-lgpl.html&gt;.
+    </pre>
+
+    <hr/>
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/report.html b/html/report.html
new file mode 100644 (file)
index 0000000..9ef2250
--- /dev/null
@@ -0,0 +1,116 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;Support and contact information</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, support, bug report, feature request, contact information"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_report">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>Support and contact information for OpenRocket</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <h2>Bug reports</h2>
+
+    <p>If you encounter problems with OpenRocket, please report them
+    so they can be fixed in future versions.  Please follow the
+    instructions below to report a bug:</p>
+
+    <ul>
+      <li>Search the bug repository to see if the bug has already been
+      reported.  If it is, please add extra information to that bug
+      report:<br/>
+      <form action="https://sourceforge.net/search/index.php" method="get">
+        <input type="hidden" name="group_id" value="260357" />
+       <input type="hidden" name="type_of_search" value="artifact"/>
+<!--    <input type="hidden" name="group_artifact_id" value="1127606" /> -->
+<!--    <input type="hidden" name="artifact_group" value="Bug" /> -->
+        <input type="hidden" name="search_summary" value="1" />
+        <input type="hidden" name="search_details" value="1" />
+        <input type="hidden" name="search_comments" value="1" />
+
+       <input type="text" name="all_words" value="" />
+       <input type="submit" name="form_submit" value="Search" />
+      </form>
+      </li>
+
+      <li>Report the bug using the 
+      <a href="https://sourceforge.net/tracker/?func=add&group_id=260357&atid=1127606">bug
+      tracker</a>.  Follow the instructions provided to fill in the 
+      report.</li>
+
+      <li>If you are unsure about some issue, you can discuss it in
+      the appropriate 
+      <a href="http://apps.sourceforge.net/phpbb/openrocket/">support 
+      forum</a>.</li>
+    </ul>
+
+    <h2>Feature requests</h2>
+
+    <p>Good ideas on how to make OpenRocket better are always welcome!
+    The features will be implemented as there is time.  However, no
+    promises are made of when or whether some feature will be
+    provided.</p>
+
+    <p>If you would like to implement some feature yourself, patches
+    are sincerely welcome.  Please <a href="contact.html#contact">contact
+    me</a> in order to coordinate our efforts.</p>
+
+    <p>When requesting a feature:</p>
+
+    <ul>
+      <li>Check that the feature is not already in the
+      <a href="features.html#future">planned future features</a> or
+      the <a href="https://sourceforge.net/tracker/?group_id=260357&atid=1127606&artgroup=899287">enhancement requests</a>.</li>
+      <li>Send the request to the <a href="https://sourceforge.net/tracker/?func=add&group_id=260357&atid=1127606">bug tracker</a> as an
+      enhancement request.  Please send multiple enhancements as
+      individual items.</li>
+    </ul>
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/screenshots.html b/html/screenshots.html
new file mode 100644 (file)
index 0000000..f7e26bc
--- /dev/null
@@ -0,0 +1,95 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>OpenRocket&mdash;Screenshots</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+  <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, screenshots"/>
+  <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_screenshots">
+  <!--[if lte IE 6]>
+  <div id="iewarn">
+    You are using a browser that is <strong>8 years old!</strong>  
+    &nbsp;&nbsp;&nbsp;
+    In Internet-years that is <em>prehistoric!</em><br/>
+    For the sanity of all webmasterkind, 
+    <em>please <a href="http://www.mozilla.com/">upgrade</a></em>.  It's easy!
+  </div>
+  <![endif]-->
+
+  <h1>Screenshots of OpenRocket</h1>
+
+  <div class="menucontainer">
+  <div class="menu">
+    <ul>
+      <li>OpenRocket</li>
+      <li><a href="index.html">Home</a></li>
+      <li><a href="features.html">Features</a></li>
+      <li><a href="screenshots.html">Screenshots</a></li>
+      <li><a href="download.html">Download</a></li>
+      <li><a href="documentation.html">Documentation</a></li>
+      <li><a href="contact.html">
+        Mailing lists<br/>
+        Support forums<br/>
+       Contact info</a></li>
+      <li><a href="report.html">
+        Report a bug<br/>
+       Request a feature</a></li>
+      <li><a href="license.html">License</a></li>
+    </ul>
+    <div class="logo">
+      <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&amp;type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+    </div>
+  </div>
+  </div>
+
+  <div class="content">
+
+    <p>Below are screenshots of the <strong>OpenRocket</strong> model
+      rocket simulator.  Click on the images for a full view.  You can
+      also <a href="download.html">download the program</a> and start
+      experimenting yourself!</p>
+
+
+    <div class="smallshotconst"><a href="shots/main.png">
+      <img src="shots-small/main.jpg" alt="Main window"/><br/>
+      The main rocket design window is used to design the rocket and
+      it also provides information about a flight simulation in
+      real-time.
+    </a></div>
+    <div class="smallshotconst"><a href="shots/dialog-edit.png">
+      <img src="shots-small/dialog-edit.jpg" alt="Component edit dialog"/><br/>
+      The component shape and properties are defined in their own
+      dialog.
+    </a></div>
+    <div class="smallshotconst"><a href="shots/dialog-analysis.png">
+      <img src="shots-small/dialog-analysis.jpg" alt="Analysis dialog"/><br/>
+      You can analyze the effect of individual components on the
+      stability, drag and roll characteristics of the rocket.
+    </a></div>
+    <div class="smallshotconst"><a href="shots/dialog-plot-options.png">
+      <img src="shots-small/dialog-plot-options.jpg" 
+        alt="Simulation plot options"/><br/>
+      The simulation results can be plotted in a multitude
+      of ways.  You can either use the predefined plot
+      configurations or define your own.<br/>
+    </a></div>
+    <div class="smallshotconst"><a href="shots/dialog-plot.png">
+      <img src="shots-small/dialog-plot.jpg" alt="Simulation plot"/><br/>
+      The simulations are plotted using the
+      <em>JFreeChart</em> plotting library.
+    </a></div>
+    <div class="clear"></div>
+
+  </div>
+
+  <div class="valid">
+    <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+       <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+    </p>
+  </div>
+
+</body>
+</html>
+
diff --git a/html/shots-small/dialog-analysis.jpg b/html/shots-small/dialog-analysis.jpg
new file mode 100644 (file)
index 0000000..570c484
Binary files /dev/null and b/html/shots-small/dialog-analysis.jpg differ
diff --git a/html/shots-small/dialog-edit.jpg b/html/shots-small/dialog-edit.jpg
new file mode 100644 (file)
index 0000000..c2dc593
Binary files /dev/null and b/html/shots-small/dialog-edit.jpg differ
diff --git a/html/shots-small/dialog-plot-options.jpg b/html/shots-small/dialog-plot-options.jpg
new file mode 100644 (file)
index 0000000..ad977ab
Binary files /dev/null and b/html/shots-small/dialog-plot-options.jpg differ
diff --git a/html/shots-small/dialog-plot.jpg b/html/shots-small/dialog-plot.jpg
new file mode 100644 (file)
index 0000000..d0fc66d
Binary files /dev/null and b/html/shots-small/dialog-plot.jpg differ
diff --git a/html/shots-small/main.jpg b/html/shots-small/main.jpg
new file mode 100644 (file)
index 0000000..7fcd946
Binary files /dev/null and b/html/shots-small/main.jpg differ
diff --git a/html/shots/dialog-analysis.png b/html/shots/dialog-analysis.png
new file mode 100644 (file)
index 0000000..b71f1aa
Binary files /dev/null and b/html/shots/dialog-analysis.png differ
diff --git a/html/shots/dialog-edit.png b/html/shots/dialog-edit.png
new file mode 100644 (file)
index 0000000..9107859
Binary files /dev/null and b/html/shots/dialog-edit.png differ
diff --git a/html/shots/dialog-plot-options.png b/html/shots/dialog-plot-options.png
new file mode 100644 (file)
index 0000000..ca627fa
Binary files /dev/null and b/html/shots/dialog-plot-options.png differ
diff --git a/html/shots/dialog-plot.png b/html/shots/dialog-plot.png
new file mode 100644 (file)
index 0000000..225dca8
Binary files /dev/null and b/html/shots/dialog-plot.png differ
diff --git a/html/shots/main.png b/html/shots/main.png
new file mode 100644 (file)
index 0000000..2b9af49
Binary files /dev/null and b/html/shots/main.png differ
diff --git a/html/valid-xhtml10.png b/html/valid-xhtml10.png
new file mode 100644 (file)
index 0000000..b81de91
Binary files /dev/null and b/html/valid-xhtml10.png differ
diff --git a/html/vcss.gif b/html/vcss.gif
new file mode 100644 (file)
index 0000000..020c75a
Binary files /dev/null and b/html/vcss.gif differ
diff --git a/lib/jcommon-1.0.16.jar b/lib/jcommon-1.0.16.jar
new file mode 100644 (file)
index 0000000..4cd6807
Binary files /dev/null and b/lib/jcommon-1.0.16.jar differ
diff --git a/lib/jfreechart-1.0.13.jar b/lib/jfreechart-1.0.13.jar
new file mode 100644 (file)
index 0000000..83c6993
Binary files /dev/null and b/lib/jfreechart-1.0.13.jar differ
diff --git a/lib/miglayout15-swing.jar b/lib/miglayout15-swing.jar
new file mode 100644 (file)
index 0000000..2e53012
Binary files /dev/null and b/lib/miglayout15-swing.jar differ
diff --git a/pix-src/componenticons/bodyoutline.xcf.gz b/pix-src/componenticons/bodyoutline.xcf.gz
new file mode 100644 (file)
index 0000000..1a1f245
Binary files /dev/null and b/pix-src/componenticons/bodyoutline.xcf.gz differ
diff --git a/pix-src/componenticons/bodytube.xcf.gz b/pix-src/componenticons/bodytube.xcf.gz
new file mode 100644 (file)
index 0000000..94c2395
Binary files /dev/null and b/pix-src/componenticons/bodytube.xcf.gz differ
diff --git a/pix-src/componenticons/bulkhead.xcf.gz b/pix-src/componenticons/bulkhead.xcf.gz
new file mode 100644 (file)
index 0000000..a188588
Binary files /dev/null and b/pix-src/componenticons/bulkhead.xcf.gz differ
diff --git a/pix-src/componenticons/centeringring.xcf.gz b/pix-src/componenticons/centeringring.xcf.gz
new file mode 100644 (file)
index 0000000..7cab6f4
Binary files /dev/null and b/pix-src/componenticons/centeringring.xcf.gz differ
diff --git a/pix-src/componenticons/ellipticalfin.xcf.gz b/pix-src/componenticons/ellipticalfin.xcf.gz
new file mode 100644 (file)
index 0000000..474af05
Binary files /dev/null and b/pix-src/componenticons/ellipticalfin.xcf.gz differ
diff --git a/pix-src/componenticons/engineblock.xcf.gz b/pix-src/componenticons/engineblock.xcf.gz
new file mode 100644 (file)
index 0000000..03a018c
Binary files /dev/null and b/pix-src/componenticons/engineblock.xcf.gz differ
diff --git a/pix-src/componenticons/freeformfin.xcf.gz b/pix-src/componenticons/freeformfin.xcf.gz
new file mode 100644 (file)
index 0000000..25cc277
Binary files /dev/null and b/pix-src/componenticons/freeformfin.xcf.gz differ
diff --git a/pix-src/componenticons/innertube.xcf.gz b/pix-src/componenticons/innertube.xcf.gz
new file mode 100644 (file)
index 0000000..7cab6f4
Binary files /dev/null and b/pix-src/componenticons/innertube.xcf.gz differ
diff --git a/pix-src/componenticons/launchlug.xcf.gz b/pix-src/componenticons/launchlug.xcf.gz
new file mode 100644 (file)
index 0000000..7c19349
Binary files /dev/null and b/pix-src/componenticons/launchlug.xcf.gz differ
diff --git a/pix-src/componenticons/mass.xcf.gz b/pix-src/componenticons/mass.xcf.gz
new file mode 100644 (file)
index 0000000..4199344
Binary files /dev/null and b/pix-src/componenticons/mass.xcf.gz differ
diff --git a/pix-src/componenticons/nosecone.xcf.gz b/pix-src/componenticons/nosecone.xcf.gz
new file mode 100644 (file)
index 0000000..b7b7cd5
Binary files /dev/null and b/pix-src/componenticons/nosecone.xcf.gz differ
diff --git a/pix-src/componenticons/parachute.xcf.gz b/pix-src/componenticons/parachute.xcf.gz
new file mode 100644 (file)
index 0000000..ebc3ffb
Binary files /dev/null and b/pix-src/componenticons/parachute.xcf.gz differ
diff --git a/pix-src/componenticons/shockcord.xcf.gz b/pix-src/componenticons/shockcord.xcf.gz
new file mode 100644 (file)
index 0000000..f1bcfec
Binary files /dev/null and b/pix-src/componenticons/shockcord.xcf.gz differ
diff --git a/pix-src/componenticons/siiveke.fig b/pix-src/componenticons/siiveke.fig
new file mode 100644 (file)
index 0000000..b7d0006
--- /dev/null
@@ -0,0 +1,71 @@
+#FIG 3.2  Produced by xfig version 3.2.5
+Landscape
+Center
+Metric
+A4      
+100.00
+Single
+-2
+1200 2
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 4
+        2520 4050 3780 2970 5580 2970 5130 4050
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+       0 0 1.00 60.00 60.00
+        4185 2790 3780 2790
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+       0 0 1.00 60.00 60.00
+        5220 2790 5580 2790
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 1 2
+       0 0 1.00 60.00 60.00
+       0 0 1.00 60.00 60.00
+        5715 2970 5715 4050
+2 1 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        2520 2880 2520 3870
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 1 0 2
+       0 0 1.00 60.00 60.00
+        3465 2790 3780 2790
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 1 0 2
+       0 0 1.00 60.00 60.00
+        2745 2790 2520 2790
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 2
+        1665 4050 6705 4050
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+       0 0 1.00 60.00 60.00
+        3195 4230 2520 4230
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+       0 0 1.00 60.00 60.00
+        4500 4230 5130 4230
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        3780 2745 3780 2835
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        2025 4050 1800 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        2250 4050 2025 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        2475 4050 2250 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        2520 2745 2520 2835
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        5580 2745 5580 2835
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        5130 4185 5130 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        2520 4185 2520 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        5670 2970 5760 2970
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        5670 4050 5445 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        5445 4050 5220 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        5895 4050 5670 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        6120 4050 5895 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        6345 4050 6120 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+        6570 4050 6345 4275
+4 0 0 50 -1 0 11 0.0000 4 135 960 4185 2835 TIP CHORD\001
+4 0 0 50 -1 0 11 0.0000 4 135 630 2790 2835 SWEEP\001
+4 0 0 50 -1 0 11 0.0000 4 135 1185 3240 4275 ROOT CHORD\001
+4 0 0 50 -1 0 11 0.0000 4 135 690 5805 3555 HEIGHT\001
diff --git a/pix-src/componenticons/siiveke.svg b/pix-src/componenticons/siiveke.svg
new file mode 100644 (file)
index 0000000..c2ca80b
--- /dev/null
@@ -0,0 +1,218 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Creator: fig2dev Version 3.2 Patchlevel 5 -->
+<!-- CreationDate: Wed Jan  9 01:26:26 2008 -->
+<!-- Magnification: 1.050 -->
+<svg xmlns="http://www.w3.org/2000/svg" width="4.4in" height="1.4in" viewBox="1735 2818 5316 1685">
+<g style="stroke-width:.025in; stroke:black; fill:none">
+<!-- Line -->
+<polyline points="2645,4251
+3968,3118
+5858,3118
+5385,4251
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="4393,2929
+3985,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 4393 2929 - 3952 2929-->
+<polyline points="4025 2897
+3962 2929
+4025 2960
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="5480,2929
+5840,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 5480 2929 - 5874 2929-->
+<polyline points="5801 2960
+5864 2929
+5801 2897
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="5999,3135
+5999,4234
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 5999 3118 - 5999 4267-->
+<polyline points="5968 4195
+5999 4258
+6031 4195
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Arrowhead on XXXpoint 5999 4251 - 5999 3102-->
+<polyline points="6031 3174
+5999 3111
+5968 3174
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="2645,3023
+2645,4062
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+stroke-dasharray:41 41;"/>
+<!-- Line -->
+<polyline points="3637,2929
+3950,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 3637 2929 - 3984 2929-->
+<polyline points="3911 2960
+3974 2929
+3911 2897
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="2881,2929
+2662,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 2881 2929 - 2629 2929-->
+<polyline points="2702 2897
+2639 2929
+2702 2960
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="1748,4251
+7039,4251
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="3354,4440
+2662,4440
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 3354 4440 - 2629 4440-->
+<polyline points="2702 4409
+2639 4440
+2702 4472
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="4724,4440
+5367,4440
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 4724 4440 - 5401 4440-->
+<polyline points="5329 4472
+5392 4440
+5329 4409
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="3968,2881
+3968,2976
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2125,4251
+1889,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2362,4251
+2125,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2598,4251
+2362,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2645,2881
+2645,2976
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5858,2881
+5858,2976
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5385,4393
+5385,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2645,4393
+2645,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5952,3118
+6047,3118
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5952,4251
+5716,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5716,4251
+5480,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6188,4251
+5952,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6425,4251
+6188,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6661,4251
+6425,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6897,4251
+6661,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Text -->
+<text xml:space="preserve" x="4393" y="2976" stroke="#000000" fill="#000000"  font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">TIP CHORD</text>
+<!-- Text -->
+<text xml:space="preserve" x="2929" y="2976" stroke="#000000" fill="#000000"  font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">SWEEP</text>
+<!-- Text -->
+<text xml:space="preserve" x="3551" y="4488" stroke="#000000" fill="#000000"  font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">ROOT CHORD</text>
+<!-- Text -->
+<text xml:space="preserve" x="6094" y="3732" stroke="#000000" fill="#000000"  font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">HEIGHT</text>
+</g>
+</svg>
diff --git a/pix-src/componenticons/streamer.xcf.gz b/pix-src/componenticons/streamer.xcf.gz
new file mode 100644 (file)
index 0000000..e3dd8cc
Binary files /dev/null and b/pix-src/componenticons/streamer.xcf.gz differ
diff --git a/pix-src/componenticons/transition.xcf.gz b/pix-src/componenticons/transition.xcf.gz
new file mode 100644 (file)
index 0000000..e0bb80e
Binary files /dev/null and b/pix-src/componenticons/transition.xcf.gz differ
diff --git a/pix-src/componenticons/trapezoidfin.xcf.gz b/pix-src/componenticons/trapezoidfin.xcf.gz
new file mode 100644 (file)
index 0000000..e146920
Binary files /dev/null and b/pix-src/componenticons/trapezoidfin.xcf.gz differ
diff --git a/pix-src/componenticons/tubecoupler.xcf.gz b/pix-src/componenticons/tubecoupler.xcf.gz
new file mode 100644 (file)
index 0000000..f6d8f58
Binary files /dev/null and b/pix-src/componenticons/tubecoupler.xcf.gz differ
diff --git a/pix-src/spheres/blue-16x16.png b/pix-src/spheres/blue-16x16.png
new file mode 100644 (file)
index 0000000..1f330f0
Binary files /dev/null and b/pix-src/spheres/blue-16x16.png differ
diff --git a/pix-src/spheres/blue-cyan-large.png b/pix-src/spheres/blue-cyan-large.png
new file mode 100644 (file)
index 0000000..129b7bd
Binary files /dev/null and b/pix-src/spheres/blue-cyan-large.png differ
diff --git a/pix-src/spheres/copyright.txt b/pix-src/spheres/copyright.txt
new file mode 100644 (file)
index 0000000..366864b
--- /dev/null
@@ -0,0 +1 @@
+Originally from http://flyosity.com/tutorial/how-to-draw-a-mac-internet-globe-icon.php
diff --git a/pix-src/spheres/gray-16x16.png b/pix-src/spheres/gray-16x16.png
new file mode 100644 (file)
index 0000000..d0951a9
Binary files /dev/null and b/pix-src/spheres/gray-16x16.png differ
diff --git a/pix-src/spheres/gray-large.xcf.gz b/pix-src/spheres/gray-large.xcf.gz
new file mode 100644 (file)
index 0000000..e33d3c0
Binary files /dev/null and b/pix-src/spheres/gray-large.xcf.gz differ
diff --git a/pix-src/spheres/green-16x16.png b/pix-src/spheres/green-16x16.png
new file mode 100644 (file)
index 0000000..29d785d
Binary files /dev/null and b/pix-src/spheres/green-16x16.png differ
diff --git a/pix-src/spheres/green-large.xcf.gz b/pix-src/spheres/green-large.xcf.gz
new file mode 100644 (file)
index 0000000..c91a929
Binary files /dev/null and b/pix-src/spheres/green-large.xcf.gz differ
diff --git a/pix-src/spheres/red-16x16.png b/pix-src/spheres/red-16x16.png
new file mode 100644 (file)
index 0000000..79c7161
Binary files /dev/null and b/pix-src/spheres/red-16x16.png differ
diff --git a/pix-src/spheres/red-large.xcf.gz b/pix-src/spheres/red-large.xcf.gz
new file mode 100644 (file)
index 0000000..5e3f4a3
Binary files /dev/null and b/pix-src/spheres/red-large.xcf.gz differ
diff --git a/pix-src/spheres/step4c.png b/pix-src/spheres/step4c.png
new file mode 100644 (file)
index 0000000..129b7bd
Binary files /dev/null and b/pix-src/spheres/step4c.png differ
diff --git a/pix-src/spheres/yellow-16x16.png b/pix-src/spheres/yellow-16x16.png
new file mode 100644 (file)
index 0000000..9ed11ea
Binary files /dev/null and b/pix-src/spheres/yellow-16x16.png differ
diff --git a/pix-src/spheres/yellow-large.xcf.gz b/pix-src/spheres/yellow-large.xcf.gz
new file mode 100644 (file)
index 0000000..54d917f
Binary files /dev/null and b/pix-src/spheres/yellow-large.xcf.gz differ
diff --git a/pix-src/splashscreen.xcf.gz b/pix-src/splashscreen.xcf.gz
new file mode 100644 (file)
index 0000000..a38f0f2
Binary files /dev/null and b/pix-src/splashscreen.xcf.gz differ
diff --git a/pix/componenticons/bodytube-large.png b/pix/componenticons/bodytube-large.png
new file mode 100644 (file)
index 0000000..517af76
Binary files /dev/null and b/pix/componenticons/bodytube-large.png differ
diff --git a/pix/componenticons/bodytube-small.png b/pix/componenticons/bodytube-small.png
new file mode 100644 (file)
index 0000000..450d1d2
Binary files /dev/null and b/pix/componenticons/bodytube-small.png differ
diff --git a/pix/componenticons/bulkhead-large.png b/pix/componenticons/bulkhead-large.png
new file mode 100644 (file)
index 0000000..1aeb8bc
Binary files /dev/null and b/pix/componenticons/bulkhead-large.png differ
diff --git a/pix/componenticons/bulkhead-small.png b/pix/componenticons/bulkhead-small.png
new file mode 100644 (file)
index 0000000..78bbdb2
Binary files /dev/null and b/pix/componenticons/bulkhead-small.png differ
diff --git a/pix/componenticons/centeringring-large.png b/pix/componenticons/centeringring-large.png
new file mode 100644 (file)
index 0000000..6505bc2
Binary files /dev/null and b/pix/componenticons/centeringring-large.png differ
diff --git a/pix/componenticons/centeringring-small.png b/pix/componenticons/centeringring-small.png
new file mode 100644 (file)
index 0000000..5bd9b79
Binary files /dev/null and b/pix/componenticons/centeringring-small.png differ
diff --git a/pix/componenticons/ellipticalfin-large.png b/pix/componenticons/ellipticalfin-large.png
new file mode 100644 (file)
index 0000000..cb186f3
Binary files /dev/null and b/pix/componenticons/ellipticalfin-large.png differ
diff --git a/pix/componenticons/ellipticalfin-small.png b/pix/componenticons/ellipticalfin-small.png
new file mode 100644 (file)
index 0000000..1abd29e
Binary files /dev/null and b/pix/componenticons/ellipticalfin-small.png differ
diff --git a/pix/componenticons/engineblock-large.png b/pix/componenticons/engineblock-large.png
new file mode 100644 (file)
index 0000000..2fc7549
Binary files /dev/null and b/pix/componenticons/engineblock-large.png differ
diff --git a/pix/componenticons/engineblock-small.png b/pix/componenticons/engineblock-small.png
new file mode 100644 (file)
index 0000000..fbda45e
Binary files /dev/null and b/pix/componenticons/engineblock-small.png differ
diff --git a/pix/componenticons/freeformfin-large.png b/pix/componenticons/freeformfin-large.png
new file mode 100644 (file)
index 0000000..a1f1e69
Binary files /dev/null and b/pix/componenticons/freeformfin-large.png differ
diff --git a/pix/componenticons/freeformfin-small.png b/pix/componenticons/freeformfin-small.png
new file mode 100644 (file)
index 0000000..e9e9bc8
Binary files /dev/null and b/pix/componenticons/freeformfin-small.png differ
diff --git a/pix/componenticons/innertube-large.png b/pix/componenticons/innertube-large.png
new file mode 100644 (file)
index 0000000..4dbb3d1
Binary files /dev/null and b/pix/componenticons/innertube-large.png differ
diff --git a/pix/componenticons/innertube-small.png b/pix/componenticons/innertube-small.png
new file mode 100644 (file)
index 0000000..c6c448a
Binary files /dev/null and b/pix/componenticons/innertube-small.png differ
diff --git a/pix/componenticons/launchlug-large.png b/pix/componenticons/launchlug-large.png
new file mode 100644 (file)
index 0000000..cc8aa7e
Binary files /dev/null and b/pix/componenticons/launchlug-large.png differ
diff --git a/pix/componenticons/launchlug-small.png b/pix/componenticons/launchlug-small.png
new file mode 100644 (file)
index 0000000..6134fd0
Binary files /dev/null and b/pix/componenticons/launchlug-small.png differ
diff --git a/pix/componenticons/mass-large.png b/pix/componenticons/mass-large.png
new file mode 100644 (file)
index 0000000..bf14a1c
Binary files /dev/null and b/pix/componenticons/mass-large.png differ
diff --git a/pix/componenticons/mass-small.png b/pix/componenticons/mass-small.png
new file mode 100644 (file)
index 0000000..fa5ba8e
Binary files /dev/null and b/pix/componenticons/mass-small.png differ
diff --git a/pix/componenticons/nosecone-large.png b/pix/componenticons/nosecone-large.png
new file mode 100644 (file)
index 0000000..91c3644
Binary files /dev/null and b/pix/componenticons/nosecone-large.png differ
diff --git a/pix/componenticons/nosecone-small.png b/pix/componenticons/nosecone-small.png
new file mode 100644 (file)
index 0000000..f920c73
Binary files /dev/null and b/pix/componenticons/nosecone-small.png differ
diff --git a/pix/componenticons/parachute-large.png b/pix/componenticons/parachute-large.png
new file mode 100644 (file)
index 0000000..0828c45
Binary files /dev/null and b/pix/componenticons/parachute-large.png differ
diff --git a/pix/componenticons/parachute-small.png b/pix/componenticons/parachute-small.png
new file mode 100644 (file)
index 0000000..eb4dea3
Binary files /dev/null and b/pix/componenticons/parachute-small.png differ
diff --git a/pix/componenticons/shockcord-large.png b/pix/componenticons/shockcord-large.png
new file mode 100644 (file)
index 0000000..8a7586b
Binary files /dev/null and b/pix/componenticons/shockcord-large.png differ
diff --git a/pix/componenticons/shockcord-small.png b/pix/componenticons/shockcord-small.png
new file mode 100644 (file)
index 0000000..13ff97e
Binary files /dev/null and b/pix/componenticons/shockcord-small.png differ
diff --git a/pix/componenticons/streamer-large.png b/pix/componenticons/streamer-large.png
new file mode 100644 (file)
index 0000000..57a42d1
Binary files /dev/null and b/pix/componenticons/streamer-large.png differ
diff --git a/pix/componenticons/streamer-small.png b/pix/componenticons/streamer-small.png
new file mode 100644 (file)
index 0000000..317e469
Binary files /dev/null and b/pix/componenticons/streamer-small.png differ
diff --git a/pix/componenticons/transition-large.png b/pix/componenticons/transition-large.png
new file mode 100644 (file)
index 0000000..7e5cbd0
Binary files /dev/null and b/pix/componenticons/transition-large.png differ
diff --git a/pix/componenticons/transition-small.png b/pix/componenticons/transition-small.png
new file mode 100644 (file)
index 0000000..da25735
Binary files /dev/null and b/pix/componenticons/transition-small.png differ
diff --git a/pix/componenticons/trapezoidfin-large.png b/pix/componenticons/trapezoidfin-large.png
new file mode 100644 (file)
index 0000000..ee36f31
Binary files /dev/null and b/pix/componenticons/trapezoidfin-large.png differ
diff --git a/pix/componenticons/trapezoidfin-small.png b/pix/componenticons/trapezoidfin-small.png
new file mode 100644 (file)
index 0000000..db3c288
Binary files /dev/null and b/pix/componenticons/trapezoidfin-small.png differ
diff --git a/pix/componenticons/tubecoupler-large.png b/pix/componenticons/tubecoupler-large.png
new file mode 100644 (file)
index 0000000..ea927a6
Binary files /dev/null and b/pix/componenticons/tubecoupler-large.png differ
diff --git a/pix/componenticons/tubecoupler-small.png b/pix/componenticons/tubecoupler-small.png
new file mode 100644 (file)
index 0000000..7e08d67
Binary files /dev/null and b/pix/componenticons/tubecoupler-small.png differ
diff --git a/pix/icons/application-exit.png b/pix/icons/application-exit.png
new file mode 100644 (file)
index 0000000..6323241
Binary files /dev/null and b/pix/icons/application-exit.png differ
diff --git a/pix/icons/copyright.txt b/pix/icons/copyright.txt
new file mode 100644 (file)
index 0000000..aa0f8c7
--- /dev/null
@@ -0,0 +1,29 @@
+
+Copyright of the icons:
+-----------------------
+
+
+From the "Nuvola" package by David Vignoni:
+http://www.icon-king.com/projects/nuvola/
+
+application-exit.png
+document-close.png
+document-new.png
+document-open.png
+document-save-as.png
+document-save.png
+edit-copy.png
+edit-cut.png
+edit-delete.png
+edit-paste.png
+edit-redo.png
+edit-undo.png
+
+
+From the "Crystal" project:
+http://www.everaldo.com/crystal/
+
+preferences.png
+zoom-in.png
+zoom-out.png
+
diff --git a/pix/icons/document-close.png b/pix/icons/document-close.png
new file mode 100644 (file)
index 0000000..3bf5029
Binary files /dev/null and b/pix/icons/document-close.png differ
diff --git a/pix/icons/document-new.png b/pix/icons/document-new.png
new file mode 100644 (file)
index 0000000..f38d02e
Binary files /dev/null and b/pix/icons/document-new.png differ
diff --git a/pix/icons/document-open.png b/pix/icons/document-open.png
new file mode 100644 (file)
index 0000000..2d8e3ba
Binary files /dev/null and b/pix/icons/document-open.png differ
diff --git a/pix/icons/document-save-as.png b/pix/icons/document-save-as.png
new file mode 100644 (file)
index 0000000..71602bc
Binary files /dev/null and b/pix/icons/document-save-as.png differ
diff --git a/pix/icons/document-save.png b/pix/icons/document-save.png
new file mode 100644 (file)
index 0000000..fd0048d
Binary files /dev/null and b/pix/icons/document-save.png differ
diff --git a/pix/icons/edit-copy.png b/pix/icons/edit-copy.png
new file mode 100644 (file)
index 0000000..b7c938a
Binary files /dev/null and b/pix/icons/edit-copy.png differ
diff --git a/pix/icons/edit-cut.png b/pix/icons/edit-cut.png
new file mode 100644 (file)
index 0000000..49f3591
Binary files /dev/null and b/pix/icons/edit-cut.png differ
diff --git a/pix/icons/edit-delete.png b/pix/icons/edit-delete.png
new file mode 100644 (file)
index 0000000..d33c344
Binary files /dev/null and b/pix/icons/edit-delete.png differ
diff --git a/pix/icons/edit-paste.png b/pix/icons/edit-paste.png
new file mode 100644 (file)
index 0000000..4c43ddf
Binary files /dev/null and b/pix/icons/edit-paste.png differ
diff --git a/pix/icons/edit-redo.png b/pix/icons/edit-redo.png
new file mode 100644 (file)
index 0000000..f1e45cf
Binary files /dev/null and b/pix/icons/edit-redo.png differ
diff --git a/pix/icons/edit-undo.png b/pix/icons/edit-undo.png
new file mode 100644 (file)
index 0000000..6129fa0
Binary files /dev/null and b/pix/icons/edit-undo.png differ
diff --git a/pix/icons/preferences.png b/pix/icons/preferences.png
new file mode 100644 (file)
index 0000000..a5f3308
Binary files /dev/null and b/pix/icons/preferences.png differ
diff --git a/pix/icons/zoom-in.png b/pix/icons/zoom-in.png
new file mode 100644 (file)
index 0000000..192d7ec
Binary files /dev/null and b/pix/icons/zoom-in.png differ
diff --git a/pix/icons/zoom-out.png b/pix/icons/zoom-out.png
new file mode 100644 (file)
index 0000000..a33559e
Binary files /dev/null and b/pix/icons/zoom-out.png differ
diff --git a/pix/spheres/blue-16x16.png b/pix/spheres/blue-16x16.png
new file mode 100644 (file)
index 0000000..1f330f0
Binary files /dev/null and b/pix/spheres/blue-16x16.png differ
diff --git a/pix/spheres/gray-16x16.png b/pix/spheres/gray-16x16.png
new file mode 100644 (file)
index 0000000..d0951a9
Binary files /dev/null and b/pix/spheres/gray-16x16.png differ
diff --git a/pix/spheres/green-16x16.png b/pix/spheres/green-16x16.png
new file mode 100644 (file)
index 0000000..29d785d
Binary files /dev/null and b/pix/spheres/green-16x16.png differ
diff --git a/pix/spheres/red-16x16.png b/pix/spheres/red-16x16.png
new file mode 100644 (file)
index 0000000..79c7161
Binary files /dev/null and b/pix/spheres/red-16x16.png differ
diff --git a/pix/spheres/yellow-16x16.png b/pix/spheres/yellow-16x16.png
new file mode 100644 (file)
index 0000000..9ed11ea
Binary files /dev/null and b/pix/spheres/yellow-16x16.png differ
diff --git a/pix/splashscreen.png b/pix/splashscreen.png
new file mode 100644 (file)
index 0000000..816b84a
Binary files /dev/null and b/pix/splashscreen.png differ
diff --git a/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java b/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java
new file mode 100644 (file)
index 0000000..efdd14b
--- /dev/null
@@ -0,0 +1,581 @@
+package net.sf.openrocket.aerodynamics;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A class that is the base of all aerodynamical calculations.
+ * <p>
+ * A {@link Configuration} object must be assigned to this class before any 
+ * operations are allowed.  This can be done using the constructor or using 
+ * the {@link #setConfiguration(Configuration)} method.  The default is a
+ * <code>null</code> configuration, in which case the calculation
+ * methods throw {@link NullPointerException}.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class AerodynamicCalculator {
+       
+       private static final double MIN_MASS = 0.001 * MathUtil.EPSILON;
+
+       /** Number of divisions used when calculating worst CP. */
+       public static final int DIVISIONS = 360;
+       
+       /**
+        * A <code>WarningSet</code> that can be used if <code>null</code> is passed
+        * to a calculation method.
+        */
+       protected WarningSet ignoreWarningSet = new WarningSet();
+       
+       /**
+        * The <code>Rocket</code> currently being calculated.
+        */
+       protected Rocket rocket = null;
+       
+       protected Configuration configuration = null;
+       
+       
+       /*
+        * Cached data.  All CG data is in absolute coordinates.  All moments of inertia
+        * are relative to their respective CG.
+        */
+       private Coordinate[] cgCache = null;
+       private Coordinate[] origCG = null;  // CG of non-overridden stage
+       private double longitudalInertiaCache[] = null;
+       private double rotationalInertiaCache[] = null;
+       
+       
+       // TODO: LOW: Do not void unnecessary data (mass/aero separately)
+       private int rocketModID = -1;
+//     private int aeroModID = -1;
+//     private int massModID = -1;
+
+       /**
+        * No-options constructor.  The rocket is left as <code>null</code>.
+        */
+       public AerodynamicCalculator() {
+               
+       }
+       
+       
+       
+       /**
+        * A constructor that sets the Configuration to be used.
+        * 
+        * @param config  the configuration to use
+        */
+       public AerodynamicCalculator(Configuration config) {
+               setConfiguration(config);
+       }
+
+       
+       
+       public Configuration getConfiguration() {
+               return configuration;
+       }
+       
+       public void setConfiguration(Configuration config) {
+               this.configuration = config;
+               this.rocket = config.getRocket();
+       }
+       
+       
+       public abstract AerodynamicCalculator newInstance();
+       
+       
+       //////////////////  Mass property calculations  ///////////////////
+       
+       
+       /**
+        * Get the CG and mass of the current configuration with motors at the specified
+        * time.  The motor ignition times are taken from the configuration.
+        */
+       public Coordinate getCG(double time) {
+               Coordinate totalCG;
+               
+               totalCG = getEmptyCG();
+               
+               Iterator<MotorMount> iterator = configuration.motorIterator();
+               while (iterator.hasNext()) {
+                       MotorMount mount = iterator.next();
+                       double ignition = configuration.getIgnitionTime(mount);
+                       Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
+                       RocketComponent component = (RocketComponent) mount;
+               
+                       double position = (component.getLength() - motor.getLength() 
+                                       + mount.getMotorOverhang());
+                       
+                       for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
+                                       add(position,0,0))) {
+                               totalCG = totalCG.average(c);
+                       }
+               }
+               
+               return totalCG;
+       }
+       
+       
+       /**
+        * Get the CG and mass of the current configuration without motors.
+        * 
+        * @return                      the CG of the configuration
+        */
+       public Coordinate getEmptyCG() {
+               checkCache();
+               
+               if (cgCache == null) {
+                       calculateStageCache();
+               }
+               
+               Coordinate totalCG = null;
+               for (int stage: configuration.getActiveStages()) {
+                       totalCG = cgCache[stage].average(totalCG);
+               }
+               
+               if (totalCG == null)
+                       totalCG = Coordinate.NUL;
+               
+               return totalCG;
+       }
+       
+       
+       /**
+        * Return the longitudal inertia of the current configuration with motors at 
+        * the specified time.  The motor ignition times are taken from the configuration.
+        * 
+        * @param time          the time.
+        * @return                      the longitudal moment of inertia of the configuration.
+        */
+       public double getLongitudalInertia(double time) {
+               checkCache();
+               
+               if (cgCache == null) {
+                       calculateStageCache();
+               }
+               
+               final Coordinate totalCG = getCG(time);
+               double totalInertia = 0;
+               
+               // Stages
+               for (int stage: configuration.getActiveStages()) {
+                       Coordinate stageCG = cgCache[stage];
+                       
+                       totalInertia += (longitudalInertiaCache[stage] + 
+                                       stageCG.weight * MathUtil.pow2(stageCG.x - totalCG.x));
+               }
+               
+               
+               // Motors
+               Iterator<MotorMount> iterator = configuration.motorIterator();
+               while (iterator.hasNext()) {
+                       MotorMount mount = iterator.next();
+                       double ignition = configuration.getIgnitionTime(mount);
+                       Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
+                       RocketComponent component = (RocketComponent) mount;
+               
+                       double position = (component.getLength() - motor.getLength() 
+                                       + mount.getMotorOverhang());
+                       
+                       double inertia = motor.getLongitudalInertia(time - ignition);
+                       for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
+                                       add(position,0,0))) {
+                               totalInertia += inertia + c.weight * MathUtil.pow2(c.x - totalCG.x);
+                       }
+               }
+               
+               return totalInertia;
+       }
+       
+       
+       /**
+        * Return the rotational inertia of the configuration with motors at the specified time.
+        * The motor ignition times are taken from the configuration.
+        * 
+        * @param time          the time.
+        * @return                      the rotational moment of inertia of the configuration.
+        */
+       public double getRotationalInertia(double time) {
+               checkCache();
+               
+               if (cgCache == null) {
+                       calculateStageCache();
+               }
+               
+               final Coordinate totalCG = getCG(time);
+               double totalInertia = 0;
+               
+               // Stages
+               for (int stage: configuration.getActiveStages()) {
+                       Coordinate stageCG = cgCache[stage];
+                       
+                       totalInertia += rotationalInertiaCache[stage] + stageCG.weight * (
+                                       MathUtil.pow2(stageCG.y-totalCG.y) + MathUtil.pow2(stageCG.z-totalCG.z)
+                       );
+               }
+               
+               
+               // Motors
+               Iterator<MotorMount> iterator = configuration.motorIterator();
+               while (iterator.hasNext()) {
+                       MotorMount mount = iterator.next();
+                       double ignition = configuration.getIgnitionTime(mount);
+                       Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
+                       RocketComponent component = (RocketComponent) mount;
+               
+                       double position = (component.getLength() - motor.getLength() 
+                                       + mount.getMotorOverhang());
+                       
+                       double inertia = motor.getRotationalInertia(time - ignition);
+                       for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
+                                       add(position,0,0))) {
+                               totalInertia += inertia + c.weight * (
+                                               MathUtil.pow2(c.y - totalCG.y) + MathUtil.pow2(c.z - totalCG.z)
+                               );
+                       }
+               }
+               
+               return totalInertia;
+       }
+       
+       
+       
+       private void calculateStageCache() {
+               int stages = rocket.getStageCount();
+               
+               cgCache = new Coordinate[stages];
+               longitudalInertiaCache = new double[stages];
+               rotationalInertiaCache = new double[stages];
+               
+               for (int i=0; i < stages; i++) {
+                       RocketComponent stage = rocket.getChild(i);
+                       MassData data = calculateAssemblyMassData(stage);
+                       cgCache[i] = stage.toAbsolute(data.cg)[0];
+                       longitudalInertiaCache[i] = data.longitudalInertia;
+                       rotationalInertiaCache[i] = data.rotationalInetria;
+               }
+       }
+       
+       
+       
+//     /**
+//      * Updates the stage CGs.
+//      */
+//     private void calculateStageCGs() {
+//             int stages = rocket.getStageCount();
+//             
+//             cgCache = new Coordinate[stages];
+//             origCG = new Coordinate[stages];
+//             
+//             for (int i=0; i < stages; i++) {
+//                     Stage stage = (Stage) rocket.getChild(i);
+//                     Coordinate stageCG = null;
+//                     
+//                     Iterator<RocketComponent> iterator = stage.deepIterator();
+//                     while (iterator.hasNext()) {
+//                             RocketComponent component = iterator.next();
+//
+//                             for (Coordinate c: component.toAbsolute(component.getCG())) {
+//                                     stageCG = c.average(stageCG);
+//                             }
+//                     }
+//
+//                     if (stageCG == null)
+//                             stageCG = Coordinate.NUL;
+//                     
+//                     origCG[i] = stageCG;
+//                     
+//                     if (stage.isMassOverridden()) {
+//                             stageCG = stageCG.setWeight(stage.getOverrideMass());
+//                     }
+//                     if (stage.isCGOverridden()) {
+//                             stageCG = stageCG.setXYZ(stage.getOverrideCG());
+//                     }
+//                     
+////                   System.out.println("Stage "+i+" CG:"+stageCG);
+//                     
+//                     cgCache[i] = stageCG;
+//             }
+//     }
+//     
+//     
+//     private Coordinate calculateCG(RocketComponent component) {
+//             Coordinate componentCG = Coordinate.NUL;
+//             
+//             // Compute CG of this component
+//             Coordinate cg = component.getCG();
+//             if (cg.weight < MIN_MASS)
+//                     cg = cg.setWeight(MIN_MASS);
+//
+//             for (Coordinate c: component.toAbsolute(cg)) {
+//                     componentCG = componentCG.average(c);
+//             }
+//             
+//             // Compute CG with subcomponents
+//             for (RocketComponent sibling: component.getChildren()) {
+//                     componentCG = componentCG.average(calculateCG(sibling));
+//             }
+//             
+//             // Override mass/CG if subcomponents are also overridden
+//             if (component.getOverrideSubcomponents()) {
+//                     if (component.isMassOverridden()) {
+//                             componentCG = componentCG.setWeight(
+//                                             MathUtil.max(component.getOverrideMass(), MIN_MASS));
+//                     }
+//                     if (component.isCGOverridden()) {
+//                             componentCG = componentCG.setXYZ(component.getOverrideCG());
+//                     }
+//             }
+//             
+//             return componentCG;
+//     }
+//     
+//     
+//     
+//     private void calculateStageInertias() {
+//             int stages = rocket.getStageCount();
+//             
+//             if (cgCache == null)
+//                     calculateStageCGs();
+//             
+//             longitudalInertiaCache = new double[stages];
+//             rotationalInertiaCache = new double[stages];
+//
+//             for (int i=0; i < stages; i++) {
+//                     Coordinate stageCG = cgCache[i];
+//                     double stageLongitudalInertia = 0;
+//                     double stageRotationalInertia = 0;
+//                     
+//                     Iterator<RocketComponent> iterator = rocket.getChild(i).deepIterator();
+//                     while (iterator.hasNext()) {
+//                             RocketComponent component = iterator.next();
+//                             double li = component.getLongitudalInertia();
+//                             double ri = component.getRotationalInertia();
+//                             double mass = component.getMass();
+//                             
+//                             for (Coordinate c: component.toAbsolute(component.getCG())) {
+//                                     stageLongitudalInertia += li + mass * MathUtil.pow2(c.x - stageCG.x);
+//                                     stageRotationalInertia += ri + mass * (MathUtil.pow2(c.y - stageCG.y) +
+//                                                     MathUtil.pow2(c.z - stageCG.z));
+//                             }
+//                     }
+//                     
+//                     // Check for mass override of complete stage
+//                     if ((origCG[i].weight != cgCache[i].weight) && origCG[i].weight > 0.0000001) {
+//                             stageLongitudalInertia = (stageLongitudalInertia * cgCache[i].weight / 
+//                                             origCG[i].weight);
+//                             stageRotationalInertia = (stageRotationalInertia * cgCache[i].weight / 
+//                                             origCG[i].weight);
+//                     }
+//                     
+//                     longitudalInertiaCache[i] = stageLongitudalInertia;
+//                     rotationalInertiaCache[i] = stageRotationalInertia;
+//             }
+//     }
+//     
+       
+       
+       /**
+        * Returns the mass and inertia data for this component and all subcomponents.
+        * The inertia is returned relative to the CG, and the CG is in the coordinates
+        * of the specified component, not global coordinates.
+        */
+       private MassData calculateAssemblyMassData(RocketComponent parent) {
+               MassData parentData = new MassData();
+               
+               // Calculate data for this component
+               parentData.cg = parent.getComponentCG();
+               if (parentData.cg.weight < MIN_MASS)
+                       parentData.cg = parentData.cg.setWeight(MIN_MASS);
+               
+               
+               // Override only this component's data
+               if (!parent.getOverrideSubcomponents()) {
+                       if (parent.isMassOverridden())
+                               parentData.cg = parentData.cg.setWeight(MathUtil.max(parent.getOverrideMass(),MIN_MASS));
+                       if (parent.isCGOverridden())
+                               parentData.cg = parentData.cg.setXYZ(parent.getOverrideCG());
+               }
+               
+               parentData.longitudalInertia = parent.getLongitudalUnitInertia() * parentData.cg.weight;
+               parentData.rotationalInetria = parent.getRotationalUnitInertia() * parentData.cg.weight;
+               
+               
+               // Combine data for subcomponents
+               for (RocketComponent sibling: parent.getChildren()) {
+                       Coordinate combinedCG;
+                       double dx2, dr2;
+                       
+                       // Compute data of sibling
+                       MassData siblingData = calculateAssemblyMassData(sibling);
+                       Coordinate[] siblingCGs = sibling.toRelative(siblingData.cg, parent);
+                       
+                       for (Coordinate siblingCG: siblingCGs) {
+                       
+                               // Compute CG of this + sibling
+                               combinedCG = parentData.cg.average(siblingCG);
+                               
+                               // Add effect of this CG change to parent inertia
+                               dx2 = pow2(parentData.cg.x - combinedCG.x);
+                               parentData.longitudalInertia += parentData.cg.weight * dx2;
+                               
+                               dr2 = pow2(parentData.cg.y - combinedCG.y) + pow2(parentData.cg.z - combinedCG.z);
+                               parentData.rotationalInetria += parentData.cg.weight * dr2;
+                               
+                               
+                               // Add inertia of sibling
+                               parentData.longitudalInertia += siblingData.longitudalInertia;
+                               parentData.rotationalInetria += siblingData.rotationalInetria;
+                               
+                               // Add effect of sibling CG change
+                               dx2 = pow2(siblingData.cg.x - combinedCG.x);
+                               parentData.longitudalInertia += siblingData.cg.weight * dx2;
+                               
+                               dr2 = pow2(siblingData.cg.y - combinedCG.y) + pow2(siblingData.cg.z - combinedCG.z);
+                               parentData.rotationalInetria += siblingData.cg.weight * dr2;
+                               
+                               // Set combined CG
+                               parentData.cg = combinedCG;
+                       }
+               }
+
+               // Override total data
+               if (parent.getOverrideSubcomponents()) {
+                       if (parent.isMassOverridden()) {
+                               double oldMass = parentData.cg.weight;
+                               double newMass = MathUtil.max(parent.getOverrideMass(), MIN_MASS);
+                               parentData.longitudalInertia = parentData.longitudalInertia * newMass / oldMass;
+                               parentData.rotationalInetria = parentData.rotationalInetria * newMass / oldMass;
+                               parentData.cg = parentData.cg.setWeight(newMass);
+                       }
+                       if (parent.isCGOverridden()) {
+                               double oldx = parentData.cg.x;
+                               double newx = parent.getOverrideCGX();
+                               parentData.longitudalInertia += parentData.cg.weight * pow2(oldx - newx);
+                               parentData.cg = parentData.cg.setX(newx);
+                       }
+               }
+               
+               return parentData;
+       }
+       
+       
+       private static class MassData {
+               public Coordinate cg = Coordinate.NUL;
+               public double longitudalInertia = 0;
+               public double rotationalInetria = 0;
+       }
+       
+       
+       
+       
+       
+       ////////////////  Aerodynamic calculators  ////////////////
+       
+       public abstract Coordinate getCP(FlightConditions conditions, WarningSet warnings);
+       
+       /*
+       public abstract List<AerodynamicForces> getCPAnalysis(FlightConditions conditions, 
+                       WarningSet warnings);
+       */
+       
+       public abstract Map<RocketComponent, AerodynamicForces> 
+               getForceAnalysis(FlightConditions conditions, WarningSet warnings);
+       
+       public abstract AerodynamicForces getAerodynamicForces(double time,
+                       FlightConditions conditions, WarningSet warnings);
+       
+       
+       /* Calculate only axial forces (and do not warn about insane AOA etc) */
+       public abstract AerodynamicForces getAxialForces(double time,
+                       FlightConditions conditions, WarningSet warnings);
+
+       
+       
+       public Coordinate getWorstCP() {
+               return getWorstCP(new FlightConditions(configuration), ignoreWarningSet);
+       }
+       
+       /*
+        * The worst theta angle is stored in conditions.
+        */
+       public Coordinate getWorstCP(FlightConditions conditions, WarningSet warnings) {
+               FlightConditions cond = conditions.clone();
+               Coordinate worst = new Coordinate(Double.MAX_VALUE);
+               Coordinate cp;
+               double theta = 0;
+               
+               for (int i=0; i < DIVISIONS; i++) {
+                       cond.setTheta(2*Math.PI*i/DIVISIONS);
+                       cp = getCP(cond, warnings);
+                       if (cp.x < worst.x) {
+                               worst = cp;
+                               theta = cond.getTheta();
+                       }
+               }
+               
+               conditions.setTheta(theta);
+               
+               return worst;
+       }
+       
+       
+       
+       
+       
+       /**
+        * Check the current cache consistency.  This method must be called by all
+        * methods that may use any cached data before any other operations are
+        * performed.  If the rocket has changed since the previous call to
+        * <code>checkCache()</code>, then either {@link #voidAerodynamicCache()} or
+        * {@link #voidMassCache()} (or both) are called.
+        * <p>
+        * This method performs the checking based on the rocket's modification IDs,
+        * so that these method may be called from listeners of the rocket itself.
+        */
+       protected final void checkCache() {
+               if (rocketModID != rocket.getModID()) {
+                       rocketModID = rocket.getModID();
+                       voidMassCache();
+                       voidAerodynamicCache();
+               }
+       }
+       
+       
+       /**
+        * Void cached mass data.  This method is called whenever a change occurs in 
+        * the rocket structure that affects the mass of the rocket and when a new 
+        * Rocket is set.  This method must be overridden to void any cached data 
+        * necessary.  The method must call <code>super.voidMassCache()</code> during its 
+        * execution.
+        */
+       protected void voidMassCache() {
+               cgCache = null;
+               longitudalInertiaCache = null;
+               rotationalInertiaCache = null;
+       }
+       
+       /**
+        * Void cached aerodynamic data.  This method is called whenever a change occurs in 
+        * the rocket structure that affects the aerodynamics of the rocket and when a new 
+        * Rocket is set.  This method must be overridden to void any cached data 
+        * necessary.  The method must call <code>super.voidAerodynamicCache()</code> during 
+        * its execution.
+        */
+       protected void voidAerodynamicCache() {
+               // No-op
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java b/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java
new file mode 100644 (file)
index 0000000..5a6abcb
--- /dev/null
@@ -0,0 +1,180 @@
+package net.sf.openrocket.aerodynamics;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.Coordinate;
+
+public class AerodynamicForces implements Cloneable {
+       
+       /** 
+        * The component this data is referring to.  Used in analysis methods.
+        * A total value is indicated by the component being the Rocket component. 
+        */
+       public RocketComponent component = null;
+       
+
+       /** CG and mass */
+       public Coordinate cg = null;
+
+       /** Longitudal moment of inertia with reference to the CG. */
+       public double longitudalInertia = Double.NaN;
+       
+       /** Rotational moment of inertia with reference to the CG. */
+       public double rotationalInertia = Double.NaN;
+       
+       
+
+       /** CP and CNa. */
+       public Coordinate cp = null;
+       
+       
+       /**
+        * Normal force coefficient derivative.  At values close to zero angle of attack
+        * this value may be poorly defined or NaN if the calculation method does not
+        * compute CNa directly.
+        */
+       public double CNa = Double.NaN;
+
+       
+       /** Normal force coefficient. */
+       public double CN = Double.NaN;
+
+       /** Pitching moment coefficient, relative to the coordinate origin. */
+       public double Cm = Double.NaN;
+       
+       /** Side force coefficient, Cy */
+       public double Cside = Double.NaN;
+       
+       /** Yaw moment coefficient, Cn, relative to the coordinate origin. */
+       public double Cyaw = Double.NaN;
+       
+       /** Roll moment coefficient, Cl, relative to the coordinate origin. */
+       public double Croll = Double.NaN;
+       
+       /** Roll moment damping coefficient */
+       public double CrollDamp = Double.NaN;
+       
+       /** Roll moment forcing coefficient */
+       public double CrollForce = Double.NaN;
+       
+
+       
+       /** Axial drag coefficient, CA */
+       public double Caxial = Double.NaN;
+       
+       /** Total drag force coefficient, parallel to the airflow. */
+       public double CD = Double.NaN;
+       
+       /** Drag coefficient due to fore pressure drag. */
+       public double pressureCD = Double.NaN;
+       
+       /** Drag coefficient due to base drag. */
+       public double baseCD = Double.NaN;
+       
+       /** Drag coefficient due to friction drag. */
+       public double frictionCD = Double.NaN;
+       
+       
+       public double pitchDampingMoment = Double.NaN;
+       public double yawDampingMoment = Double.NaN;
+       
+       
+       /**
+        * Reset all values to null/NaN.
+        */
+       public void reset() {
+               component = null;
+               cg = null;
+               longitudalInertia = Double.NaN;
+               rotationalInertia = Double.NaN;
+
+               cp = null;
+               CNa = Double.NaN;
+               CN = Double.NaN;
+               Cm = Double.NaN;
+               Cside = Double.NaN;
+               Cyaw = Double.NaN;
+               Croll = Double.NaN;
+               CrollDamp = Double.NaN;
+               CrollForce = Double.NaN;
+               Caxial = Double.NaN;
+               CD = Double.NaN;
+               pitchDampingMoment = Double.NaN;
+               yawDampingMoment = Double.NaN;
+       }
+       
+       /**
+        * Zero all values to 0 / Coordinate.NUL.  Component is left as it was.
+        */
+       public void zero() {
+               // component untouched
+               cg = Coordinate.NUL;
+               longitudalInertia = 0;
+               rotationalInertia = 0;
+
+               cp = Coordinate.NUL;
+               CNa = 0;
+               CN = 0;
+               Cm = 0;
+               Cside = 0;
+               Cyaw = 0;
+               Croll = 0;
+               CrollDamp = 0;
+               CrollForce = 0;
+               Caxial = 0;
+               CD = 0;
+               pitchDampingMoment = 0;
+               yawDampingMoment = 0;
+       }
+
+       
+       @Override
+       public AerodynamicForces clone() {
+               try {
+                       return (AerodynamicForces)super.clone();
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("CloneNotSupportedException?!?");
+               }
+       }
+       
+       @Override
+       public String toString() {
+               String text="AerodynamicForces[";
+               
+               if (component != null)
+                       text += "component:" + component + ",";
+               if (cg != null)
+                       text += "cg:" + cg + ",";
+               if (cp != null)
+                       text += "cp:" + cp + ",";
+               if (!Double.isNaN(longitudalInertia))
+                       text += "longIn:" + longitudalInertia + ",";
+               if (!Double.isNaN(rotationalInertia))
+                       text += "rotIn:" + rotationalInertia + ",";
+               
+               if (!Double.isNaN(CNa))
+                       text += "CNa:" + CNa + ",";
+               if (!Double.isNaN(CN))
+                       text += "CN:" + CN + ",";
+               if (!Double.isNaN(Cm))
+                       text += "Cm:" + Cm + ",";
+               
+               if (!Double.isNaN(Cside))
+                       text += "Cside:" + Cside + ",";
+               if (!Double.isNaN(Cyaw))
+                       text += "Cyaw:" + Cyaw + ",";
+               
+               if (!Double.isNaN(Croll))
+                       text += "Croll:" + Croll + ",";
+               if (!Double.isNaN(Caxial))
+                       text += "Caxial:" + Caxial + ",";
+               
+               if (!Double.isNaN(CD))
+                       text += "CD:" + CD + ",";
+
+               if (text.charAt(text.length()-1) == ',')
+                       text = text.substring(0, text.length()-1);
+               
+               text += "]";
+               return text;
+       }
+}
diff --git a/src/net/sf/openrocket/aerodynamics/AtmosphericConditions.java b/src/net/sf/openrocket/aerodynamics/AtmosphericConditions.java
new file mode 100644 (file)
index 0000000..c36c525
--- /dev/null
@@ -0,0 +1,105 @@
+package net.sf.openrocket.aerodynamics;
+
+public class AtmosphericConditions implements Cloneable {
+
+       /** Specific gas constant of dry air. */
+       public static final double R = 287.053;
+       
+       /** Specific heat ratio of air. */
+       public static final double GAMMA = 1.4;
+       
+       /** The standard air pressure (1.01325 bar). */
+       public static final double STANDARD_PRESSURE = 101325.0;
+       
+       /** The standard air temperature (20 degrees Celcius). */
+       public static final double STANDARD_TEMPERATURE = 293.15;
+       
+       
+       
+       /** Air pressure, in Pascals. */
+       public double pressure = STANDARD_PRESSURE;
+       
+       /** Air temperature, in Kelvins. */
+       public double temperature = STANDARD_TEMPERATURE;
+       
+       
+       /**
+        * Construct standard atmospheric conditions.
+        */
+       public AtmosphericConditions() {
+               
+       }
+       
+       /**
+        * Construct specified atmospheric conditions.
+        * 
+        * @param temperature   the temperature in Kelvins.
+        * @param pressure              the pressure in Pascals.
+        */
+       public AtmosphericConditions(double temperature, double pressure) {
+               this.temperature = temperature;
+               this.pressure = pressure;
+       }
+       
+       
+       
+       /**
+        * Return the current density of air for dry air.
+        * 
+        * @return   the current density of air.
+        */
+       public double getDensity() {
+               return pressure / (R*temperature);
+       }
+       
+       
+       /**
+        * Return the current speed of sound for dry air.
+        * <p>
+        * The speed of sound is calculated using the expansion around the temperature 0 C
+        * <code> c = 331.3 + 0.606*T </code> where T is in Celcius.  The result is accurate
+        * to about 0.5 m/s for temperatures between -30 and 30 C, and within 2 m/s
+        * for temperatures between -55 and 30 C.
+        * 
+        * @return   the current speed of sound.
+        */
+       public double getMachSpeed() {
+               return 165.77 + 0.606 * temperature;
+       }
+       
+       
+       /**
+        * Return the current kinematic viscosity of the air.
+        * <p>
+        * The effect of temperature on the viscosity of a gas can be computed using
+        * Sutherland's formula.  In the region of -40 ... 40 degrees Celcius the effect
+        * is highly linear, and thus a linear approximation is used in its stead.
+        * This is divided by the result of {@link #getDensity()} to achieve the
+        * kinematic viscosity.
+        * 
+        * @return      the current kinematic viscosity.
+        */
+       public double getKinematicViscosity() {
+               double v = 3.7291e-06 + 4.9944e-08 * temperature;
+               return v / getDensity();
+       }
+       
+       /**
+        * Return a copy of the atmospheric conditions.
+        */
+       @Override
+       public AtmosphericConditions clone() {
+               try {
+                       return (AtmosphericConditions) super.clone();
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("BUG:  CloneNotSupportedException encountered!");
+               }
+       }
+       
+       
+       @Override
+       public String toString() {
+               return String.format("AtmosphericConditions[T=%.2f,P=%.2f]", temperature, pressure);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/aerodynamics/AtmosphericModel.java b/src/net/sf/openrocket/aerodynamics/AtmosphericModel.java
new file mode 100644 (file)
index 0000000..ea2ead0
--- /dev/null
@@ -0,0 +1,41 @@
+package net.sf.openrocket.aerodynamics;
+
+public abstract class AtmosphericModel {
+       /** Layer thickness of interpolated altitude. */
+       private static final double DELTA = 500;
+       
+       private AtmosphericConditions[] levels = null;
+       
+       
+       public AtmosphericConditions getConditions(double altitude) {
+               if (levels == null)
+                       computeLayers();
+               
+               if (altitude <= 0)
+                       return levels[0];
+               if (altitude >= DELTA*(levels.length-1))
+                       return levels[levels.length-1];
+               
+               int n = (int)(altitude/DELTA);
+               double d = (altitude - n*DELTA)/DELTA;
+               AtmosphericConditions c = new AtmosphericConditions();
+               c.temperature = levels[n].temperature * (1-d) + levels[n+1].temperature * d;
+               c.pressure = levels[n].pressure * (1-d) + levels[n+1].pressure * d;
+                       
+               return c;
+       }
+       
+       
+       private void computeLayers() {
+               double max = getMaxAltitude();
+               int n = (int)(max/DELTA) + 1;
+               levels = new AtmosphericConditions[n];
+               for (int i=0; i < n; i++) {
+                       levels[i] = getExactConditions(i*DELTA);
+               }
+       }
+
+       
+       public abstract double getMaxAltitude();
+       public abstract AtmosphericConditions getExactConditions(double altitude);
+}
diff --git a/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java
new file mode 100644 (file)
index 0000000..8ace04a
--- /dev/null
@@ -0,0 +1,897 @@
+package net.sf.openrocket.aerodynamics;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import net.sf.openrocket.aerodynamics.barrowman.FinSetCalc;
+import net.sf.openrocket.aerodynamics.barrowman.RocketComponentCalc;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.PolyInterpolator;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.Test;
+
+/**
+ * An aerodynamic calculator that uses the extended Barrowman method to 
+ * calculate the CP of a rocket.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class BarrowmanCalculator extends AerodynamicCalculator {
+       
+       private static final String BARROWMAN_PACKAGE = "net.sf.openrocket.aerodynamics.barrowman";
+       private static final String BARROWMAN_SUFFIX = "Calc";
+
+       
+       private Map<RocketComponent, RocketComponentCalc> calcMap = null;
+       
+       private double cacheDiameter = -1;
+       private double cacheLength = -1;
+       
+       
+       
+       public BarrowmanCalculator() {
+
+       }
+
+       public BarrowmanCalculator(Configuration config) {
+               super(config);
+       }
+
+       
+       @Override
+       public BarrowmanCalculator newInstance() {
+               return new BarrowmanCalculator();
+       }
+       
+       
+       /**
+        * Calculate the CP according to the extended Barrowman method.
+        */
+       @Override
+       public Coordinate getCP(FlightConditions conditions, WarningSet warnings) {
+               AerodynamicForces forces = getNonAxial(conditions, null, warnings);
+               return forces.cp;
+       }
+
+
+       
+       @Override
+       public Map<RocketComponent, AerodynamicForces> getForceAnalysis(FlightConditions conditions,
+                       WarningSet warnings) {
+               
+               AerodynamicForces f;
+               Map<RocketComponent, AerodynamicForces> map = 
+                       new LinkedHashMap<RocketComponent, AerodynamicForces>();
+               
+               // Add all components to the map
+               for (RocketComponent c: configuration) {
+                       f = new AerodynamicForces();
+                       f.component = c;
+                       
+                       // Calculate CG
+                       f.cg = Coordinate.NUL;
+                       for (Coordinate coord: c.toAbsolute(c.getCG())) {
+                               f.cg = f.cg.average(coord);
+                       }
+
+                       map.put(c, f);
+               }
+
+               
+               // Calculate non-axial force data
+               AerodynamicForces total = getNonAxial(conditions, map, warnings);
+               
+               
+               // Calculate friction data
+               total.frictionCD = calculateFrictionDrag(conditions, map, warnings);
+               total.pressureCD = calculatePressureDrag(conditions, map, warnings);
+               total.baseCD = calculateBaseDrag(conditions, map, warnings);
+               total.cg = getCG(0);
+
+               total.component = rocket;
+               map.put(rocket, total);
+               
+               
+               for (RocketComponent c: map.keySet()) {
+                       f = map.get(c);
+                       if (Double.isNaN(f.baseCD) && Double.isNaN(f.pressureCD) && 
+                                       Double.isNaN(f.frictionCD))
+                               continue;
+                       if (Double.isNaN(f.baseCD))
+                               f.baseCD = 0;
+                       if (Double.isNaN(f.pressureCD))
+                               f.pressureCD = 0;
+                       if (Double.isNaN(f.frictionCD))
+                               f.frictionCD = 0;
+                       f.CD = f.baseCD + f.pressureCD + f.frictionCD;
+                       f.Caxial = calculateAxialDrag(conditions, f.CD);
+               }
+               
+               return map;
+       }
+
+       
+       
+       @Override
+       public AerodynamicForces getAerodynamicForces(double time, FlightConditions conditions, 
+                       WarningSet warnings) {
+
+               if (warnings == null)
+                       warnings = ignoreWarningSet;
+
+               // Calculate non-axial force data
+               AerodynamicForces total = getNonAxial(conditions, null, warnings);
+               
+               // Calculate friction data
+               total.frictionCD = calculateFrictionDrag(conditions, null, warnings);
+               total.pressureCD = calculatePressureDrag(conditions, null, warnings);
+               total.baseCD = calculateBaseDrag(conditions, null, warnings);
+
+               total.CD = total.frictionCD + total.pressureCD + total.baseCD;
+               
+               total.Caxial = calculateAxialDrag(conditions, total.CD);
+               
+               
+               // Calculate CG and moments of inertia
+               total.cg = this.getCG(time);
+               total.longitudalInertia = this.getLongitudalInertia(time);
+               total.rotationalInertia = this.getRotationalInertia(time);
+
+               
+               // Calculate pitch and yaw damping moments
+               calculateDampingMoments(conditions, total);
+               total.Cm -= total.pitchDampingMoment;
+               total.Cyaw -= total.yawDampingMoment;
+               
+               
+//             System.out.println("Conditions are "+conditions + " 
+//             pitch rate="+conditions.getPitchRate());
+//             System.out.println("Total Cm="+total.Cm+" damping effect="+
+//                             (12 * Math.signum(conditions.getPitchRate()) * 
+//                             MathUtil.pow2(conditions.getPitchRate()) /
+//                             MathUtil.pow2(conditions.getVelocity())));
+               
+//             double ef = Math.abs(12 *
+//                             MathUtil.pow2(conditions.getPitchRate()) /
+//                             MathUtil.pow2(conditions.getVelocity()));
+//             
+////           System.out.println("maxEffect="+maxEffect);
+//             total.Cm -= 12 * Math.signum(conditions.getPitchRate()) *
+//                             MathUtil.pow2(conditions.getPitchRate()) /
+//                             MathUtil.pow2(conditions.getVelocity());
+//                             
+//             total.Cyaw -= 0.06 * Math.signum(conditions.getYawRate()) * 
+//                                             MathUtil.pow2(conditions.getYawRate()) /
+//                                             MathUtil.pow2(conditions.getVelocity());
+               
+               return total;
+       }
+               
+       
+       
+       @Override
+       public AerodynamicForces getAxialForces(double time,
+                       FlightConditions conditions, WarningSet warnings) {
+
+               if (warnings == null)
+                       warnings = ignoreWarningSet;
+
+               AerodynamicForces total = new AerodynamicForces();
+               total.zero();
+               
+               // Calculate friction data
+               total.frictionCD = calculateFrictionDrag(conditions, null, warnings);
+               total.pressureCD = calculatePressureDrag(conditions, null, warnings);
+               total.baseCD = calculateBaseDrag(conditions, null, warnings);
+
+               total.CD = total.frictionCD + total.pressureCD + total.baseCD;
+               
+               total.Caxial = calculateAxialDrag(conditions, total.CD);
+               
+               // Calculate CG and moments of inertia
+               total.cg = this.getCG(time);
+               total.longitudalInertia = this.getLongitudalInertia(time);
+               total.rotationalInertia = this.getRotationalInertia(time);
+
+               return total;
+       }
+
+
+       
+       
+       
+       /*
+        * Perform the actual CP calculation.
+        */
+       private AerodynamicForces getNonAxial(FlightConditions conditions,
+                       Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) {
+
+               AerodynamicForces total = new AerodynamicForces();
+               total.zero();
+               
+               double radius = 0;      // aft radius of previous component
+               double componentX = 0;  // aft coordinate of previous component
+               AerodynamicForces forces = new AerodynamicForces();
+               
+               if (warnings == null)
+                       warnings = ignoreWarningSet;
+               
+               if (conditions.getAOA() > 17.5*Math.PI/180)
+                       warnings.add(new Warning.LargeAOA(conditions.getAOA()));
+               
+               checkCache();
+
+               if (calcMap == null)
+                       buildCalcMap();
+               
+               for (RocketComponent component: configuration) {
+                       
+                       // Skip non-aerodynamic components
+                       if (!component.isAerodynamic())
+                               continue;
+                       
+                       // Check for discontinuities
+                       if (component instanceof SymmetricComponent) {
+                               SymmetricComponent sym = (SymmetricComponent) component;
+                               // TODO:LOW: Ignores other cluster components (not clusterable)
+                               double x = component.toAbsolute(Coordinate.NUL)[0].x;
+                               
+                               // Check for lengthwise discontinuity
+                               if (x > componentX + 0.0001){
+                                       if (!MathUtil.equals(radius, 0)) {
+                                               warnings.add(Warning.DISCONTINUITY);
+                                               radius = 0;
+                                       }
+                               }
+                               componentX = component.toAbsolute(new Coordinate(component.getLength()))[0].x;
+
+                               // Check for radius discontinuity
+                               if (!MathUtil.equals(sym.getForeRadius(), radius)) {
+                                       warnings.add(Warning.DISCONTINUITY);
+                                       // TODO: MEDIUM: Apply correction to values to cp and to map
+                               }
+                               radius = sym.getAftRadius();
+                       }
+                       
+                       // Call calculation method
+                       forces.zero();
+                       calcMap.get(component).calculateNonaxialForces(conditions, forces, warnings);
+                       forces.cp = component.toAbsolute(forces.cp)[0];
+                       forces.Cm = forces.CN * forces.cp.x / conditions.getRefLength();
+//                     System.out.println("  CN="+forces.CN+" cp.x="+forces.cp.x+" Cm="+forces.Cm);
+                       
+                       if (map != null) {
+                               AerodynamicForces f = map.get(component);
+                               
+                               f.cp = forces.cp;
+                               f.CNa = forces.CNa;
+                               f.CN = forces.CN;
+                               f.Cm = forces.Cm;
+                               f.Cside = forces.Cside;
+                               f.Cyaw = forces.Cyaw;
+                               f.Croll = forces.Croll;
+                               f.CrollDamp = forces.CrollDamp;
+                               f.CrollForce = forces.CrollForce;
+                       }
+                       
+                       total.cp = total.cp.average(forces.cp);
+                       total.CNa += forces.CNa;
+                       total.CN += forces.CN;
+                       total.Cm += forces.Cm;
+                       total.Cside += forces.Cside;
+                       total.Cyaw += forces.Cyaw;
+                       total.Croll += forces.Croll;
+                       total.CrollDamp += forces.CrollDamp;
+                       total.CrollForce += forces.CrollForce;
+               }
+               
+               return total;
+       }
+
+       
+
+       
+       ////////////////  DRAG CALCULATIONS  ////////////////
+       
+       
+       private double calculateFrictionDrag(FlightConditions conditions, 
+                       Map<RocketComponent, AerodynamicForces> map, WarningSet set) {
+               double c1=1.0, c2=1.0;
+               
+               double mach = conditions.getMach();
+               double Re;
+               double Cf;
+               
+               if (calcMap == null)
+                       buildCalcMap();
+
+               Re = conditions.getVelocity() * configuration.getLength() / 
+               conditions.getAtmosphericConditions().getKinematicViscosity();
+
+//             System.out.printf("Re=%.3e   ", Re);
+               
+               // Calculate the skin friction coefficient (assume non-roughness limited)
+               if (configuration.getRocket().isPerfectFinish()) {
+                       
+//                     System.out.printf("Perfect finish: Re=%f ",Re);
+                       // Assume partial laminar layer.  Roughness-limitation is checked later.
+                       if (Re < 1e4) {
+                               // Too low, constant
+                               Cf = 1.33e-2;
+//                             System.out.printf("constant Cf=%f ",Cf);
+                       } else if (Re < 5.39e5) {
+                               // Fully laminar
+                               Cf = 1.328 / Math.sqrt(Re);
+//                             System.out.printf("basic Cf=%f ",Cf);
+                       } else {
+                               // Transitional
+                               Cf = 1.0/pow2(1.50 * Math.log(Re) - 5.6) - 1700/Re;
+//                             System.out.printf("transitional Cf=%f ",Cf);
+                       }
+                       
+                       // Compressibility correction
+
+                       if (mach < 1.1) {
+                               // Below Re=1e6 no correction
+                               if (Re > 1e6) {
+                                       if (Re < 3e6) {
+                                               c1 = 1 - 0.1*pow2(mach)*(Re-1e6)/2e6;  // transition to turbulent
+                                       } else {
+                                               c1 = 1 - 0.1*pow2(mach);
+                                       }
+                               }
+                       }
+                       if (mach > 0.9) {
+                               if (Re > 1e6) {
+                                       if (Re < 3e6) {
+                                               c2 = 1 + (1.0 / Math.pow(1+0.045*pow2(mach), 0.25) -1) * (Re-1e6)/2e6;
+                                       } else {
+                                               c2 = 1.0 / Math.pow(1+0.045*pow2(mach), 0.25);
+                                       }
+                               }
+                       }
+                       
+//                     System.out.printf("c1=%f c2=%f\n", c1,c2);
+                       // Applying continuously around Mach 1
+                       if (mach < 0.9) {
+                               Cf *= c1;
+                       } else if (mach < 1.1) {
+                               Cf *= (c2 * (mach-0.9)/0.2 + c1 * (1.1-mach)/0.2);
+                       } else {
+                               Cf *= c2;
+                       }
+                       
+//                     System.out.printf("M=%f Cf=%f (smooth)\n",mach,Cf);
+               
+               } else {
+                       
+                       // Assume fully turbulent.  Roughness-limitation is checked later.
+                       if (Re < 1e4) {
+                               // Too low, constant
+                               Cf = 1.48e-2;
+//                             System.out.printf("LOW-TURB  ");
+                       } else {
+                               // Turbulent
+                               Cf = 1.0/pow2(1.50 * Math.log(Re) - 5.6);
+//                             System.out.printf("NORMAL-TURB  ");
+                       }
+                       
+                       // Compressibility correction
+                       
+                       if (mach < 1.1) {
+                               c1 = 1 - 0.1*pow2(mach);
+                       }
+                       if (mach > 0.9) {
+                               c2 = 1/Math.pow(1 + 0.15*pow2(mach), 0.58);
+                       }
+                       // Applying continuously around Mach 1
+                       if (mach < 0.9) {
+                               Cf *= c1;
+                       } else if (mach < 1.1) {
+                               Cf *= c2 * (mach-0.9)/0.2 + c1 * (1.1-mach)/0.2;
+                       } else {
+                               Cf *= c2;
+                       }
+                       
+//                     System.out.printf("M=%f, Cd=%f (turbulent)\n", mach,Cf);
+
+               }
+               
+               // Roughness-limited value correction term
+               double roughnessCorrection;
+               if (mach < 0.9) {
+                       roughnessCorrection = 1 - 0.1*pow2(mach);
+               } else if (mach > 1.1) {
+                       roughnessCorrection = 1/(1 + 0.18*pow2(mach));
+               } else {
+                       c1 = 1 - 0.1*pow2(0.9);
+                       c2 = 1.0/(1+0.18 * pow2(1.1));
+                       roughnessCorrection = c2 * (mach-0.9)/0.2 + c1 * (1.1-mach)/0.2;
+               }
+               
+//             System.out.printf("Cf=%.3f  ", Cf);
+               
+               
+               /*
+                * Calculate the friction drag coefficient.
+                * 
+                * The body wetted area is summed up and finally corrected with the rocket
+                * fineness ratio (calculated in the same iteration).  The fins are corrected
+                * for thickness as we go on.
+                */
+               
+               double finFriction = 0;
+               double bodyFriction = 0;
+               double maxR=0, len=0;
+               
+               double[] roughnessLimited = new double[Finish.values().length];
+               Arrays.fill(roughnessLimited, Double.NaN);
+               
+               for (RocketComponent c: configuration) {
+
+                       // Consider only SymmetricComponents and FinSets:
+                       if (!(c instanceof SymmetricComponent) &&
+                                       !(c instanceof FinSet))
+                               continue;
+                       
+                       // Calculate the roughness-limited friction coefficient
+                       Finish finish = ((ExternalComponent)c).getFinish();
+                       if (Double.isNaN(roughnessLimited[finish.ordinal()])) {
+                               roughnessLimited[finish.ordinal()] = 
+                                       0.032 * Math.pow(finish.getRoughnessSize()/configuration.getLength(), 0.2) *
+                                       roughnessCorrection;
+                               
+//                             System.out.printf("roughness["+finish+"]=%.3f  ", 
+//                                             roughnessLimited[finish.ordinal()]);
+                       }
+                       
+                       /*
+                        * Actual Cf is maximum of Cf and the roughness-limited value.
+                        * For perfect finish require additionally that Re > 1e6
+                        */
+                       double componentCf;
+                       if (configuration.getRocket().isPerfectFinish()) {
+                               
+                               // For perfect finish require Re > 1e6
+                               if ((Re > 1.0e6) && (roughnessLimited[finish.ordinal()] > Cf)) {
+                                       componentCf = roughnessLimited[finish.ordinal()];
+//                                     System.out.printf("    rl=%f Cf=%f (perfect=%b)\n",
+//                                     roughnessLimited[finish.ordinal()], 
+//                                     Cf,rocket.isPerfectFinish());
+                                       
+//                                     System.out.printf("LIMITED  ");
+                               } else {
+                                       componentCf = Cf;
+//                                     System.out.printf("NORMAL  ");
+                               }
+                               
+                       } else {
+                               
+                               // For fully turbulent use simple max
+                               componentCf = Math.max(Cf, roughnessLimited[finish.ordinal()]);
+
+                       }
+                       
+//                     System.out.printf("compCf=%.3f  ", componentCf);
+                       
+
+                       
+
+                       // Calculate the friction drag:
+                       if (c instanceof SymmetricComponent) {
+                               
+                               SymmetricComponent s = (SymmetricComponent)c;
+                               
+                               bodyFriction += componentCf * s.getComponentWetArea();
+                               
+                               if (map != null) {
+                                       // Corrected later
+                                       map.get(c).frictionCD = componentCf * s.getComponentWetArea()
+                                               / conditions.getRefArea();
+                               }
+                               
+                               double r = Math.max(s.getForeRadius(), s.getAftRadius());
+                               if (r > maxR)
+                                       maxR = r;
+                               len += c.getLength();
+                               
+                       } else if (c instanceof FinSet) {
+                               
+                               FinSet f = (FinSet)c;
+                               double mac = ((FinSetCalc)calcMap.get(c)).getMACLength();
+                               double cd = componentCf * (1 + 2*f.getThickness()/mac) *
+                                       2*f.getFinCount() * f.getFinArea();
+                               finFriction += cd;
+                               
+                               if (map != null) {
+                                       map.get(c).frictionCD = cd / conditions.getRefArea();
+                               }
+                               
+                       }
+                       
+               }
+               // fB may be POSITIVE_INFINITY, but that's ok for us
+               double fB = (len+0.0001) / maxR;
+               double correction = (1 + 1.0/(2*fB));
+               
+               // Correct body data in map
+               if (map != null) {
+                       for (RocketComponent c: map.keySet()) {
+                               if (c instanceof SymmetricComponent) {
+                                       map.get(c).frictionCD *= correction;
+                               }
+                       }
+               }
+
+//             System.out.printf("\n");
+               return (finFriction + correction*bodyFriction) / conditions.getRefArea();
+       }
+       
+       
+       
+       private double calculatePressureDrag(FlightConditions conditions,
+                       Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) {
+               
+               double stagnation, base, total;
+               double radius = 0;
+               
+               if (calcMap == null)
+                       buildCalcMap();
+               
+               stagnation = calculateStagnationCD(conditions.getMach());
+               base = calculateBaseCD(conditions.getMach());
+               
+               total = 0;
+               for (RocketComponent c: configuration) {
+                       if (!c.isAerodynamic())
+                               continue;
+                       
+                       // Pressure fore drag
+                       double cd = calcMap.get(c).calculatePressureDragForce(conditions, stagnation, base, 
+                                       warnings);
+                       total += cd;
+                       
+                       if (map != null) {
+                               map.get(c).pressureCD = cd;
+                       }
+                       
+                       
+                       // Stagnation drag
+                       if (c instanceof SymmetricComponent) {
+                               SymmetricComponent s = (SymmetricComponent)c;
+
+                               if (radius < s.getForeRadius()) {
+                                       double area = Math.PI*(pow2(s.getForeRadius()) - pow2(radius));
+                                       cd = stagnation * area / conditions.getRefArea();
+                                       total += cd;
+                                       if (map != null) {
+                                               map.get(c).pressureCD += cd;
+                                       }
+                               }
+
+                               radius = s.getAftRadius();
+                       }
+               }
+
+               return total;
+       }
+       
+       
+       private double calculateBaseDrag(FlightConditions conditions,
+                       Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) {
+               
+               double base, total;
+               double radius = 0;
+               RocketComponent prevComponent = null;
+               
+               if (calcMap == null)
+                       buildCalcMap();
+               
+               base = calculateBaseCD(conditions.getMach());
+               total = 0;
+
+               for (RocketComponent c: configuration) {
+                       if (!(c instanceof SymmetricComponent))
+                               continue;
+
+                       SymmetricComponent s = (SymmetricComponent)c;
+
+                       if (radius > s.getForeRadius()) {
+                               double area = Math.PI*(pow2(radius) - pow2(s.getForeRadius()));
+                               double cd = base * area / conditions.getRefArea();
+                               total += cd;
+                               if (map != null) {
+                                       map.get(prevComponent).baseCD = cd;
+                               }
+                       }
+
+                       radius = s.getAftRadius();
+                       prevComponent = c;
+               }
+               
+               if (radius > 0) {
+                       double area = Math.PI*pow2(radius);
+                       double cd = base * area / conditions.getRefArea();
+                       total += cd;
+                       if (map != null) {
+                               map.get(prevComponent).baseCD = cd;
+                       }
+               }
+
+               return total;
+       }
+       
+       
+       
+       public static double calculateStagnationCD(double m) {
+               double pressure;
+               if (m <=1) {
+                       pressure = 1 + pow2(m)/4 + pow2(pow2(m))/40;
+               } else {
+                       pressure = 1.84 - 0.76/pow2(m) + 0.166/pow2(pow2(m)) + 0.035/pow2(m*m*m);
+               }
+               return 0.85 * pressure;
+       }
+       
+       
+       public static double calculateBaseCD(double m) {
+               if (m <= 1) {
+                       return 0.12 + 0.13 * m*m;
+               } else {
+                       return 0.25 / m;
+               }
+       }
+       
+       
+       
+       private static final double[] axialDragPoly1, axialDragPoly2;
+       static {
+               PolyInterpolator interpolator;
+               interpolator = new PolyInterpolator(
+                               new double[] { 0, 17*Math.PI/180 },
+                               new double[] { 0, 17*Math.PI/180 }
+               );
+               axialDragPoly1 = interpolator.interpolator(1, 1.3, 0, 0);
+               
+               interpolator = new PolyInterpolator(
+                               new double[] { 17*Math.PI/180, Math.PI/2 },
+                               new double[] { 17*Math.PI/180, Math.PI/2 },
+                               new double[] { Math.PI/2 }
+               );
+               axialDragPoly2 = interpolator.interpolator(1.3, 0, 0, 0, 0);
+       }
+       
+       
+       /**
+        * Calculate the axial drag from the total drag coefficient.
+        * 
+        * @param conditions
+        * @param cd
+        * @return
+        */
+       private double calculateAxialDrag(FlightConditions conditions, double cd) {
+               double aoa = MathUtil.clamp(conditions.getAOA(), 0, Math.PI);
+               double mul;
+               
+//             double sinaoa = conditions.getSinAOA();
+//             return cd * (1 + Math.min(sinaoa, 0.25));
+
+               
+               if (aoa > Math.PI/2)
+                       aoa = Math.PI - aoa;
+               if (aoa < 17*Math.PI/180)
+                       mul = PolyInterpolator.eval(aoa, axialDragPoly1);
+               else
+                       mul = PolyInterpolator.eval(aoa, axialDragPoly2);
+                       
+               if (conditions.getAOA() < Math.PI/2)
+                       return mul * cd;
+               else
+                       return -mul * cd;
+       }
+       
+       
+       private void calculateDampingMoments(FlightConditions conditions, 
+                       AerodynamicForces total) {
+               
+               // Calculate pitch and yaw damping moments
+               if (conditions.getPitchRate() > 0.1 || conditions.getYawRate() > 0.1 || true) {
+                       double mul = getDampingMultiplier(conditions, total.cg.x);
+                       double pitch = conditions.getPitchRate();
+                       double yaw = conditions.getYawRate();
+                       double vel = conditions.getVelocity();
+                       
+//                     double Cm = total.Cm - total.CN * total.cg.x / conditions.getRefLength();
+//                     System.out.printf("Damping pitch/yaw, mul=%.4f pitch rate=%.4f "+
+//                                     "Cm=%.4f / %.4f effect=%.4f aoa=%.4f\n", mul, pitch, total.Cm, Cm, 
+//                                     -(mul * MathUtil.sign(pitch) * pow2(pitch/vel)), 
+//                                     conditions.getAOA()*180/Math.PI);
+                       
+                       mul *= 3;   // TODO: Higher damping yields much more realistic apogee turn
+
+//                     total.Cm -= mul * pitch / pow2(vel);
+//                     total.Cyaw -= mul * yaw / pow2(vel);
+                       total.pitchDampingMoment = mul * MathUtil.sign(pitch) * pow2(pitch/vel);
+                       total.yawDampingMoment = mul * MathUtil.sign(yaw) * pow2(yaw/vel);
+               } else {
+                       total.pitchDampingMoment = 0;
+                       total.yawDampingMoment = 0;
+               }
+
+       }
+       
+       // TODO: MEDIUM: Are the rotation etc. being added correctly?  sin/cos theta?
+
+       
+       private double getDampingMultiplier(FlightConditions conditions, double cgx) {
+               if (cacheDiameter < 0) {
+                       double area = 0;
+                       cacheLength = 0;
+                       cacheDiameter = 0;
+                       
+                       for (RocketComponent c: configuration) {
+                               if (c instanceof SymmetricComponent) {
+                                       SymmetricComponent s = (SymmetricComponent)c;
+                                       area += s.getComponentPlanformArea();
+                                       cacheLength += s.getLength();
+                               }
+                       }
+                       if (cacheLength > 0)
+                               cacheDiameter = area / cacheLength;
+               }
+               
+               double mul;
+               
+               // Body
+               mul = 0.275 * cacheDiameter / (conditions.getRefArea() * conditions.getRefLength());
+               mul *= (MathUtil.pow4(cgx) + MathUtil.pow4(cacheLength - cgx));
+               
+               // Fins
+               // TODO: LOW: This could be optimized a lot...
+               for (RocketComponent c: configuration) {
+                       if (c instanceof FinSet) {
+                               FinSet f = (FinSet)c;
+                               mul += 0.6 * Math.min(f.getFinCount(), 4) * f.getFinArea() * 
+                                               MathUtil.pow3(Math.abs(f.toAbsolute(new Coordinate(
+                                                                               ((FinSetCalc)calcMap.get(f)).getMidchordPos()))[0].x
+                                                                               - cgx)) /
+                                                                               (conditions.getRefArea() * conditions.getRefLength());
+                       }
+               }
+               
+               return mul;
+       }
+
+       
+       
+       ////////  The calculator map
+       
+       @Override
+       protected void voidAerodynamicCache() {
+               super.voidAerodynamicCache();
+               
+               calcMap = null;
+               cacheDiameter = -1;
+               cacheLength = -1;
+       }
+       
+       
+       private void buildCalcMap() {
+               Iterator<RocketComponent> iterator;
+               
+               calcMap = new HashMap<RocketComponent, RocketComponentCalc>();
+
+               iterator = rocket.deepIterator();
+               while (iterator.hasNext()) {
+                       RocketComponent c = iterator.next();
+
+                       if (!c.isAerodynamic())
+                               continue;
+                       
+                       calcMap.put(c, (RocketComponentCalc) Reflection.construct(BARROWMAN_PACKAGE, 
+                                       c, BARROWMAN_SUFFIX, c));
+               }
+       }
+       
+       
+       
+       
+       public static void main(String[] arg) {
+               
+               PolyInterpolator interpolator;
+               
+               interpolator = new PolyInterpolator(
+                               new double[] { 0, 17*Math.PI/180 },
+                               new double[] { 0, 17*Math.PI/180 }
+               );
+               double[] poly1 = interpolator.interpolator(1, 1.3, 0, 0);
+               
+               interpolator = new PolyInterpolator(
+                               new double[] { 17*Math.PI/180, Math.PI/2 },
+                               new double[] { 17*Math.PI/180, Math.PI/2 },
+                               new double[] { Math.PI/2 }
+               );
+               double[] poly2 = interpolator.interpolator(1.3, 0, 0, 0, 0);
+                               
+               
+               for (double a=0; a<=180.1; a++) {
+                       double r = a*Math.PI/180;
+                       if (r > Math.PI/2)
+                               r = Math.PI - r;
+                       
+                       double value;
+                       if (r < 18*Math.PI/180)
+                               value = PolyInterpolator.eval(r, poly1);
+                       else
+                               value = PolyInterpolator.eval(r, poly2);
+                       
+                       System.out.println(""+a+" "+value);
+               }
+               
+               System.exit(0);
+               
+               
+               Rocket normal = Test.makeRocket();
+               Rocket perfect = Test.makeRocket();
+               normal.setPerfectFinish(false);
+               perfect.setPerfectFinish(true);
+               
+               Configuration confNormal = new Configuration(normal);
+               Configuration confPerfect = new Configuration(perfect);
+               
+               for (RocketComponent c: confNormal) {
+                       if (c instanceof ExternalComponent) {
+                               ((ExternalComponent)c).setFinish(Finish.NORMAL);
+                       }
+               }
+               for (RocketComponent c: confPerfect) {
+                       if (c instanceof ExternalComponent) {
+                               ((ExternalComponent)c).setFinish(Finish.NORMAL);
+                       }
+               }
+               
+               
+               confNormal.setToStage(0);
+               confPerfect.setToStage(0);
+               
+               
+               
+               BarrowmanCalculator calcNormal = new BarrowmanCalculator(confNormal);
+               BarrowmanCalculator calcPerfect = new BarrowmanCalculator(confPerfect);
+               
+               FlightConditions conditions = new FlightConditions(confNormal);
+               
+               for (double mach=0; mach < 3; mach += 0.1) {
+                       conditions.setMach(mach);
+
+                       Map<RocketComponent, AerodynamicForces> data = 
+                               calcNormal.getForceAnalysis(conditions, null);
+                       AerodynamicForces forcesNormal = data.get(normal);
+                       
+                       data = calcPerfect.getForceAnalysis(conditions, null);
+                       AerodynamicForces forcesPerfect = data.get(perfect);
+                       
+                       System.out.printf("%f %f %f %f %f %f %f\n",mach, 
+                                       forcesNormal.pressureCD, forcesPerfect.pressureCD, 
+                                       forcesNormal.frictionCD, forcesPerfect.frictionCD,
+                                       forcesNormal.CD, forcesPerfect.CD);
+               }
+               
+               
+               
+       }
+
+}
diff --git a/src/net/sf/openrocket/aerodynamics/ConeDragTest.java b/src/net/sf/openrocket/aerodynamics/ConeDragTest.java
new file mode 100644 (file)
index 0000000..b2d2c03
--- /dev/null
@@ -0,0 +1,103 @@
+package net.sf.openrocket.aerodynamics;
+
+import net.sf.openrocket.util.PolyInterpolator;
+
+public class ConeDragTest {
+
+       private static final double DELTA = 0.01;
+       private static final double SUBSONIC = 0.0;
+       private static final double SUPERSONIC = 1.3;
+       
+       
+       private static final PolyInterpolator polyInt2 = new PolyInterpolator(
+                       new double[] {1.0, SUPERSONIC}
+                       ,new double[] {1.0, SUPERSONIC}
+                       ,new double[] {SUPERSONIC}
+       );
+       
+       private final double angle;
+       private final double sin;
+       private final double ratio;
+       
+       private final double[] int2;
+
+       // Coefficients for subsonic interpolation  a * M^b + c
+       private final double a, b, c;
+       
+       
+       
+       public ConeDragTest(double angle) {
+               this.angle = angle;
+               this.sin = Math.sin(angle);
+               this.ratio = 1.0 / (2*Math.tan(angle));
+               
+               double dsuper = (supersonic(SUPERSONIC+DELTA) - supersonic(SUPERSONIC))/DELTA;
+               
+
+               c = subsonic(0);
+               a = sonic() - c;
+               b = sonicDerivative() / a;
+               
+               
+               int2 = polyInt2.interpolator(
+                               sonic(), supersonic(SUPERSONIC)
+                               , sonicDerivative(), dsuper
+                               ,0
+               );
+               
+               System.err.println("At mach1: CD="+sin+" dCD/dM="+(4.0/2.4*(1-0.5*sin)));
+               
+       }
+       
+       private double subsonic(double m) {
+               return 0.8*sin*sin/Math.sqrt(1-m*m);
+       }
+       
+       private double sonic() {
+               return sin;
+       }
+       
+       private double sonicDerivative() {
+               return 4.0/2.4*(1-0.5*sin);
+       }
+       
+       private double supersonic(double m) {
+               return 2.1 * sin*sin + 0.5*sin/Math.sqrt(m*m-1);
+       }
+       
+       
+       
+       
+       public double getCD(double m) {
+               if (m >= SUPERSONIC)
+                       return supersonic(m);
+
+               if (m <= 1.0)
+                       return a * Math.pow(m,b) + c;
+               return PolyInterpolator.eval(m, int2);
+                       
+//             return PolyInterpolator.eval(m, interpolator);
+       }
+       
+       
+       
+       public static void main(String[] arg) {
+               
+               ConeDragTest cone10 = new ConeDragTest(10.0*Math.PI/180);
+               ConeDragTest cone20 = new ConeDragTest(20.0*Math.PI/180);
+               ConeDragTest cone30 = new ConeDragTest(30.0*Math.PI/180);
+               
+               ConeDragTest coneX = new ConeDragTest(5.0*Math.PI/180);
+
+               for (double m=0; m < 4.0001; m+=0.02) {
+                       System.out.println(m + " " 
+                                       + cone10.getCD(m) + " "
+                                       + cone20.getCD(m) + " "
+                                       + cone30.getCD(m) + " "
+//                                     + coneX.getCD(m)
+                       );
+               }
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/aerodynamics/ExactAtmosphericConditions.java b/src/net/sf/openrocket/aerodynamics/ExactAtmosphericConditions.java
new file mode 100644 (file)
index 0000000..3da8cf8
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.aerodynamics;
+
+/**
+ * A class containing more accurate methods for computing the atmospheric properties.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ExactAtmosphericConditions extends AtmosphericConditions {
+
+       @Override
+       public double getDensity() {
+               // TODO Auto-generated method stub
+               return super.getDensity();
+       }
+
+       @Override
+       public double getKinematicViscosity() {
+               // TODO Auto-generated method stub
+               return super.getKinematicViscosity();
+       }
+
+       @Override
+       public double getMachSpeed() {
+               return 331.3 * Math.sqrt(1 + (temperature - 273.15)/273.15);
+       }
+
+}
diff --git a/src/net/sf/openrocket/aerodynamics/ExtendedISAModel.java b/src/net/sf/openrocket/aerodynamics/ExtendedISAModel.java
new file mode 100644 (file)
index 0000000..fa8c8a5
--- /dev/null
@@ -0,0 +1,123 @@
+package net.sf.openrocket.aerodynamics;
+
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.R;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * An atmospheric temperature/pressure model based on the International Standard Atmosphere
+ * (ISA).  The no-argument constructor creates an {@link AtmosphericModel} that corresponds
+ * to the ISA model.  It is extended by the other constructors to allow defining a custom
+ * first layer.  The base temperature and pressure are as given, and all other values
+ * are calculated based on these.
+ * <p>
+ * TODO:  LOW:  Values at altitudes over 32km differ from standard results by ~5%.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ExtendedISAModel extends AtmosphericModel {
+       
+       public static final double STANDARD_TEMPERATURE = 288.15;
+       public static final double STANDARD_PRESSURE  = 101325;
+       
+       private static final double G = 9.80665;
+
+       private final double[] layer = {0, 11000, 20000, 32000, 47000, 51000, 71000, 84852};
+       private final double[] baseTemperature = {
+                       288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.95  
+       };
+       private final double[] basePressure = new double[layer.length]; 
+       
+
+       /**
+        * Construct the standard ISA model.
+        */
+       public ExtendedISAModel() {
+               this(STANDARD_TEMPERATURE, STANDARD_PRESSURE);
+       }
+       
+       /**
+        * Construct an extended model with the given temperature and pressure at MSL.
+        * 
+        * @param temperature   the temperature at MSL.
+        * @param pressure              the pressure at MSL.
+        */
+       public ExtendedISAModel(double temperature, double pressure) {
+               this(0, temperature, pressure);
+       }
+       
+       
+       /**
+        * Construct an extended model with the given temperature and pressure at the
+        * specified altitude.  Conditions below the given altitude cannot be calculated,
+        * and the values at the specified altitude will be returned instead.  The altitude
+        * must be lower than the altitude of the next ISA standard layer (below 11km).
+        * 
+        * @param altitude              the altitude of the measurements.
+        * @param temperature   the temperature.
+        * @param pressure              the pressure.
+        * @throws IllegalArgumentException  if the altitude exceeds the second layer boundary
+        *                                                                       of the ISA model (over 11km).
+        */
+       public ExtendedISAModel(double altitude, double temperature, double pressure) {
+               if (altitude >= layer[1]) {
+                       throw new IllegalArgumentException("Too high first altitude: "+altitude);
+               }
+               
+               layer[0] = altitude;
+               baseTemperature[0] = temperature;
+               basePressure[0] = pressure;
+               
+               for (int i=1; i < basePressure.length; i++) {
+                       basePressure[i] = getExactConditions(layer[i]-1).pressure;
+               }
+       }
+       
+       
+       @Override
+       public AtmosphericConditions getExactConditions(double altitude) {
+               altitude = MathUtil.clamp(altitude, layer[0], layer[layer.length-1]);
+               int n;
+               for (n=0; n < layer.length-1; n++) {
+                       if (layer[n+1] > altitude)
+                               break;
+               }
+
+               double rate = (baseTemperature[n+1] - baseTemperature[n]) / (layer[n+1] - layer[n]);
+               
+               double t = baseTemperature[n] + (altitude - layer[n]) * rate;
+               double p;
+               if (Math.abs(rate) > 0.001) {
+                       p = basePressure[n] *
+                               Math.pow(1 + (altitude-layer[n])*rate/baseTemperature[n], -G/(rate*R));
+               } else {
+                       p = basePressure[n] *
+                               Math.exp(-(altitude-layer[n])*G/(R*baseTemperature[n]));
+               }
+               
+               return new AtmosphericConditions(t,p);
+       }
+
+       @Override
+       public double getMaxAltitude() {
+               return layer[layer.length-1];
+       }
+       
+       
+       public static void main(String foo[]) {
+               ExtendedISAModel model1 = new ExtendedISAModel();
+               ExtendedISAModel model2 = new ExtendedISAModel(278.15,100000);
+               
+               for (double alt=0; alt < 80000; alt += 500) {
+                       AtmosphericConditions cond1 = model1.getConditions(alt);
+                       AtmosphericConditions cond2 = model2.getConditions(alt);
+                       
+                       AtmosphericConditions diff = new AtmosphericConditions();
+                       diff.pressure = (cond2.pressure - cond1.pressure)/cond1.pressure*100;
+                       diff.temperature = (cond2.temperature - cond1.temperature)/cond1.temperature*100;
+                       System.out.println("alt=" + alt + 
+                                       ": std:" + cond1 + " mod:" + cond2 + " diff:" + diff); 
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/aerodynamics/FlightConditions.java b/src/net/sf/openrocket/aerodynamics/FlightConditions.java
new file mode 100644 (file)
index 0000000..58d964e
--- /dev/null
@@ -0,0 +1,395 @@
+package net.sf.openrocket.aerodynamics;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class FlightConditions implements Cloneable, ChangeSource {
+
+       private List<ChangeListener> listenerList = new ArrayList<ChangeListener>();
+       private ChangeEvent event = new ChangeEvent(this);
+       
+       /** Modification count */
+       private int modCount = 0;
+       
+       /** Reference length used in calculations. */
+       private double refLength = 1.0;
+       
+       /** Reference area used in calculations. */
+       private double refArea = Math.PI * 0.25;
+
+       
+       /** Angle of attack. */
+       private double aoa = 0;
+       
+       /** Sine of the angle of attack. */
+       private double sinAOA = 0;
+       
+       /** 
+        * The fraction <code>sin(aoa) / aoa</code>.  At an AOA of zero this value 
+        * must be one.  This value may be used in many cases to avoid checking for
+        * division by zero. 
+        */
+       private double sincAOA = 1.0;
+       
+       /** Lateral wind direction. */
+       private double theta = 0;
+       
+       /** Current Mach speed. */
+       private double mach = 0.3;
+       
+       /**
+        * Sqrt(1 - M^2)  for M<1
+        * Sqrt(M^2 - 1)  for M>1
+        */
+       private double beta = Math.sqrt(1 - mach*mach);
+
+       
+       /** Current roll rate. */
+       private double rollRate = 0;
+       
+       private double pitchRate = 0;
+       private double yawRate = 0;
+       
+       
+       private AtmosphericConditions atmosphericConditions = new AtmosphericConditions();
+       
+       
+       
+       /**
+        * Sole constructor.  The reference length is initialized to the reference length
+        * of the <code>Configuration</code>, and the reference area accordingly.
+        * If <code>config</code> is <code>null</code>, then the reference length is set
+        * to 1 meter.
+        * 
+        * @param config   the configuration of which the reference length is taken.
+        */
+       public FlightConditions(Configuration config) {
+               if (config != null)
+                       setRefLength(config.getReferenceLength());
+       }
+       
+       
+       /**
+        * Set the reference length from the given configuration.
+        * @param config        the configuration from which to get the reference length.
+        */
+       public void setReference(Configuration config) {
+               setRefLength(config.getReferenceLength());
+       }
+       
+       
+       /**
+        * Set the reference length and area.
+        */
+       public void setRefLength(double length) {
+               refLength = length;
+
+               refArea = Math.PI * MathUtil.pow2(length/2);
+               fireChangeEvent();
+       }
+
+       /**
+        * Return the reference length.
+        */
+       public double getRefLength() {
+               return refLength;
+       }
+
+       /**
+        * Set the reference area and length.
+        */
+       public void setRefArea(double area) {
+               refArea = area;
+               refLength = Math.sqrt(area / Math.PI)*2;
+               fireChangeEvent();
+       }
+       
+       /**
+        * Return the reference area.
+        */
+       public double getRefArea() {
+               return refArea;
+       }
+
+       
+       /**
+        * Sets the angle of attack.  It calculates values also for the methods 
+        * {@link #getSinAOA()} and {@link #getSincAOA()}. 
+        * 
+        * @param aoa   the angle of attack.
+        */
+       public void setAOA(double aoa) {
+               aoa = MathUtil.clamp(aoa, 0, Math.PI);
+               if (MathUtil.equals(this.aoa, aoa))
+                       return;
+               
+               this.aoa = aoa;
+               if (aoa < 0.001) {
+                       this.sinAOA = aoa;
+                       this.sincAOA = 1.0;
+               } else {
+                       this.sinAOA = Math.sin(aoa);
+                       this.sincAOA = sinAOA / aoa;
+               }
+               fireChangeEvent();
+       }
+
+       
+       /**
+        * Sets the angle of attack with the sine.  The value <code>sinAOA</code> is assumed
+        * to be the sine of <code>aoa</code> for cases in which this value is known.
+        * The AOA must still be specified, as the sine is not unique in the range
+        * of 0..180 degrees.
+        * 
+        * @param aoa           the angle of attack in radians.
+        * @param sinAOA        the sine of the angle of attack.
+        */
+       public void setAOA(double aoa, double sinAOA) {
+               aoa = MathUtil.clamp(aoa, 0, Math.PI);
+               sinAOA = MathUtil.clamp(sinAOA, 0, 1);
+               if (MathUtil.equals(this.aoa, aoa))
+                       return;
+               
+               assert(Math.abs(Math.sin(aoa) - sinAOA) < 0.0001) : 
+                       "Illegal sine: aoa="+aoa+" sinAOA="+sinAOA;
+               
+               this.aoa = aoa;
+               this.sinAOA = sinAOA;
+               if (aoa < 0.001) {
+                       this.sincAOA = 1.0;
+               } else {
+                       this.sincAOA = sinAOA / aoa;
+               }
+               fireChangeEvent();
+       }
+
+
+       /**
+        * Return the angle of attack.
+        */
+       public double getAOA() {
+               return aoa;
+       }
+
+       /**
+        * Return the sine of the angle of attack.
+        */
+       public double getSinAOA() {
+               return sinAOA;
+       }
+
+       /**
+        * Return the sinc of the angle of attack (sin(AOA) / AOA).  This method returns
+        * one if the angle of attack is zero.
+        */
+       public double getSincAOA() {
+               return sincAOA;
+       }
+       
+       
+       /**
+        * Set the direction of the lateral airflow.
+        */
+       public void setTheta(double theta) {
+               if (MathUtil.equals(this.theta, theta))
+                       return;
+               this.theta = theta;
+               fireChangeEvent();
+       }
+
+       /**
+        * Return the direction of the lateral airflow.
+        */
+       public double getTheta() {
+               return theta;
+       }
+       
+
+       /**
+        * Set the current Mach speed.  This should be (but is not required to be) in 
+        * reference to the speed of sound of the atmospheric conditions.
+        */
+       public void setMach(double mach) {
+               mach = Math.max(mach, 0);
+               if (MathUtil.equals(this.mach, mach))
+                       return;
+               
+               this.mach = mach;
+               if (mach < 1)
+                       this.beta = Math.sqrt(1 - mach*mach);
+               else
+                       this.beta = Math.sqrt(mach*mach - 1);
+               fireChangeEvent();
+       }
+       
+       /**
+        * Return the current Mach speed.
+        */
+       public double getMach() {
+               return mach;
+       }
+       
+       /**
+        * Returns the current rocket velocity, calculated from the Mach number and the
+        * speed of sound.  If either of these parameters are changed, the velocity changes
+        * as well.
+        * 
+        * @return  the velocity of the rocket.
+        */
+       public double getVelocity() {
+               return mach * atmosphericConditions.getMachSpeed();
+       }
+       
+       /**
+        * Sets the Mach speed according to the given velocity and the current speed of sound.
+        * 
+        * @param velocity      the current velocity.
+        */
+       public void setVelocity(double velocity) {
+               setMach(velocity / atmosphericConditions.getMachSpeed());
+       }
+       
+
+       /**
+        * Return sqrt(abs(1 - Mach^2)).  This is calculated in the setting call and is
+        * therefore fast.
+        */
+       public double getBeta() {
+               return beta;
+       }
+       
+       
+       /**
+        * Return the current roll rate.
+        */
+       public double getRollRate() {
+               return rollRate;
+       }
+       
+       
+       /**
+        * Set the current roll rate.
+        */
+       public void setRollRate(double rate) {
+               if (MathUtil.equals(this.rollRate, rate))
+                       return;
+               
+               this.rollRate = rate;
+               fireChangeEvent();
+       }
+
+       
+       public double getPitchRate() {
+               return pitchRate;
+       }
+
+
+       public void setPitchRate(double pitchRate) {
+               if (MathUtil.equals(this.pitchRate, pitchRate))
+                       return;
+               this.pitchRate = pitchRate;
+               fireChangeEvent();
+       }
+
+
+       public double getYawRate() {
+               return yawRate;
+       }
+
+
+       public void setYawRate(double yawRate) {
+               if (MathUtil.equals(this.yawRate, yawRate))
+                       return;
+               this.yawRate = yawRate;
+               fireChangeEvent();
+       }
+
+
+       /**
+        * Return the current atmospheric conditions.  Note that this method returns a
+        * reference to the {@link AtmosphericConditions} object used by this object.
+        * Changes made to the object will modify the encapsulated object, but will NOT
+        * generate change events.
+        * 
+        * @return              the current atmospheric conditions.
+        */
+       public AtmosphericConditions getAtmosphericConditions() {
+               return atmosphericConditions;
+       }
+
+       /**
+        * Set the current atmospheric conditions.  This method will fire a change event
+        * if a change occurs.
+        */
+       public void setAtmosphericConditions(AtmosphericConditions cond) {
+               if (atmosphericConditions == cond)
+                       return;
+               atmosphericConditions = cond;
+               fireChangeEvent();
+       }
+       
+       
+       /**
+        * Retrieve the modification count of this object.  Each time it is modified
+        * the modification count is increased by one.
+        * 
+        * @return      the number of times this object has been modified since instantiation.
+        */
+       public int getModCount() {
+               return modCount;
+       }
+       
+       
+       @Override
+       public String toString() {
+               return String.format("FlightConditions[aoa=%.2f\u00b0,theta=%.2f\u00b0,"+
+                               "mach=%.2f,rollRate=%.2f]", 
+                               aoa*180/Math.PI, theta*180/Math.PI, mach, rollRate);
+       }
+       
+       
+       /**
+        * Return a copy of the flight conditions.  The copy has no listeners.  The
+        * atmospheric conditions is also cloned.
+        */
+       @Override
+       public FlightConditions clone() {
+               try {
+                       FlightConditions cond = (FlightConditions) super.clone();
+                       cond.listenerList = new ArrayList<ChangeListener>();
+                       cond.event = new ChangeEvent(cond);
+                       cond.atmosphericConditions = atmosphericConditions.clone();
+                       return cond;
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("BUG: clone not supported!",e);
+               }
+       }
+
+       
+       
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listenerList.add(0,listener);
+       }
+
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listenerList.remove(listener);
+       }
+       
+       protected void fireChangeEvent() {
+               modCount++;
+               ChangeListener[] listeners = listenerList.toArray(new ChangeListener[0]);
+               for (ChangeListener l: listeners) {
+                       l.stateChanged(event);
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/aerodynamics/GravityModel.java b/src/net/sf/openrocket/aerodynamics/GravityModel.java
new file mode 100644 (file)
index 0000000..1bc10c6
--- /dev/null
@@ -0,0 +1,28 @@
+package net.sf.openrocket.aerodynamics;
+
+/**
+ * A gravity model based on the International Gravity Formula of 1967.  The gravity
+ * value is computed when the object is constructed and later returned as a static
+ * value.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class GravityModel {
+       
+       private final double g;
+       
+       /**
+        * Construct the static gravity model at the specific latitude (in degrees).
+        * @param latitude      the latitude in degrees (-90 ... 90)
+        */
+       public GravityModel(double latitude) {
+               double sin = Math.sin(latitude * Math.PI/180);
+               double sin2 = Math.sin(2 * latitude * Math.PI/180);
+               g = 9.780327 * (1 + 0.0053024 * sin - 0.0000058 * sin2);
+       }
+
+       public double getGravity() {
+               return g;
+       }
+
+}
diff --git a/src/net/sf/openrocket/aerodynamics/Warning.java b/src/net/sf/openrocket/aerodynamics/Warning.java
new file mode 100644 (file)
index 0000000..7981d37
--- /dev/null
@@ -0,0 +1,154 @@
+package net.sf.openrocket.aerodynamics;
+
+import net.sf.openrocket.unit.UnitGroup;
+
+public abstract class Warning {
+       
+       
+       /**
+        * Return a Warning with the specific text.
+        */
+       public static Warning fromString(String text) {
+               return new Warning.Other(text);
+       }
+       
+       
+       /**
+        * Return <code>true</code> if the <code>other</code> warning should replace
+        * this warning.  The method should return <code>true</code> if the other
+        * warning indicates a "worse" condition than the current warning.
+        * 
+        * @param other  the warning to compare to
+        * @return       whether this warning should be replaced
+        */
+       public abstract boolean replaceBy(Warning other);
+       
+       
+       /**
+        * Two <code>Warning</code>s are by default considered equal if they are of
+        * the same class.  Therefore only one instance of a particular warning type 
+        * is stored in a {@link WarningSet}.  Subclasses may override this method for
+        * more specific functionality.
+        */
+       @Override
+       public boolean equals(Object o) {
+               return (o.getClass() == this.getClass());
+       }
+       
+       /**
+        * A <code>hashCode</code> method compatible with the <code>equals</code> method.
+        */
+       @Override
+       public int hashCode() {
+               return this.getClass().hashCode();
+       }
+       
+       
+       
+       
+       /////////////  Specific warning classes  /////////////
+       
+       
+       /**
+        * A <code>Warning</code> indicating a large angle of attack was encountered.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       public static class LargeAOA extends Warning {
+               private double aoa;
+               
+               /**
+                * Sole constructor.  The argument is the AOA that caused this warning.
+                * 
+                * @param aoa  the angle of attack that caused this warning
+                */
+               public LargeAOA(double aoa) {
+                       this.aoa = aoa;
+               }
+               
+               @Override
+               public String toString() {
+                       if (Double.isNaN(aoa))
+                               return "Large angle of attack encountered.";
+                       return ("Large angle of attack encountered (" +
+                                       UnitGroup.UNITS_ANGLE.getDefaultUnit().toString(aoa) + ").");
+               }
+
+               @Override
+               public boolean replaceBy(Warning other) {
+                       if (!(other instanceof LargeAOA))
+                               return false;
+                       
+                       LargeAOA o = (LargeAOA)other;
+                       if (Double.isNaN(this.aoa))   // If this has value NaN then replace
+                               return true;
+                       return (o.aoa > this.aoa);
+               }
+       }
+       
+       
+       
+       /**
+        * An unspecified warning type.  This warning type holds a <code>String</code>
+        * describing it.  Two warnings of this type are considered equal if the strings
+        * are identical.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       public static class Other extends Warning {
+               private String description;
+               
+               public Other(String description) {
+                       this.description = description;
+               }
+               
+               @Override
+               public String toString() {
+                       return description;
+               }
+               
+               @Override
+               public boolean equals(Object other) {
+                       if (!(other instanceof Other))
+                               return false;
+                       
+                       Other o = (Other)other;
+                       return (o.description.equals(this.description));
+               }
+               
+               @Override
+               public int hashCode() {
+                       return description.hashCode();
+               }
+
+               @Override
+               public boolean replaceBy(Warning other) {
+                       return false;
+               }
+       }
+       
+       
+       /** A <code>Warning</code> that the body diameter is discontinuous. */
+       public static final Warning DISCONTINUITY = 
+               new Other("Discontinuity in rocket body diameter.");
+       
+       /** A <code>Warning</code> that the fins are thick compared to the rocket body. */
+       public static final Warning THICK_FIN =
+               new Other("Thick fins may not be modeled accurately.");
+       
+       /** A <code>Warning</code> that the fins have jagged edges. */
+       public static final Warning JAGGED_EDGED_FIN =
+               new Other("Jagged-edged fin predictions may be inaccurate.");
+       
+       /** A <code>Warning</code> that simulation listeners have affected the simulation */
+       public static final Warning LISTENERS_AFFECTED =
+               new Other("Listeners modified the flight simulation");
+       
+       public static final Warning RECOVERY_DEPLOYMENT_WHILE_BURNING =
+               new Other("Recovery device opened while motor still burning.");
+       
+       
+       
+       public static final Warning FILE_INVALID_PARAMETER =
+               new Other("Invalid parameter encountered, ignoring.");
+}
diff --git a/src/net/sf/openrocket/aerodynamics/WarningSet.java b/src/net/sf/openrocket/aerodynamics/WarningSet.java
new file mode 100644 (file)
index 0000000..32355bc
--- /dev/null
@@ -0,0 +1,91 @@
+package net.sf.openrocket.aerodynamics;
+
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * A set that contains multiple <code>Warning</code>s.  When adding a
+ * {@link Warning} to this set, the contents is checked for a warning of the
+ * same type.  If one is found, then the warning left in the set is determined
+ * by the method {@link #Warning.replaceBy(Warning)}.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class WarningSet extends AbstractSet<Warning> implements Cloneable {
+
+       private ArrayList<Warning> warnings = new ArrayList<Warning>();
+       
+       
+       /**
+        * Add a <code>Warning</code> to the set.  If a warning of the same type
+        * exists in the set, the warning that is left in the set is defined by the
+        * method {@link Warning#replaceBy(Warning)}.
+        */
+       @Override
+       public boolean add(Warning w) {
+               int index = warnings.indexOf(w);
+               
+               if (index < 0) {
+                       warnings.add(w);
+                       return false;
+               }
+               
+               Warning old = warnings.get(index);
+               if (old.replaceBy(w)) {
+                       warnings.set(index, w);
+               }
+               
+               return true;
+       }
+       
+       /**
+        * Add a <code>Warning</code> with the specified text to the set.  The Warning object
+        * is created using the {@link Warning#fromString(String)} method.  If a warning of the
+        * same type exists in the set, the warning that is left in the set is defined by the
+        * method {@link Warning#replaceBy(Warning)}.
+        * 
+        * @param s             the warning text.
+        */
+       public boolean add(String s) {
+               return add(Warning.fromString(s));
+       }
+       
+       
+       @Override
+       public Iterator<Warning> iterator() {
+               return warnings.iterator();
+       }
+
+       @Override
+       public int size() {
+               return warnings.size();
+       }
+       
+       @SuppressWarnings("unchecked")
+       @Override
+       public WarningSet clone() {
+               try {
+                       
+                       WarningSet newSet = (WarningSet) super.clone();
+                       newSet.warnings = (ArrayList<Warning>) this.warnings.clone();
+                       return newSet;
+                       
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("CloneNotSupportedException occurred, report bug!",e);
+               }
+       }
+       
+       
+       @Override
+       public String toString() {
+               String s = "";
+               
+               for (Warning w: warnings) {
+                       if (s.length() > 0)
+                               s = s+",";
+                       s += w.toString();
+               }
+               return "WarningSet[" + s + "]";
+       }
+}
diff --git a/src/net/sf/openrocket/aerodynamics/WindSimulator.java b/src/net/sf/openrocket/aerodynamics/WindSimulator.java
new file mode 100644 (file)
index 0000000..5bf08ef
--- /dev/null
@@ -0,0 +1,183 @@
+package net.sf.openrocket.aerodynamics;\r
+\r
+import java.util.Random;\r
+\r
+import net.sf.openrocket.util.MathUtil;\r
+import net.sf.openrocket.util.PinkNoise;\r
+\r
+\r
+public class WindSimulator {\r
+       \r
+       /** Source for seed numbers. */\r
+       private static final Random seedSource = new Random();\r
+\r
+    /** Pink noise alpha parameter. */\r
+    private static final double ALPHA = 5.0/3.0;\r
+    \r
+    /** Number of poles to use in the pink noise IIR filter. */\r
+    private static final int POLES = 2;\r
+    \r
+    /** The standard deviation of the generated pink noise with the specified number of poles. */\r
+    private static final double STDDEV = 2.252;\r
+    \r
+    /** Time difference between random samples. */\r
+    private static final double DELTA_T = 0.05;\r
+\r
+    \r
+    private double average = 0;\r
+    private double standardDeviation = 0;\r
+    \r
+    private int seed;\r
+    \r
+    private PinkNoise randomSource = null;\r
+    private double time1;\r
+    private double value1, value2;\r
+    \r
+\r
+    /**\r
+     * Construct a new wind simulator with a random starting seed value.\r
+     */\r
+    public WindSimulator() {\r
+       synchronized(seedSource) {\r
+               seed = seedSource.nextInt();\r
+       }\r
+    }\r
+    \r
+    \r
+    \r
+    /**\r
+     * Return the average wind speed.\r
+     * \r
+     * @return the average wind speed.\r
+     */\r
+    public double getAverage() {\r
+        return average;\r
+    }\r
+    /**\r
+     * Set the average wind speed.  This method will also modify the\r
+     * standard deviation such that the turbulence intensity remains constant.\r
+     * \r
+     * @param average the average wind speed to set\r
+     */\r
+    public void setAverage(double average) {\r
+        double intensity = getTurbulenceIntensity();\r
+        this.average = Math.max(average, 0);\r
+        setTurbulenceIntensity(intensity);\r
+    }\r
+    \r
+    \r
+    \r
+    /**\r
+     * Return the standard deviation from the average wind speed.\r
+     * \r
+     * @return the standard deviation of the wind speed\r
+     */\r
+    public double getStandardDeviation() {\r
+        return standardDeviation;\r
+    }\r
+    \r
+    /**\r
+     * Set the standard deviation of the average wind speed.\r
+     * \r
+     * @param standardDeviation the standardDeviation to set\r
+     */\r
+    public void setStandardDeviation(double standardDeviation) {\r
+        this.standardDeviation = Math.max(standardDeviation, 0);\r
+    }\r
+    \r
+    \r
+    /**\r
+     * Return the turbulence intensity (standard deviation / average).\r
+     * \r
+     * @return  the turbulence intensity\r
+     */\r
+    public double getTurbulenceIntensity() {\r
+        if (MathUtil.equals(average, 0)) {\r
+            if (MathUtil.equals(standardDeviation, 0))\r
+                return 0;\r
+            else\r
+                return 1000;\r
+        }\r
+        return standardDeviation / average;\r
+    }\r
+\r
+    /**\r
+     * Set the standard deviation to match the turbulence intensity.\r
+     * \r
+     * @param intensity   the turbulence intensity\r
+     */\r
+    public void setTurbulenceIntensity(double intensity) {\r
+        setStandardDeviation(intensity * average);\r
+    }\r
+    \r
+    \r
+    \r
+    \r
+    \r
+    public int getSeed() {\r
+        return seed;\r
+    }\r
+    \r
+    public void setSeed(int seed) {\r
+        if (this.seed == seed)\r
+            return;\r
+        this.seed = seed;\r
+    }\r
+    \r
+    \r
+    \r
+    public double getWindSpeed(double time) {\r
+       if (time < 0) {\r
+               throw new IllegalArgumentException("Requesting wind speed at t="+time);\r
+       }\r
+       \r
+        if (randomSource == null) {\r
+               randomSource = new PinkNoise(ALPHA, POLES, new Random(seed));\r
+            time1 = 0;\r
+            value1 = randomSource.nextValue();\r
+            value2 = randomSource.nextValue();\r
+        }\r
+        \r
+        if (time < time1) {\r
+               reset();\r
+               return getWindSpeed(time);\r
+        }\r
+        \r
+        while (time1 + DELTA_T < time) {\r
+            value1 = value2;\r
+            value2 = randomSource.nextValue();\r
+            time1 += DELTA_T;\r
+        }\r
+        \r
+        double a = (time - time1)/DELTA_T;\r
+\r
+        \r
+        return average + (value1 * (1-a) + value2 * a) * standardDeviation / STDDEV;\r
+    }\r
+\r
+    \r
+    private void reset() {\r
+        randomSource = null;\r
+    }\r
+    \r
+    \r
+    public static void main(String[] str) {\r
+        \r
+        WindSimulator sim = new WindSimulator();\r
+        \r
+        sim.setAverage(2);\r
+        sim.setStandardDeviation(0.5);\r
+        \r
+        for (int i=0; i < 10000; i++) {\r
+            double t = 0.01*i;\r
+            double v = sim.getWindSpeed(t);\r
+            System.out.printf("%d.%03d  %d.%03d\n", (int)t,((int)(t*1000))%1000, (int)v, ((int)(v*1000))%1000);\r
+//            if ((i % 5) == 0)\r
+//                System.out.println(" ***");\r
+//            else\r
+//                System.out.println("");\r
+        }\r
+        \r
+    }\r
+    \r
+}\r
diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java
new file mode 100644 (file)
index 0000000..05144ef
--- /dev/null
@@ -0,0 +1,693 @@
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import static java.lang.Math.pow;
+import static java.lang.Math.sqrt;
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LinearInterpolator;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.PolyInterpolator;
+import net.sf.openrocket.util.Test;
+
+
+public class FinSetCalc extends RocketComponentCalc {
+       private static final double STALL_ANGLE = (20 * Math.PI/180);
+       
+       /** Number of divisions in the fin chords. */
+       protected static final int DIVISIONS = 48;
+       
+       
+       
+
+       private FinSet component;
+       
+
+       protected double macLength = Double.NaN;    // MAC length
+       protected double macLead = Double.NaN;      // MAC leading edge position
+       protected double macSpan = Double.NaN;      // MAC spanwise position
+       protected double finArea = Double.NaN;      // Fin area
+       protected double ar = Double.NaN;           // Fin aspect ratio
+       protected double span = Double.NaN;                 // Fin span
+       protected double cosGamma = Double.NaN;     // Cosine of midchord sweep angle
+       protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle
+       protected double rollSum = Double.NaN;      // Roll damping sum term
+       
+       protected double[] chordLead = new double[DIVISIONS];
+       protected double[] chordTrail = new double[DIVISIONS];
+       protected double[] chordLength = new double[DIVISIONS];
+
+       protected final WarningSet geometryWarnings = new WarningSet();
+       
+       private double[] poly = new double[6];
+
+       
+       public FinSetCalc(RocketComponent component) {
+               super(component);
+               if (!(component instanceof FinSet)) {
+                       throw new IllegalArgumentException("Illegal component type "+component);
+               }
+               this.component = (FinSet) component;
+       }
+       
+
+       /*
+        * Calculates the non-axial forces produced by the fins (normal and side forces,
+        * pitch, yaw and roll moments, CP position, CNa).
+        */
+       @Override
+       public void calculateNonaxialForces(FlightConditions conditions, 
+                       AerodynamicForces forces, WarningSet warnings) {
+               
+               // Compute and cache the fin geometry
+               if (Double.isNaN(macLength)) {
+                       calculateFinGeometry();
+                       calculatePoly();
+               }
+               
+               if (span < 0.001) {
+                       forces.Cm = 0;
+                       forces.CN = 0;
+                       forces.CNa = 0;
+                       forces.cp = Coordinate.NUL;
+                       forces.Croll = 0;
+                       forces.CrollDamp = 0;
+                       forces.CrollForce = 0;
+                       forces.Cside = 0;
+                       forces.Cyaw = 0;
+                       return;
+               }
+
+               
+               // Add warnings  (radius/2 == diameter/4)
+               if (component.getThickness() > component.getBodyRadius()/2) {
+                       warnings.add(Warning.THICK_FIN);
+               }
+               warnings.addAll(geometryWarnings);
+
+               
+               
+               //////// Calculate CNa.  /////////
+
+               // One fin without interference (both sub- and supersonic):
+               double cna1 = calculateFinCNa1(conditions);
+               
+               
+               
+               // Multiple fins with fin-fin interference
+               double cna;
+               
+               // TODO: MEDIUM:  Take into account multiple fin sets
+               int fins = component.getFinCount();
+               double theta = conditions.getTheta();
+               double angle = component.getBaseRotation();
+               
+               switch (fins) {
+               case 1:
+               case 2:
+                       // from geometry
+                       double mul = 0;
+                       for (int i=0; i < fins; i++) {
+                               mul += MathUtil.pow2(Math.sin(theta - angle));
+                               angle += 2 * Math.PI / fins;
+                       }
+                       cna = cna1*mul;
+                       break;
+                       
+               case 3:
+                       // multiplier 1.5, sinusoidal reduction of 15%
+                       cna = cna1 * 1.5 * (1 - 0.15*pow2(Math.cos(1.5 * (theta-angle))));
+                       break;
+                       
+               case 4:
+                       // multiplier 2.0, sinusoidal reduction of 6%
+                       cna = cna1 * 2.0 * (1 - 0.06*pow2(Math.sin(2 * (theta-angle))));
+                       break;
+                       
+               case 5:
+                       cna = 2.37 * cna1;
+                       break;
+                       
+               case 6:
+                       cna = 2.74 * cna1;
+                       break;
+                       
+               case 7:
+                       cna = 2.99 * cna1;
+                       break;
+                       
+               case 8:
+                       cna = 3.24 * cna1;
+                       break;
+                       
+               default:
+                       // Assume N/2 * 3/4 efficiency for more fins
+                       cna = cna1 * fins * 3.0/8.0;
+                       break;
+               }
+               
+               
+               // Body-fin interference effect
+               double r = component.getBodyRadius();
+               double tau = r / (span+r);
+               if (Double.isNaN(tau) || Double.isInfinite(tau))
+                       tau = 0;
+               cna *= 1 + tau;                 // Classical Barrowman
+//             cna *= pow2(1 + tau);   // Barrowman thesis (too optimistic??)
+               
+               
+               
+               // TODO: LOW: check for fin tip mach cone interference
+               // (Barrowman thesis pdf-page 40)
+
+               // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25
+               
+               
+
+               // Calculate CP position
+               double x = macLead + calculateCPPos(conditions) * macLength;
+               
+
+               
+               // Calculate roll forces, reduce forcing above stall angle
+               
+               // Without body-fin interference effect:
+//             forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() / 
+//                     conditions.getRefLength();
+               // With body-fin interference effect:
+               forces.CrollForce = fins * (macSpan+r) * cna1 * (1+tau) * component.getCantAngle() / 
+                       conditions.getRefLength();
+
+
+               
+               
+               if (conditions.getAOA() > STALL_ANGLE) {
+//                     System.out.println("Fin stalling in roll");
+                       forces.CrollForce *= MathUtil.clamp(
+                                       1-(conditions.getAOA() - STALL_ANGLE)/(STALL_ANGLE/2), 0, 1);
+               }
+               forces.CrollDamp = calculateDampingMoment(conditions);
+               forces.Croll = forces.CrollForce - forces.CrollDamp;
+               
+                               
+               
+//             System.out.printf(component.getName() + ":  roll rate:%.3f  force:%.3f  damp:%.3f  " +
+//                             "total:%.3f\n",
+//                             conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll);
+               
+               forces.CNa = cna;
+               forces.CN = cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE);
+               forces.cp = new Coordinate(x, 0, 0, cna);
+               forces.Cm = forces.CN * x / conditions.getRefLength();
+               
+               if (fins == 1) {
+                       forces.Cside = cna1 * Math.cos(theta-angle) * Math.sin(theta-angle);
+                       forces.Cyaw = forces.Cside * x / conditions.getRefLength();
+               } else {
+                       forces.Cside = 0;
+                       forces.Cyaw = 0;
+               }
+               
+       }
+       
+       
+       /**
+        * Returns the MAC length of the fin.  This is required in the friction drag
+        * computation.
+        * 
+        * @return  the MAC length of the fin.
+        */
+       public double getMACLength() {
+               // Compute and cache the fin geometry
+               if (Double.isNaN(macLength)) {
+                       calculateFinGeometry();
+                       calculatePoly();
+               }
+               
+               return macLength;
+       }
+       
+       public double getMidchordPos() {
+               // Compute and cache the fin geometry
+               if (Double.isNaN(macLength)) {
+                       calculateFinGeometry();
+                       calculatePoly();
+               }
+               
+               return macLead + 0.5 * macLength;
+       }
+       
+       
+       
+       /**
+        * Pre-calculates the fin geometry values.
+        */
+       protected void calculateFinGeometry() {
+               
+               span = component.getSpan();
+               finArea = component.getFinArea();
+               ar = 2 * pow2(span) / finArea;
+
+               Coordinate[] points = component.getFinPoints();
+               
+               // Check for jagged edges
+               geometryWarnings.clear();
+               boolean down = false;
+               for (int i=1; i < points.length; i++) {
+                       if ((points[i].y > points[i-1].y + 0.001)  &&  down) {
+                               geometryWarnings.add(Warning.JAGGED_EDGED_FIN);
+                               break;
+                       }
+                       if (points[i].y < points[i-1].y - 0.001) {
+                               down = true;
+                       }
+               }
+               
+               
+               // Calculate the chord lead and trail positions and length
+               
+               Arrays.fill(chordLead, Double.POSITIVE_INFINITY);
+               Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY);
+               Arrays.fill(chordLength, 0);
+
+               for (int point=1; point < points.length; point++) {
+                       double x1 = points[point-1].x;
+                       double y1 = points[point-1].y;
+                       double x2 = points[point].x;
+                       double y2 = points[point].y;
+                       
+                       if (MathUtil.equals(y1, y2))
+                               continue;
+                       
+                       int i1 = (int)(y1*1.0001/span*(DIVISIONS-1));
+                       int i2 = (int)(y2*1.0001/span*(DIVISIONS-1));
+                       i1 = MathUtil.clamp(i1, 0, DIVISIONS-1);
+                       i2 = MathUtil.clamp(i2, 0, DIVISIONS-1);
+                       if (i1 > i2) {
+                               int tmp = i2;
+                               i2 = i1;
+                               i1 = tmp;
+                       }
+                       
+                       for (int i = i1; i <= i2; i++) {
+                               // Intersection point (x,y)
+                               double y = i*span/(DIVISIONS-1);
+                               double x = (y-y2)/(y1-y2)*x1 + (y1-y)/(y1-y2)*x2;
+                               if (x < chordLead[i])
+                                       chordLead[i] = x;
+                               if (x > chordTrail[i])
+                                       chordTrail[i] = x;
+                               
+                               // TODO: LOW:  If fin point exactly on chord line, might be counted twice:
+                               if (y1 < y2) {
+                                       chordLength[i] -= x;
+                               } else {
+                                       chordLength[i] += x;
+                               }
+                       }
+               }
+               
+               // Check and correct any inconsistencies
+               for (int i=0; i < DIVISIONS; i++) {
+                       if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) ||
+                                       Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) {
+                               chordLead[i] = 0;
+                               chordTrail[i] = 0;
+                       }
+                       if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) {
+                               chordLength[i] = 0;
+                       }
+                       if (chordLength[i] > chordTrail[i] - chordLead[i]) {
+                               chordLength[i] = chordTrail[i] - chordLead[i];
+                       }
+               }
+               
+               
+               /* Calculate fin properties:
+                * 
+                * macLength // MAC length
+                * macLead   // MAC leading edge position
+                * macSpan   // MAC spanwise position
+                * ar        // Fin aspect ratio (already set)
+                * span      // Fin span (already set)
+                */
+               macLength = 0;
+               macLead = 0;
+               macSpan = 0;
+               cosGamma = 0;
+               cosGammaLead = 0;
+               rollSum = 0;
+               double area = 0;
+               double radius = component.getBodyRadius();
+
+               final double dy = span/(DIVISIONS-1);
+               for (int i=0; i < DIVISIONS; i++) {
+                       double length = chordTrail[i] - chordLead[i];
+                       double y = i*dy;
+
+                       macLength += length * length;
+                       macSpan += y * length;
+                       macLead += chordLead[i] * length;
+                       area += length;
+                       rollSum += chordLength[i] * pow2(radius + y);
+                       
+                       if (i>0) {
+                               double dx = (chordTrail[i]+chordLead[i])/2 - (chordTrail[i-1]+chordLead[i-1])/2;
+                               cosGamma += dy/MathUtil.hypot(dx, dy);
+                               
+                               dx = chordLead[i] - chordLead[i-1];
+                               cosGammaLead += dy/MathUtil.hypot(dx, dy);
+                       }
+               }
+               
+               macLength *= dy;
+               macSpan *= dy;
+               macLead *= dy;
+               area *= dy;
+               rollSum *= dy;
+               
+               macLength /= area;
+               macSpan /= area;
+               macLead /= area;
+               cosGamma /= (DIVISIONS-1);
+               cosGammaLead /= (DIVISIONS-1);
+       }
+       
+       
+       ///////////////  CNa1 calculation  ////////////////
+       
+       private static final double CNA_SUBSONIC = 0.9;
+       private static final double CNA_SUPERSONIC = 1.5;
+       private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC)-1, 1.5);
+       private static final double GAMMA = 1.4;
+       private static final LinearInterpolator K1, K2, K3;
+       private static final PolyInterpolator cnaInterpolator = new PolyInterpolator(
+                       new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
+                       new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
+                       new double[] { CNA_SUBSONIC }
+       );
+       /* Pre-calculate the values for K1, K2 and K3 */
+       static {
+               // Up to Mach 5
+               int n = (int)((5.0-CNA_SUPERSONIC)*10);
+               double[] x = new double[n];
+               double[] k1 = new double[n];
+               double[] k2 = new double[n];
+               double[] k3 = new double[n];
+               for (int i=0; i<n; i++) {
+                       double M = CNA_SUPERSONIC + i*0.1;
+                       double beta = sqrt(M*M - 1);
+                       x[i] = M;
+                       k1[i] = 2.0/beta;
+                       k2[i] = ((GAMMA+1)*pow(M, 4) - 4*pow2(beta)) / (4*pow(beta,4));
+                       k3[i] = ((GAMMA+1)*pow(M, 8) + (2*pow2(GAMMA) - 7*GAMMA - 5) * pow(M,6) +
+                                       10*(GAMMA+1)*pow(M,4) + 8) / (6*pow(beta,7));
+               }
+               K1 = new LinearInterpolator(x,k1);
+               K2 = new LinearInterpolator(x,k2);
+               K3 = new LinearInterpolator(x,k3);
+               
+//             System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]);
+//             System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]);
+//             System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]);
+       }
+       
+
+       protected double calculateFinCNa1(FlightConditions conditions) {
+               double mach = conditions.getMach();
+               double ref = conditions.getRefArea();
+               double alpha = MathUtil.min(conditions.getAOA(), 
+                               Math.PI - conditions.getAOA(), STALL_ANGLE);
+               
+               // Subsonic case
+               if (mach <= CNA_SUBSONIC) {
+                       return 2*Math.PI*pow2(span)/(1 + sqrt(1 + (1-pow2(mach))*
+                                       pow2(pow2(span)/(finArea*cosGamma)))) / ref;
+               }
+               
+               // Supersonic case
+               if (mach >= CNA_SUPERSONIC) {
+                       return finArea * (K1.getValue(mach) + K2.getValue(mach)*alpha +
+                                       K3.getValue(mach)*pow2(alpha)) / ref;
+               }
+               
+               // Transonic case, interpolate
+               double subV, superV;
+               double subD, superD;
+               
+               double sq = sqrt(1 + (1-pow2(CNA_SUBSONIC)) * pow2(span*span/(finArea*cosGamma)));
+               subV = 2*Math.PI*pow2(span)/ref / (1+sq);
+               subD = 2*mach*Math.PI*pow(span,6) / (pow2(finArea*cosGamma) * ref * 
+                               sq * pow2(1+sq));
+               
+               superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC)*alpha +
+               K3.getValue(CNA_SUPERSONIC)*pow2(alpha)) / ref;
+               superD = -finArea/ref * 2*CNA_SUPERSONIC / CNA_SUPERSONIC_B; 
+               
+//             System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD);
+               
+               return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0);
+       }
+       
+       
+
+       
+       private double calculateDampingMoment(FlightConditions conditions) {
+               double rollRate = conditions.getRollRate();
+
+               if (Math.abs(rollRate) < 0.1)
+                       return 0;
+               
+               double mach = conditions.getMach();
+               double radius = component.getBodyRadius();
+               double absRate = Math.abs(rollRate);
+               
+               
+               /*
+                * At low speeds and relatively large roll rates (i.e. near apogee) the
+                * fin tips rotate well above stall angle.  In this case sum the chords
+                * separately.
+                */
+               if (absRate * (radius + span) / conditions.getVelocity() > 15*Math.PI/180) {
+                       double sum = 0;
+                       for (int i=0; i < DIVISIONS; i++) {
+                               double dist = radius + span*i/DIVISIONS;
+                               double aoa = Math.min(absRate*dist/conditions.getVelocity(), 15*Math.PI/180);
+                               sum += chordLength[i] * dist * aoa;
+                       }
+                       sum = sum * (span/DIVISIONS) * 2*Math.PI/conditions.getBeta() /
+                                       (conditions.getRefArea() * conditions.getRefLength());
+                       
+//                     System.out.println("SPECIAL: " + 
+//                                     (MathUtil.sign(rollRate) *component.getFinCount() * sum));
+                       return MathUtil.sign(rollRate) * component.getFinCount() * sum;
+               }
+               
+               
+               
+               if (mach <= CNA_SUBSONIC) {
+//                     System.out.println("BASIC:   "+
+//                                     (component.getFinCount() * 2*Math.PI * rollRate * rollSum / 
+//                     (conditions.getRefArea() * conditions.getRefLength() * 
+//                                     conditions.getVelocity() * conditions.getBeta())));
+                       
+                       return component.getFinCount() * 2*Math.PI * rollRate * rollSum / 
+                       (conditions.getRefArea() * conditions.getRefLength() * 
+                                       conditions.getVelocity() * conditions.getBeta());
+               }
+               if (mach >= CNA_SUPERSONIC) {
+                       
+                       double vel = conditions.getVelocity();
+                       double k1 = K1.getValue(mach);
+                       double k2 = K2.getValue(mach);
+                       double k3 = K3.getValue(mach);
+
+                       double sum = 0;
+                       
+                       for (int i=0; i < DIVISIONS; i++) {
+                               double y = i*span/(DIVISIONS-1);
+                               double angle = rollRate * (radius+y) / vel;
+                               
+                               sum += (k1 * angle + k2 * angle*angle + k3 * angle*angle*angle) 
+                                       * chordLength[i] * (radius+y);
+                       }
+                       
+                       return component.getFinCount() * sum * span/(DIVISIONS-1) / 
+                               (conditions.getRefArea() * conditions.getRefLength());
+               }
+               
+               // Transonic, do linear interpolation
+               
+               FlightConditions cond = conditions.clone();
+               cond.setMach(CNA_SUBSONIC - 0.01);
+               double subsonic = calculateDampingMoment(cond);
+               cond.setMach(CNA_SUPERSONIC + 0.01);
+               double supersonic = calculateDampingMoment(cond);
+               
+               return subsonic * (CNA_SUPERSONIC - mach)/(CNA_SUPERSONIC - CNA_SUBSONIC) +
+                          supersonic * (mach - CNA_SUBSONIC)/(CNA_SUPERSONIC - CNA_SUBSONIC);
+       }
+       
+       
+       
+       
+       /**
+        * Return the relative position of the CP along the mean aerodynamic chord.
+        * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an
+        * empirical formula, between these two using an interpolation polynomial.
+        * 
+        * @param cond   Mach speed used
+        * @return               CP position along the MAC
+        */
+       private double calculateCPPos(FlightConditions cond) {
+               double m = cond.getMach();
+               if (m <= 0.5) {
+                       // At subsonic speeds CP at quarter chord
+                       return 0.25;
+               }
+               if (m >= 2) {
+                       // At supersonic speeds use empirical formula
+                       double beta = cond.getBeta();
+                       return (ar * beta - 0.67) / (2*ar*beta - 1);
+               }
+               
+               // In between use interpolation polynomial
+               double x = 1.0;
+               double val = 0;
+               
+               for (int i=0; i < poly.length; i++) {
+                       val += poly[i] * x;
+                       x *= m;
+               }
+               
+               return val;
+       }
+       
+       /**
+        * Calculate CP position interpolation polynomial coefficients from the
+        * fin geometry.  This is a fifth order polynomial that satisfies
+        * 
+        * p(0.5)=0.25
+        * p'(0.5)=0
+        * p(2) = f(2)
+        * p'(2) = f'(2)
+        * p''(2) = 0
+        * p'''(2) = 0
+        * 
+        * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1).
+        * 
+        * The values were calculated analytically in Mathematica.  The coefficients
+        * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ...
+        */
+       private void calculatePoly() {
+               double denom = pow2(1 - 3.4641*ar);  // common denominator
+               
+               poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom;
+               poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom;
+               poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom;
+               poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom;
+               poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom;
+               poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom;
+       }
+       
+       
+       @SuppressWarnings("null")
+       public static void main(String arg[]) {
+               Rocket rocket = Test.makeRocket();
+               FinSet finset = null;
+               
+               Iterator<RocketComponent> iter = rocket.deepIterator();
+               while (iter.hasNext()) {
+                       RocketComponent c = iter.next();
+                       if (c instanceof FinSet) {
+                               finset = (FinSet)c;
+                               break;
+                       }
+               }
+               
+               ((TrapezoidFinSet)finset).setHeight(0.10);
+               ((TrapezoidFinSet)finset).setRootChord(0.10);
+               ((TrapezoidFinSet)finset).setTipChord(0.10);
+               ((TrapezoidFinSet)finset).setSweep(0.0);
+
+               
+               FinSetCalc calc = new FinSetCalc(finset);
+               
+               calc.calculateFinGeometry();
+               FlightConditions cond = new FlightConditions(new Configuration(rocket));
+               for (double m=0; m < 3; m+=0.05) {
+                       cond.setMach(m);
+                       cond.setAOA(0.0*Math.PI/180);
+                       double cna = calc.calculateFinCNa1(cond);
+                       System.out.printf("%5.2f "+cna+"\n", m);
+               }
+               
+       }
+
+
+       @Override
+       public double calculatePressureDragForce(FlightConditions conditions,
+                       double stagnationCD, double baseCD, WarningSet warnings) {
+
+               // Compute and cache the fin geometry
+               if (Double.isNaN(cosGammaLead)) {
+                       calculateFinGeometry();
+                       calculatePoly();
+               }
+
+               
+               FinSet.CrossSection profile = component.getCrossSection();
+               double mach = conditions.getMach();
+               double drag = 0;
+
+               // Pressure fore-drag
+               if (profile == FinSet.CrossSection.AIRFOIL ||
+                               profile == FinSet.CrossSection.ROUNDED) {
+                       
+                       // Round leading edge
+                       if (mach < 0.9) {
+                               drag = Math.pow(1 - pow2(mach), -0.417) - 1;
+                       } else if (mach < 1) {
+                               drag = 1 - 1.785 * (mach-0.9);
+                       } else {
+                               drag = 1.214 - 0.502/pow2(mach) + 0.1095/pow2(pow2(mach));
+                       }
+                       
+               } else if (profile == FinSet.CrossSection.SQUARE) {
+                       drag = stagnationCD;
+               } else {
+                       throw new UnsupportedOperationException("Unsupported fin profile: "+profile);
+               }
+               
+               // Slanted leading edge
+               drag *= pow2(cosGammaLead);
+               
+               // Trailing edge drag
+               if (profile == FinSet.CrossSection.SQUARE) {
+                       drag += baseCD;
+               } else if (profile == FinSet.CrossSection.ROUNDED) {
+                       drag += baseCD/2;
+               }
+               // Airfoil assumed to have zero base drag
+               
+               
+               // Scale to correct reference area
+               drag *= component.getFinCount() * component.getSpan() * component.getThickness() /
+                               conditions.getRefArea();
+               
+               return drag;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java
new file mode 100644 (file)
index 0000000..88cf91f
--- /dev/null
@@ -0,0 +1,39 @@
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.MathUtil;
+
+public class LaunchLugCalc extends RocketComponentCalc {
+
+       private double CDmul;
+       private double refArea;
+       
+       public LaunchLugCalc(RocketComponent component) {
+               super(component);
+               
+               LaunchLug lug = (LaunchLug)component;
+               double ld = lug.getLength() / (2*lug.getRadius());
+               
+               CDmul = Math.max(1.3 - ld, 1);
+               refArea = Math.PI * MathUtil.pow2(lug.getRadius()) - 
+                                 Math.PI * MathUtil.pow2(lug.getInnerRadius()) * Math.max(1 - ld, 0);
+       }
+
+       @Override
+       public void calculateNonaxialForces(FlightConditions conditions,
+                       AerodynamicForces forces, WarningSet warnings) {
+               // Nothing to be done
+       }
+
+       @Override
+       public double calculatePressureDragForce(FlightConditions conditions,
+                       double stagnationCD, double baseCD, WarningSet warnings) {
+
+               return CDmul*stagnationCD * refArea / conditions.getRefArea();
+       }
+
+}
diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java
new file mode 100644 (file)
index 0000000..71f4ef0
--- /dev/null
@@ -0,0 +1,42 @@
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+public abstract class RocketComponentCalc {
+
+       public RocketComponentCalc(RocketComponent component) {
+
+       }
+       
+       /**
+        * Calculate the non-axial forces produced by the component (normal and side forces,
+        * pitch, yaw and roll moments and CP position).  The values are stored in the
+        * <code>AerodynamicForces</code> object.  Additionally the value of CNa is computed
+        * and stored if possible without large amount of extra calculation, otherwise
+        * NaN is stored.  The CP coordinate is stored in local coordinates and moments are
+        * computed around the local origin.
+        * 
+        * @param conditions    the flight conditions.
+        * @param forces                the object in which to store the values.
+        * @param warnings              set in which to store possible warnings.
+        */
+       public abstract void calculateNonaxialForces(FlightConditions conditions, 
+                       AerodynamicForces forces, WarningSet warnings);
+
+       
+       /**
+        * Calculates the pressure drag of the component.  This component does NOT include
+        * the effect of discontinuities in the rocket body.
+        * 
+        * @param conditions    the flight conditions.
+        * @param stagnationCD  the current stagnation drag coefficient
+        * @param baseCD                the current base drag coefficient
+        * @param warnings              set in which to store possible warnings
+        * @return                              the pressure drag of the component
+        */
+       public abstract double calculatePressureDragForce(FlightConditions conditions, 
+                       double stagnationCD, double baseCD, WarningSet warnings);
+}
diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java
new file mode 100644 (file)
index 0000000..3f0e4e7
--- /dev/null
@@ -0,0 +1,443 @@
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.GAMMA;
+import static net.sf.openrocket.util.MathUtil.pow2;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LinearInterpolator;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.PolyInterpolator;
+
+
+
+/**
+ * Calculates the aerodynamic properties of a <code>SymmetricComponent</code>.
+ * <p>
+ * CP and CNa are calculated by the Barrowman method extended to account for body lift
+ * by the method presented by Galejs.  Supersonic CNa and CP are assumed to be the
+ * same as the subsonic values.
+ * 
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SymmetricComponentCalc extends RocketComponentCalc {
+
+       public static final double BODY_LIFT_K = 1.1;
+       
+       private final SymmetricComponent component;
+       
+       private final double length;
+       private final double r1, r2;
+       private final double fineness;
+       private final Transition.Shape shape;
+       private final double param;
+       private final double area;
+       
+       public SymmetricComponentCalc(RocketComponent c) {
+               super(c);
+               if (!(c instanceof SymmetricComponent)) {
+                       throw new IllegalArgumentException("Illegal component type "+c);
+               }
+               this.component = (SymmetricComponent) c;
+               
+
+               length = component.getLength();
+               r1 = component.getForeRadius();
+               r2 = component.getAftRadius();
+               
+               fineness = length / (2*Math.abs(r2-r1));
+               
+               if (component instanceof BodyTube) {
+                       shape = null;
+                       param = 0;
+                       area = 0;
+               } else if (component instanceof Transition) {
+                       shape = ((Transition)component).getType();
+                       param = ((Transition)component).getShapeParameter();
+                       area = Math.abs(Math.PI * (r1*r1 - r2*r2));
+               } else {
+                       throw new UnsupportedOperationException("Unknown component type " +
+                                       component.getComponentName());
+               }
+       }
+       
+
+       private boolean isTube = false;
+       private double cnaCache = Double.NaN;
+       private double cpCache = Double.NaN;
+       
+       
+       /**
+        * Calculates the non-axial forces produced by the fins (normal and side forces,
+        * pitch, yaw and roll moments, CP position, CNa).
+        * <p> 
+        * This method uses the Barrowman method for CP and CNa calculation and the 
+        * extension presented by Galejs for the effect of body lift.
+        * <p>
+        * The CP and CNa at supersonic speeds are assumed to be the same as those at
+        * subsonic speeds.
+        */
+       @Override
+       public void calculateNonaxialForces(FlightConditions conditions, 
+                       AerodynamicForces forces, WarningSet warnings) {
+
+               // Pre-calculate and store the results
+               if (Double.isNaN(cnaCache)) {
+                       final double r0 = component.getForeRadius();
+                       final double r1 = component.getAftRadius();
+                       
+                       if (MathUtil.equals(r0, r1)) {
+                               isTube = true;
+                               cnaCache = 0;
+                       } else { 
+                               isTube = false;
+                               
+                               final double A0 = Math.PI * pow2(r0);
+                               final double A1 = Math.PI * pow2(r1);
+                       
+                               cnaCache = 2 * (A1 - A0);
+                               System.out.println("cnaCache = "+cnaCache);
+                               cpCache = (component.getLength() * A1 - component.getFullVolume()) / (A1 - A0);
+                       }
+               }
+               
+               Coordinate cp;
+               
+               // If fore == aft, only body lift is encountered
+               if (isTube) {
+                       cp = getLiftCP(conditions, warnings);
+               } else {
+                       cp = new Coordinate(cpCache,0,0,cnaCache * conditions.getSincAOA() / 
+                                       conditions.getRefArea()).average(getLiftCP(conditions,warnings));
+               }
+               
+               forces.cp = cp;
+               forces.CNa = cp.weight;
+               forces.CN = forces.CNa * conditions.getAOA();
+               forces.Cm = forces.CN * cp.x / conditions.getRefLength();
+               forces.Croll = 0;
+               forces.CrollDamp = 0;
+               forces.CrollForce = 0;
+               forces.Cside = 0;
+               forces.Cyaw = 0;
+               
+               
+               // Add warning on supersonic flight
+               if (conditions.getMach() > 1.1) {
+                       warnings.add("Body calculations may not be entirely accurate at supersonic speeds.");
+               }
+               
+       }
+       
+       
+
+       /**
+        * Calculate the body lift effect according to Galejs.
+        */
+       protected Coordinate getLiftCP(FlightConditions conditions, WarningSet warnings) {
+               double area = component.getComponentPlanformArea();
+               double center = component.getComponentPlanformCenter();
+               
+               /*
+                * Without this extra multiplier the rocket may become unstable at apogee
+                * when turning around, and begin oscillating horizontally.  During the flight
+                * of the rocket this has no effect.  It is effective only when AOA > 45 deg
+                * and the velocity is less than 15 m/s.
+                */
+               double mul = 1;
+               if ((conditions.getMach() < 0.05) && (conditions.getAOA() > Math.PI/4)) {
+                       mul = pow2(conditions.getMach() / 0.05);
+               }
+               
+               return new Coordinate(center, 0, 0, mul*BODY_LIFT_K * area/conditions.getRefArea() * 
+                               conditions.getSinAOA() * conditions.getSincAOA());  // sin(aoa)^2 / aoa
+       }
+
+
+       
+       private LinearInterpolator interpolator = null;
+       
+       @Override
+       public double calculatePressureDragForce(FlightConditions conditions,
+                       double stagnationCD, double baseCD, WarningSet warnings) {
+
+               if (component instanceof BodyTube)
+                       return 0;
+               
+               if (!(component instanceof Transition)) {
+                       throw new RuntimeException("Pressure calculation of unknown type: "+
+                                       component.getComponentName());
+               }
+               
+               // Check for simple cases first
+               if (r1 == r2)
+                       return 0;
+               
+               if (length < 0.001) {
+                       if (r1 < r2) {
+                               return stagnationCD * area / conditions.getRefArea();
+                       } else {
+                               return baseCD * area / conditions.getRefArea();
+                       }
+               }
+               
+               
+               // Boattail drag computed directly from base drag
+               if (r2 < r1) {
+                       if (fineness >= 3)
+                               return 0;
+                       double cd = baseCD * area / conditions.getRefArea();
+                       if (fineness <= 1)
+                               return cd;
+                       return cd * (3-fineness)/2;
+               }
+               
+
+               assert(r1 < r2);  // Tube and boattail have been checked already
+               
+               
+               // All nose cones and shoulders from pre-calculated and interpolating 
+               if (interpolator == null) {
+                       calculateNoseInterpolator();
+               }
+               
+               return interpolator.getValue(conditions.getMach()) * area / conditions.getRefArea();
+       }
+       
+       
+       
+       /* 
+        * Experimental values of pressure drag for different nose cone shapes with a fineness
+        * ratio of 3.  The data is taken from 'Collection of Zero-Lift Drag Data on Bodies
+        * of Revolution from Free-Flight Investigations', NASA TR-R-100, NTRS 19630004995,
+        * page 16.
+        * 
+        * This data is extrapolated for other fineness ratios.
+        */
+       
+       private static final LinearInterpolator ellipsoidInterpolator = new LinearInterpolator(
+                       new double[] {  1.2,  1.25,   1.3,   1.4,   1.6,   2.0,   2.4 },
+                       new double[] {0.110, 0.128, 0.140, 0.148, 0.152, 0.159, 0.162 /* constant */ } 
+       );
+       private static final LinearInterpolator x14Interpolator = new LinearInterpolator(
+                       new double[] {  1.2,   1.3,   1.4,   1.6,   1.8,   2.2,   2.6,   3.0,   3.6},
+                       new double[] {0.140, 0.156, 0.169, 0.192, 0.206, 0.227, 0.241, 0.249, 0.252}
+       );
+       private static final LinearInterpolator x12Interpolator = new LinearInterpolator(
+                       new double[] {0.925,  0.95,   1.0,  1.05,   1.1,   1.2,   1.3,   1.7,   2.0},
+                       new double[] {    0, 0.014, 0.050, 0.060, 0.059, 0.081, 0.084, 0.085, 0.078}
+       );
+       private static final LinearInterpolator x34Interpolator = new LinearInterpolator(
+                       new double[] { 0.8,   0.9,   1.0,  1.06,   1.2,   1.4,   1.6,   2.0,   2.8,   3.4},
+                       new double[] {   0, 0.015, 0.078, 0.121, 0.110, 0.098, 0.090, 0.084, 0.078, 0.074}
+       );
+       private static final LinearInterpolator vonKarmanInterpolator = new LinearInterpolator(
+                       new double[] { 0.9,  0.95,   1.0,  1.05,   1.1,   1.2,   1.4,   1.6,   2.0,   3.0},
+                       new double[] {   0, 0.010, 0.027, 0.055, 0.070, 0.081, 0.095, 0.097, 0.091, 0.083}
+       );
+       private static final LinearInterpolator lvHaackInterpolator = new LinearInterpolator(
+                       new double[] { 0.9,  0.95,   1.0,  1.05,   1.1,   1.2,   1.4,   1.6,   2.0 },
+                       new double[] {   0, 0.010, 0.024, 0.066, 0.084, 0.100, 0.114, 0.117, 0.113 }
+       );
+       private static final LinearInterpolator parabolicInterpolator = new LinearInterpolator(
+                       new double[] {0.95, 0.975,   1.0,  1.05,   1.1,   1.2,   1.4,   1.7},
+                       new double[] {   0, 0.016, 0.041, 0.092, 0.109, 0.119, 0.113, 0.108} 
+       );
+       private static final LinearInterpolator parabolic12Interpolator = new LinearInterpolator(
+                       new double[] { 0.8,   0.9,  0.95,   1.0,  1.05,   1.1,   1.3,   1.5,   1.8},
+                       new double[] {   0, 0.016, 0.042, 0.100, 0.126, 0.125, 0.100, 0.090, 0.088}
+       );
+       private static final LinearInterpolator parabolic34Interpolator = new LinearInterpolator(
+                       new double[] { 0.9,  0.95,   1.0,  1.05,   1.1,   1.2,   1.4,   1.7},
+                       new double[] {   0, 0.023, 0.073, 0.098, 0.107, 0.106, 0.089, 0.082}
+       );
+       private static final LinearInterpolator bluntInterpolator = new LinearInterpolator();
+       static {
+               for (double m=0; m<3; m+=0.05)
+                       bluntInterpolator.addPoint(m, BarrowmanCalculator.calculateStagnationCD(m));
+       }
+       
+       /**
+        * Calculate the LinearInterpolator 'interpolator'.  After this call, if can be used
+        * to get the pressure drag coefficient at any Mach number.
+        * 
+        * First, the transonic/supersonic region is computed.  For conical and ogive shapes
+        * this is calculated directly.  For other shapes, the values for fineness-ratio 3
+        * transitions are taken from the experimental values stored above (for parameterized
+        * shapes the values are interpolated between the parameter values).  These are then
+        * extrapolated to the current fineness ratio.
+        * 
+        * Finally, if the first data points in the interpolator are not zero, the subsonic
+        * region is interpolated in the form   Cd = a*M^b + Cd(M=0).
+        */
+       @SuppressWarnings("null")
+       private void calculateNoseInterpolator() {
+               LinearInterpolator int1=null, int2=null;
+               double p = 0;
+               
+               interpolator = new LinearInterpolator();
+               
+               double r = component.getRadius(0.99*length);
+               double sinphi = (r2-r)/MathUtil.hypot(r2-r, 0.01*length);
+               
+               /*
+                * Take into account nose cone shape.  Conical and ogive generate the interpolator
+                * directly.  Others store a interpolator for fineness ratio 3 into int1, or
+                * for parameterized shapes store the bounding fineness ratio 3 interpolators into
+                * int1 and int2 and set 0 <= p <= 1 according to the bounds. 
+                */
+               switch (shape) {
+               case CONICAL:
+                       interpolator = calculateOgiveNoseInterpolator(0, sinphi);  // param==0 -> conical
+                       break;
+                       
+               case OGIVE:
+                       interpolator = calculateOgiveNoseInterpolator(param, sinphi);
+                       break;
+                       
+               case ELLIPSOID:
+                       int1 = ellipsoidInterpolator;
+                       break;
+
+               case POWER:
+                       if (param <= 0.25) {
+                               int1 = bluntInterpolator;
+                               int2 = x14Interpolator;
+                               p = param*4;
+                       } else if (param <= 0.5) {
+                               int1 = x14Interpolator;
+                               int2 = x12Interpolator;
+                               p = (param-0.25)*4;
+                       } else if (param <= 0.75) {
+                               int1 = x12Interpolator;
+                               int2 = x34Interpolator;
+                               p = (param-0.5)*4;
+                       } else {
+                               int1 = x34Interpolator;
+                               int2 = calculateOgiveNoseInterpolator(0, 1/Math.sqrt(1+4*pow2(fineness)));
+                               p = (param-0.75)*4;
+                       }
+                       break;
+                       
+               case PARABOLIC:
+                       if (param <= 0.5) {
+                               int1 = calculateOgiveNoseInterpolator(0, 1/Math.sqrt(1+4*pow2(fineness)));
+                               int2 = parabolic12Interpolator;
+                               p = param*2;
+                       } else if (param <= 0.75) {
+                               int1 = parabolic12Interpolator;
+                               int2 = parabolic34Interpolator;
+                               p = (param-0.5)*4;
+                       } else {
+                               int1 = parabolic34Interpolator;
+                               int2 = parabolicInterpolator;
+                               p = (param-0.75)*4;
+                       }
+                       break;
+                       
+               case HAACK:
+                       int1 = vonKarmanInterpolator;
+                       int2 = lvHaackInterpolator;
+                       p = param*3;
+                       break;
+                       
+               default:
+                       throw new UnsupportedOperationException("Unknown transition shape: "+shape);
+               }
+               
+               assert(p >= 0);
+               assert(p <= 1.001);
+               
+               
+               // Check for parameterized shape and interpolate if necessary
+               if (int2 != null) {
+                       LinearInterpolator int3 = new LinearInterpolator();
+                       for (double m: int1.getXPoints()) {
+                               int3.addPoint(m, p*int2.getValue(m) + (1-p)*int1.getValue(m));
+                       }
+                       for (double m: int2.getXPoints()) {
+                               int3.addPoint(m, p*int2.getValue(m) + (1-p)*int1.getValue(m));
+                       }
+                       int1 = int3;
+               }
+
+               // Extrapolate for fineness ratio if necessary
+               if (int1 != null) {
+                       double log4 = Math.log(fineness+1) / Math.log(4);
+                       for (double m: int1.getXPoints()) {
+                               double stag = bluntInterpolator.getValue(m);
+                               interpolator.addPoint(m, stag*Math.pow(int1.getValue(m)/stag, log4));
+                       }
+               }
+               
+               
+               /*
+                * Now the transonic/supersonic region is ok.  We still need to interpolate
+                * the subsonic region, if the values are non-zero.
+                */
+               
+               double min = interpolator.getXPoints()[0];
+               double minValue = interpolator.getValue(min);
+               if (minValue < 0.001) {
+                       // No interpolation necessary
+                       return;
+               }
+               
+               double cdMach0 = 0.8 * pow2(sinphi);
+               double minDeriv = (interpolator.getValue(min+0.01) - minValue)/0.01;
+
+               // These should not occur, but might cause havoc for the interpolation
+               if ((cdMach0 >= minValue-0.01) || (minDeriv <= 0.01)) {
+                       return;
+               }
+               
+               // Cd = a*M^b + cdMach0
+               double a = minValue - cdMach0;
+               double b = minDeriv / a;
+               
+               for (double m=0; m < minValue; m+= 0.05) {
+                       interpolator.addPoint(m, a*Math.pow(m, b) + cdMach0);
+               }
+       }
+       
+       
+       private static final PolyInterpolator conicalPolyInterpolator = 
+               new PolyInterpolator(new double[] {1.0, 1.3}, new double[] {1.0, 1.3});
+
+       private static LinearInterpolator calculateOgiveNoseInterpolator(double param, 
+                       double sinphi) {
+               LinearInterpolator interpolator = new LinearInterpolator();
+               
+               // In the range M = 1 ... 1.3 use polynomial approximation
+               double cdMach1 = 2.1*pow2(sinphi) + 0.6019*sinphi;
+               
+               double[] poly = conicalPolyInterpolator.interpolator(
+                               1.0*sinphi, cdMach1,
+                               4/(GAMMA+1) * (1 - 0.5*cdMach1), -1.1341*sinphi
+               );
+               
+               // Shape parameter multiplier
+               double mul = 0.72 * pow2(param-0.5) + 0.82;
+               
+               for (double m = 1; m < 1.3001; m += 0.02) {
+                       interpolator.addPoint(m, mul * PolyInterpolator.eval(m, poly));
+               }
+               
+               // Above M = 1.3 use direct formula
+               for (double m = 1.32; m < 4; m += 0.02) {
+                       interpolator.addPoint(m, mul * (2.1*pow2(sinphi) + 0.5*sinphi/Math.sqrt(m*m - 1)));
+               }
+
+               return interpolator;
+       }
+       
+       
+
+}
diff --git a/src/net/sf/openrocket/database/Database.java b/src/net/sf/openrocket/database/Database.java
new file mode 100644 (file)
index 0000000..b7986cb
--- /dev/null
@@ -0,0 +1,270 @@
+package net.sf.openrocket.database;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.CodeSource;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import net.sf.openrocket.file.Loader;
+import net.sf.openrocket.util.ChangeSource;
+
+
+
+/**
+ * A database set.  This class functions as a <code>Set</code> that contains items
+ * of a specific type.  Additionally, the items can be accessed via an index number.
+ * The elements are always kept in their natural order.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+// TODO: HIGH: Database saving
+public class Database<T extends Comparable<T>> extends AbstractSet<T> implements ChangeSource {
+
+       private final List<T> list = new ArrayList<T>();
+       private final EventListenerList listenerList = new EventListenerList();
+       private final Loader<T> loader;
+       
+       
+       public Database() {
+               loader = null;
+       }
+       
+       public Database(Loader<T> loader) {
+               this.loader = loader;
+       }
+       
+               
+       @Override
+       public Iterator<T> iterator() {
+               return new DBIterator();
+       }
+
+       @Override
+       public int size() {
+               return list.size();
+       }
+       
+       @Override
+       public boolean add(T element) {
+               int index;
+               
+               index = Collections.binarySearch(list, element);
+               if (index >= 0) {
+                       // List might contain the element
+                       if (list.contains(element)) {
+                               return false;
+                       }
+               } else {
+                       index = -(index+1);
+               }
+               list.add(index,element);
+               fireChangeEvent();
+               return true;
+       }
+       
+       
+       /**
+        * Get the element with the specified index.
+        * @param index the index to retrieve.
+        * @return              the element at the index.
+        */
+       public T get(int index) {
+               return list.get(index);
+       }
+
+       /**
+        * Return the index of the given <code>Motor</code>, or -1 if not in the database.
+        * 
+        * @param m   the motor
+        * @return        the index of the motor
+        */
+       public int indexOf(T m) {
+               return list.indexOf(m);
+       }
+       
+
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listenerList .add(ChangeListener.class, listener);
+       }
+
+
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listenerList .remove(ChangeListener.class, listener);
+       }
+
+       
+       protected void fireChangeEvent() {
+               Object[] listeners = listenerList.getListenerList();
+               ChangeEvent e = null;
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i]==ChangeListener.class) {
+                               // Lazily create the event:
+                               if (e == null)
+                                       e = new ChangeEvent(this);
+                               ((ChangeListener)listeners[i+1]).stateChanged(e);
+                       }
+               }
+       }
+       
+
+       
+       ////////  Directory loading
+       
+       
+       
+       /**
+        * Load all files in a directory to the motor database.  Only files with file
+        * names matching the given pattern (as matched by <code>String.matches(String)</code>)
+        * are processed.
+        * 
+        * @param dir                   the directory to read.
+        * @param pattern               the pattern to match the file names to.
+        * @throws IOException  if an IO error occurs when reading the JAR archive
+        *                                              (errors reading individual files are printed to stderr).
+        */
+       public void loadDirectory(File dir, final String pattern) throws IOException {
+               if (loader == null) {
+                       throw new IllegalStateException("no file loader set");
+               }
+               
+               File[] files = dir.listFiles(new FilenameFilter() {
+                       @Override
+                       public boolean accept(File dir, String name) {
+                               return name.matches(pattern);
+                       }
+               });
+               if (files == null) {
+                       throw new IOException("not a directory: "+dir);
+               }
+               for (File file: files) {
+                       try {
+                               this.addAll(loader.load(new FileInputStream(file), file.getName()));
+                       } catch (IOException e) {
+                               System.err.println("Error loading file "+file+": " + e.getMessage());
+                       }
+               }
+       }
+       
+       
+       /**
+        * Read all files in a directory contained in the JAR file that this class belongs to.
+        * Only files whose names match the given pattern (as matched by
+        * <code>String.matches(String)</code>) will be read.
+        * 
+        * @param dir                   the directory within the JAR archive to read.
+        * @param pattern               the pattern to match the file names to.
+        * @throws IOException  if an IO error occurs when reading the JAR archive
+        *                                              (errors reading individual files are printed to stderr).
+        */
+       public void loadJarDirectory(String dir, String pattern) throws IOException {
+               
+               // Process directory and extension
+               if (!dir.endsWith("/")) {
+                       dir += "/";
+               }
+
+               // Find the jar file this class is contained in and open it
+               URL jarUrl = null;
+               CodeSource codeSource = Database.class.getProtectionDomain().getCodeSource();
+               if (codeSource != null)
+                       jarUrl = codeSource.getLocation();
+               
+               if (jarUrl == null) {
+                       throw new IOException("Could not find containing JAR file.");
+               }
+               File file = urlToFile(jarUrl);
+               JarFile jarFile = new JarFile(file);
+               
+               try {
+
+                       // Loop through JAR entries searching for files to load
+                       Enumeration<JarEntry> entries = jarFile.entries();
+                       while (entries.hasMoreElements()) {
+                               JarEntry entry = entries.nextElement();
+                               String name = entry.getName();
+                               if (name.startsWith(dir) && name.matches(pattern)) {
+                                       try {
+                                               InputStream stream = jarFile.getInputStream(entry);
+                                               this.addAll(loader.load(stream, name));
+                                       } catch (IOException e) {
+                                               System.err.println("Error loading file " + file + ": "
+                                                               + e.getMessage());
+                                       }
+                               }
+                       }
+
+               } finally {
+                       jarFile.close();
+               }
+       }
+       
+       
+       static File urlToFile(URL url) {
+               URI uri;
+               try {
+                       uri = url.toURI();
+               } catch (URISyntaxException e) {
+                       try {
+                               uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), 
+                                               url.getPath(), url.getQuery(), url.getRef());
+                       } catch (URISyntaxException e1) {
+                               throw new IllegalArgumentException("Broken URL: " + url);
+                       }
+               }
+               return new File(uri);
+       }
+       
+       
+       
+       public void load(File file) throws IOException {
+               if (loader == null) {
+                       throw new IllegalStateException("no file loader set");
+               }
+               this.addAll(loader.load(new FileInputStream(file), file.getName()));
+       }
+
+       
+       
+       /**
+        * Iterator class implementation that fires changes if remove() is called.
+        */
+       private class DBIterator implements Iterator<T> {
+               private Iterator<T> iterator = list.iterator();
+               
+               @Override
+               public boolean hasNext() {
+                       return iterator.hasNext();
+               }
+
+               @Override
+               public T next() {
+                       return iterator.next();
+               }
+
+               @Override
+               public void remove() {
+                       iterator.remove();
+                       fireChangeEvent();
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/database/Databases.java b/src/net/sf/openrocket/database/Databases.java
new file mode 100644 (file)
index 0000000..4efb545
--- /dev/null
@@ -0,0 +1,205 @@
+package net.sf.openrocket.database;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A class that contains single instances of {@link Database} for specific purposes.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class Databases {
+
+       /* Static implementations of specific databases: */
+       /**
+        * The motor database.
+        */
+       public static final Database<Motor> MOTOR = new Database<Motor>(new MotorLoader());
+       
+       
+       /**
+        * A database of bulk materials (with bulk densities).
+        */
+       public static final Database<Material> BULK_MATERIAL = new Database<Material>();
+       /**
+        * A database of surface materials (with surface densities).
+        */
+       public static final Database<Material> SURFACE_MATERIAL = new Database<Material>();
+       /**
+        * A database of linear material (with length densities).
+        */
+       public static final Database<Material> LINE_MATERIAL = new Database<Material>();
+       
+       
+
+       // TODO: HIGH: loading the thrust curves and other databases
+       static {
+               
+               try {
+                       MOTOR.loadJarDirectory("datafiles/thrustcurves/", ".*\\.[eE][nN][gG]$");
+               } catch (IOException e) {
+                       System.out.println("Could not read thrust curves from JAR: "+e.getMessage());
+                       
+                       try {
+                               MOTOR.loadDirectory(new File("datafiles/thrustcurves/"),".*\\.[eE][nN][gG]$");
+                       } catch (IOException e1) {
+                               System.out.println("Could not read thrust curves from directory either.");
+                               throw new RuntimeException(e1);
+                       }
+               }
+       }
+       
+       // TODO: HIGH: Move materials into data files
+       static {
+               
+               BULK_MATERIAL.add(new Material.Bulk("Acrylic",          1190));
+               BULK_MATERIAL.add(new Material.Bulk("Balsa",             170));
+               BULK_MATERIAL.add(new Material.Bulk("Birch",             670));
+               BULK_MATERIAL.add(new Material.Bulk("Cardboard",         680));
+               BULK_MATERIAL.add(new Material.Bulk("Carbon fiber",     1780));
+               BULK_MATERIAL.add(new Material.Bulk("Cork",                      240));
+               BULK_MATERIAL.add(new Material.Bulk("Fiberglass",       1850));
+               BULK_MATERIAL.add(new Material.Bulk("Kraft phenolic",950));
+               BULK_MATERIAL.add(new Material.Bulk("Maple",             755));
+               BULK_MATERIAL.add(new Material.Bulk("Paper (office)",820));
+               BULK_MATERIAL.add(new Material.Bulk("Pine",                      530));
+               BULK_MATERIAL.add(new Material.Bulk("Plywood (birch)",630));
+               BULK_MATERIAL.add(new Material.Bulk("Polycarbonate (Lexan)",1200));
+               BULK_MATERIAL.add(new Material.Bulk("Polystyrene",  1050));
+               BULK_MATERIAL.add(new Material.Bulk("PVC",                      1390));
+               BULK_MATERIAL.add(new Material.Bulk("Spruce",            450));
+               BULK_MATERIAL.add(new Material.Bulk("Quantum tubing",1050));
+               
+               SURFACE_MATERIAL.add(new Material.Surface("Ripstop nylon",                      0.067));
+               SURFACE_MATERIAL.add(new Material.Surface("Mylar",                                      0.021));
+               SURFACE_MATERIAL.add(new Material.Surface("Polyethylene (thin)",        0.015));
+               SURFACE_MATERIAL.add(new Material.Surface("Polyethylene (heavy)",       0.040));
+               SURFACE_MATERIAL.add(new Material.Surface("Silk",                                       0.060));
+               SURFACE_MATERIAL.add(new Material.Surface("Paper (office)",                     0.080));
+               SURFACE_MATERIAL.add(new Material.Surface("Cellophane",                         0.018));
+               SURFACE_MATERIAL.add(new Material.Surface("Cr\u00eape paper",           0.025));
+               
+               LINE_MATERIAL.add(new Material.Line("Thread (heavy-duty)",                              0.0003));
+               LINE_MATERIAL.add(new Material.Line("Elastic cord (round 2mm, 1/16 in)",0.0018));
+               LINE_MATERIAL.add(new Material.Line("Elastic cord (flat  6mm, 1/4 in)", 0.0043));
+               LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 12mm, 1/2 in)", 0.008));
+               LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 19mm, 3/4 in)", 0.0012));
+               LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 25mm, 1 in)",   0.0016));
+               LINE_MATERIAL.add(new Material.Line("Braided nylon (2 mm, 1/16 in)",    0.001));
+               LINE_MATERIAL.add(new Material.Line("Braided nylon (3 mm, 1/8 in)",     0.0035));
+               LINE_MATERIAL.add(new Material.Line("Tubular nylon (11 mm, 7/16 in)",   0.013));
+               LINE_MATERIAL.add(new Material.Line("Tubular nylon (14 mm, 9/16 in)",   0.016));
+               LINE_MATERIAL.add(new Material.Line("Tubular nylon (25 mm, 1 in)",              0.029));
+       }
+       
+       
+       /**
+        * Find a material from the database with the specified type and name.  Returns
+        * <code>null</code> if the specified material could not be found.
+        * 
+        * @param type  the material type.
+        * @param name  the material name in the database.
+        * @return              the material, or <code>null</code> if not found.
+        */
+       public static Material findMaterial(Material.Type type, String name) {
+               Database<Material> db;
+               switch (type) {
+               case BULK:
+                       db = BULK_MATERIAL;
+                       break;
+               case SURFACE:
+                       db = SURFACE_MATERIAL;
+                       break;
+               case LINE:
+                       db = LINE_MATERIAL;
+                       break;
+               default:
+                       throw new IllegalArgumentException("Illegal material type: "+type);
+               }
+               
+               for (Material m: db) {
+                       if (m.getName().equalsIgnoreCase(name)) {
+                               return m;
+                       }
+               }
+               return null;
+       }
+       
+       
+       /**
+        * Find a material from the database or return a new material if the specified
+        * material with the specified density is not found.
+        * 
+        * @param type          the material type.
+        * @param name          the material name.
+        * @param density       the density of the material.
+        * @return                      the material object from the database or a new material.
+        */
+       public static Material findMaterial(Material.Type type, String name, double density) {
+               Database<Material> db;
+               switch (type) {
+               case BULK:
+                       db = BULK_MATERIAL;
+                       break;
+               case SURFACE:
+                       db = SURFACE_MATERIAL;
+                       break;
+               case LINE:
+                       db = LINE_MATERIAL;
+                       break;
+               default:
+                       throw new IllegalArgumentException("Illegal material type: "+type);
+               }
+
+               for (Material m: db) {
+                       if (m.getName().equalsIgnoreCase(name) && MathUtil.equals(m.getDensity(), density)) {
+                               return m;
+                       }
+               }
+               return Material.newMaterial(type, name, density);
+       }       
+       
+       
+
+       /**
+        * Return all motor in the database matching a search criteria.  Any search criteria that
+        * is null or NaN is ignored.
+        * 
+        * @param type                  the motor type, or null.
+        * @param manufacturer  the manufacturer, or null.
+        * @param designation   the designation, or null.
+        * @param diameter              the diameter, or NaN.
+        * @param length                the length, or NaN.
+        * @return                              an array of all the matching motors.
+        */
+       public static Motor[] findMotors(Motor.Type type, String manufacturer, String designation, double diameter, double length) {
+               ArrayList<Motor> results = new ArrayList<Motor>();
+               
+               for (Motor m: MOTOR) {
+                       boolean match = true;
+                       if (type != null  &&  type != m.getMotorType())
+                               match = false;
+                       else if (manufacturer != null  &&  !manufacturer.equalsIgnoreCase(m.getManufacturer()))
+                               match = false;
+                       else if (designation != null  &&  !designation.equalsIgnoreCase(m.getDesignation()))
+                               match = false;
+                       else if (!Double.isNaN(diameter)  &&  (Math.abs(diameter - m.getDiameter()) > 0.0015))
+                               match = false;
+                       else if (!Double.isNaN(length) && (Math.abs(length - m.getLength()) > 0.0015))
+                               match = false;
+                       
+                       if (match)
+                               results.add(m);
+               }
+               
+               return results.toArray(new Motor[0]);
+       }
+
+}
diff --git a/src/net/sf/openrocket/document/OpenRocketDocument.java b/src/net/sf/openrocket/document/OpenRocketDocument.java
new file mode 100644 (file)
index 0000000..ae6c886
--- /dev/null
@@ -0,0 +1,370 @@
+package net.sf.openrocket.document;
+//TODO: LOW: move class somewhere else?
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.util.Icons;
+
+
+public class OpenRocketDocument implements ComponentChangeListener {
+       /**
+        * The minimum number of undo levels that are stored.
+        */
+       public static final int UNDO_LEVELS = 50;
+       /**
+        * The margin of the undo levels.  After the number of undo levels exceeds 
+        * UNDO_LEVELS by this amount the undo is purged to that length.
+        */
+       public static final int UNDO_MARGIN = 10;
+
+       
+       private final Rocket rocket;
+       private final Configuration configuration;
+
+       private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
+       
+       
+       private int undoPosition = -1;  // Illegal position, init in constructor
+       private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
+       private LinkedList<String> undoDescription = new LinkedList<String>();
+       
+       private String nextDescription = null;
+       
+       
+       private File file = null;
+       private int savedID = -1;
+       
+       private final StorageOptions storageOptions = new StorageOptions();
+       
+       
+       /* These must be initialized after undo history is set up. */
+       private final UndoRedoAction undoAction;
+       private final UndoRedoAction redoAction;
+               
+       
+       public OpenRocketDocument(Rocket rocket) {
+               this(rocket.getDefaultConfiguration());
+       }
+       
+
+       private OpenRocketDocument(Configuration configuration) {
+               this.configuration = configuration;
+               this.rocket = configuration.getRocket();
+               
+               undoHistory.add(rocket.copy());
+               undoDescription.add(null);
+               undoPosition = 0;
+               
+               undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
+               redoAction = new UndoRedoAction(UndoRedoAction.REDO);
+               
+               rocket.addComponentChangeListener(this);
+               
+               
+       }
+       
+       
+       
+       
+       public Rocket getRocket() {
+               return rocket;
+       }
+
+       
+       public Configuration getDefaultConfiguration() {
+               return configuration;
+       }
+
+
+       public File getFile() {
+               return file;
+       }
+
+       public void setFile(File file) {
+               this.file = file;
+       }
+       
+
+       public boolean isSaved() {
+               return rocket.getModID() == savedID;
+       }
+
+       public void setSaved(boolean saved) {
+               if (saved == false)
+                       this.savedID = -1;
+               else
+                       this.savedID = rocket.getModID();
+       }
+       
+       /**
+        * Retrieve the default storage options for this document.
+        * 
+        * @return      the storage options.
+        */
+       public StorageOptions getDefaultStorageOptions() {
+               return storageOptions;
+       }
+       
+       
+       
+       
+       
+       @SuppressWarnings("unchecked")
+       public List<Simulation> getSimulations() {
+               return (ArrayList<Simulation>)simulations.clone();
+       }
+       public int getSimulationCount() {
+               return simulations.size();
+       }
+       public Simulation getSimulation(int n) {
+               return simulations.get(n);
+       }
+       public int getSimulationIndex(Simulation simulation) {
+               return simulations.indexOf(simulation);
+       }
+       public void addSimulation(Simulation simulation) {
+               simulations.add(simulation);
+       }
+       public void addSimulation(Simulation simulation, int n) {
+               simulations.add(n, simulation);
+       }
+       public void removeSimulation(Simulation simulation) {
+               simulations.remove(simulation);
+       }
+       public Simulation removeSimulation(int n) {
+               return simulations.remove(n);
+       }
+       
+       
+       
+       /**
+        * Adds an undo point at this position.  This method should be called *before* any
+        * action that is to be undoable.  All actions after the call will be undone by a 
+        * single "Undo" action.
+        * <p>
+        * The description should be a short, descriptive string of the actions that will 
+        * follow.  This is shown to the user e.g. in the Edit-menu, for example 
+        * "Undo (Modify body tube)".  If the actions are not known (in general should not
+        * be the case!) description may be null.
+        * <p>
+        * If this method is called successively without any change events occurring between the
+        * calls, only the last call will have any effect.
+        * 
+        * @param description A short description of the following actions.
+        */
+       public void addUndoPosition(String description) {
+
+               // Check whether modifications have been done since last call
+               if (isCleanState()) {
+                       // No modifications
+                       nextDescription = description;
+                       return;
+               }
+
+               
+               /*
+                * Modifications have been made to the rocket.  We should be at the end of the
+                * undo history, but check for consistency.
+                */
+               assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
+               while (undoPosition < undoHistory.size()-1) {
+                       undoHistory.removeLast();
+                       undoDescription.removeLast();
+               }
+               
+               
+               // Add the current state to the undo history
+               undoHistory.add(rocket.copy());
+               undoDescription.add(description);
+               nextDescription = description;
+               undoPosition++;
+               
+               
+               // Maintain maximum undo size
+               if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) {
+                       for (int i=0; i < UNDO_MARGIN+1; i++) {
+                               undoHistory.removeFirst();
+                               undoDescription.removeFirst();
+                               undoPosition--;
+                       }
+               }
+       }
+
+       
+       public Action getUndoAction() {
+               return undoAction;
+       }
+       
+       
+       public Action getRedoAction() {
+               return redoAction;
+       }
+       
+       
+       @Override
+       public void componentChanged(ComponentChangeEvent e) {
+               
+               if (!e.isUndoChange()) {
+                       // Remove any redo information if available
+                       while (undoPosition < undoHistory.size()-1) {
+                               undoHistory.removeLast();
+                               undoDescription.removeLast();
+                       }
+                       
+                       // Set the latest description
+                       undoDescription.set(undoPosition, nextDescription);
+               }
+               
+               undoAction.setAllValues();
+               redoAction.setAllValues();
+       }
+
+       
+       public boolean isUndoAvailable() {
+               if (undoPosition > 0)
+                       return true;
+               
+               return !isCleanState();
+       }
+       
+       public String getUndoDescription() {
+               if (!isUndoAvailable())
+                       return null;
+               
+               if (isCleanState()) {
+                       return undoDescription.get(undoPosition-1);
+               } else {
+                       return undoDescription.get(undoPosition);
+               }
+       }
+
+       
+       public boolean isRedoAvailable() {
+               return undoPosition < undoHistory.size()-1;
+       }
+       
+       public String getRedoDescription() {
+               if (!isRedoAvailable())
+                       return null;
+               
+               return undoDescription.get(undoPosition);
+       }
+       
+       
+       
+       public void undo() {
+               if (!isUndoAvailable()) {
+                       throw new IllegalStateException("Undo not available.");
+               }
+
+               // Update history position
+               
+               if (isCleanState()) {
+                       // We are in a clean state, simply move backwards in history
+                       undoPosition--;
+               } else {
+                       // Modifications have been made, save the state and restore previous state
+                       undoHistory.add(rocket.copy());
+                       undoDescription.add(null);
+               }
+               
+               rocket.loadFrom(undoHistory.get(undoPosition).copy());
+       }
+       
+       
+       public void redo() {
+               if (!isRedoAvailable()) {
+                       throw new IllegalStateException("Redo not available.");
+               }
+               
+               undoPosition++;
+               
+               rocket.loadFrom(undoHistory.get(undoPosition).copy());
+       }
+       
+       
+       private boolean isCleanState() {
+               return rocket.getModID() == undoHistory.get(undoPosition).getModID();
+       }
+       
+       
+       
+       
+       
+       
+       /**
+        * Inner class to implement undo/redo actions.
+        */
+       private class UndoRedoAction extends AbstractAction {
+               public static final int UNDO = 1;
+               public static final int REDO = 2;
+               
+               private final int type;
+               
+               // Sole constructor
+               public UndoRedoAction(int type) {
+                       if (type != UNDO && type != REDO) {
+                               throw new IllegalArgumentException("Unknown type = "+type);
+                       }
+                       this.type = type;
+                       setAllValues();
+               }
+
+               
+               // Actual action to make
+               public void actionPerformed(ActionEvent e) {
+                       switch (type) {
+                       case UNDO:
+                               undo();
+                               break;
+                               
+                       case REDO:
+                               redo();
+                               break;
+                       }
+               }
+
+               
+               // Set all the values correctly (name and enabled/disabled status)
+               public void setAllValues() {
+                       String name,desc;
+                       boolean enabled;
+                       
+                       switch (type) {
+                       case UNDO:
+                               name = "Undo";
+                               desc = getUndoDescription();
+                               enabled = isUndoAvailable();
+                               this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
+                               break;
+                               
+                       case REDO:
+                               name = "Redo";
+                               desc = getRedoDescription();
+                               enabled = isRedoAvailable();
+                               this.putValue(SMALL_ICON, Icons.EDIT_REDO);
+                               break;
+                               
+                       default:
+                               throw new RuntimeException("EEEK!");
+                       }
+                       
+                       if (desc != null)
+                               name = name + " ("+desc+")";
+                       
+                       putValue(NAME, name);
+                       setEnabled(enabled);
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/document/Simulation.java b/src/net/sf/openrocket/document/Simulation.java
new file mode 100644 (file)
index 0000000..4c4cf2c
--- /dev/null
@@ -0,0 +1,361 @@
+package net.sf.openrocket.document;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightSimulator;
+import net.sf.openrocket.simulation.RK4Simulator;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationListenerException;
+import net.sf.openrocket.util.ChangeSource;
+
+
+public class Simulation implements ChangeSource {
+       
+       public static enum Status {
+               /** Up-to-date */
+               UPTODATE,
+               
+               /** Loaded from file, status probably up-to-date */
+               LOADED,
+               
+               /** Data outdated */
+               OUTDATED,
+               
+               /** Imported external data */
+               EXTERNAL,
+               
+               /** Not yet simulated */
+               NOT_SIMULATED
+       }
+       
+
+       private final Rocket rocket;
+       
+       private String name = "";
+
+       private Status status = Status.NOT_SIMULATED;
+       
+       /** The conditions to use */
+       private final SimulationConditions conditions;
+       
+       private List<String> simulationListeners = new ArrayList<String>();
+       
+       private Class<? extends FlightSimulator> simulatorClass = RK4Simulator.class;
+       private Class<? extends AerodynamicCalculator> calculatorClass = BarrowmanCalculator.class;
+
+       
+       
+       /** Listeners for this object */
+       private final List<ChangeListener> listeners = new ArrayList<ChangeListener>();
+       
+       
+       /** The conditions actually used in the previous simulation, or null */
+       private SimulationConditions simulatedConditions = null;
+       private String simulatedMotors = null;
+       private FlightData simulatedData = null;
+       private int simulatedRocketID = -1;
+       
+       
+       /**
+        * Create a new simulation for the rocket.  The initial motor configuration is
+        * taken from the default rocket configuration.
+        * 
+        * @param rocket        the rocket associated with the simulation.
+        */
+       public Simulation(Rocket rocket) {
+               this.rocket = rocket;
+               this.status = Status.NOT_SIMULATED;
+               
+               conditions = new SimulationConditions(rocket);
+               conditions.setMotorConfigurationID(
+                               rocket.getDefaultConfiguration().getMotorConfigurationID());
+               conditions.addChangeListener(new ConditionListener());
+       }
+       
+       
+       public Simulation(Rocket rocket, Status status, String name, SimulationConditions conditions,
+                       List<String> listeners, FlightData data) {
+               
+               if (rocket == null) 
+                       throw new IllegalArgumentException("rocket cannot be null");
+               if (status == null) 
+                       throw new IllegalArgumentException("status cannot be null");
+               if (name == null) 
+                       throw new IllegalArgumentException("name cannot be null");
+               if (conditions == null) 
+                       throw new IllegalArgumentException("conditions cannot be null");
+               
+               this.rocket = rocket;
+               
+               if (status == Status.UPTODATE) {
+                       this.status = Status.LOADED;
+               } else if (data == null) {
+                       this.status = Status.NOT_SIMULATED;
+               } else {
+                       this.status = status;
+               }
+               
+               this.name = name;
+               
+               this.conditions = conditions;
+               conditions.addChangeListener(new ConditionListener());
+               
+               if (listeners != null) {
+                       this.simulationListeners.addAll(listeners);
+               }
+               
+               
+               if (data != null && this.status != Status.NOT_SIMULATED) {
+                       simulatedData = data;
+                       if (this.status == Status.LOADED) {
+                               simulatedConditions = conditions.clone();
+                               simulatedRocketID = rocket.getModID();
+                       }
+               }
+               
+       }
+       
+       
+       
+
+       /**
+        * Return a newly created Configuration for this simulation.  The configuration
+        * has the motor ID set and all stages active.
+        * 
+        * @return      a newly created Configuration of the launch conditions.
+        */
+       public Configuration getConfiguration() {
+               Configuration c = new Configuration(rocket);
+               c.setMotorConfigurationID(conditions.getMotorConfigurationID());
+               c.setAllStages();
+               return c;
+       }
+       
+       /**
+        * Returns the simulation conditions attached to this simulation.  The conditions
+        * may be modified freely, and the status of the simulation will change to reflect
+        * the changes.
+        * 
+        * @return the simulation conditions.
+        */
+       public SimulationConditions getConditions() {
+               return conditions;
+       }
+
+       
+       /**
+        * Get the list of simulation listeners.  The returned list is the one used by
+        * this object; changes to it will reflect changes in the simulation.
+        * 
+        * @return      the actual list of simulation listeners.
+        */
+       public List<String> getSimulationListeners() {
+               return simulationListeners;
+       }
+       
+       
+       /**
+        * Return the user-defined name of the simulation.
+        * 
+        * @return      the name for the simulation.
+        */
+       public String getName() {
+               return name;
+       }
+       
+       /**
+        * Set the user-defined name of the simulation.  Setting the name to
+        * null yields an empty name.
+        * 
+        * @param name  the name of the simulation.
+        */
+       public void setName(String name) {
+               if (this.name.equals(name))
+                       return;
+               
+               if (name == null)
+                       this.name = "";
+               else
+                       this.name = name;
+               
+               fireChangeEvent();
+       }
+
+
+       /**
+        * Returns the status of this simulation.  This method examines whether the
+        * simulation has been outdated and returns {@link Status#OUTDATED} accordingly.
+        * 
+        * @return the status
+        * @see Status
+        */
+       public Status getStatus() {
+               if (status == Status.UPTODATE || status == Status.LOADED) {
+                       if (rocket.getFunctionalModID() != simulatedRocketID || 
+                                       !conditions.equals(simulatedConditions))
+                               return Status.OUTDATED;
+               }
+               
+               return status;
+       }
+
+       
+       
+       
+       public void simulate(SimulationListener ... additionalListeners) 
+                                               throws SimulationException {
+               
+               if (this.status == Status.EXTERNAL) {
+                       throw new SimulationException("Cannot simulate imported simulation.");
+               }
+               Configuration configuration;
+               AerodynamicCalculator calculator;
+               FlightSimulator simulator;
+       
+               try {
+                       calculator = calculatorClass.newInstance();
+                       simulator = simulatorClass.newInstance();
+               } catch (InstantiationException e) {
+                       throw new IllegalStateException("Cannot instantiate calculator/simulator.",e);
+               } catch (IllegalAccessException e) {
+                       throw new IllegalStateException("Cannot access calc/sim instance?! BUG!",e);
+               } catch (NullPointerException e) {
+                       throw new IllegalStateException("Calculator or simulator null",e);
+               }
+
+               configuration = this.getConfiguration();
+               calculator.setConfiguration(configuration);
+               simulator.setCalculator(calculator);
+               
+               for (SimulationListener l: additionalListeners) {
+                       simulator.addSimulationListener(l);
+               }
+               
+               for (String className: simulationListeners) {
+                       SimulationListener l = null;
+                       try {
+                               Class<?> c = Class.forName(className);
+                               l = (SimulationListener)c.newInstance();
+                       } catch (Exception e) {
+                               throw new SimulationListenerException("Could not instantiate listener of " +
+                                               "class: " + className);
+                       }
+                       simulator.addSimulationListener(l);
+               }
+               
+               long t1, t2;
+               System.out.println("Simulation: calling simulator");
+               t1 = System.currentTimeMillis();
+               simulatedData = simulator.simulate(conditions);
+               t2 = System.currentTimeMillis();
+               System.out.println("Simulation: returning from simulator, " +
+                               "simulation took "+(t2-t1)+"ms");
+               
+               // Set simulated info after simulation, will not be set in case of exception
+               simulatedConditions = conditions.clone();
+               simulatedMotors = configuration.getMotorConfigurationDescription();
+               simulatedRocketID = rocket.getFunctionalModID();
+
+               status = Status.UPTODATE;
+               fireChangeEvent();
+       }
+
+       
+       /**
+        * Return the conditions used in the previous simulation, or <code>null</code>
+        * if this simulation has not been run.
+        * 
+        * @return      the conditions used in the previous simulation, or <code>null</code>.
+        */
+       public SimulationConditions getSimulatedConditions() {
+               return simulatedConditions;
+       }
+       
+       /**
+        * Return the warnings generated in the previous simulation, or
+        * <code>null</code> if this simulation has not been run.  This is the same
+        * warning set as contained in the <code>FlightData</code> object.
+        * 
+        * @return      the warnings during the previous simulation, or <code>null</code>.
+        * @see         FlightData#getWarningSet()
+        */
+       public WarningSet getSimulatedWarnings() {
+               if (simulatedData == null)
+                       return null;
+               return simulatedData.getWarningSet();
+       }
+       
+       
+       /**
+        * Return a string describing the motor configuration of the previous simulation,
+        * or <code>null</code> if this simulation has not been run.
+        * 
+        * @return      a description of the motor configuration of the previous simulation, or
+        *                      <code>null</code>.
+        * @see         Rocket#getMotorConfigurationDescription(String)
+        */
+       public String getSimulatedMotorDescription() {
+               return simulatedMotors;
+       }
+       
+       /**
+        * Return the flight data of the previous simulation, or <code>null</code> if
+        * this simulation has not been run.
+        * 
+        * @return      the flight data of the previous simulation, or <code>null</code>.
+        */
+       public FlightData getSimulatedData() {
+               return simulatedData;
+       }
+       
+       
+
+       
+       
+
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listeners.add(listener);
+       }
+
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listeners.remove(listener);
+       }
+       
+       protected void fireChangeEvent() {
+               ChangeListener[] ls = listeners.toArray(new ChangeListener[0]);
+               ChangeEvent e = new ChangeEvent(this);
+               for (ChangeListener l: ls) {
+                       l.stateChanged(e);
+               }
+       }
+       
+
+
+       
+       private class ConditionListener implements ChangeListener {
+
+               private Status oldStatus = null;
+               
+               @Override
+               public void stateChanged(ChangeEvent e) {
+                       if (getStatus() != oldStatus) {
+                               oldStatus = getStatus();
+                               fireChangeEvent();
+                       }
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/document/StorageOptions.java b/src/net/sf/openrocket/document/StorageOptions.java
new file mode 100644 (file)
index 0000000..3a817ca
--- /dev/null
@@ -0,0 +1,51 @@
+package net.sf.openrocket.document;
+
+public class StorageOptions implements Cloneable {
+       
+       public static final double SIMULATION_DATA_NONE = Double.POSITIVE_INFINITY;
+       public static final double SIMULATION_DATA_ALL = 0;
+       
+       private boolean compressionEnabled = true;
+       
+       private double simulationTimeSkip = SIMULATION_DATA_NONE;
+
+       private boolean explicitlySet = false;
+       
+
+       public boolean isCompressionEnabled() {
+               return compressionEnabled;
+       }
+
+       public void setCompressionEnabled(boolean compression) {
+               this.compressionEnabled = compression;
+       }
+
+       public double getSimulationTimeSkip() {
+               return simulationTimeSkip;
+       }
+
+       public void setSimulationTimeSkip(double simulationTimeSkip) {
+               this.simulationTimeSkip = simulationTimeSkip;
+       }
+       
+       
+       
+       public boolean isExplicitlySet() {
+               return explicitlySet;
+       }
+
+       public void setExplicitlySet(boolean explicitlySet) {
+               this.explicitlySet = explicitlySet;
+       }
+
+       
+       
+       @Override
+       public StorageOptions clone() {
+               try {
+                       return (StorageOptions)super.clone();
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("CloneNotSupportedException?!?", e);
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/file/GeneralRocketLoader.java b/src/net/sf/openrocket/file/GeneralRocketLoader.java
new file mode 100644 (file)
index 0000000..bba71d2
--- /dev/null
@@ -0,0 +1,71 @@
+package net.sf.openrocket.file;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+/**
+ * A rocket loader that auto-detects the document type and uses the appropriate
+ * loading.  Supports loading of GZIPed files as well with transparent
+ * uncompression.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class GeneralRocketLoader extends RocketLoader {
+
+       private static final int READ_BYTES = 300;
+       
+       private static final byte[] GZIP_SIGNATURE = { 31, -117 };  // 0x1f, 0x8b
+       private static final byte[] OPENROCKET_SIGNATURE = "<openrocket".getBytes();
+       
+       private static final OpenRocketLoader openRocketLoader = new OpenRocketLoader();
+       
+       @Override
+       protected OpenRocketDocument loadFromStream(InputStream source) throws IOException,
+                       RocketLoadException {
+
+               // Check for mark() support
+               if (!source.markSupported()) {
+                       source = new BufferedInputStream(source);
+               }
+               
+               // Read using mark()
+               byte[] buffer = new byte[READ_BYTES];
+               int count;
+               source.mark(READ_BYTES + 10);
+               count = source.read(buffer);
+               source.reset();
+               
+               if (count < 10) {
+                       throw new RocketLoadException("Unsupported or corrupt file.");
+               }
+               
+               // Detect the appropriate loader
+
+               // Check for GZIP
+               if (buffer[0] == GZIP_SIGNATURE[0]  &&  buffer[1] == GZIP_SIGNATURE[1]) {
+                       OpenRocketDocument doc = loadFromStream(new GZIPInputStream(source));
+                       doc.getDefaultStorageOptions().setCompressionEnabled(true);
+                       return doc;
+               }
+               
+               // Check for OpenRocket
+               int match = 0;
+               for (int i=0; i < count; i++) {
+                       if (buffer[i] == OPENROCKET_SIGNATURE[match]) {
+                               match++;
+                               if (match == OPENROCKET_SIGNATURE.length) {
+                                       return openRocketLoader.load(source);
+                               }
+                       } else {
+                               match = 0;
+                       }
+               }
+               
+               throw new RocketLoadException("Unsupported or corrupt file.");
+       }
+}
diff --git a/src/net/sf/openrocket/file/Loader.java b/src/net/sf/openrocket/file/Loader.java
new file mode 100644 (file)
index 0000000..005d4d6
--- /dev/null
@@ -0,0 +1,11 @@
+package net.sf.openrocket.file;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+
+public interface Loader<T> {
+
+       public Collection<T> load(InputStream stream, String filename) throws IOException;
+       
+}
diff --git a/src/net/sf/openrocket/file/MotorLoader.java b/src/net/sf/openrocket/file/MotorLoader.java
new file mode 100644 (file)
index 0000000..056f23b
--- /dev/null
@@ -0,0 +1,445 @@
+package net.sf.openrocket.file;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.ThrustCurveMotor;
+import net.sf.openrocket.util.Coordinate;
+
+
+public class MotorLoader implements Loader<Motor> {
+       
+       /** The charset used when reading RASP files. */
+       public static final String RASP_CHARSET = "ISO-8859-1";
+
+       
+       
+       public List<Motor> load(InputStream stream, String filename) throws IOException {
+               return loadMotor(stream, filename);
+       }
+       
+       
+       /**
+        * Load <code>Motor</code> objects from the specified <code>InputStream</code>.
+        * The file type is detected based on the filename extension. 
+        * 
+        * @param stream        the stream from which to read the file.
+        * @param filename      the file name, by which the format is detected.
+        * @return                      a list of <code>Motor</code> objects defined in the file.
+        * @throws IOException  if an I/O exception occurs, the file format is unknown
+        *                                              or illegal.
+        */
+       public static List<Motor> loadMotor(InputStream stream, 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 loadRASP(stream);
+               }
+               
+               throw new IOException("Unknown file type.");
+       }
+       
+       
+       
+       
+       //////////////  RASP file format  //////////////
+       
+       
+       /** Manufacturer codes to expand in RASP files */
+       private static final Map<String,String> manufacturerCodes =
+               new HashMap<String,String>();
+       static {
+               manufacturerCodes.put("A", "AeroTech");
+               manufacturerCodes.put("AT", "AeroTech");
+               manufacturerCodes.put("AT-RMS", "AeroTech");
+               manufacturerCodes.put("AT/RCS", "AeroTech");
+               manufacturerCodes.put("AERO", "AeroTech");
+               manufacturerCodes.put("AEROT", "AeroTech");
+               manufacturerCodes.put("ISP", "AeroTech");
+               manufacturerCodes.put("AEROTECH", "AeroTech");
+               manufacturerCodes.put("AEROTECH/APOGEE", "AeroTech");
+               manufacturerCodes.put("AMW", "Animal Motor Works");
+               manufacturerCodes.put("AW", "Animal Motor Works");
+               manufacturerCodes.put("ANIMAL", "Animal Motor Works");
+               manufacturerCodes.put("AP", "Apogee");
+               manufacturerCodes.put("APOG", "Apogee");
+               manufacturerCodes.put("P", "Apogee");
+               manufacturerCodes.put("CES", "Cesaroni");
+               manufacturerCodes.put("CTI", "Cesaroni");
+               manufacturerCodes.put("CS", "Cesaroni");
+               manufacturerCodes.put("CSR", "Cesaroni");
+               manufacturerCodes.put("PRO38", "Cesaroni");
+               manufacturerCodes.put("CR", "Contrail Rocket");
+               manufacturerCodes.put("CONTR", "Contrail Rocket");
+               manufacturerCodes.put("E", "Estes");
+               manufacturerCodes.put("ES", "Estes");
+               manufacturerCodes.put("EM", "Ellis Mountain");
+               manufacturerCodes.put("ELLIS", "Ellis Mountain");
+               manufacturerCodes.put("GR", "Gorilla Rocket Motors");
+               manufacturerCodes.put("GORILLA", "Gorilla Rocket Motors");
+               manufacturerCodes.put("H", "HyperTEK");
+               manufacturerCodes.put("HT", "HyperTEK");
+               manufacturerCodes.put("HYPER", "HyperTEK");
+               manufacturerCodes.put("HYPERTEK", "HyperTEK");
+               manufacturerCodes.put("K", "Kosdon by AeroTech");
+               manufacturerCodes.put("KBA", "Kosdon by AeroTech");
+               manufacturerCodes.put("K/AT", "Kosdon by AeroTech");
+               manufacturerCodes.put("KOSDON", "Kosdon by AeroTech");
+               manufacturerCodes.put("KOSDON/AT", "Kosdon by AeroTech");
+               manufacturerCodes.put("KOSDON-BY-AEROTECH", "Kosdon by AeroTech");
+               manufacturerCodes.put("LOKI", "Loki Research");
+               manufacturerCodes.put("LR", "Loki Research");
+               manufacturerCodes.put("PM", "Public Missiles");
+               manufacturerCodes.put("PML", "Public Missiles");
+               manufacturerCodes.put("PP", "Propulsion Polymers");
+               manufacturerCodes.put("PROP", "Propulsion Polymers");
+               manufacturerCodes.put("PROPULSION", "Propulsion Polymers");
+               manufacturerCodes.put("PROPULSION-POLYMERS", "Propulsion Polymers");
+               manufacturerCodes.put("Q", "Quest");
+               manufacturerCodes.put("QU", "Quest");
+               manufacturerCodes.put("RATT", "RATT Works");
+               manufacturerCodes.put("RT", "RATT Works");
+               manufacturerCodes.put("RTW", "RATT Works");
+               manufacturerCodes.put("RR", "Roadrunner Rocketry");
+               manufacturerCodes.put("ROADRUNNER", "Roadrunner Rocketry");
+               manufacturerCodes.put("RV", "Rocketvision");
+               manufacturerCodes.put("SR", "Sky Ripper Systems");
+               manufacturerCodes.put("SRS", "Sky Ripper Systems");
+               manufacturerCodes.put("SKYR", "Sky Ripper Systems");
+               manufacturerCodes.put("SKYRIPPER", "Sky Ripper Systems");
+               manufacturerCodes.put("WCH", "West Coast Hybrids");
+               manufacturerCodes.put("WCR", "West Coast Hybrids");
+               
+               manufacturerCodes.put("SF", "WECO Feuerwerk");  // Previously Sachsen Feuerwerks
+               manufacturerCodes.put("WECO", "WECO Feuerwerk");
+               
+       }
+       
+       /**
+        * A helper method to load a <code>Motor</code> from a RASP file, read from the
+        * specified <code>InputStream</code>.  The charset used is defined in 
+        * {@link #RASP_CHARSET}.
+        * 
+        * @param stream        the InputStream to read.
+        * @return                      the <code>Motor</code> object. 
+        * @throws IOException  if an I/O error occurs or if the file format is illegal.
+        * @see #loadRASP(Reader)
+        */
+       public static List<Motor> loadRASP(InputStream stream) throws IOException {
+               return loadRASP(new InputStreamReader(stream, RASP_CHARSET));
+       }
+       
+       
+       
+       /**
+        * Load a <code>Motor</code> from a RASP file specified by the <code>Reader</code>.
+        * The <code>Reader</code> is responsible for using the correct charset.
+        * <p>
+        * The CG is assumed to be located at the center of the motor casing and the mass
+        * is calculated from the thrust curve by assuming a constant exhaust velocity.
+        * 
+        * @param reader  the source of the file.
+        * @return                a list of the {@link Motor} objects defined in the file.
+        * @throws IOException  if an I/O error occurs or if the file format is illegal.
+        */
+       public static List<Motor> loadRASP(Reader reader) throws IOException {
+               List<Motor> motors = new ArrayList<Motor>();
+               BufferedReader in = new BufferedReader(reader);
+
+               String manufacturer = "";
+               String designation = "";
+               String comment = "";
+               
+               double length = 0;
+               double diameter = 0;
+               ArrayList<Double> delays = null;
+               
+               List<Double> time = new ArrayList<Double>();
+               List<Double> thrust = new ArrayList<Double>();
+               
+               double propW = 0;
+               double totalW = 0;
+               
+               try {
+                       String line;
+                       String[] pieces, buf;
+
+                       line = in.readLine();
+                       main: while (line != null) {   // Until EOF
+
+                               manufacturer = "";
+                               designation = "";
+                               comment = "";
+                               length = 0;
+                               diameter = 0;
+                               delays = new ArrayList<Double>();
+                               propW = 0;
+                               totalW = 0;
+                               time.clear();
+                               thrust .clear();
+                       
+                               // Read comment
+                               while (line.length()==0 || line.charAt(0)==';') {
+                                       if (line.length() > 0) {
+                                               comment += line.substring(1).trim() + "\n";
+                                       }
+                                       line = in.readLine();
+                                       if (line == null)
+                                               break main;
+                               }
+                               comment = comment.trim();
+                               
+                               // Parse header line, example:
+                               // F32 24 124 5-10-15-P .0377 .0695 RV
+                               // desig diam len delays prop.w tot.w manufacturer
+                               pieces = split(line);
+                               if (pieces.length != 7) {
+                                       throw new IOException("Illegal file format.");
+                               }
+                               
+                               designation = pieces[0];
+                               diameter = Double.parseDouble(pieces[1]) / 1000.0;
+                               length = Double.parseDouble(pieces[2]) / 1000.0;
+                               
+                               if (pieces[3].equalsIgnoreCase("None")) {
+
+                               } else {
+                                       buf = split(pieces[3],"[-,]+");
+                                       for (int i=0; i < buf.length; i++) {
+                                               if (buf[i].equalsIgnoreCase("P")) {
+                                                       delays.add(Motor.PLUGGED);
+                                               } else {
+                                                       // Many RASP files have "100" as an only delay
+                                                       double d = Double.parseDouble(buf[i]);
+                                                       if (d < 99)
+                                                               delays.add(d);
+                                               }
+                                       }
+                                       Collections.sort(delays);
+                               }
+                               
+                               propW = Double.parseDouble(pieces[4]);
+                               totalW = Double.parseDouble(pieces[5]);
+                               if (manufacturerCodes.containsKey(pieces[6].toUpperCase())) {
+                                       manufacturer = manufacturerCodes.get(pieces[6].toUpperCase());
+                               } else {
+                                       manufacturer = pieces[6].replace('_', ' ');
+                               }
+                               
+                               // Read the data
+                               for (line = in.readLine(); 
+                                        (line != null) && (line.length()==0 || line.charAt(0) != ';');
+                                        line = in.readLine()) {
+                                       
+                                       buf = split(line);
+                                       if (buf.length == 0) {
+                                               continue;
+                                       } else if (buf.length == 2) {
+                                               
+                                               time.add(Double.parseDouble(buf[0]));
+                                               thrust .add(Double.parseDouble(buf[1]));
+                                               
+                                       } else {
+                                               throw new IOException("Illegal file format.");
+                                       }
+                               }
+                               
+                               // Comment of EOF encountered, marks the start of the next motor
+                               if (time.size() < 2) {
+                                       throw new IOException("Illegal file format, too short thrust-curve.");
+                               }
+                               double[] delayArray = new double[delays.size()];
+                               for (int i=0; i<delays.size(); i++) {
+                                       delayArray[i] = delays.get(i);
+                               }
+                               motors.add(createRASPMotor(manufacturer, designation, comment,
+                                               length, diameter, delayArray, propW, totalW, time, thrust));
+                       }
+                       
+               } catch (NumberFormatException e) {
+                       
+                       throw new IOException("Illegal file format.");
+                       
+               } finally {
+                       
+                       in.close();
+                       
+               }
+               
+               return motors;
+       }
+       
+       
+       /**
+        * Create a motor from RASP file data.
+        * @throws IOException  if the data is illegal for a thrust curve
+        */
+       private static Motor createRASPMotor(String manufacturer, String designation,
+                       String comment, double length, double diameter, double[] delays,
+                       double propW, double totalW, List<Double> time, List<Double> thrust) 
+                       throws IOException {
+               
+               // Add zero time/thrust if necessary
+               if (time.get(0) > 0) {
+                       time.add(0, 0.0);
+                       thrust.add(0, 0.0);
+               }
+               
+               List<Double> mass = calculateMass(time,thrust,totalW,propW);
+               
+               double[] timeArray = new double[time.size()];
+               double[] thrustArray = new double[time.size()];
+               Coordinate[] cgArray = new Coordinate[time.size()];
+               for (int i=0; i < time.size(); i++) {
+                       timeArray[i] = time.get(i);
+                       thrustArray[i] = thrust.get(i);
+                       cgArray[i] = new Coordinate(length/2,0,0,mass.get(i));
+               }
+               
+               try {
+                       
+                       return new ThrustCurveMotor(manufacturer, designation, comment, Motor.Type.UNKNOWN,
+                                       delays, diameter, length, timeArray, thrustArray, cgArray);
+                       
+               } catch (IllegalArgumentException e) {
+                       
+                       // Bad data read from file.
+                       throw new IOException("Illegal file format.", e);
+                       
+               }
+       }
+       
+       
+       
+       /**
+        * Calculate the mass of a motor at distinct points in time based on the
+        * initial total mass, propellant weight and thrust.
+        * <p>
+        * This calculation assumes that the velocity of the exhaust remains constant
+        * during the burning.  This derives from the mass-flow and thrust relation
+        * <pre>F = m' * v</pre>
+        *  
+        * @param time    list of time points
+        * @param thrust  thrust at the discrete times
+        * @param total   total weight of the motor
+        * @param prop    propellant amount consumed during burning
+        * @return                a list of the mass at the specified time points
+        */
+       private static List<Double> calculateMass(List<Double> time, List<Double> thrust,
+                       double total, double prop) {
+               List<Double> mass = new ArrayList<Double>();
+               List<Double> deltam = new ArrayList<Double>();
+
+               double t0, f0;
+               double totalMassChange = 0;
+               double scale;
+
+               // First calculate mass change between points
+               t0 = time.get(0);
+               f0 = thrust.get(0);
+               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);
+                       deltam.add(dm);
+                       totalMassChange += dm;
+               }
+               
+               // Scale mass change and calculate mass
+               mass.add(total);
+               scale = prop / totalMassChange;
+               for (double dm: deltam) {
+                       total -= dm*scale;
+                       mass.add(total);
+               }
+               
+               return mass;
+       }
+       
+       
+       /**
+        * Tokenizes a string using whitespace as the delimiter.
+        */
+       private static String[] split(String str) {
+               return split(str,"\\s+");
+       }
+       
+       /**
+        * Tokenizes a string using the given delimiter.
+        */
+       private static String[] split(String str, String delim) {
+               String[] pieces = str.split(delim);
+               if (pieces.length==0 || !pieces[0].equals(""))
+                       return pieces;
+               return Arrays.copyOfRange(pieces, 1, pieces.length);
+       }
+       
+       
+       
+       
+       
+       /**
+        * For testing purposes.
+        */
+       public static void main(String[] args) throws IOException {
+               List<Motor> motors;
+               
+               if (args.length != 1) {
+                       System.out.println("Run with one argument, the RAPS file.");
+                       System.exit(1);
+               }
+               
+               motors = loadRASP(new FileInputStream(new File(args[0])));
+               
+               for (Motor motor: motors) {
+                       double time = motor.getTotalTime();
+
+                       System.out.println("Motor " + motor);
+                       System.out.println("Manufacturer:    "+motor.getManufacturer());
+                       System.out.println("Designation:     "+motor.getDesignation());
+                       System.out.println("Type:            "+motor.getMotorType().getName());
+                       System.out.printf( "Length:          %.1f mm\n",motor.getLength()*1000);
+                       System.out.printf( "Diameter:        %.1f mm\n",motor.getDiameter()*1000);
+                       System.out.println("Comment:\n" + motor.getDescription());
+
+                       System.out.printf( "Total burn time: %.2f s\n", time);
+                       System.out.printf( "Avg. burn time:  %.2f s\n", motor.getAverageTime());
+                       System.out.printf( "Avg. thrust:     %.2f N\n", motor.getAverageThrust());
+                       System.out.printf( "Max. thrust:     %.2f N\n", motor.getMaxThrust());
+                       System.out.printf( "Total impulse:   %.2f Ns\n", motor.getTotalImpulse());
+                       System.out.println("Delay times:     " + 
+                                       Arrays.toString(motor.getStandardDelays()));
+                       System.out.println("");
+                       
+                       final double COUNT = 20;
+                       for (int i=0; i <= COUNT; i++) {
+                               double t = time * i/COUNT;
+                               System.out.printf("t=%.2fs F=%.2fN m=%.4fkg\n",
+                                               t, motor.getThrust(t), motor.getMass(t));
+                       }
+                       System.out.println("");
+               }
+               
+       }
+}
diff --git a/src/net/sf/openrocket/file/OpenRocketLoader.java b/src/net/sf/openrocket/file/OpenRocketLoader.java
new file mode 100644 (file)
index 0000000..e1978a7
--- /dev/null
@@ -0,0 +1,2103 @@
+package net.sf.openrocket.file;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Stack;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.document.Simulation.Status;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.ClusterConfiguration;
+import net.sf.openrocket.rocketcomponent.Clusterable;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.InternalComponent;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.MassObject;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.ReferenceType;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.ShockCord;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Streamer;
+import net.sf.openrocket.rocketcomponent.StructuralComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.simulation.FlightEvent.Type;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.Reflection;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+
+/**
+ * Class that loads a rocket definition from an OpenRocket rocket file.
+ * <p>
+ * This class uses SAX to read the XML file format.  The 
+ * {@link #loadFromStream(InputStream)} method simply sets the system up and 
+ * starts the parsing, while the actual logic is in the private inner class
+ * <code>OpenRocketHandler</code>.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class OpenRocketLoader extends RocketLoader {
+       
+       @Override
+       public OpenRocketDocument loadFromStream(InputStream source) throws RocketLoadException,
+                       IOException {
+               InputSource xmlSource = new InputSource(source);
+               OpenRocketHandler handler = new OpenRocketHandler();
+
+               DelegatorHandler xmlhandler = new DelegatorHandler(handler, warnings);
+
+               try {
+                       XMLReader parser = XMLReaderFactory.createXMLReader();
+                       parser.setContentHandler(xmlhandler);
+                       parser.setErrorHandler(xmlhandler);
+                       parser.parse(xmlSource);
+               } catch (SAXException e) {
+                       throw new RocketLoadException("Malformed XML in input.", e);
+               }
+
+               
+               OpenRocketDocument doc = handler.getDocument();
+               doc.getDefaultConfiguration().setAllStages();
+               
+               // Deduce suitable time skip
+               double timeSkip = StorageOptions.SIMULATION_DATA_NONE;
+               for (Simulation s: doc.getSimulations()) {
+                       if (s.getStatus() == Simulation.Status.EXTERNAL ||
+                                       s.getStatus() == Simulation.Status.NOT_SIMULATED)
+                               continue;
+                       if (s.getSimulatedData() == null)
+                               continue;
+                       if (s.getSimulatedData().getBranchCount() == 0)
+                               continue;
+                       FlightDataBranch branch = s.getSimulatedData().getBranch(0);
+                       if (branch == null)
+                               continue;
+                       List<Double> list = branch.get(FlightDataBranch.TYPE_TIME);
+                       if (list == null)
+                               continue;
+                       
+                       double previousTime = Double.NaN;
+                       for (double time: list) {
+                               if (time - previousTime < timeSkip)
+                                       timeSkip = time-previousTime;
+                               previousTime = time;
+                       }
+               }
+               // Round value
+               timeSkip = Math.rint(timeSkip*100)/100;
+
+               doc.getDefaultStorageOptions().setSimulationTimeSkip(timeSkip);
+               doc.getDefaultStorageOptions().setCompressionEnabled(false); // Set by caller if compressed
+               doc.getDefaultStorageOptions().setExplicitlySet(false);
+               return doc;
+       }
+
+}
+
+
+
+class DocumentConfig {
+
+       /* Remember to update OpenRocketSaver as well! */
+       public static final String[] SUPPORTED_VERSIONS = { "0.9", "1.0" };
+
+
+       ////////  Component constructors
+       static final HashMap<String, Constructor<? extends RocketComponent>> constructors = new HashMap<String, Constructor<? extends RocketComponent>>();
+       static {
+               try {
+                       // External components
+                       constructors.put("bodytube", BodyTube.class.getConstructor(new Class<?>[0]));
+                       constructors.put("transition", Transition.class.getConstructor(new Class<?>[0]));
+                       constructors.put("nosecone", NoseCone.class.getConstructor(new Class<?>[0]));
+                       constructors.put("trapezoidfinset", TrapezoidFinSet.class.getConstructor(new Class<?>[0]));
+                       constructors.put("ellipticalfinset", EllipticalFinSet.class.getConstructor(new Class<?>[0]));
+                       constructors.put("freeformfinset", FreeformFinSet.class.getConstructor(new Class<?>[0]));
+                       constructors.put("launchlug", LaunchLug.class.getConstructor(new Class<?>[0]));
+
+                       // Internal components
+                       constructors.put("engineblock", EngineBlock.class.getConstructor(new Class<?>[0]));
+                       constructors.put("innertube", InnerTube.class.getConstructor(new Class<?>[0]));
+                       constructors.put("tubecoupler", TubeCoupler.class.getConstructor(new Class<?>[0]));
+                       constructors.put("bulkhead", Bulkhead.class.getConstructor(new Class<?>[0]));
+                       constructors.put("centeringring", CenteringRing.class.getConstructor(new Class<?>[0]));
+                       
+                       constructors.put("masscomponent", MassComponent.class.getConstructor(new Class<?>[0]));
+                       constructors.put("shockcord", ShockCord.class.getConstructor(new Class<?>[0]));
+                       constructors.put("parachute", Parachute.class.getConstructor(new Class<?>[0]));
+                       constructors.put("streamer", Streamer.class.getConstructor(new Class<?>[0]));
+                       
+                       // Other
+                       constructors.put("stage", Stage.class.getConstructor(new Class<?>[0]));
+                       
+               } catch (NoSuchMethodException e) {
+                       throw new RuntimeException(
+                                       "Error in constructing the 'constructors' HashMap.");
+               }
+       }
+
+
+       ////////  Parameter setters
+       /*
+        * The keys are of the form Class:param, where Class is the class name and param
+        * the element name.  Setters are searched for in descending class order.
+        * A setter of null means setting the parameter is not allowed.
+        */
+       static final HashMap<String, Setter> setters = new HashMap<String, Setter>();
+       static {
+               // RocketComponent
+               setters.put("RocketComponent:name", new StringSetter(
+                               Reflection.findMethodStatic(RocketComponent.class, "setName", String.class)));
+               setters.put("RocketComponent:color", new ColorSetter(
+                               Reflection.findMethodStatic(RocketComponent.class, "setColor", Color.class)));
+               setters.put("RocketComponent:linestyle", new EnumSetter<LineStyle>(
+                               Reflection.findMethodStatic(RocketComponent.class, "setLineStyle", LineStyle.class),
+                               LineStyle.class));
+               setters.put("RocketComponent:position", new PositionSetter());
+               setters.put("RocketComponent:overridemass", new OverrideSetter(
+                               Reflection.findMethodStatic(RocketComponent.class, "setOverrideMass", double.class),
+                               Reflection.findMethodStatic(RocketComponent.class, "setMassOverridden", boolean.class)));
+               setters.put("RocketComponent:overridecg", new OverrideSetter(
+                               Reflection.findMethodStatic(RocketComponent.class, "setOverrideCGX", double.class),
+                               Reflection.findMethodStatic(RocketComponent.class, "setCGOverridden", boolean.class)));
+               setters.put("RocketComponent:overridesubcomponents", new BooleanSetter(
+                               Reflection.findMethodStatic(RocketComponent.class, "setOverrideSubcomponents", boolean.class)));
+               setters.put("RocketComponent:comment", new StringSetter(
+                               Reflection.findMethodStatic(RocketComponent.class, "setComment", String.class)));
+               
+               // ExternalComponent
+               setters.put("ExternalComponent:finish", new EnumSetter<Finish>(
+                               Reflection.findMethodStatic(ExternalComponent.class, "setFinish", Finish.class),
+                               Finish.class));
+               setters.put("ExternalComponent:material", new MaterialSetter(
+                               Reflection.findMethodStatic(ExternalComponent.class, "setMaterial", Material.class),
+                               Material.Type.BULK));
+                               
+               // BodyComponent
+               setters.put("BodyComponent:length", new DoubleSetter(
+                               Reflection.findMethodStatic(BodyComponent.class, "setLength", double.class)));
+               
+               // SymmetricComponent
+               setters.put("SymmetricComponent:thickness", new DoubleSetter(
+                               Reflection.findMethodStatic(SymmetricComponent.class,"setThickness", double.class), 
+                               "filled", 
+                               Reflection.findMethodStatic(SymmetricComponent.class,"setFilled", boolean.class)));
+               
+               // BodyTube
+               setters.put("BodyTube:radius", new DoubleSetter(
+                               Reflection.findMethodStatic(BodyTube.class, "setRadius", double.class), 
+                               "auto",
+                               Reflection.findMethodStatic(BodyTube.class,"setRadiusAutomatic", boolean.class)));
+                               
+               // Transition
+               setters.put("Transition:shape", new EnumSetter<Transition.Shape>(
+                               Reflection.findMethodStatic(Transition.class, "setType", Transition.Shape.class),
+                               Transition.Shape.class));
+               setters.put("Transition:shapeclipped", new BooleanSetter(
+                               Reflection.findMethodStatic(Transition.class, "setClipped", boolean.class)));
+               setters.put("Transition:shapeparameter", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setShapeParameter", double.class)));
+                               
+               setters.put("Transition:foreradius", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setForeRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(Transition.class, "setForeRadiusAutomatic", boolean.class)));
+               setters.put("Transition:aftradius", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setAftRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(Transition.class, "setAftRadiusAutomatic", boolean.class)));
+
+               setters.put("Transition:foreshoulderradius", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setForeShoulderRadius", double.class)));
+               setters.put("Transition:foreshoulderlength", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setForeShoulderLength", double.class)));
+               setters.put("Transition:foreshoulderthickness", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setForeShoulderThickness", double.class)));
+               setters.put("Transition:foreshouldercapped", new BooleanSetter(
+                               Reflection.findMethodStatic(Transition.class, "setForeShoulderCapped", boolean.class)));
+               
+               setters.put("Transition:aftshoulderradius", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setAftShoulderRadius", double.class)));
+               setters.put("Transition:aftshoulderlength", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setAftShoulderLength", double.class)));
+               setters.put("Transition:aftshoulderthickness", new DoubleSetter(
+                               Reflection.findMethodStatic(Transition.class, "setAftShoulderThickness", double.class)));
+               setters.put("Transition:aftshouldercapped", new BooleanSetter(
+                               Reflection.findMethodStatic(Transition.class, "setAftShoulderCapped", boolean.class)));
+               
+               // NoseCone - disable disallowed elements
+               setters.put("NoseCone:foreradius", null);
+               setters.put("NoseCone:foreshoulderradius", null);
+               setters.put("NoseCone:foreshoulderlength", null);
+               setters.put("NoseCone:foreshoulderthickness", null);
+               setters.put("NoseCone:foreshouldercapped", null);
+               
+               // FinSet
+               setters.put("FinSet:fincount", new IntSetter(
+                               Reflection.findMethodStatic(FinSet.class, "setFinCount", int.class)));
+               setters.put("FinSet:rotation", new DoubleSetter(
+                               Reflection.findMethodStatic(FinSet.class, "setBaseRotation", double.class), Math.PI/180.0));
+               setters.put("FinSet:thickness", new DoubleSetter(
+                               Reflection.findMethodStatic(FinSet.class, "setThickness", double.class)));
+               setters.put("FinSet:crosssection", new EnumSetter<FinSet.CrossSection>(
+                               Reflection.findMethodStatic(FinSet.class, "setCrossSection", FinSet.CrossSection.class),
+                               FinSet.CrossSection.class));
+               setters.put("FinSet:cant", new DoubleSetter(
+                               Reflection.findMethodStatic(FinSet.class, "setCantAngle", double.class), Math.PI/180.0));
+               
+               // TrapezoidFinSet
+               setters.put("TrapezoidFinSet:rootchord", new DoubleSetter(
+                               Reflection.findMethodStatic(TrapezoidFinSet.class, "setRootChord", double.class)));
+               setters.put("TrapezoidFinSet:tipchord", new DoubleSetter(
+                               Reflection.findMethodStatic(TrapezoidFinSet.class, "setTipChord", double.class)));
+               setters.put("TrapezoidFinSet:sweeplength", new DoubleSetter(
+                               Reflection.findMethodStatic(TrapezoidFinSet.class, "setSweep", double.class)));
+               setters.put("TrapezoidFinSet:height", new DoubleSetter(
+                               Reflection.findMethodStatic(TrapezoidFinSet.class, "setHeight", double.class)));
+
+               // EllipticalFinSet
+               setters.put("EllipticalFinSet:rootchord", new DoubleSetter(
+                               Reflection.findMethodStatic(EllipticalFinSet.class, "setLength", double.class)));
+               setters.put("EllipticalFinSet:height", new DoubleSetter(
+                               Reflection.findMethodStatic(EllipticalFinSet.class, "setHeight", double.class)));
+               
+               // FreeformFinSet points handled as a special handler
+               
+               // LaunchLug
+               setters.put("LaunchLug:radius", new DoubleSetter(
+                               Reflection.findMethodStatic(LaunchLug.class, "setRadius", double.class)));
+               setters.put("LaunchLug:length", new DoubleSetter(
+                               Reflection.findMethodStatic(LaunchLug.class, "setLength", double.class)));
+               setters.put("LaunchLug:thickness", new DoubleSetter(
+                               Reflection.findMethodStatic(LaunchLug.class, "setThickness", double.class)));
+               setters.put("LaunchLug:radialDirection", new DoubleSetter(
+                               Reflection.findMethodStatic(LaunchLug.class, "setRadialDirection", double.class),
+                               Math.PI/180.0));
+               
+               // InternalComponent - nothing
+               
+               // StructuralComponent
+               setters.put("StructuralComponent:material", new MaterialSetter(
+                               Reflection.findMethodStatic(StructuralComponent.class, "setMaterial", Material.class),
+                               Material.Type.BULK));
+               
+               // RingComponent
+               setters.put("RingComponent:length", new DoubleSetter(
+                               Reflection.findMethodStatic(RingComponent.class, "setLength", double.class)));
+               setters.put("RingComponent:radialposition", new DoubleSetter(
+                               Reflection.findMethodStatic(RingComponent.class, "setRadialPosition", double.class)));
+               setters.put("RingComponent:radialdirection", new DoubleSetter(
+                               Reflection.findMethodStatic(RingComponent.class, "setRadialDirection", double.class),
+                               Math.PI / 180.0));
+               
+               // ThicknessRingComponent - radius on separate components due to differing automatics
+               setters.put("ThicknessRingComponent:thickness", new DoubleSetter(
+                               Reflection.findMethodStatic(ThicknessRingComponent.class, "setThickness", double.class)));
+
+               // EngineBlock
+               setters.put("EngineBlock:outerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(EngineBlock.class, "setOuterRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(EngineBlock.class, "setOuterRadiusAutomatic", boolean.class)));
+
+               // TubeCoupler
+               setters.put("TubeCoupler:outerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(TubeCoupler.class, "setOuterRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(TubeCoupler.class, "setOuterRadiusAutomatic", boolean.class)));
+               
+               // InnerTube
+               setters.put("InnerTube:outerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(InnerTube.class, "setOuterRadius", double.class)));
+               setters.put("InnerTube:clusterconfiguration", new ClusterConfigurationSetter());
+               setters.put("InnerTube:clusterscale", new DoubleSetter(
+                               Reflection.findMethodStatic(InnerTube.class, "setClusterScale", double.class)));
+               setters.put("InnerTube:clusterrotation", new DoubleSetter(
+                               Reflection.findMethodStatic(InnerTube.class, "setClusterRotation", double.class),
+                               Math.PI / 180.0));
+               
+               // RadiusRingComponent
+               
+               // Bulkhead
+               setters.put("RadiusRingComponent:innerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(RadiusRingComponent.class, "setInnerRadius", double.class)));
+               setters.put("Bulkhead:outerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(Bulkhead.class, "setOuterRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(Bulkhead.class, "setOuterRadiusAutomatic", boolean.class)));
+               
+               // CenteringRing
+               setters.put("CenteringRing:innerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(CenteringRing.class, "setInnerRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(CenteringRing.class, "setInnerRadiusAutomatic", boolean.class)));
+               setters.put("CenteringRing:outerradius", new DoubleSetter(
+                               Reflection.findMethodStatic(CenteringRing.class, "setOuterRadius", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(CenteringRing.class, "setOuterRadiusAutomatic", boolean.class)));
+               
+               
+               // MassObject
+               setters.put("MassObject:packedlength", new DoubleSetter(
+                               Reflection.findMethodStatic(MassObject.class, "setLength", double.class)));
+               setters.put("MassObject:packedradius", new DoubleSetter(
+                               Reflection.findMethodStatic(MassObject.class, "setRadius", double.class)));
+               setters.put("MassObject:radialposition", new DoubleSetter(
+                               Reflection.findMethodStatic(MassObject.class, "setRadialPosition", double.class)));
+               setters.put("MassObject:radialdirection", new DoubleSetter(
+                               Reflection.findMethodStatic(MassObject.class, "setRadialDirection", double.class),
+                               Math.PI / 180.0));
+               
+               // MassComponent
+               setters.put("MassComponent:mass", new DoubleSetter(
+                               Reflection.findMethodStatic(MassComponent.class, "setComponentMass", double.class)));
+               
+               // ShockCord
+               setters.put("ShockCord:cordlength", new DoubleSetter(
+                               Reflection.findMethodStatic(ShockCord.class, "setCordLength", double.class)));
+               setters.put("ShockCord:material", new MaterialSetter(
+                               Reflection.findMethodStatic(ShockCord.class, "setMaterial", Material.class),
+                               Material.Type.LINE));
+               
+               // RecoveryDevice
+               setters.put("RecoveryDevice:cd", new DoubleSetter(
+                               Reflection.findMethodStatic(RecoveryDevice.class, "setCD", double.class),
+                               "auto",
+                               Reflection.findMethodStatic(RecoveryDevice.class, "setCDAutomatic", boolean.class)));
+               setters.put("RecoveryDevice:deployevent", new EnumSetter<RecoveryDevice.DeployEvent>(
+                               Reflection.findMethodStatic(RecoveryDevice.class, "setDeployEvent", RecoveryDevice.DeployEvent.class),
+                               RecoveryDevice.DeployEvent.class));
+               setters.put("RecoveryDevice:deployaltitude", new DoubleSetter(
+                               Reflection.findMethodStatic(RecoveryDevice.class, "setDeployAltitude", double.class)));
+               setters.put("RecoveryDevice:deploydelay", new DoubleSetter(
+                               Reflection.findMethodStatic(RecoveryDevice.class, "setDeployDelay", double.class)));
+               setters.put("RecoveryDevice:material", new MaterialSetter(
+                               Reflection.findMethodStatic(RecoveryDevice.class, "setMaterial", Material.class),
+                               Material.Type.SURFACE));
+               
+               // Parachute
+               setters.put("Parachute:diameter", new DoubleSetter(
+                               Reflection.findMethodStatic(Parachute.class, "setDiameter", double.class)));
+               setters.put("Parachute:linecount", new IntSetter(
+                               Reflection.findMethodStatic(Parachute.class, "setLineCount", int.class)));
+               setters.put("Parachute:linelength", new DoubleSetter(
+                               Reflection.findMethodStatic(Parachute.class, "setLineLength", double.class)));
+               setters.put("Parachute:linematerial", new MaterialSetter(
+                               Reflection.findMethodStatic(Parachute.class, "setLineMaterial", Material.class),
+                               Material.Type.LINE));
+
+               // Streamer
+               setters.put("Streamer:striplength", new DoubleSetter(
+                               Reflection.findMethodStatic(Streamer.class, "setStripLength", double.class)));
+               setters.put("Streamer:stripwidth", new DoubleSetter(
+                               Reflection.findMethodStatic(Streamer.class, "setStripWidth", double.class)));
+               
+               // Rocket
+               // <motorconfiguration> handled by separate handler
+               setters.put("Rocket:referencetype", new EnumSetter<ReferenceType>(
+                               Reflection.findMethodStatic(Rocket.class, "setReferenceType", ReferenceType.class),
+                               ReferenceType.class));
+               setters.put("Rocket:customreference", new DoubleSetter(
+                               Reflection.findMethodStatic(Rocket.class, "setCustomReferenceLength", double.class)));
+               setters.put("Rocket:designer", new StringSetter(
+                               Reflection.findMethodStatic(Rocket.class, "setDesigner", String.class)));
+               setters.put("Rocket:revision", new StringSetter(
+                               Reflection.findMethodStatic(Rocket.class, "setRevision", String.class)));
+       }
+       
+       
+       /**
+        * Search for a enum value that has the corresponding name as an XML value.  The current
+        * conversion from enum name to XML value is to lowercase the name and strip out all 
+        * underscore characters.  This method returns a match to these criteria, or <code>null</code>
+        * if no such enum exists.
+        * 
+        * @param <T>                   then enum type.
+        * @param name                  the XML value, null ok.
+        * @param enumClass             the class of the enum.
+        * @return                              the found enum value, or <code>null</code>.
+        */
+       public static <T extends Enum<T>> Enum<T> findEnum(String name, Class<? extends Enum<T>> enumClass) {
+               
+               if (name == null)
+                       return null;
+               name = name.trim();
+               for (Enum<T> e: enumClass.getEnumConstants()) {
+                       if (e.name().toLowerCase().replace("_", "").equals(name)) {
+                               return e;
+                       }
+               }
+               return null;
+       }
+       
+       
+       /**
+        * Convert a string to a double including formatting specifications of the OpenRocket
+        * file format.  This accepts all formatting that is valid for 
+        * <code>Double.parseDouble(s)</code> and a few others as well ("Inf", "-Inf").
+        * 
+        * @param s             the string to parse.
+        * @return              the numerical value.
+        * @throws NumberFormatException        the the string cannot be parsed.
+        */
+       public static double stringToDouble(String s) throws NumberFormatException {
+               if (s == null)
+                       throw new NumberFormatException("null string");
+               if (s.equalsIgnoreCase("NaN"))
+                       return Double.NaN;
+               if (s.equalsIgnoreCase("Inf"))
+                       return Double.POSITIVE_INFINITY;
+               if (s.equalsIgnoreCase("-Inf"))
+                       return Double.NEGATIVE_INFINITY;
+               return Double.parseDouble(s);
+       }
+}
+
+
+
+
+/**
+ * The actual handler class.  Contains the necessary methods for parsing the SAX source.
+ */
+class DelegatorHandler extends DefaultHandler {
+       private final WarningSet warnings;
+
+       private final Stack<ElementHandler> handlerStack = new Stack<ElementHandler>();
+       private final Stack<StringBuilder> elementData = new Stack<StringBuilder>();
+       private final Stack<HashMap<String, String>> elementAttributes = new Stack<HashMap<String, String>>();
+
+
+       // Ignore all elements as long as ignore > 0
+       private int ignore = 0;
+
+
+       public DelegatorHandler(ElementHandler initialHandler, WarningSet warnings) {
+               this.warnings = warnings;
+               handlerStack.add(initialHandler);
+               elementData.add(new StringBuilder()); // Just in case
+       }
+
+
+       /////////  SAX handlers
+
+       @Override
+       public void startElement(String uri, String localName, String name,
+                       Attributes attributes) throws SAXException {
+
+               // Check for ignore
+               if (ignore > 0) {
+                       ignore++;
+                       return;
+               }
+
+               // Check for unknown namespace
+               if (!uri.equals("")) {
+                       warnings.add(Warning.fromString("Unknown namespace element '" + uri
+                                       + "' encountered, ignoring."));
+                       ignore++;
+                       return;
+               }
+
+               // Add layer to data stacks
+               elementData.push(new StringBuilder());
+               elementAttributes.push(copyAttributes(attributes));
+
+               // Call the handler
+               ElementHandler h = handlerStack.peek();
+               h = h.openElement(localName, elementAttributes.peek(), warnings);
+               if (h != null) {
+                       handlerStack.push(h);
+               } else {
+                       // Start ignoring elements
+                       ignore++;
+               }
+       }
+
+
+       /**
+        * Stores encountered characters in the elementData stack.
+        */
+       @Override
+       public void characters(char[] chars, int start, int length) throws SAXException {
+               // Check for ignore
+               if (ignore > 0)
+                       return;
+
+               StringBuilder sb = elementData.peek();
+               sb.append(chars, start, length);
+       }
+
+
+       /**
+        * Removes the last layer from the stack.
+        */
+       @Override
+       public void endElement(String uri, String localName, String name) throws SAXException {
+
+               // Check for ignore
+               if (ignore > 0) {
+                       ignore--;
+                       return;
+               }
+
+               // Remove data from stack
+               String data = elementData.pop().toString(); // throws on error
+               HashMap<String, String> attr = elementAttributes.pop();
+
+               // Remove last handler and call the next one
+               ElementHandler h;
+               
+               h = handlerStack.pop();
+               h.endHandler(localName, attr, data, warnings);
+               
+               h = handlerStack.peek();
+               h.closeElement(localName, attr, data, warnings);
+       }
+
+
+       private static HashMap<String, String> copyAttributes(Attributes atts) {
+               HashMap<String, String> ret = new HashMap<String, String>();
+               for (int i = 0; i < atts.getLength(); i++) {
+                       ret.put(atts.getLocalName(i), atts.getValue(i));
+               }
+               return ret;
+       }
+}
+
+
+
+
+abstract class ElementHandler {
+
+       /**
+        * Called when an opening element is encountered.  Returns the handler that will handle
+        * the elements within that element, or <code>null</code> if the element and all of
+        * its contents is to be ignored.
+        * 
+        * @param element               the element name.
+        * @param attributes    attributes of the element.
+        * @param warnings              the warning set to store warnings in.
+        * @return                              the handler that handles elements encountered within this element,
+        *                                              or <code>null</code> if the element is to be ignored.
+        */
+       public abstract ElementHandler openElement(String element,
+                       HashMap<String, String> attributes, WarningSet warnings);
+
+       /**
+        * Called when an element is closed.  The default implementation checks whether there is
+        * any non-space text within the element and if there exists any attributes, and adds
+        * a warning of both.  This can be used at the and of the method to check for 
+        * spurious data.
+        * 
+        * @param element               the element name.
+        * @param attributes    attributes of the element.
+        * @param content               the textual content of the element.
+        * @param warnings              the warning set to store warnings in.
+        */
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               if (!content.trim().equals("")) {
+                       warnings.add(Warning.fromString("Unknown text in element " + element
+                                       + ", ignoring."));
+               }
+               if (!attributes.isEmpty()) {
+                       warnings.add(Warning.fromString("Unknown attributes in element " + element
+                                       + ", ignoring."));
+               }
+       }
+       
+       
+       /**
+        * Called when the element block that this handler is handling ends.
+        * The default implementation is a no-op.
+        * 
+        * @param warnings              the warning set to store warnings in.
+        */
+       public void endHandler(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               // No-op
+       }
+       
+}
+
+
+/**
+ * The starting point of the handlers.  Accepts a single <openrocket> element and hands
+ * the contents to be read by a OpenRocketContentsHandler.
+ */
+class OpenRocketHandler extends ElementHandler {
+       private OpenRocketContentHandler handler = null;
+
+       /**
+        * Return the OpenRocketDocument read from the file, or <code>null</code> if a document
+        * has not been read yet.
+        * 
+        * @return      the document read, or null.
+        */
+       public OpenRocketDocument getDocument() {
+               return handler.getDocument();
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               // Check for unknown elements
+               if (!element.equals("openrocket")) {
+                       warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+                       return null;
+               }
+
+               // Check for first call
+               if (handler != null) {
+                       warnings.add(Warning.fromString("Multiple document elements found, ignoring later "
+                                                       + "ones."));
+                       return null;
+               }
+
+               // Check version number
+               String version = null;
+               String docVersion = attributes.remove("version");
+               for (String v : DocumentConfig.SUPPORTED_VERSIONS) {
+                       if (v.equals(docVersion)) {
+                               version = v;
+                               break;
+                       }
+               }
+               if (version == null) {
+                       if (docVersion != null)
+                               warnings.add(Warning.fromString("Unsupported document version "
+                                               + docVersion + ", attempting to read anyway."));
+                       else
+                               warnings.add(Warning.fromString("Unsupported document version, attempting to"
+                                                               + " read anyway."));
+               }
+
+               handler = new OpenRocketContentHandler();
+               return handler;
+       }
+}
+
+
+/**
+ * Handles the content of the <openrocket> tag.
+ */
+class OpenRocketContentHandler extends ElementHandler {
+       private final OpenRocketDocument doc;
+       private final Rocket rocket;
+
+       private boolean rocketDefined = false;
+       private boolean simulationsDefined = false;
+
+       public OpenRocketContentHandler() {
+               this.rocket = new Rocket();
+               this.doc = new OpenRocketDocument(rocket);
+       }
+
+
+       public OpenRocketDocument getDocument() {
+               if (!rocketDefined)
+                       return null;
+               return doc;
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               if (element.equals("rocket")) {
+                       if (rocketDefined) {
+                               warnings.add(Warning
+                                               .fromString("Multiple rocket designs within one document, "
+                                                               + "ignoring later ones."));
+                               return null;
+                       }
+                       rocketDefined = true;
+                       return new ComponentParameterHandler(rocket);
+               }
+               
+               if (element.equals("simulations")) {
+                       if (simulationsDefined) {
+                               warnings.add(Warning
+                                               .fromString("Multiple simulation definitions within one document, "
+                                                               + "ignoring later ones."));
+                               return null;
+                       }
+                       simulationsDefined = true;
+                       return new SimulationsHandler(doc);
+               }
+
+               warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+
+               return null;
+       }
+}
+
+
+/**
+ * An element handler that does not allow any sub-elements.  If any are encountered
+ * a warning is generated and they are ignored.
+ */
+class PlainTextHandler extends ElementHandler {
+       public static final PlainTextHandler INSTANCE = new PlainTextHandler();
+
+       private PlainTextHandler() {
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+               return null;
+       }
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               // Warning from openElement is sufficient.
+       }
+}
+
+
+
+/**
+ * A handler that creates components from the corresponding elements.  The control of the
+ * contents is passed on to ComponentParameterHandler.
+ */
+class ComponentHandler extends ElementHandler {
+       private final RocketComponent parent;
+
+       public ComponentHandler(RocketComponent parent) {
+               this.parent = parent;
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               // Attempt to construct new component
+               Constructor<? extends RocketComponent> constructor = DocumentConfig.constructors
+                               .get(element);
+               if (constructor == null) {
+                       warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+                       return null;
+               }
+
+               RocketComponent c;
+               try {
+                       c = constructor.newInstance();
+               } catch (InstantiationException e) {
+                       throw new RuntimeException("Error constructing component.", e);
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException("Error constructing component.", e);
+               } catch (InvocationTargetException e) {
+                       throw new RuntimeException("Error constructing component.", e);
+               }
+
+               parent.addChild(c);
+
+               return new ComponentParameterHandler(c);
+       }
+}
+
+
+/**
+ * A handler that populates the parameters of a previously constructed rocket component.
+ * This uses the setters, or delegates the handling to another handler for specific
+ * elements.
+ */
+class ComponentParameterHandler extends ElementHandler {
+       private final RocketComponent component;
+
+       public ComponentParameterHandler(RocketComponent c) {
+               this.component = c;
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               // Check for specific elements that contain other elements
+               if (element.equals("subcomponents")) {
+                       return new ComponentHandler(component);
+               }
+               if (element.equals("motormount")) {
+                       if (!(component instanceof MotorMount)) {
+                               warnings.add(Warning.fromString("Illegal component defined as motor mount."));
+                               return null;
+                       }
+                       return new MotorMountHandler((MotorMount)component);
+               }
+               if (element.equals("finpoints")) {
+                       if (!(component instanceof FreeformFinSet)) {
+                               warnings.add(Warning.fromString("Illegal component defined for fin points."));
+                               return null;
+                       }
+                       return new FinSetPointHandler((FreeformFinSet)component);
+               }
+               if (element.equals("motorconfiguration")) {
+                       if (!(component instanceof Rocket)) {
+                               warnings.add(Warning.fromString("Illegal component defined for motor configuration."));
+                               return null;
+                       }
+                       return new MotorConfigurationHandler((Rocket)component);
+               }
+               
+               
+               return PlainTextHandler.INSTANCE;
+       }
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               if (element.equals("subcomponents") || element.equals("motormount") ||
+                               element.equals("finpoints") || element.equals("motorconfiguration")) {
+                       return;
+               }
+               
+               // Search for the correct setter class
+
+               Class<?> c;
+               for (c = component.getClass(); c != null; c = c.getSuperclass()) {
+                       String setterKey = c.getSimpleName() + ":" + element;
+                       Setter s = DocumentConfig.setters.get(setterKey);
+                       if (s != null) {
+                               // Setter found
+                               System.out.println("Calling with key "+setterKey);
+                               s.set(component, content, attributes, warnings);
+                               break;
+                       }
+                       if (DocumentConfig.setters.containsKey(setterKey)) {
+                               // Key exists but is null -> invalid parameter
+                               c = null;
+                               break;
+                       }
+               }
+               if (c == null) {
+                       warnings.add(Warning.fromString("Unknown parameter type " + element + " for "
+                                       + component.getComponentName()));
+               }
+       }
+}
+
+
+/**
+ * A handler that reads the <point> specifications within the freeformfinset's
+ * <finpoints> elements.
+ */
+class FinSetPointHandler extends ElementHandler {
+       private final FreeformFinSet finset;
+       private final ArrayList<Coordinate> coordinates = new ArrayList<Coordinate>();
+       
+       public FinSetPointHandler(FreeformFinSet finset) {
+               this.finset = finset;
+       }
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               return PlainTextHandler.INSTANCE;
+       }
+       
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               String strx = attributes.remove("x");
+               String stry = attributes.remove("y");
+               if (strx == null || stry == null) {
+                       warnings.add(Warning.fromString("Illegal fin points specification, ignoring."));
+                       return;
+               }
+               try {
+                       double x = Double.parseDouble(strx);
+                       double y = Double.parseDouble(stry);
+                       coordinates.add(new Coordinate(x,y));
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.fromString("Illegal fin points specification, ignoring."));
+                       return;
+               }
+               
+               super.closeElement(element, attributes, content, warnings);
+       }
+       
+       @Override
+       public void endHandler(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               try {
+                       finset.setPoints(coordinates.toArray(new Coordinate[0]));
+               } catch (IllegalArgumentException e) {
+                       warnings.add(Warning.fromString("Freeform fin set point definitions illegal, ignoring."));
+               }
+       }
+}
+
+
+class MotorMountHandler extends ElementHandler {
+       private final MotorMount mount;
+       private MotorHandler motorHandler;
+       
+       public MotorMountHandler(MotorMount mount) {
+               this.mount = mount;
+               mount.setMotorMount(true);
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               if (element.equals("motor")) {
+                       motorHandler = new MotorHandler();
+                       return motorHandler;
+               }
+               
+               if (element.equals("ignitionevent") ||
+                               element.equals("ignitiondelay") ||
+                               element.equals("overhang")) {
+                       return PlainTextHandler.INSTANCE;
+               }
+               
+               warnings.add(Warning.fromString("Unknown element '"+element+"' encountered, ignoring."));
+               return null;
+       }
+       
+       
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               if (element.equals("motor")) {
+                       String id = attributes.get("configid");
+                       if (id == null || id.equals("")) {
+                               warnings.add(Warning.fromString("Illegal motor specification, ignoring."));
+                               return;
+                       }
+
+                       Motor motor = motorHandler.getMotor(warnings);
+                       mount.setMotor(id, motor);
+                       mount.setMotorDelay(id, motorHandler.getDelay(warnings));
+                       return;
+               }
+
+               if (element.equals("ignitionevent")) { 
+                       MotorMount.IgnitionEvent event = null;
+                       for (MotorMount.IgnitionEvent e : MotorMount.IgnitionEvent.values()) {
+                               if (e.name().toLowerCase().replaceAll("_", "").equals(content)) {
+                                       event = e;
+                                       break;
+                               }
+                       }
+                       if (event == null) {
+                               warnings.add(Warning.fromString("Unknown ignition event type '"+content+"', ignoring."));
+                               return;
+                       }
+                       mount.setIgnitionEvent(event);
+                       return;
+               }
+
+               if (element.equals("ignitiondelay")) {
+                       double d;
+                       try {
+                               d = Double.parseDouble(content);
+                       } catch (NumberFormatException nfe) {
+                               warnings.add(Warning.fromString("Illegal ignition delay specified, ignoring."));
+                               return;
+                       }
+                       mount.setIgnitionDelay(d);
+                       return;
+               }
+               
+               if (element.equals("overhang")) {
+                       double d;
+                       try {
+                               d = Double.parseDouble(content);
+                       } catch (NumberFormatException nfe) {
+                               warnings.add(Warning.fromString("Illegal overhang specified, ignoring."));
+                               return;
+                       }
+                       mount.setMotorOverhang(d);
+                       return;
+               }
+               
+               super.closeElement(element, attributes, content, warnings);
+       }
+}
+
+
+
+
+class MotorConfigurationHandler extends ElementHandler {
+       private final Rocket rocket;
+       private String name = null;
+       private boolean inNameElement = false;
+       
+       public MotorConfigurationHandler(Rocket rocket) {
+               this.rocket = rocket;
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               if (inNameElement || !element.equals("name")) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return null;
+               }
+               inNameElement = true;
+               
+               return PlainTextHandler.INSTANCE;
+       }
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               name = content;
+       }
+
+       @Override
+       public void endHandler(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               String configid = attributes.remove("configid");
+               if (configid == null || configid.equals("")) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+               
+               if (!rocket.addMotorConfigurationID(configid)) {
+                       warnings.add("Duplicate motor configuration ID used.");
+                       return;
+               }
+               
+               if (name != null && name.trim().length() > 0) {
+                       rocket.setMotorConfigurationName(configid, name);
+               }
+               
+               if ("true".equals(attributes.remove("default"))) {
+                       rocket.getDefaultConfiguration().setMotorConfigurationID(configid);
+               }
+               
+               super.closeElement(element, attributes, content, warnings);
+       }
+}
+
+
+class MotorHandler extends ElementHandler {
+       private Motor.Type type = null;
+       private String manufacturer = null;
+       private String designation = null;
+       private double diameter = Double.NaN;
+       private double length = Double.NaN;
+       private double delay = Double.NaN;
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               return PlainTextHandler.INSTANCE;
+       }
+       
+       
+       /**
+        * Return the motor to use, or null.
+        */
+       public Motor getMotor(WarningSet warnings) {
+               if (designation == null) {
+                       warnings.add(Warning.fromString("No motor specified, ignoring."));
+                       return null;
+               }
+               Motor[] motors = Databases.findMotors(type, manufacturer, designation, diameter, length);
+               if (motors.length == 0) {
+                       String str = "No motor with designation '"+designation+"'";
+                       if (manufacturer != null)
+                               str += " for manufacturer '" + manufacturer + "'";
+                       warnings.add(Warning.fromString(str + " found."));
+                       return null;
+               }
+               if (motors.length > 1) {
+                       String str = "Multiple motors with designation '"+designation+"'";
+                       if (manufacturer != null)
+                               str += " for manufacturer '" + manufacturer + "'";
+                       warnings.add(Warning.fromString(str + " found, one chosen arbitrarily."));
+               }
+               return motors[0];
+       }
+       
+       
+       /**
+        * Return the delay to use for the motor.
+        */
+       public double getDelay(WarningSet warnings) {
+               if (Double.isNaN(delay)) {
+                       warnings.add(Warning.fromString("Motor delay not specified, assuming no ejection charge."));
+                       return Motor.PLUGGED;
+               }
+               return delay;
+       }
+       
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               
+               content = content.trim();
+               
+               if (element.equals("type")) {
+                       
+                       // Motor type
+                       type = null;
+                       for (Motor.Type t: Motor.Type.values()) {
+                               if (t.name().toLowerCase().equals(content)) {
+                                       type = t;
+                                       break;
+                               }
+                       }
+                       if (type == null) {
+                               warnings.add(Warning.fromString("Unknown motor type '"+content+"', ignoring."));
+                       }
+                       
+               } else if (element.equals("manufacturer")) {
+                       
+                       // Manufacturer
+                       manufacturer = content;
+
+               } else if (element.equals("designation")) {
+                       
+                       // Designation
+                       designation = content;
+
+               } else if (element.equals("diameter")) {
+               
+                       // Diameter
+                       diameter = Double.NaN;
+                       try {
+                               diameter = Double.parseDouble(content);
+                       } catch (NumberFormatException e) {
+                               // Ignore
+                       }
+                       if (Double.isNaN(diameter)) {
+                               warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring."));
+                       }
+                       
+               } else if (element.equals("length")) {
+
+                       // Length
+                       length = Double.NaN;
+                       try {
+                               length = Double.parseDouble(content);
+                       } catch (NumberFormatException ignore) { }
+                       
+                       if (Double.isNaN(length)) {
+                               warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring."));
+                       }
+                       
+               } else if (element.equals("delay")) {
+                       
+                       // Delay
+                       delay = Double.NaN;
+                       if (content.equals("none")) {
+                               delay = Motor.PLUGGED;
+                       } else {
+                               try {
+                                       delay = Double.parseDouble(content);
+                               } catch (NumberFormatException ignore) { }
+                               
+                               if (Double.isNaN(delay)) {
+                                       warnings.add(Warning.fromString("Illegal motor delay specified, ignoring."));
+                               }
+                               
+                       }
+
+               } else {
+                       super.closeElement(element, attributes, content, warnings);
+               }
+       }
+       
+}
+
+
+
+class SimulationsHandler extends ElementHandler {
+       private final OpenRocketDocument doc;
+       private SingleSimulationHandler handler;
+       
+       public SimulationsHandler(OpenRocketDocument doc) {
+               this.doc = doc;
+       }
+
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               if (!element.equals("simulation")) {
+                       warnings.add("Unknown element '"+element+"', ignoring.");
+                       return null;
+               }
+               
+               handler = new SingleSimulationHandler(doc);
+               return handler;
+       }
+}
+
+class SingleSimulationHandler extends ElementHandler {
+
+       private final OpenRocketDocument doc;
+       
+       private String name;
+       
+       private SimulationConditionsHandler conditionHandler;
+       private FlightDataHandler dataHandler;
+       
+       private final List<String> listeners = new ArrayList<String>();
+       
+       public SingleSimulationHandler(OpenRocketDocument doc) {
+               this.doc = doc;
+       }
+       
+       
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               if (element.equals("name") || element.equals("simulator") || 
+                               element.equals("calculator") || element.equals("listener")) {
+                       return PlainTextHandler.INSTANCE;
+               } else if (element.equals("conditions")) {
+                       conditionHandler = new SimulationConditionsHandler(doc.getRocket());
+                       return conditionHandler;
+               } else if (element.equals("flightdata")) {
+                       dataHandler = new FlightDataHandler();
+                       return dataHandler;
+               } else {
+                       warnings.add("Unknown element '"+element+"', ignoring.");
+                       return null;
+               }
+       }
+       
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               
+               if (element.equals("name")) {
+                       name = content;
+               } else if (element.equals("simulator")) {
+                       if (!content.equals("RK4Simulator")) {
+                               warnings.add("Unknown simulator specified, ignoring.");
+                       }
+               } else if (element.equals("calculator")) {
+                       if (!content.equals("BarrowmanSimulator")) {
+                               warnings.add("Unknown calculator specified, ignoring.");
+                       }
+               } else if (element.equals("listener") && content.trim().length() > 0) {
+                       listeners.add(content.trim());
+               }
+
+       }
+
+       @Override
+       public void endHandler(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               String s = attributes.get("status");
+               Simulation.Status status = (Status) DocumentConfig.findEnum(s, Simulation.Status.class);
+               if (status == null) {
+                       warnings.add("Simulation status unknown, assuming outdated.");
+                       status = Simulation.Status.OUTDATED;
+               }
+               
+               SimulationConditions conditions;
+               if (conditionHandler != null) {
+                       conditions = conditionHandler.getConditions();
+               } else {
+                       warnings.add("Simulation conditions not defined, using defaults.");
+                       conditions = new SimulationConditions(doc.getRocket());
+               }
+               
+               if (name == null)
+                       name = "Simulation";
+               
+               FlightData data;
+               if (dataHandler == null)
+                       data = null;
+               else
+                       data = dataHandler.getFlightData();
+
+               Simulation simulation = new Simulation(doc.getRocket(), status, name,
+                               conditions, listeners, data);
+               
+               doc.addSimulation(simulation);
+       }
+}
+
+
+
+class SimulationConditionsHandler extends ElementHandler {
+       private SimulationConditions conditions;
+       private AtmosphereHandler atmosphereHandler;
+       
+       public SimulationConditionsHandler(Rocket rocket) {
+               conditions = new SimulationConditions(rocket);
+       }
+       
+       public SimulationConditions getConditions() {
+               return conditions;
+       }
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               if (element.equals("atmosphere")) {
+                       atmosphereHandler = new AtmosphereHandler(attributes.get("model"));
+                       return atmosphereHandler;
+               }
+               return PlainTextHandler.INSTANCE;
+       }       
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               
+               double d = Double.NaN;
+               try {
+                       d = Double.parseDouble(content);
+               } catch (NumberFormatException ignore) { }
+               
+
+               if (element.equals("configid")) {
+                       if (content.equals("")) {
+                               conditions.setMotorConfigurationID(null);
+                       } else {
+                               conditions.setMotorConfigurationID(content);
+                       }
+               } else if (element.equals("launchrodlength")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal launch rod length defined, ignoring.");
+                       } else {
+                               conditions.setLaunchRodLength(d);
+                       }
+               } else if (element.equals("launchrodangle")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal launch rod angle defined, ignoring.");
+                       } else {
+                               conditions.setLaunchRodAngle(d*Math.PI/180);
+                       }
+               } else if (element.equals("launchroddirection")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal launch rod direction defined, ignoring.");
+                       } else {
+                               conditions.setLaunchRodDirection(d*Math.PI/180);
+                       }
+               } else if (element.equals("windaverage")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal average windspeed defined, ignoring.");
+                       } else {
+                               conditions.setWindSpeedAverage(d);
+                       }
+               } else if (element.equals("windturbulence")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal wind turbulence intensity defined, ignoring.");
+                       } else {
+                               conditions.setWindTurbulenceIntensity(d);
+                       }
+               } else if (element.equals("launchaltitude")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal launch altitude defined, ignoring.");
+                       } else {
+                               conditions.setLaunchAltitude(d);
+                       }
+               } else if (element.equals("launchlatitude")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal launch latitude defined, ignoring.");
+                       } else {
+                               conditions.setLaunchLatitude(d);
+                       }
+               } else if (element.equals("atmosphere")) {
+                       atmosphereHandler.storeSettings(conditions, warnings);
+               } else if (element.equals("timestep")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal time step defined, ignoring.");
+                       } else {
+                               conditions.setTimeStep(d);
+                       }
+               }
+       }
+}
+
+
+class AtmosphereHandler extends ElementHandler {
+       private final String model;
+       private double temperature = Double.NaN;
+       private double pressure = Double.NaN;
+       
+       public AtmosphereHandler(String model) {
+               this.model = model;
+       }
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               return PlainTextHandler.INSTANCE;
+       }
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               double d = Double.NaN;
+               try {
+                       d = Double.parseDouble(content);
+               } catch (NumberFormatException ignore) { }
+               
+               if (element.equals("basetemperature")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal base temperature specified, ignoring.");
+                       }
+                       temperature = d;
+               } else if (element.equals("basepressure")) {
+                       if (Double.isNaN(d)) {
+                               warnings.add("Illegal base pressure specified, ignoring.");
+                       }
+                       pressure = d;
+               } else {
+                       super.closeElement(element, attributes, content, warnings);
+               }
+       }
+
+       
+       public void storeSettings(SimulationConditions cond, WarningSet warnings) {
+               if (!Double.isNaN(pressure)) {
+                       cond.setLaunchPressure(pressure);
+               }
+               if (!Double.isNaN(temperature)) {
+                       cond.setLaunchTemperature(temperature);
+               }
+               
+               if ("isa".equals(model)) {
+                       cond.setISAAtmosphere(true);
+               } else if ("extendedisa".equals(model)){
+                       cond.setISAAtmosphere(false);
+               } else {
+                       cond.setISAAtmosphere(true);
+                       warnings.add("Unknown atmospheric model, using ISA.");
+               }
+       }
+       
+}
+
+
+class FlightDataHandler extends ElementHandler {
+       
+       private FlightDataBranchHandler dataHandler;
+       private WarningSet warningSet = new WarningSet();
+       private List<FlightDataBranch> branches = new ArrayList<FlightDataBranch>();
+
+       private FlightData data;
+       
+       public FlightData getFlightData() {
+               return data;
+       }
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               if (element.equals("warning")) {
+                       return PlainTextHandler.INSTANCE;
+               }
+               if (element.equals("databranch")) {
+                       if (attributes.get("name") == null || attributes.get("types")==null) {
+                               warnings.add("Illegal flight data definition, ignoring.");
+                               return null;
+                       }
+                       dataHandler =  new FlightDataBranchHandler(attributes.get("name"),
+                                       attributes.get("types"));
+                       return dataHandler;
+               }
+               
+               warnings.add("Unknown element '"+element+"' encountered, ignoring.");
+               return null;
+       }
+
+       
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               
+               if (element.equals("databranch")) {
+                       FlightDataBranch branch = dataHandler.getBranch();
+                       if (branch.getLength() > 0) {
+                               branches.add(branch);
+                       }
+               } else if (element.equals("warning")) {
+                       warningSet.add(Warning.fromString(content));
+               }
+       }
+
+
+       @Override
+       public void endHandler(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+
+               if (branches.size() > 0) {
+                       data = new FlightData(branches.toArray(new FlightDataBranch[0]));
+               } else {
+                       double maxAltitude = Double.NaN;
+                       double maxVelocity = Double.NaN;
+                       double maxAcceleration = Double.NaN;
+                       double maxMach = Double.NaN;
+                       double timeToApogee = Double.NaN;
+                       double flightTime = Double.NaN;
+                       double groundHitVelocity = Double.NaN;
+                       
+                       try { 
+                               maxAltitude = DocumentConfig.stringToDouble(attributes.get("maxaltitude"));
+                       } catch (NumberFormatException ignore) { }
+                       try { 
+                               maxVelocity = DocumentConfig.stringToDouble(attributes.get("maxvelocity"));
+                       } catch (NumberFormatException ignore) { }
+                       try { 
+                               maxAcceleration = DocumentConfig.stringToDouble(attributes.get("maxacceleration"));
+                       } catch (NumberFormatException ignore) { }
+                       try { 
+                               maxMach = DocumentConfig.stringToDouble(attributes.get("maxmach"));
+                       } catch (NumberFormatException ignore) { }
+                       try { 
+                               timeToApogee = DocumentConfig.stringToDouble(attributes.get("timetoapogee"));
+                       } catch (NumberFormatException ignore) { }
+                       try { 
+                               flightTime = DocumentConfig.stringToDouble(attributes.get("flighttime"));
+                       } catch (NumberFormatException ignore) { }
+                       try { 
+                               groundHitVelocity = 
+                                       DocumentConfig.stringToDouble(attributes.get("groundhitvelocity"));
+                       } catch (NumberFormatException ignore) { }
+                       
+                       data = new FlightData(maxAltitude, maxVelocity, maxAcceleration, maxMach,
+                                       timeToApogee, flightTime, groundHitVelocity);
+               }
+               
+               data.getWarningSet().addAll(warningSet);
+       }
+       
+       
+}
+
+
+class FlightDataBranchHandler extends ElementHandler {
+       private final FlightDataBranch.Type[] types;
+       private final FlightDataBranch branch;
+       
+       public FlightDataBranchHandler(String name, String typeList) {
+               String[] split = typeList.split(",");
+               types = new FlightDataBranch.Type[split.length];
+               for (int i=0; i < split.length; i++) {
+                       types[i] = FlightDataBranch.getType(split[i], UnitGroup.UNITS_NONE);
+               }
+               
+               // TODO: LOW: May throw an IllegalArgumentException
+               branch = new FlightDataBranch(name, types);
+       }
+
+       public FlightDataBranch getBranch() {
+               branch.immute();
+               return branch;
+       }
+       
+       @Override
+       public ElementHandler openElement(String element, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               if (element.equals("datapoint"))
+                       return PlainTextHandler.INSTANCE;
+               if (element.equals("event"))
+                       return PlainTextHandler.INSTANCE;
+               
+               warnings.add("Unknown element '"+element+"' encountered, ignoring.");
+               return null;
+       }
+       
+
+       @Override
+       public void closeElement(String element, HashMap<String, String> attributes,
+                       String content, WarningSet warnings) {
+               
+               if (element.equals("event")) {
+                       double time;
+                       FlightEvent.Type type;
+                       
+                       try {
+                               time = DocumentConfig.stringToDouble(attributes.get("time"));
+                       } catch (NumberFormatException e) {
+                               warnings.add("Illegal event specification, ignoring.");
+                               return;
+                       }
+                       
+                       type = (Type) DocumentConfig.findEnum(attributes.get("type"), FlightEvent.Type.class);
+                       if (type == null) {
+                               warnings.add("Illegal event specification, ignoring.");
+                               return;
+                       }
+
+                       branch.addEvent(time, new FlightEvent(type, time));
+                       return;
+               }
+               
+               if (!element.equals("datapoint")) {
+                       warnings.add("Unknown element '"+element+"' encountered, ignoring.");
+                       return;
+               }
+
+               // element == "datapoint"
+               
+               
+               // Check line format
+               String[] split = content.split(",");
+               if (split.length != types.length) {
+                       warnings.add("Data point did not contain correct amount of values, ignoring point.");
+                       return;
+               }
+               
+               // Parse the doubles
+               double[] values = new double[split.length];
+               for (int i=0; i < values.length; i++) {
+                       try {
+                               values[i] = DocumentConfig.stringToDouble(split[i]);
+                       } catch (NumberFormatException e) {
+                               warnings.add("Data point format error, ignoring point.");
+                               return;
+                       }
+               }
+               
+               // Add point to branch
+               branch.addPoint();
+               for (int i=0; i < types.length; i++) {
+                       branch.setValue(types[i], values[i]);
+               }
+       }
+}
+
+
+
+
+
+
+/////////////////    Setters implementation
+
+
+////  Interface
+interface Setter {
+       /**
+        * Set the specified value to the given component.
+        * 
+        * @param component             the component to which to set.
+        * @param value                 the value within the element.
+        * @param attributes    attributes for the element.
+        * @param warnings              the warning set to use.
+        */
+       public void set(RocketComponent component, String value,
+                       HashMap<String, String> attributes, WarningSet warnings);
+}
+
+
+////  StringSetter - sets the value to the contained String
+class StringSetter implements Setter {
+       private final Reflection.Method setMethod;
+
+       public StringSetter(Reflection.Method set) {
+               setMethod = set;
+       }
+
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               setMethod.invoke(c, s);
+       }
+}
+
+////  IntSetter - set an integer value
+class IntSetter implements Setter {
+       private final Reflection.Method setMethod;
+
+       public IntSetter(Reflection.Method set) {
+               setMethod = set;
+       }
+
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               try {
+                       int n = Integer.parseInt(s);
+                       setMethod.invoke(c, n);
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+               }
+       }
+}
+
+
+//// BooleanSetter - set a boolean value
+class BooleanSetter implements Setter {
+       private final Reflection.Method setMethod;
+
+       public BooleanSetter(Reflection.Method set) {
+               setMethod = set;
+       }
+
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               s = s.trim();
+               if (s.equalsIgnoreCase("true")) {
+                       setMethod.invoke(c, true);
+               } else if (s.equalsIgnoreCase("false")) {
+                       setMethod.invoke(c, false);
+               } else {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+               }
+       }
+}
+
+
+
+////  DoubleSetter - sets a double value or (alternatively) if a specific string is encountered
+////  calls a setXXX(boolean) method.
+class DoubleSetter implements Setter {
+       private final Reflection.Method setMethod;
+       private final String specialString;
+       private final Reflection.Method specialMethod;
+       private final double multiplier;
+
+       /**
+        * Set only the double value.
+        * @param set   set method for the double value. 
+        */
+       public DoubleSetter(Reflection.Method set) {
+               this.setMethod = set;
+               this.specialString = null;
+               this.specialMethod = null;
+               this.multiplier = 1.0;
+       }
+
+       /**
+        * Multiply with the given multiplier and set the double value.
+        * @param set   set method for the double value.
+        * @param mul   multiplier.
+        */
+       public DoubleSetter(Reflection.Method set, double mul) {
+               this.setMethod = set;
+               this.specialString = null;
+               this.specialMethod = null;
+               this.multiplier = mul;
+       }
+
+       /**
+        * Set the double value, or if the value equals the special string, use the
+        * special setter and set it to true.
+        * 
+        * @param set                   double setter.
+        * @param special               special string
+        * @param specialMethod boolean setter.
+        */
+       public DoubleSetter(Reflection.Method set, String special,
+                       Reflection.Method specialMethod) {
+               this.setMethod = set;
+               this.specialString = special;
+               this.specialMethod = specialMethod;
+               this.multiplier = 1.0;
+       }
+
+
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               s = s.trim();
+
+               // Check for special case
+               if (specialMethod != null && s.equalsIgnoreCase(specialString)) {
+                       specialMethod.invoke(c, true);
+                       return;
+               }
+
+               // Normal case
+               try {
+                       double d = Double.parseDouble(s);
+                       setMethod.invoke(c, d * multiplier);
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+               }
+       }
+}
+
+
+class OverrideSetter implements Setter {
+       private final Reflection.Method setMethod;
+       private final Reflection.Method enabledMethod;
+
+       public OverrideSetter(Reflection.Method set, Reflection.Method enabledMethod) {
+               this.setMethod = set;
+               this.enabledMethod = enabledMethod;
+       }
+
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               try {
+                       double d = Double.parseDouble(s);
+                       setMethod.invoke(c, d);
+                       enabledMethod.invoke(c, true);
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+               }
+       }
+}
+
+////  EnumSetter  -  sets a generic enum type
+class EnumSetter<T extends Enum<T>> implements Setter {
+       private final Reflection.Method setter;
+       private final Class<T> enumClass;
+
+       public EnumSetter(Reflection.Method set, Class<T> enumClass) {
+               this.setter = set;
+               this.enumClass = enumClass;
+       }
+
+       @Override
+       public void set(RocketComponent c, String name, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               Enum<?> setEnum = DocumentConfig.findEnum(name, enumClass);
+               if (setEnum == null) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+
+               setter.invoke(c, setEnum);
+       }
+}
+
+
+////  ColorSetter  -  sets a Color value
+class ColorSetter implements Setter {
+       private final Reflection.Method setMethod;
+
+       public ColorSetter(Reflection.Method set) {
+               setMethod = set;
+       }
+
+       public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+
+               String red = attributes.get("red");
+               String green = attributes.get("green");
+               String blue = attributes.get("blue");
+
+               if (red == null || green == null || blue == null) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+               
+               int r, g, b;
+               try {
+                       r = Integer.parseInt(red);
+                       g = Integer.parseInt(green);
+                       b = Integer.parseInt(blue);
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+               
+               if (r < 0 || g < 0 || b < 0 || r > 255 || g > 255 || b > 255) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+
+               Color color = new Color(r, g, b);
+               setMethod.invoke(c, color);
+               
+               if (!s.trim().equals("")) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+               }
+       }
+}
+
+
+
+class MaterialSetter implements Setter {
+       private final Reflection.Method setMethod;
+       private final Material.Type type;
+
+       public MaterialSetter(Reflection.Method set, Material.Type type) {
+               this.setMethod = set;
+               this.type = type;
+       }
+
+       public void set(RocketComponent c, String name, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               Material mat;
+               
+               // Check name != ""
+               name = name.trim();
+               if (name.equals("")) {
+                       warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+                       return;
+               }
+               
+               // Parse density
+               double density;
+               String str;
+               str = attributes.remove("density");
+               if (str == null) {
+                       warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+                       return;
+               }
+               try {
+                       density = Double.parseDouble(str);
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+                       return;
+               }
+
+               // Parse thickness
+//             double thickness = 0;
+//             str = attributes.remove("thickness");
+//             try {
+//                     if (str != null)
+//                             thickness = Double.parseDouble(str);
+//             } catch (NumberFormatException e){
+//                     warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+//                     return;
+//             }
+
+               // Check type if specified
+               str = attributes.remove("type");
+               if (str != null  &&  !type.name().toLowerCase().equals(str)) {
+                       warnings.add(Warning.fromString("Illegal material type specified, ignoring."));
+                       return;
+               }
+
+               mat = Material.newMaterial(type, name, density);
+               
+               setMethod.invoke(c, mat);
+       }
+}
+
+
+
+
+class PositionSetter implements Setter {
+
+       public void set(RocketComponent c, String value, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               RocketComponent.Position type = (Position) DocumentConfig.findEnum(attributes.get("type"), 
+                               RocketComponent.Position.class);
+               if (type == null) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+               
+               double pos;
+               try {
+                       pos = Double.parseDouble(value);
+               } catch (NumberFormatException e) {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+                       return;
+               }
+               
+               if (c instanceof FinSet) {
+                       ((FinSet)c).setRelativePosition(type);
+                       c.setPositionValue(pos);
+               } else if (c instanceof LaunchLug) {
+                       ((LaunchLug)c).setRelativePosition(type);
+                       c.setPositionValue(pos);
+               } else if (c instanceof InternalComponent) {
+                       ((InternalComponent)c).setRelativePosition(type);
+                       c.setPositionValue(pos);
+               } else {
+                       warnings.add(Warning.FILE_INVALID_PARAMETER);
+               }
+               
+       }
+}
+
+
+
+class ClusterConfigurationSetter implements Setter {
+
+       public void set(RocketComponent component, String value, HashMap<String, String> attributes,
+                       WarningSet warnings) {
+               
+               if (!(component instanceof Clusterable)) {
+                       warnings.add("Illegal component defined as cluster.");
+                       return;
+               }
+               
+               ClusterConfiguration config = null;
+               for (ClusterConfiguration c: ClusterConfiguration.CONFIGURATIONS) {
+                       if (c.getXMLName().equals(value)) {
+                               config = c;
+                               break;
+                       }
+               }
+               
+               if (config == null) {
+                       warnings.add("Illegal cluster configuration specified.");
+                       return;
+               }
+               
+               ((Clusterable)component).setClusterConfiguration(config);
+       }
+}
+
+
diff --git a/src/net/sf/openrocket/file/OpenRocketSaver.java b/src/net/sf/openrocket/file/OpenRocketSaver.java
new file mode 100644 (file)
index 0000000..970aa2a
--- /dev/null
@@ -0,0 +1,431 @@
+package net.sf.openrocket.file;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.zip.GZIPOutputStream;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+
+public class OpenRocketSaver extends RocketSaver {
+       
+       /* Remember to update OpenRocketLoader as well! */
+       public static final String FILE_VERSION = "1.0";
+       
+       private static final String OPENROCKET_CHARSET = "UTF-8";
+       
+       private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket";
+       private static final String METHOD_SUFFIX = "Saver";
+       
+       private int indent;
+       private Writer dest;
+       
+       @Override
+       public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
+       throws IOException {
+               
+               if (options.isCompressionEnabled()) {
+                       output = new GZIPOutputStream(output);
+               }
+               
+               dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET)); 
+               
+               
+               this.indent = 0;
+               
+               System.out.println("Writing...");
+               
+               writeln("<?xml version='1.0' encoding='utf-8'?>");
+               writeln("<openrocket version=\""+FILE_VERSION+"\" creator=\"OpenRocket "
+                               +Prefs.getVersion()+ "\">");
+               indent++;
+               
+               // Recursively save the rocket structure
+               saveComponent(document.getRocket());
+               
+               writeln("");
+               
+               // Save all simulations
+               writeln("<simulations>");
+               indent++;
+               boolean first = true;
+               for (Simulation s: document.getSimulations()) {
+                       if (!first)
+                               writeln("");
+                       first = false;
+                       saveSimulation(s, options.getSimulationTimeSkip());
+               }
+               indent--;
+               writeln("</simulations>");
+               
+               indent--;
+               writeln("</openrocket>");
+               
+               dest.flush();
+               if (output instanceof GZIPOutputStream)
+                       ((GZIPOutputStream)output).finish();
+       }
+       
+
+       
+       @SuppressWarnings("unchecked")
+       private void saveComponent(RocketComponent component) throws IOException {
+               
+               Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
+                               "getElements", RocketComponent.class);
+               if (m==null) {
+                       throw new RuntimeException("Unable to find saving class for component "+
+                                       component.getComponentName());
+               }
+
+               // Get the strings to save
+               List<String> list = (List<String>) m.invokeStatic(component);
+               int length = list.size();
+               
+               if (length == 0)  // Nothing to do
+                       return;
+
+               if (length < 2) {
+                       throw new RuntimeException("BUG, component data length less than two lines.");
+               }
+               
+               // Open element
+               writeln(list.get(0));
+               indent++;
+               
+               // Write parameters
+               for (int i=1; i<length-1; i++) {
+                       writeln(list.get(i));
+               }
+               
+               // Recursively write subcomponents
+               if (component.getChildCount() > 0) {
+                       writeln("");
+                       writeln("<subcomponents>");
+                       indent++;
+                       boolean emptyline = false;
+                       for (RocketComponent subcomponent: component) {
+                               if (emptyline)
+                                       writeln("");
+                               emptyline = true;
+                               saveComponent(subcomponent);
+                       }
+                       indent--;
+                       writeln("</subcomponents>");
+               }
+               
+               // Close element
+               indent--;
+               writeln(list.get(length-1));
+       }
+
+       
+       
+       private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
+               SimulationConditions cond = simulation.getConditions();
+               
+               writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) +"\">");
+               indent++;
+               
+               writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
+               // TODO: MEDIUM: Other simulators/calculators
+               writeln("<simulator>RK4Simulator</simulator>");
+               writeln("<calculator>BarrowmanCalculator</calculator>");
+               writeln("<conditions>");
+               indent++;
+               
+               writeElement("configid", cond.getMotorConfigurationID());
+               writeElement("launchrodlength", cond.getLaunchRodLength());
+               writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0/Math.PI); 
+               writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0/Math.PI);
+               writeElement("windaverage", cond.getWindSpeedAverage());
+               writeElement("windturbulence", cond.getWindTurbulenceIntensity());
+               writeElement("launchaltitude", cond.getLaunchAltitude());
+               writeElement("launchlatitude", cond.getLaunchLatitude());
+               
+               if (cond.isISAAtmosphere()) {
+                       writeln("<atmosphere model=\"isa\"/>");
+               } else {
+                       writeln("<atmosphere model=\"extendedisa\">");
+                       indent++;
+                       writeElement("basetemperature", cond.getLaunchTemperature());
+                       writeElement("basepressure", cond.getLaunchPressure());
+                       indent--;
+                       writeln("</atmosphere>");
+               }
+
+               writeElement("timestep", cond.getTimeStep());
+               
+               indent--;
+               writeln("</conditions>");
+               
+               
+               for (String s: simulation.getSimulationListeners()) {
+                       writeElement("listener", escapeXML(s));
+               }
+               
+               
+               // Write basic simulation data
+               
+               FlightData data = simulation.getSimulatedData();
+               if (data != null) {
+                       String str = "<flightdata";
+                       if (!Double.isNaN(data.getMaxAltitude()))
+                               str += " maxaltitude=\"" + doubleToString(data.getMaxAltitude()) + "\"";
+                       if (!Double.isNaN(data.getMaxVelocity()))
+                               str += " maxvelocity=\"" + doubleToString(data.getMaxVelocity()) + "\"";
+                       if (!Double.isNaN(data.getMaxAcceleration()))
+                               str += " maxacceleration=\"" + doubleToString(data.getMaxAcceleration()) + "\"";
+                       if (!Double.isNaN(data.getMaxMachNumber()))
+                               str += " maxmach=\"" + doubleToString(data.getMaxMachNumber()) + "\"";
+                       if (!Double.isNaN(data.getTimeToApogee()))
+                               str += " timetoapogee=\"" + doubleToString(data.getTimeToApogee()) + "\"";
+                       if (!Double.isNaN(data.getFlightTime()))
+                               str += " flighttime=\"" + doubleToString(data.getFlightTime()) + "\"";
+                       if (!Double.isNaN(data.getGroundHitVelocity()))
+                               str += " groundhitvelocity=\"" + doubleToString(data.getGroundHitVelocity()) + "\"";
+                       str += ">";
+                       writeln(str);
+                       indent++;
+                       
+                       for (Warning w: data.getWarningSet()) {
+                               writeElement("warning", escapeXML(w.toString()));
+                       }
+                       
+                       // Check whether to store data
+                       if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
+                               timeSkip = 0;
+                       
+                       if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
+                               for (int i=0; i<data.getBranchCount(); i++) {
+                                       FlightDataBranch branch = data.getBranch(i);
+                                       saveFlightDataBranch(branch, timeSkip);
+                               }
+                       }
+                       
+                       indent--;
+                       writeln("</flightdata>");
+               }
+               
+               indent--;
+               writeln("</simulation>");
+               
+       }
+       
+       
+       
+       private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) throws IOException {
+               double previousTime = -100;
+               
+               if (branch == null)
+                       return;
+               
+               // Retrieve the types from the branch
+               FlightDataBranch.Type[] types = branch.getTypes();
+               
+               if (types.length == 0)
+                       return;
+               
+               // Retrieve the data from the branch
+               List<List<Double>> data = new ArrayList<List<Double>>(types.length);
+               for (int i=0; i<types.length; i++) {
+                       data.add(branch.get(types[i]));
+               }
+               List<Double> timeData = branch.get(FlightDataBranch.TYPE_TIME);
+               if (timeData == null) {
+                       // TODO: MEDIUM: External data may not have time data
+                       throw new IllegalArgumentException("Data did not contain time data");
+               }
+               
+               // Build the <databranch> tag
+               StringBuilder sb = new StringBuilder();
+               sb.append("<databranch name=\"");
+               sb.append(escapeXML(branch.getBranchName()));
+               sb.append("\" types=\"");
+               for (int i=0; i<types.length; i++) {
+                       if (i > 0)
+                               sb.append(",");
+                       sb.append(escapeXML(types[i].getName()));
+               }
+               sb.append("\">");
+               writeln(sb.toString());
+               indent++;
+               
+               // Write events
+               for (Pair<Double,FlightEvent> p: branch.getEvents()) {
+                       writeln("<event time=\"" + doubleToString(p.getU())
+                                       + "\" type=\"" + enumToXMLName(p.getV().getType()) + "\"/>");
+               }
+               
+               // Write the data
+               int length = branch.getLength();
+               if (length > 0) {
+                       writeDataPointString(data, 0, sb);
+                       previousTime = timeData.get(0);
+               }
+               
+               for (int i=1; i < length-1; i++) {
+                       if (Math.abs(timeData.get(i) - previousTime - timeSkip) < 
+                                       Math.abs(timeData.get(i+1) - previousTime - timeSkip)) {
+                               writeDataPointString(data, i, sb);
+                               previousTime = timeData.get(i);
+                       }
+               }
+               
+               if (length > 1) {
+                       writeDataPointString(data, length-1, sb);
+               }
+               
+               indent--;
+               writeln("</databranch>");
+       }
+       
+       private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
+       throws IOException {
+               sb.setLength(0);
+               sb.append("<datapoint>");
+               for (int j=0; j < data.size(); j++) {
+                       if (j > 0)
+                               sb.append(",");
+                       sb.append(doubleToString(data.get(j).get(index)));
+               }
+               sb.append("</datapoint>");
+               writeln(sb.toString());
+       }
+       
+       
+       
+       private void writeElement(String element, Object content) throws IOException {
+               if (content == null)
+                       content = "";
+               writeln("<"+element+">"+content+"</"+element+">");
+       }
+
+
+       
+       private void writeln(String str) throws IOException {
+               if (str.length() == 0) {
+                       dest.write("\n");
+                       return;
+               }
+               String s="";
+               for (int i=0; i<indent; i++)
+                       s=s+"  ";
+               s = s+str+"\n";
+               dest.write(s);
+       }
+       
+       
+       /**
+        * Return a string of the double value with suitable precision.
+        * The string is the shortest representation of the value including the
+        * required precision.
+        * 
+        * @param d             the value to present.
+        * @return              a representation with suitable precision.
+        */
+       public static final String doubleToString(double d) {
+               
+               // Check for special cases
+               if (MathUtil.equals(d, 0))
+                       return "0";
+               
+               if (Double.isNaN(d))
+                       return "NaN";
+               
+               if (Double.isInfinite(d)) {
+                       if (d < 0)
+                               return "-Inf";
+                       else
+                               return "Inf";
+               }
+               
+               
+               double abs = Math.abs(d);
+               
+               if (abs < 0.001) {
+                       // Compact exponential notation
+                       int exp = 0;
+                       
+                       while (abs < 1.0) {
+                               abs *= 10;
+                               exp++;
+                       }
+                       
+                       String sign = (d < 0) ? "-" : "";
+                       return sign + String.format((Locale)null, "%.4fe-%d", abs, exp);
+               }
+               if (abs < 0.01)
+                       return String.format((Locale)null, "%.7f", d);
+               if (abs < 0.1)
+                       return String.format((Locale)null, "%.6f", d);
+               if (abs < 1)
+                       return String.format((Locale)null, "%.5f", d);
+               if (abs < 10)
+                       return String.format((Locale)null, "%.4f", d);
+               if (abs < 100)
+                       return String.format((Locale)null, "%.3f", d);
+               if (abs < 1000)
+                       return String.format((Locale)null, "%.2f", d);
+               if (abs < 10000)
+                       return String.format((Locale)null, "%.1f", d);
+               if (abs < 100000000.0)
+                       return String.format((Locale)null, "%.0f", d);
+                       
+               // Compact exponential notation
+               int exp = 0;
+               while (abs >= 10.0) {
+                       abs /= 10;
+                       exp++;
+               }
+               
+               String sign = (d < 0) ? "-" : "";
+               return sign + String.format((Locale)null, "%.4fe%d", abs, exp);
+       }
+       
+       
+       
+       public static void main(String[] arg) {
+               double d = -0.000000123456789123;
+               
+               
+               for (int i=0; i< 20; i++) {
+                       String str = doubleToString(d);
+                       System.out.println(str + "   ->   " + Double.parseDouble(str));
+                       d *= 10;
+               }
+               
+               
+               System.out.println("Value: "+ Double.parseDouble("1.2345e9"));
+               
+       }
+
+       
+       /**
+        * Return the XML equivalent of an enum name.
+        * 
+        * @param e             the enum to save.
+        * @return              the corresponding XML name.
+        */
+       public static String enumToXMLName(Enum<?> e) {
+               return e.name().toLowerCase().replace("_", "");
+       }
+       
+}
diff --git a/src/net/sf/openrocket/file/RocketLoadException.java b/src/net/sf/openrocket/file/RocketLoadException.java
new file mode 100644 (file)
index 0000000..2fdd177
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file;
+
+public class RocketLoadException extends Exception {
+
+       public RocketLoadException() {
+       }
+
+       public RocketLoadException(String message) {
+               super(message);
+       }
+
+       public RocketLoadException(Throwable cause) {
+               super(cause);
+       }
+
+       public RocketLoadException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/RocketLoader.java b/src/net/sf/openrocket/file/RocketLoader.java
new file mode 100644 (file)
index 0000000..13f888c
--- /dev/null
@@ -0,0 +1,67 @@
+package net.sf.openrocket.file;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+public abstract class RocketLoader {
+       protected final WarningSet warnings = new WarningSet();
+
+
+       /**
+        * Loads a rocket from the specified File object.
+        */
+       public final OpenRocketDocument load(File source) throws RocketLoadException {
+               warnings.clear();
+
+               try {
+                       return load(new BufferedInputStream(new FileInputStream(source)));
+               } catch (FileNotFoundException e) {
+                       throw new RocketLoadException("File not found: " + source);
+               }
+       }
+
+       /**
+        * Loads a rocket from the specified InputStream.
+        */
+       public final OpenRocketDocument load(InputStream source) throws RocketLoadException {
+               warnings.clear();
+
+               try {
+                       return loadFromStream(source);
+               } catch (RocketLoadException e) {
+                       throw e;
+               } catch (IOException e) {
+                       throw new RocketLoadException("I/O error: " + e.getMessage());
+               } catch (Exception e) {
+                       throw new RocketLoadException("An unknown error occurred.  Please report a bug.", e);
+               } catch (Throwable e) {
+                       throw new RocketLoadException("A serious error occurred and the software may be "
+                                       + "unstable.  Save your designs and restart OpenRocket.", e);
+               }
+       }
+
+       
+       
+       /**
+        * This method is called by the default implementations of {@link #load(File)} 
+        * and {@link #load(InputStream)} to load the rocket.
+        * 
+        * @throws RocketLoadException  if an error occurs during loading.
+        */
+       protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException,
+                       RocketLoadException;
+
+
+
+       public final WarningSet getWarnings() {
+               return warnings;
+       }
+}
diff --git a/src/net/sf/openrocket/file/RocketSaver.java b/src/net/sf/openrocket/file/RocketSaver.java
new file mode 100644 (file)
index 0000000..b6f8b60
--- /dev/null
@@ -0,0 +1,92 @@
+package net.sf.openrocket.file;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.StorageOptions;
+
+
+public abstract class RocketSaver {
+       
+       /**
+        * Save the document to the specified file using the default storage options.
+        * 
+        * @param dest                  the destination file.
+        * @param document              the document to save.
+        * @throws IOException  in case of an I/O error.
+        */
+       public final void save(File dest, OpenRocketDocument document) throws IOException {
+               save(dest, document, document.getDefaultStorageOptions());
+       }
+
+       
+       /**
+        * Save the document to the specified file using the given storage options.
+        * 
+        * @param dest                  the destination file.
+        * @param document              the document to save.
+        * @param options               the storage options.
+        * @throws IOException  in case of an I/O error.
+        */
+       public void save(File dest, OpenRocketDocument document, StorageOptions options) 
+       throws IOException {
+               OutputStream s = new BufferedOutputStream(new FileOutputStream(dest));
+               try {
+                       save(s, document, options);
+               } finally {
+                       s.close();
+               }
+       }
+       
+       
+       /**
+        * Save the document to the specified output stream using the default storage options.
+        * 
+        * @param dest                  the destination stream.
+        * @param doc                   the document to save.
+        * @throws IOException  in case of an I/O error.
+        */
+       public final void save(OutputStream dest, OpenRocketDocument doc) throws IOException {
+               save(dest, doc, doc.getDefaultStorageOptions());
+       }
+       
+       
+       /**
+        * Save the document to the specified output stream using the given storage options.
+        * 
+        * @param dest                  the destination stream.
+        * @param doc                   the document to save.
+        * @param options               the storage options.
+        * @throws IOException  in case of an I/O error.
+        */
+       public abstract void save(OutputStream dest, OpenRocketDocument doc, 
+                       StorageOptions options) throws IOException;
+       
+       
+       
+       
+       
+       
+       
+       public static String escapeXML(String s) {
+
+               s = s.replace("&", "&amp;");
+               s = s.replace("<", "&lt;");
+               s = s.replace(">", "&gt;");
+               s = s.replace("\"","&quot;");
+               s = s.replace("'", "&apos;");
+               
+               for (int i=0; i < s.length(); i++) {
+                       char n = s.charAt(i);
+                       if (((n < 32) && (n != 9) && (n != 10) && (n != 13)) || (n == 127)) {
+                               s = s.substring(0,i) + "&#" + ((int)n) + ";" + s.substring(i+1);
+                       }
+               }
+               
+               return s;
+       }
+}
diff --git a/src/net/sf/openrocket/file/openrocket/BodyComponentSaver.java b/src/net/sf/openrocket/file/openrocket/BodyComponentSaver.java
new file mode 100644 (file)
index 0000000..97dc5eb
--- /dev/null
@@ -0,0 +1,15 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class BodyComponentSaver extends ExternalComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               // Body components have a natural length, store it now
+               elements.add("<length>"+((net.sf.openrocket.rocketcomponent.BodyComponent)c).getLength()+"</length>");
+       }
+       
+}
diff --git a/src/net/sf/openrocket/file/openrocket/BodyTubeSaver.java b/src/net/sf/openrocket/file/openrocket/BodyTubeSaver.java
new file mode 100644 (file)
index 0000000..865c6c8
--- /dev/null
@@ -0,0 +1,36 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BodyTubeSaver extends SymmetricComponentSaver {
+
+       private static final BodyTubeSaver instance = new BodyTubeSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<bodytube>");
+               instance.addParams(c, list);
+               list.add("</bodytube>");
+
+               return list;
+       }
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube) c;
+
+               if (tube.isRadiusAutomatic())
+                       elements.add("<radius>auto</radius>");
+               else
+                       elements.add("<radius>" + tube.getRadius() + "</radius>");
+
+               if (tube.isMotorMount()) {
+                       elements.addAll(motorMountParams(tube));
+               }
+       }
+
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/BulkheadSaver.java b/src/net/sf/openrocket/file/openrocket/BulkheadSaver.java
new file mode 100644 (file)
index 0000000..c4e5b6a
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BulkheadSaver extends RadiusRingComponentSaver {
+
+       private static final BulkheadSaver instance = new BulkheadSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<bulkhead>");
+               instance.addParams(c, list);
+               list.add("</bulkhead>");
+
+               return list;
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/CenteringRingSaver.java b/src/net/sf/openrocket/file/openrocket/CenteringRingSaver.java
new file mode 100644 (file)
index 0000000..9014fb0
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CenteringRingSaver extends RadiusRingComponentSaver {
+
+       private static final CenteringRingSaver instance = new CenteringRingSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<centeringring>");
+               instance.addParams(c, list);
+               list.add("</centeringring>");
+
+               return list;
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/ComponentAssemblySaver.java b/src/net/sf/openrocket/file/openrocket/ComponentAssemblySaver.java
new file mode 100644 (file)
index 0000000..8a73471
--- /dev/null
@@ -0,0 +1,7 @@
+package net.sf.openrocket.file.openrocket;
+
+public class ComponentAssemblySaver extends RocketComponentSaver {
+
+       // No-op
+       
+}
diff --git a/src/net/sf/openrocket/file/openrocket/EllipticalFinSetSaver.java b/src/net/sf/openrocket/file/openrocket/EllipticalFinSetSaver.java
new file mode 100644 (file)
index 0000000..8874a7a
--- /dev/null
@@ -0,0 +1,29 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EllipticalFinSetSaver extends FinSetSaver {
+
+       private static final EllipticalFinSetSaver instance = new EllipticalFinSetSaver();
+       
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+               
+               list.add("<ellipticalfinset>");
+               instance.addParams(c,list);
+               list.add("</ellipticalfinset>");
+               
+               return list;
+       }
+       
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               net.sf.openrocket.rocketcomponent.EllipticalFinSet fins = (net.sf.openrocket.rocketcomponent.EllipticalFinSet)c;
+               elements.add("<rootchord>"+fins.getLength()+"</rootchord>");
+               elements.add("<height>"+fins.getHeight()+"</height>");
+       }
+       
+}
diff --git a/src/net/sf/openrocket/file/openrocket/EngineBlockSaver.java b/src/net/sf/openrocket/file/openrocket/EngineBlockSaver.java
new file mode 100644 (file)
index 0000000..4932322
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EngineBlockSaver extends ThicknessRingComponentSaver {
+
+       private static final EngineBlockSaver instance = new EngineBlockSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<engineblock>");
+               instance.addParams(c, list);
+               list.add("</engineblock>");
+
+               return list;
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/ExternalComponentSaver.java b/src/net/sf/openrocket/file/openrocket/ExternalComponentSaver.java
new file mode 100644 (file)
index 0000000..7ca8b8f
--- /dev/null
@@ -0,0 +1,23 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+
+
+public class ExternalComponentSaver extends RocketComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               ExternalComponent ext = (ExternalComponent)c;
+               
+               // Finish enum names are currently the same except for case
+               elements.add("<finish>" + ext.getFinish().name().toLowerCase() + "</finish>");
+               
+               // Material
+               elements.add(materialParam(ext.getMaterial()));
+       }
+               
+}
diff --git a/src/net/sf/openrocket/file/openrocket/FinSetSaver.java b/src/net/sf/openrocket/file/openrocket/FinSetSaver.java
new file mode 100644 (file)
index 0000000..42f4d89
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class FinSetSaver extends ExternalComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               net.sf.openrocket.rocketcomponent.FinSet fins = (net.sf.openrocket.rocketcomponent.FinSet) c;
+               elements.add("<fincount>" + fins.getFinCount() + "</fincount>");
+               elements.add("<rotation>" + (fins.getBaseRotation() * 180.0 / Math.PI) + "</rotation>");
+               elements.add("<thickness>" + fins.getThickness() + "</thickness>");
+               elements.add("<crosssection>" + fins.getCrossSection().name().toLowerCase()
+                               + "</crosssection>");
+               elements.add("<cant>" + (fins.getCantAngle() * 180.0 / Math.PI) + "</cant>");
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/FreeformFinSetSaver.java b/src/net/sf/openrocket/file/openrocket/FreeformFinSetSaver.java
new file mode 100644 (file)
index 0000000..6aa1778
--- /dev/null
@@ -0,0 +1,36 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.util.Coordinate;
+
+
+public class FreeformFinSetSaver extends FinSetSaver {
+
+       private static final FreeformFinSetSaver instance = new FreeformFinSetSaver();
+       
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+               
+               list.add("<freeformfinset>");
+               instance.addParams(c,list);
+               list.add("</freeformfinset>");
+               
+               return list;
+       }
+       
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               FreeformFinSet fins = (FreeformFinSet)c;
+               elements.add("<finpoints>");
+               for (Coordinate p: fins.getFinPoints()) {
+                       elements.add("  <point x=\"" + p.x + "\" y=\"" + p.y + "\"/>");
+               }
+               elements.add("</finpoints>");
+       }
+       
+}
diff --git a/src/net/sf/openrocket/file/openrocket/InnerTubeSaver.java b/src/net/sf/openrocket/file/openrocket/InnerTubeSaver.java
new file mode 100644 (file)
index 0000000..f706917
--- /dev/null
@@ -0,0 +1,43 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.InnerTube;
+
+
+public class InnerTubeSaver extends ThicknessRingComponentSaver {
+
+       private static final InnerTubeSaver instance = new InnerTubeSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<innertube>");
+               instance.addParams(c, list);
+               list.add("</innertube>");
+
+               return list;
+       }
+
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               InnerTube tube = (InnerTube) c;
+
+               elements.add("<clusterconfiguration>" + tube.getClusterConfiguration().getXMLName()
+                               + "</clusterconfiguration>");
+               elements.add("<clusterscale>" + tube.getClusterScale() + "</clusterscale>");
+               elements.add("<clusterrotation>" + (tube.getClusterRotation() * 180.0 / Math.PI)
+                               + "</clusterrotation>");
+
+               if (tube.isMotorMount()) {
+                       elements.addAll(motorMountParams(tube));
+               }
+
+
+       }
+
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/InternalComponentSaver.java b/src/net/sf/openrocket/file/openrocket/InternalComponentSaver.java
new file mode 100644 (file)
index 0000000..45c0a56
--- /dev/null
@@ -0,0 +1,14 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class InternalComponentSaver extends RocketComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               // Nothing to save
+       }
+               
+}
diff --git a/src/net/sf/openrocket/file/openrocket/LaunchLugSaver.java b/src/net/sf/openrocket/file/openrocket/LaunchLugSaver.java
new file mode 100644 (file)
index 0000000..06d3da5
--- /dev/null
@@ -0,0 +1,35 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+
+
+public class LaunchLugSaver extends ExternalComponentSaver {
+
+       private static final LaunchLugSaver instance = new LaunchLugSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<launchlug>");
+               instance.addParams(c, list);
+               list.add("</launchlug>");
+
+               return list;
+       }
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               LaunchLug lug = (LaunchLug) c;
+
+               elements.add("<radius>" + lug.getRadius() + "</radius>");
+               elements.add("<length>" + lug.getLength() + "</length>");
+               elements.add("<thickness>" + lug.getThickness() + "</thickness>");
+               elements.add("<radialdirection>" + (lug.getRadialDirection()*180.0/Math.PI) + "</radialdirection>");
+       }
+
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/MassComponentSaver.java b/src/net/sf/openrocket/file/openrocket/MassComponentSaver.java
new file mode 100644 (file)
index 0000000..11a5ce6
--- /dev/null
@@ -0,0 +1,32 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.MassComponent;
+
+
+public class MassComponentSaver extends MassObjectSaver {
+
+       private static final MassComponentSaver instance = new MassComponentSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<masscomponent>");
+               instance.addParams(c, list);
+               list.add("</masscomponent>");
+
+               return list;
+       }
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               MassComponent mass = (MassComponent) c;
+
+               elements.add("<mass>" + mass.getMass() + "</mass>");
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/MassObjectSaver.java b/src/net/sf/openrocket/file/openrocket/MassObjectSaver.java
new file mode 100644 (file)
index 0000000..f1ee440
--- /dev/null
@@ -0,0 +1,23 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.MassObject;
+
+
+public class MassObjectSaver extends InternalComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               MassObject mass = (MassObject) c;
+
+               elements.add("<packedlength>" + mass.getLength() + "</packedlength>");
+               elements.add("<packedradius>" + mass.getRadius() + "</packedradius>");
+               elements.add("<radialposition>" + mass.getRadialPosition() + "</radialposition>");
+               elements.add("<radialdirection>" + (mass.getRadialDirection() * 180.0 / Math.PI)
+                               + "</radialdirection>");
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/NoseConeSaver.java b/src/net/sf/openrocket/file/openrocket/NoseConeSaver.java
new file mode 100644 (file)
index 0000000..95feafb
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NoseConeSaver extends TransitionSaver {
+
+       private static final NoseConeSaver instance = new NoseConeSaver();
+       
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+               
+               list.add("<nosecone>");
+               instance.addParams(c,list);
+               list.add("</nosecone>");
+               
+               return list;
+       }
+       
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               // Transition handles nose cone saving as well
+       }
+}
diff --git a/src/net/sf/openrocket/file/openrocket/ParachuteSaver.java b/src/net/sf/openrocket/file/openrocket/ParachuteSaver.java
new file mode 100644 (file)
index 0000000..64e44ff
--- /dev/null
@@ -0,0 +1,35 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.Parachute;
+
+
+public class ParachuteSaver extends RecoveryDeviceSaver {
+
+       private static final ParachuteSaver instance = new ParachuteSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<parachute>");
+               instance.addParams(c, list);
+               list.add("</parachute>");
+
+               return list;
+       }
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               Parachute para = (Parachute) c;
+
+               elements.add("<diameter>" + para.getDiameter() + "</diameter>");
+               elements.add("<linecount>" + para.getLineCount() + "</linecount>");
+               elements.add("<linelength>" + para.getLineLength() + "</linelength>");
+               elements.add(materialParam("linematerial", para.getLineMaterial()));
+       }
+
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/RadiusRingComponentSaver.java b/src/net/sf/openrocket/file/openrocket/RadiusRingComponentSaver.java
new file mode 100644 (file)
index 0000000..02cf015
--- /dev/null
@@ -0,0 +1,28 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+
+
+public class RadiusRingComponentSaver extends RingComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               RadiusRingComponent comp = (RadiusRingComponent)c;
+               if (comp.isOuterRadiusAutomatic())
+                       elements.add("<outerradius>auto</outerradius>");
+               else
+                       elements.add("<outerradius>" + comp.getOuterRadius() + "</outerradius>");
+               if (!(comp instanceof Bulkhead)) {
+                       if (comp.isInnerRadiusAutomatic())
+                               elements.add("<innerradius>auto</innerradius>");
+                       else
+                               elements.add("<innerradius>" + comp.getInnerRadius() + "</innerradius>");
+               }
+       }
+               
+}
diff --git a/src/net/sf/openrocket/file/openrocket/RecoveryDeviceSaver.java b/src/net/sf/openrocket/file/openrocket/RecoveryDeviceSaver.java
new file mode 100644 (file)
index 0000000..a57747e
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+
+
+public class RecoveryDeviceSaver extends MassObjectSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               RecoveryDevice dev = (RecoveryDevice) c;
+
+               if (dev.isCDAutomatic())
+                       elements.add("<cd>auto</cd>");
+               else
+                       elements.add("<cd>" + dev.getCD() + "</cd>");
+
+               elements.add("<deployevent>" + dev.getDeployEvent().name().toLowerCase() + "</deployevent>");
+               elements.add("<deployaltitude>" + dev.getDeployAltitude() + "</deployaltitude>");
+               elements.add("<deploydelay>" + dev.getDeployDelay() + "</deploydelay>");
+               elements.add(materialParam(dev.getMaterial()));
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/RingComponentSaver.java b/src/net/sf/openrocket/file/openrocket/RingComponentSaver.java
new file mode 100644 (file)
index 0000000..53d8177
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.RingComponent;
+
+
+public class RingComponentSaver extends StructuralComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               RingComponent ring = (RingComponent) c;
+
+               elements.add("<length>" + ring.getLength() + "</length>");
+               elements.add("<radialposition>" + ring.getRadialPosition() + "</radialposition>");
+               elements.add("<radialdirection>" + (ring.getRadialDirection() * 180.0 / Math.PI)
+                               + "</radialdirection>");
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/RocketComponentSaver.java b/src/net/sf/openrocket/file/openrocket/RocketComponentSaver.java
new file mode 100644 (file)
index 0000000..1a1a315
--- /dev/null
@@ -0,0 +1,151 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.openrocket.file.RocketSaver;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.ComponentAssembly;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.LineStyle;
+
+
+public class RocketComponentSaver {
+
+       protected RocketComponentSaver() {
+               // Prevent instantiation from outside the package
+       }
+
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               elements.add("<name>" + RocketSaver.escapeXML(c.getName()) + "</name>");
+
+
+               // Save color and line style if significant
+               if (!(c instanceof Rocket || c instanceof ComponentAssembly)) {
+                       Color color = c.getColor();
+                       if (color != null) {
+                               elements.add("<color red=\"" + color.getRed() + "\" green=\"" + color.getGreen() 
+                                               + "\" blue=\"" + color.getBlue() + "\"/>");
+                       }
+
+                       LineStyle style = c.getLineStyle();
+                       if (style != null) {
+                               // Type names currently equivalent to the enum names except for case.
+                               elements.add("<linestyle>" + style.name().toLowerCase() + "</linestyle>");
+                       }
+               }
+
+
+               // Save position unless "AFTER"
+               if (c.getRelativePosition() != RocketComponent.Position.AFTER) {
+                       // The type names are currently equivalent to the enum names except for case.
+                       String type = c.getRelativePosition().name().toLowerCase();
+                       elements.add("<position type=\"" + type + "\">" + c.getPositionValue() + "</position>");
+               }
+
+
+               // Overrides
+               boolean overridden = false;
+               if (c.isMassOverridden()) {
+                       elements.add("<overridemass>" + c.getOverrideMass() + "</overridemass>");
+                       overridden = true;
+               }
+               if (c.isCGOverridden()) {
+                       elements.add("<overridecg>" + c.getOverrideCGX() + "</overridecg>");
+                       overridden = true;
+               }
+               if (overridden) {
+                       elements.add("<overridesubcomponents>" + c.getOverrideSubcomponents()
+                                       + "</overridesubcomponents>");
+               }
+
+
+               // Comment
+               if (c.getComment().length() > 0) {
+                       elements.add("<comment>" + RocketSaver.escapeXML(c.getComment()) + "</comment>");
+               }
+
+       }
+
+
+
+
+       protected final String materialParam(Material mat) {
+               return materialParam("material", mat);
+       }
+
+
+       protected final String materialParam(String tag, Material mat) {
+               String str = "<" + tag;
+
+               switch (mat.getType()) {
+               case LINE:
+                       str += " type=\"line\"";
+                       break;
+               case SURFACE:
+                       str += " type=\"surface\"";
+                       break;
+               case BULK:
+                       str += " type=\"bulk\"";
+                       break;
+               default:
+                       throw new RuntimeException("Unknown material type: " + mat.getType());
+               }
+
+               return str + " density=\"" + mat.getDensity() + "\">" + RocketSaver.escapeXML(mat.getName()) + "</"+tag+">";
+       }
+
+
+       protected final List<String> motorMountParams(MotorMount mount) {
+               if (!mount.isMotorMount())
+                       return Collections.emptyList();
+
+               String[] motorConfigIDs = ((RocketComponent) mount).getRocket().getMotorConfigurationIDs();
+               List<String> elements = new ArrayList<String>();
+
+               elements.add("<motormount>");
+
+               for (String id : motorConfigIDs) {
+                       Motor motor = mount.getMotor(id);
+
+                       // Nothing is stored if no motor loaded
+                       if (motor == null)
+                               continue;
+
+                       elements.add("  <motor configid=\"" + id + "\">");
+                       if (motor.getMotorType() != Motor.Type.UNKNOWN) {
+                               elements.add("    <type>" + motor.getMotorType().name().toLowerCase() + "</type>");
+                       }
+                       elements.add("    <manufacturer>" + RocketSaver.escapeXML(motor.getManufacturer()) + "</manufacturer>");
+                       elements.add("    <designation>" + RocketSaver.escapeXML(motor.getDesignation()) + "</designation>");
+                       elements.add("    <diameter>" + motor.getDiameter() + "</diameter>");
+                       elements.add("    <length>" + motor.getLength() + "</length>");
+                       
+                       // Motor delay
+                       if (mount.getMotorDelay(id) == Motor.PLUGGED) {
+                               elements.add("    <delay>none</delay>");
+                       } else {
+                               elements.add("    <delay>" + mount.getMotorDelay(id) + "</delay>");
+                       }
+
+                       elements.add("  </motor>");
+               }
+
+               elements.add("  <ignitionevent>"
+                               + mount.getIgnitionEvent().name().toLowerCase().replace("_", "")
+                               + "</ignitionevent>");
+
+               elements.add("  <ignitiondelay>" + mount.getIgnitionDelay() + "</ignitiondelay>");
+               elements.add("  <overhang>" + mount.getMotorOverhang() + "</overhang>");
+               
+               elements.add("</motormount>");
+
+               return elements;
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/RocketSaver.java b/src/net/sf/openrocket/file/openrocket/RocketSaver.java
new file mode 100644 (file)
index 0000000..e27fa76
--- /dev/null
@@ -0,0 +1,73 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ReferenceType;
+import net.sf.openrocket.rocketcomponent.Rocket;
+
+
+public class RocketSaver extends RocketComponentSaver {
+
+       private static final RocketSaver instance = new RocketSaver();
+
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+
+               list.add("<rocket>");
+               instance.addParams(c, list);
+               list.add("</rocket>");
+
+               return list;
+       }
+
+
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               Rocket rocket = (Rocket) c;
+               
+               if (rocket.getDesigner().length() > 0) {
+                       elements.add("<designer>" 
+                                       + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getDesigner())
+                                       + "</designer>");
+               }
+               if (rocket.getRevision().length() > 0) {
+                       elements.add("<revision>" 
+                                       + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getRevision()) 
+                                       + "</revision>");
+               }
+
+
+               // Motor configurations
+               String defId = rocket.getDefaultConfiguration().getMotorConfigurationID();
+               for (String id : rocket.getMotorConfigurationIDs()) {
+                       if (id == null)
+                               continue;
+
+                       String str = "<motorconfiguration configid=\"" + id + "\"";
+                       if (id.equals(defId))
+                               str += " default=\"true\"";
+                       
+                       if (rocket.getMotorConfigurationName(id) == "") {
+                               str += "/>";
+                       } else {
+                               str += "><name>" + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getMotorConfigurationName(id))
+                                       + "</name></motorconfiguration>";
+                       }
+                       elements.add(str);
+               }
+               
+               // Reference diameter
+               elements.add("<referencetype>" + rocket.getReferenceType().name().toLowerCase()
+                               + "</referencetype>");
+               if (rocket.getReferenceType() == ReferenceType.CUSTOM) {
+                       elements.add("<customreference>" + rocket.getCustomReferenceLength()
+                                       + "</customreference>");
+               }
+
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/ShockCordSaver.java b/src/net/sf/openrocket/file/openrocket/ShockCordSaver.java
new file mode 100644 (file)
index 0000000..0f8746e
--- /dev/null
@@ -0,0 +1,33 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ShockCord;
+
+
+public class ShockCordSaver extends MassObjectSaver {
+
+       private static final ShockCordSaver instance = new ShockCordSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<shockcord>");
+               instance.addParams(c, list);
+               list.add("</shockcord>");
+
+               return list;
+       }
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               ShockCord mass = (ShockCord) c;
+
+               elements.add("<cordlength>" + mass.getCordLength() + "</cordlength>");
+               elements.add(materialParam(mass.getMaterial()));
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/StageSaver.java b/src/net/sf/openrocket/file/openrocket/StageSaver.java
new file mode 100644 (file)
index 0000000..2c83772
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+
+public class StageSaver extends ComponentAssemblySaver {
+
+       private static final StageSaver instance = new StageSaver();
+       
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+               
+               list.add("<stage>");
+               instance.addParams(c,list);
+               list.add("</stage>");
+               
+               return list;
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/file/openrocket/StreamerSaver.java b/src/net/sf/openrocket/file/openrocket/StreamerSaver.java
new file mode 100644 (file)
index 0000000..d3e936b
--- /dev/null
@@ -0,0 +1,33 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.Streamer;
+
+
+public class StreamerSaver extends RecoveryDeviceSaver {
+
+       private static final StreamerSaver instance = new StreamerSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<streamer>");
+               instance.addParams(c, list);
+               list.add("</streamer>");
+
+               return list;
+       }
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               Streamer st = (Streamer) c;
+
+               elements.add("<striplength>" + st.getStripLength() + "</striplength>");
+               elements.add("<stripwidth>" + st.getStripWidth() + "</stripwidth>");
+       }
+
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/StructuralComponentSaver.java b/src/net/sf/openrocket/file/openrocket/StructuralComponentSaver.java
new file mode 100644 (file)
index 0000000..4d7f605
--- /dev/null
@@ -0,0 +1,18 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.StructuralComponent;
+
+
+public class StructuralComponentSaver extends InternalComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               StructuralComponent comp = (StructuralComponent)c;
+               elements.add(materialParam(comp.getMaterial()));
+       }
+               
+}
diff --git a/src/net/sf/openrocket/file/openrocket/SymmetricComponentSaver.java b/src/net/sf/openrocket/file/openrocket/SymmetricComponentSaver.java
new file mode 100644 (file)
index 0000000..741c513
--- /dev/null
@@ -0,0 +1,18 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class SymmetricComponentSaver extends BodyComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+
+               net.sf.openrocket.rocketcomponent.SymmetricComponent comp = (net.sf.openrocket.rocketcomponent.SymmetricComponent)c;
+               if (comp.isFilled())
+                       elements.add("<thickness>filled</thickness>");
+               else
+                       elements.add("<thickness>"+comp.getThickness()+"</thickness>");
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/ThicknessRingComponentSaver.java b/src/net/sf/openrocket/file/openrocket/ThicknessRingComponentSaver.java
new file mode 100644 (file)
index 0000000..d52df55
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
+
+
+public class ThicknessRingComponentSaver extends RingComponentSaver {
+
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               ThicknessRingComponent comp = (ThicknessRingComponent)c;
+               if (comp.isOuterRadiusAutomatic())
+                       elements.add("<outerradius>auto</outerradius>");
+               else
+                       elements.add("<outerradius>" + comp.getOuterRadius() + "</outerradius>");
+               elements.add("<thickness>" + comp.getThickness() + "</thickness>");
+       }
+               
+}
diff --git a/src/net/sf/openrocket/file/openrocket/TransitionSaver.java b/src/net/sf/openrocket/file/openrocket/TransitionSaver.java
new file mode 100644 (file)
index 0000000..d597d12
--- /dev/null
@@ -0,0 +1,79 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Transition;
+
+
+public class TransitionSaver extends SymmetricComponentSaver {
+
+       private static final TransitionSaver instance = new TransitionSaver();
+
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+
+               list.add("<transition>");
+               instance.addParams(c, list);
+               list.add("</transition>");
+
+               return list;
+       }
+
+
+       /*
+        * Note:  This method must be capable of handling nose cones as well.
+        */
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               net.sf.openrocket.rocketcomponent.Transition trans = (net.sf.openrocket.rocketcomponent.Transition) c;
+               boolean nosecone = (trans instanceof NoseCone);
+
+
+               Transition.Shape shape = trans.getType();
+               elements.add("<shape>" + shape.getName().toLowerCase() + "</shape>");
+               if (shape.isClippable()) {
+                       elements.add("<shapeclipped>" + trans.isClipped() + "</shapeclipped>");
+               }
+               if (shape.usesParameter()) {
+                       elements.add("<shapeparameter>" + trans.getShapeParameter() + "</shapeparameter>");
+               }
+
+
+               if (!nosecone) {
+                       if (trans.isForeRadiusAutomatic())
+                               elements.add("<foreradius>auto</foreradius>");
+                       else
+                               elements.add("<foreradius>" + trans.getForeRadius() + "</foreradius>");
+               }
+
+               if (trans.isAftRadiusAutomatic())
+                       elements.add("<aftradius>auto</aftradius>");
+               else
+                       elements.add("<aftradius>" + trans.getAftRadius() + "</aftradius>");
+
+
+               if (!nosecone) {
+                       elements.add("<foreshoulderradius>" + trans.getForeShoulderRadius()
+                                       + "</foreshoulderradius>");
+                       elements.add("<foreshoulderlength>" + trans.getForeShoulderLength()
+                                       + "</foreshoulderlength>");
+                       elements.add("<foreshoulderthickness>" + trans.getForeShoulderThickness()
+                                       + "</foreshoulderthickness>");
+                       elements.add("<foreshouldercapped>" + trans.isForeShoulderCapped()
+                                       + "</foreshouldercapped>");
+               }
+
+               elements.add("<aftshoulderradius>" + trans.getAftShoulderRadius()
+                               + "</aftshoulderradius>");
+               elements.add("<aftshoulderlength>" + trans.getAftShoulderLength()
+                               + "</aftshoulderlength>");
+               elements.add("<aftshoulderthickness>" + trans.getAftShoulderThickness()
+                               + "</aftshoulderthickness>");
+               elements.add("<aftshouldercapped>" + trans.isAftShoulderCapped()
+                               + "</aftshouldercapped>");
+       }
+
+}
diff --git a/src/net/sf/openrocket/file/openrocket/TrapezoidFinSetSaver.java b/src/net/sf/openrocket/file/openrocket/TrapezoidFinSetSaver.java
new file mode 100644 (file)
index 0000000..fc244cc
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TrapezoidFinSetSaver extends FinSetSaver {
+
+       private static final TrapezoidFinSetSaver instance = new TrapezoidFinSetSaver();
+       
+       public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               ArrayList<String> list = new ArrayList<String>();
+               
+               list.add("<trapezoidfinset>");
+               instance.addParams(c,list);
+               list.add("</trapezoidfinset>");
+               
+               return list;
+       }
+       
+       @Override
+       protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+               super.addParams(c, elements);
+               
+               net.sf.openrocket.rocketcomponent.TrapezoidFinSet fins = (net.sf.openrocket.rocketcomponent.TrapezoidFinSet)c;
+               elements.add("<rootchord>"+fins.getRootChord()+"</rootchord>");
+               elements.add("<tipchord>"+fins.getTipChord()+"</tipchord>");
+               elements.add("<sweeplength>"+fins.getSweep()+"</sweeplength>");
+               elements.add("<height>"+fins.getHeight()+"</height>");
+       }
+       
+}
diff --git a/src/net/sf/openrocket/file/openrocket/TubeCouplerSaver.java b/src/net/sf/openrocket/file/openrocket/TubeCouplerSaver.java
new file mode 100644 (file)
index 0000000..09a79c7
--- /dev/null
@@ -0,0 +1,20 @@
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TubeCouplerSaver extends ThicknessRingComponentSaver {
+
+       private static final TubeCouplerSaver instance = new TubeCouplerSaver();
+
+       public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+               List<String> list = new ArrayList<String>();
+
+               list.add("<tubecoupler>");
+               instance.addParams(c, list);
+               list.add("</tubecoupler>");
+
+               return list;
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/BasicSlider.java b/src/net/sf/openrocket/gui/BasicSlider.java
new file mode 100644 (file)
index 0000000..d22c3e8
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.gui;
+
+import javax.swing.BoundedRangeModel;
+import javax.swing.JSlider;
+import javax.swing.plaf.basic.BasicSliderUI;
+
+/**
+ * A simple slider that does not show the current value.  GTK l&f shows the value, and cannot 
+ * be configured otherwise(!).
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BasicSlider extends JSlider {
+
+       public BasicSlider(BoundedRangeModel brm) {
+               this(brm,JSlider.HORIZONTAL,false);
+       }
+       
+       public BasicSlider(BoundedRangeModel brm, int orientation) {
+               this(brm,orientation,false);
+       }
+       
+       public BasicSlider(BoundedRangeModel brm, int orientation, boolean inverted) {
+               super(brm);
+               setOrientation(orientation);
+               setInverted(inverted);
+               setUI(new BasicSliderUI(this));
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/ComponentAnalysisDialog.java b/src/net/sf/openrocket/gui/ComponentAnalysisDialog.java
new file mode 100644 (file)
index 0000000..e44e48b
--- /dev/null
@@ -0,0 +1,583 @@
+package net.sf.openrocket.gui;
+
+import static net.sf.openrocket.unit.Unit.NOUNIT2;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.table.TableCellRenderer;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.scalefigure.RocketPanel;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class ComponentAnalysisDialog extends JDialog implements ChangeListener {
+
+       private static ComponentAnalysisDialog singletonDialog = null;
+
+       
+       private final FlightConditions conditions;
+       private final Configuration configuration;
+       private final DoubleModel theta, aoa, mach, roll;
+       private final JToggleButton worstToggle;
+       private boolean fakeChange = false;
+       private AerodynamicCalculator calculator;
+       
+       private final ColumnTableModel cpTableModel;
+       private final ColumnTableModel dragTableModel;
+       private final ColumnTableModel rollTableModel;
+       
+       private final JList warningList;
+       
+       
+       private final List<AerodynamicForces> cpData = new ArrayList<AerodynamicForces>();
+       private final List<AerodynamicForces> dragData = new ArrayList<AerodynamicForces>();
+       private double totalCD = 0;
+       private final List<AerodynamicForces> rollData = new ArrayList<AerodynamicForces>();
+       
+       
+       public ComponentAnalysisDialog(final RocketPanel rocketPanel) {
+               super(SwingUtilities.getWindowAncestor(rocketPanel), "Component analysis");
+
+               JTable table;
+
+               JPanel panel = new JPanel(new MigLayout("fill","[][35lp::][fill][fill]"));
+               add(panel);
+               
+               this.configuration = rocketPanel.getConfiguration();
+               this.calculator = rocketPanel.getCalculator().newInstance();
+               this.calculator.setConfiguration(configuration);
+
+               
+               conditions = new FlightConditions(configuration);
+               
+               rocketPanel.setCPAOA(0);
+               aoa = new DoubleModel(rocketPanel, "CPAOA", UnitGroup.UNITS_ANGLE, 0, Math.PI);
+               rocketPanel.setCPMach(Prefs.getDefaultMach());
+               mach = new DoubleModel(rocketPanel, "CPMach", UnitGroup.UNITS_COEFFICIENT, 0);
+               rocketPanel.setCPTheta(rocketPanel.getFigure().getRotation());
+               theta = new DoubleModel(rocketPanel, "CPTheta", UnitGroup.UNITS_ANGLE, 0, 2*Math.PI);
+               rocketPanel.setCPRoll(0);
+               roll = new DoubleModel(rocketPanel, "CPRoll", UnitGroup.UNITS_ROLL);
+               
+               
+               panel.add(new JLabel("Wind direction:"),"width 100lp!");
+               panel.add(new UnitSelector(theta,true),"width 50lp!");
+               BasicSlider slider = new BasicSlider(theta.getSliderModel(0, 2*Math.PI));
+               panel.add(slider,"growx, split 2");
+               worstToggle = new JToggleButton("Worst");
+               worstToggle.setSelected(true);
+               worstToggle.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               stateChanged(null);
+                       }
+               });
+               slider.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               if (!fakeChange)
+                                       worstToggle.setSelected(false);
+                       }
+               });
+               panel.add(worstToggle,"");
+               
+               
+               warningList = new JList();
+               JScrollPane scrollPane = new JScrollPane(warningList);
+               scrollPane.setBorder(BorderFactory.createTitledBorder("Warnings:"));
+               panel.add(scrollPane,"gap paragraph, spany 4, width 300lp!, growy 1, height :100lp:, wrap");
+               
+               
+               panel.add(new JLabel("Angle of attack:"),"width 100lp!");
+               panel.add(new UnitSelector(aoa,true),"width 50lp!");
+               panel.add(new BasicSlider(aoa.getSliderModel(0, Math.PI)),"growx, wrap");
+               
+               panel.add(new JLabel("Mach number:"),"width 100lp!");
+               panel.add(new UnitSelector(mach,true),"width 50lp!");
+               panel.add(new BasicSlider(mach.getSliderModel(0, 3)),"growx, wrap");
+               
+               panel.add(new JLabel("Roll rate:"), "width 100lp!");
+               panel.add(new UnitSelector(roll,true),"width 50lp!");
+               panel.add(new BasicSlider(roll.getSliderModel(-20*2*Math.PI, 20*2*Math.PI)),
+                               "growx, wrap paragraph");
+               
+               
+               // Stage and motor selection:
+               
+               panel.add(new JLabel("Active stages:"),"spanx, split, gapafter rel");
+               panel.add(new StageSelector(configuration),"gapafter paragraph");
+                               
+               JLabel label = new JLabel("Motor configuration:");
+               label.setHorizontalAlignment(JLabel.RIGHT);
+               panel.add(label,"growx, right");
+               panel.add(new JComboBox(new MotorConfigurationModel(configuration)),"wrap");
+
+               
+               
+               // Tabbed pane
+               
+               JTabbedPane tabbedPane = new JTabbedPane();
+               panel.add(tabbedPane, "spanx, growx, growy");
+               
+               
+               // Create the CP data table
+               cpTableModel = new ColumnTableModel(
+                               
+                               new Column("Component") {
+                                       @Override public Object getValueAt(int row) {
+                                               RocketComponent c = cpData.get(row).component;
+                                               if (c instanceof Rocket) {
+                                                       return "Total";
+                                               }
+                                               return c.toString();
+                                       }
+                                       @Override public int getDefaultWidth() {
+                                               return 200;
+                                       }
+                               },
+                               new Column("CG / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) {
+                                       private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+                                       @Override public Object getValueAt(int row) {
+                                               return unit.toString(cpData.get(row).cg.x);
+                                       }
+                               },
+                               new Column("Mass / " + UnitGroup.UNITS_MASS.getDefaultUnit().getUnit()) {
+                                       private Unit unit = UnitGroup.UNITS_MASS.getDefaultUnit();
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               return unit.toString(cpData.get(row).cg.weight);
+                                       }
+                               },
+                               new Column("CP / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) {
+                                       private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+                                       @Override public Object getValueAt(int row) {
+                                               return unit.toString(cpData.get(row).cp.x);
+                                       }
+                               },
+                               new Column("<html>C<sub>N<sub>\u03b1</sub></sub>") {
+                                       @Override public Object getValueAt(int row) {
+                                               return NOUNIT2.toString(cpData.get(row).cp.weight);
+                                       }
+                               }
+                               
+               ) {
+                       @Override public int getRowCount() {
+                               return cpData.size();
+                       }
+               };
+               
+               table = new JTable(cpTableModel);
+               table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               table.setSelectionBackground(Color.LIGHT_GRAY);
+               table.setSelectionForeground(Color.BLACK);
+               cpTableModel.setColumnWidths(table.getColumnModel());
+               
+               table.setDefaultRenderer(Object.class, new CustomCellRenderer());
+//             table.setShowHorizontalLines(false);
+//             table.setShowVerticalLines(true);
+               
+               JScrollPane scrollpane = new JScrollPane(table);
+               scrollpane.setPreferredSize(new Dimension(600,200));
+               
+               tabbedPane.addTab("Stability", null, scrollpane, "Stability information");
+               
+               
+               
+               // Create the drag data table
+               dragTableModel = new ColumnTableModel(
+                               new Column("Component") {
+                                       @Override public Object getValueAt(int row) {
+                                               RocketComponent c = dragData.get(row).component;
+                                               if (c instanceof Rocket) {
+                                                       return "Total";
+                                               }
+                                               return c.toString();
+                                       }
+                                       @Override public int getDefaultWidth() {
+                                               return 200;
+                                       }
+                               },
+                               new Column("<html>Pressure C<sub>D</sub>") {
+                                       @Override public Object getValueAt(int row) {
+                                               return dragData.get(row).pressureCD;
+                                       }
+                               },
+                               new Column("<html>Base C<sub>D</sub>") {
+                                       @Override public Object getValueAt(int row) {
+                                               return dragData.get(row).baseCD;
+                                       }
+                               },
+                               new Column("<html>Friction C<sub>D</sub>") {
+                                       @Override public Object getValueAt(int row) {
+                                               return dragData.get(row).frictionCD;
+                                       }
+                               },
+                               new Column("<html>Total C<sub>D</sub>") {
+                                       @Override public Object getValueAt(int row) {
+                                               return dragData.get(row).CD;
+                                       }
+                               }
+               ) {
+                       @Override public int getRowCount() {
+                               return dragData.size();
+                       }                       
+               };
+               
+
+               table = new JTable(dragTableModel);
+               table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               table.setSelectionBackground(Color.LIGHT_GRAY);
+               table.setSelectionForeground(Color.BLACK);
+               dragTableModel.setColumnWidths(table.getColumnModel());
+               
+               table.setDefaultRenderer(Object.class, new DragCellRenderer(new Color(0.5f,1.0f,0.5f)));
+//             table.setShowHorizontalLines(false);
+//             table.setShowVerticalLines(true);
+               
+               scrollpane = new JScrollPane(table);
+               scrollpane.setPreferredSize(new Dimension(600,200));
+               
+               tabbedPane.addTab("Drag characteristics", null, scrollpane, "Drag characteristics");
+               
+               
+               
+               
+               // Create the roll data table
+               rollTableModel = new ColumnTableModel(
+                               new Column("Component") {
+                                       @Override public Object getValueAt(int row) {
+                                               RocketComponent c = rollData.get(row).component;
+                                               if (c instanceof Rocket) {
+                                                       return "Total";
+                                               }
+                                               return c.toString();
+                                       }
+                               },
+                               new Column("Roll forcing coefficient") {
+                                       @Override public Object getValueAt(int row) {
+                                               return rollData.get(row).CrollForce;
+                                       }
+                               },
+                               new Column("Roll damping coefficient") {
+                                       @Override public Object getValueAt(int row) {
+                                               return rollData.get(row).CrollDamp;
+                                       }
+                               },
+                               new Column("<html>Total C<sub>l</sub>") {
+                                       @Override public Object getValueAt(int row) {
+                                               return rollData.get(row).Croll;
+                                       }
+                               }
+               ) {
+                       @Override public int getRowCount() {
+                               return rollData.size();
+                       }                       
+               };
+               
+
+               table = new JTable(rollTableModel);
+               table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               table.setSelectionBackground(Color.LIGHT_GRAY);
+               table.setSelectionForeground(Color.BLACK);
+               rollTableModel.setColumnWidths(table.getColumnModel());
+               
+               scrollpane = new JScrollPane(table);
+               scrollpane.setPreferredSize(new Dimension(600,200));
+               
+               tabbedPane.addTab("Roll dynamics", null, scrollpane, "Roll dynamics");
+               
+               
+               
+               
+               
+               
+               // Add the data updater to listen to changes in aoa and theta
+               mach.addChangeListener(this);
+               theta.addChangeListener(this);
+               aoa.addChangeListener(this);
+               roll.addChangeListener(this);
+               configuration.addChangeListener(this);
+               this.stateChanged(null);
+               
+               
+               
+               // Remove listeners when closing window
+               this.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosed(WindowEvent e) {
+                               System.out.println("Closing method called: "+this);
+                               theta.removeChangeListener(ComponentAnalysisDialog.this);
+                               aoa.removeChangeListener(ComponentAnalysisDialog.this);
+                               mach.removeChangeListener(ComponentAnalysisDialog.this);
+                               roll.removeChangeListener(ComponentAnalysisDialog.this);
+                               configuration.removeChangeListener(ComponentAnalysisDialog.this);
+                               System.out.println("SETTING NAN VALUES");
+                               rocketPanel.setCPAOA(Double.NaN);
+                               rocketPanel.setCPTheta(Double.NaN);
+                               rocketPanel.setCPMach(Double.NaN);
+                               rocketPanel.setCPRoll(Double.NaN);
+                               singletonDialog = null;
+                       }
+               });
+               
+
+               panel.add(new ResizeLabel("Reference length: ", -1), 
+                               "span, split, gapleft para, gapright rel");
+               DoubleModel dm = new DoubleModel(conditions, "RefLength", UnitGroup.UNITS_LENGTH);
+               UnitSelector sel = new UnitSelector(dm, true);
+               sel.resizeFont(-1);
+               panel.add(sel, "gapright para");
+               
+               panel.add(new ResizeLabel("Reference area: ", -1), "gapright rel");
+               dm = new DoubleModel(conditions, "RefArea", UnitGroup.UNITS_AREA);
+               sel = new UnitSelector(dm, true);
+               sel.resizeFont(-1);
+               panel.add(sel, "wrap");
+               
+               
+
+               // Buttons
+               JButton button;
+               
+               // TODO: LOW: printing
+//             button = new JButton("Print");
+//             button.addActionListener(new ActionListener() {
+//                     public void actionPerformed(ActionEvent e) {
+//                             try {
+//                                     table.print();
+//                             } catch (PrinterException e1) {
+//                                     JOptionPane.showMessageDialog(ComponentAnalysisDialog.this, 
+//                                                     "An error occurred while printing.", "Print error",
+//                                                     JOptionPane.ERROR_MESSAGE);
+//                             }
+//                     }
+//             });
+//             panel.add(button,"tag ok");
+               
+               button = new JButton("Close");
+               button.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               ComponentAnalysisDialog.this.dispose();
+                       }
+               });
+               panel.add(button,"span, split, tag cancel");
+               
+
+               setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+               GUIUtil.installEscapeCloseOperation(this);
+               pack();
+       }
+       
+       
+       
+       /**
+        * Updates the data in the table and fires a table data change event.
+        */
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               AerodynamicForces forces;
+               WarningSet set = new WarningSet();
+               conditions.setAOA(aoa.getValue());
+               conditions.setTheta(theta.getValue());
+               conditions.setMach(mach.getValue());
+               conditions.setRollRate(roll.getValue());
+               conditions.setReference(configuration);
+               
+               if (worstToggle.isSelected()) {
+                       calculator.getWorstCP(conditions, null);
+                       if (!MathUtil.equals(conditions.getTheta(), theta.getValue())) {
+                               fakeChange = true;
+                               theta.setValue(conditions.getTheta());  // Fires a stateChanged event
+                               fakeChange = false;
+                               return;
+                       }
+               }
+               
+               Map<RocketComponent, AerodynamicForces> data = calculator.getForceAnalysis(conditions, set);
+               
+               cpData.clear();
+               dragData.clear();
+               rollData.clear();
+               for (RocketComponent c: configuration) {
+                       forces = data.get(c);
+                       if (forces == null)
+                               continue;
+                       if (forces.cp != null) {
+                               cpData.add(forces);
+                       }
+                       if (!Double.isNaN(forces.CD)) {
+                               dragData.add(forces);
+                       }
+                       if (c instanceof FinSet) {
+                               rollData.add(forces);
+                       }
+               }
+               forces = data.get(configuration.getRocket());
+               if (forces != null) {
+                       cpData.add(forces);
+                       dragData.add(forces);
+                       rollData.add(forces);
+                       totalCD = forces.CD;
+               } else {
+                       totalCD = 0;
+               }
+               
+               // Set warnings
+               if (set.isEmpty()) {
+                       warningList.setListData(new String[] {
+                                       "<html><i><font color=\"gray\">No warnings.</font></i>"
+                       });
+               } else {
+                       warningList.setListData(new Vector<Warning>(set));
+               }
+               
+               cpTableModel.fireTableDataChanged();
+               dragTableModel.fireTableDataChanged();
+               rollTableModel.fireTableDataChanged();
+       }
+       
+       
+       private class CustomCellRenderer extends JLabel implements TableCellRenderer {
+               private final Font normalFont;
+               private final Font boldFont;
+               
+               public CustomCellRenderer() {
+                       super();
+                       normalFont = getFont();
+                       boldFont = normalFont.deriveFont(Font.BOLD);
+               }
+               @Override
+               public Component getTableCellRendererComponent(JTable table, Object value, 
+                               boolean isSelected, boolean hasFocus, int row, int column) {
+                       
+                       this.setText(value.toString());
+                       
+                       if ((row < 0) || (row >= cpData.size()))
+                                       return this;
+                       
+                       if (cpData.get(row).component instanceof Rocket) {
+                               this.setFont(boldFont);
+                       } else {
+                               this.setFont(normalFont);
+                       }
+                       return this;
+               }
+       }
+       
+
+       
+       private class DragCellRenderer extends JLabel implements TableCellRenderer {
+               private final Font normalFont;
+               private final Font boldFont;
+               
+               private final float[] start = { 0.3333f, 0.2f, 1.0f };
+               private final float[] end = { 0.0f, 0.8f, 1.0f };
+               
+               
+               public DragCellRenderer(Color baseColor) {
+                       super();
+                       normalFont = getFont();
+                       boldFont = normalFont.deriveFont(Font.BOLD);
+               }
+               @Override
+               public Component getTableCellRendererComponent(JTable table, Object value, 
+                               boolean isSelected, boolean hasFocus, int row, int column) {
+                       
+                       if (value instanceof Double) {
+                               
+                               // A drag coefficient
+                               double cd = (Double)value;
+                               this.setText(String.format("%.2f (%.0f%%)", cd, 100*cd/totalCD));
+
+                               float r = (float)(cd/1.5);
+                               
+                               float hue = MathUtil.clamp(0.3333f * (1-2.0f*r), 0, 0.3333f);
+                               float sat = MathUtil.clamp(0.8f*r + 0.1f*(1-r), 0, 1);
+                               float val = 1.0f;
+                               
+                               this.setBackground(Color.getHSBColor(hue, sat, val));
+                               this.setOpaque(true);
+                               this.setHorizontalAlignment(SwingConstants.CENTER);
+                               
+                       } else {
+                               
+                               // Other
+                               this.setText(value.toString());
+                               this.setOpaque(false);
+                               this.setHorizontalAlignment(SwingConstants.LEFT);
+                               
+                       }
+                       
+                       if ((row < 0) || (row >= dragData.size()))
+                                       return this;
+                       
+                       if ((dragData.get(row).component instanceof Rocket) || (column == 4)){
+                               this.setFont(boldFont);
+                       } else {
+                               this.setFont(normalFont);
+                       }
+                       return this;
+               }
+       }
+       
+       
+       /////////  Singleton implementation
+       
+       public static void showDialog(RocketPanel rocketpanel) {
+               if (singletonDialog != null)
+                       singletonDialog.dispose();
+               singletonDialog = new ComponentAnalysisDialog(rocketpanel);
+               singletonDialog.setVisible(true);
+       }
+       
+       public static void hideDialog() {
+               if (singletonDialog != null)
+                       singletonDialog.dispose();
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/DescriptionArea.java b/src/net/sf/openrocket/gui/DescriptionArea.java
new file mode 100644 (file)
index 0000000..ba29be4
--- /dev/null
@@ -0,0 +1,58 @@
+package net.sf.openrocket.gui;
+
+import java.awt.Dimension;
+import java.awt.Rectangle;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ScrollPaneConstants;
+
+import net.miginfocom.swing.MigLayout;
+
+public class DescriptionArea extends JScrollPane {
+
+       private ResizeLabel text;
+       private MigLayout layout;
+       private JPanel panel;
+       
+       public DescriptionArea(int rows) {
+               this(rows, -2);
+       }
+       
+       public DescriptionArea(int rows, float size) {
+               super(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
+                               ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+               
+               layout = new MigLayout("ins 0 2px, fill");
+               panel = new JPanel(layout);
+               
+               text = new ResizeLabel(" ",size);
+               text.validate();
+               Dimension dim = text.getPreferredSize();
+               dim.height = (dim.height+2)*rows + 2;
+               this.setPreferredSize(dim);
+               
+               panel.add(text, "growx");
+               
+               this.setViewportView(panel);
+               this.revalidate();
+       }
+       
+       public void setText(String txt) {
+               if (!txt.startsWith("<html>"))
+                       txt = "<html>" + txt;
+               text.setText(txt);
+       }
+       
+       
+       @Override
+       public void validate() {
+               
+               Rectangle dim = this.getViewportBorderBounds();
+               layout.setComponentConstraints(text, "width "+ dim.width + ", growx");
+               super.validate();
+               text.validate();
+
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/DetailDialog.java b/src/net/sf/openrocket/gui/DetailDialog.java
new file mode 100644 (file)
index 0000000..f0c0830
--- /dev/null
@@ -0,0 +1,18 @@
+package net.sf.openrocket.gui;
+
+import java.awt.Component;
+
+import javax.swing.JOptionPane;
+
+public class DetailDialog {
+
+       public static void showDetailedMessageDialog(Component parentComponent, Object message, 
+                       String details, String title, int messageType)  {
+               
+               // TODO: HIGH: Detailed dialog
+               JOptionPane.showMessageDialog(parentComponent, message, title, messageType, null);
+               
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/PreferencesDialog.java b/src/net/sf/openrocket/gui/PreferencesDialog.java
new file mode 100644 (file)
index 0000000..e42d9f9
--- /dev/null
@@ -0,0 +1,389 @@
+package net.sf.openrocket.gui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+
+import net.miginfocom.swing.MigLayout;
+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 final List<DefaultUnitSelector> unitSelectors = new ArrayList<DefaultUnitSelector>();
+
+       private PreferencesDialog() {
+               super((JFrame)null, "Preferences", true);
+               
+               JPanel panel = new JPanel(new MigLayout("fill, gap unrel","[grow]","[grow][]"));
+                               
+               JTabbedPane tabbedPane = new JTabbedPane();
+               panel.add(tabbedPane,"grow, wrap");
+               
+
+               tabbedPane.addTab("Units", null, unitsPane(), "Default units");
+               tabbedPane.addTab("Confirmation", null, confirmationPane(), "Confirmation dialog settings");
+               
+               
+               
+               JButton close = new JButton("Close");
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent arg0) {
+                               PreferencesDialog.this.setVisible(false);
+                               PreferencesDialog.this.dispose();
+                       }
+               });
+               panel.add(close,"span, right, tag close");
+               
+               this.setContentPane(panel);
+               pack();
+               setAlwaysOnTop(true);
+               this.setLocationRelativeTo(null);
+               
+               this.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosed(WindowEvent e) {
+                               Prefs.storeDefaultUnits();
+                       }
+               });
+
+               GUIUtil.setDefaultButton(close);
+               GUIUtil.installEscapeCloseOperation(this);
+       }
+       
+       
+       private JPanel confirmationPane() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new JLabel("Position to insert new body components:"));
+               panel.add(new JComboBox(new PrefChoiseSelector(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY,
+                               "Always ask", "Insert in middle", "Add to end")), "wrap para, sg combos");
+               
+               panel.add(new JLabel("Confirm deletion of simulations:"));
+               panel.add(new JComboBox(new PrefBooleanSelector(Prefs.CONFIRM_DELETE_SIMULATION,
+                               "Delete", "Confirm", true)), "wrap para, sg combos");
+               
+               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");
+               
+/*
+               public static final UnitGroup UNITS_LENGTH;
+               public static final UnitGroup UNITS_MOTOR_DIMENSIONS;
+               public static final UnitGroup UNITS_DISTANCE;
+               
+               public static final UnitGroup UNITS_VELOCITY;
+               public static final UnitGroup UNITS_ACCELERATION;
+               public static final UnitGroup UNITS_MASS;
+               public static final UnitGroup UNITS_FORCE;
+               public static final UnitGroup UNITS_IMPULSE;
+
+               public static final UnitGroup UNITS_STABILITY;
+               public static final UnitGroup UNITS_FLIGHT_TIME;
+               public static final UnitGroup UNITS_ROLL;
+               
+               public static final UnitGroup UNITS_AREA;
+               public static final UnitGroup UNITS_DENSITY_LINE;
+               public static final UnitGroup UNITS_DENSITY_SURFACE;
+               public static final UnitGroup UNITS_DENSITY_BULK;
+               public static final UnitGroup UNITS_ROUGHNESS;
+               
+               public static final UnitGroup UNITS_TEMPERATURE;
+               public static final UnitGroup UNITS_PRESSURE;
+               public static final UnitGroup UNITS_ANGLE;
+*/
+               
+               panel.add(new JLabel("Rocket dimensions:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Line density:"));
+               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");
+               
+               panel.add(new JLabel("Surface density:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_SURFACE));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Distance:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Bulk density::"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_BULK));
+               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));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Angle:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ANGLE));
+               panel.add(combo, "sizegroup boxes, wrap");
+               
+
+               
+               panel.add(new JLabel("Force:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE));
+               panel.add(combo, "sizegroup boxes");
+               
+               panel.add(new JLabel("Roll rate:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROLL));
+               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");
+               
+               panel.add(new JLabel("Temperature:"));
+               combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_TEMPERATURE));
+               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)
+                                       s.fireChange();
+                       }
+               });
+               panel.add(button, "spanx, split 2, grow");
+               
+               button = new JButton("Default imperial");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               UnitGroup.setDefaultImperialUnits();
+                               for (DefaultUnitSelector s: unitSelectors)
+                                       s.fireChange();
+                       }
+               });
+               panel.add(button, "grow, wrap para");
+               
+               
+               panel.add(new ResizeLabel("The effects will take place the next time you open a window.",-2),
+                               "spanx, wrap");
+               
+
+               return panel;
+       }
+       
+       
+       
+       
+       private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel {
+               
+               private final UnitGroup group;
+               public DefaultUnitSelector(UnitGroup group) {
+                       this.group = group;
+                       unitSelectors.add(this);
+               }
+               
+               @Override
+               public Object getSelectedItem() {
+                       return group.getDefaultUnit();
+               }
+               @Override
+               public void setSelectedItem(Object item) {
+                       if (!(item instanceof Unit)) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       group.setDefaultUnit(group.getUnitIndex((Unit)item));
+               }
+               @Override
+               public Object getElementAt(int index) {
+                       return group.getUnit(index);
+               }
+               @Override
+               public int getSize() {
+                       return group.getUnitCount();
+               }
+               
+               
+               public void fireChange() {
+                       this.fireContentsChanged(this, 0, this.getSize());
+               }
+       }
+       
+
+       
+       private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel {
+               private final String preference;
+               private final String[] descriptions;
+               
+               public PrefChoiseSelector(String preference, String ... descriptions) {
+                       this.preference = preference;
+                       this.descriptions = descriptions;
+               }
+               
+               @Override
+               public Object getSelectedItem() {
+                       return descriptions[Prefs.getChoise(preference, descriptions.length, 0)];
+               }
+               
+               @Override
+               public void setSelectedItem(Object item) {
+                       if (!(item instanceof String)) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       int index;
+                       for (index = 0; index < descriptions.length; index++) {
+                               if (((String)item).equalsIgnoreCase(descriptions[index]))
+                                       break;
+                       }
+                       if (index >= descriptions.length) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       
+                       Prefs.putChoise(preference, index);
+               }
+               
+               @Override
+               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, 
+                               String trueDescription, boolean defaultState) {
+                       this.preference = preference;
+                       this.trueDesc = trueDescription;
+                       this.falseDesc = falseDescription;
+                       this.def = defaultState;
+               }
+               
+               @Override
+               public Object getSelectedItem() {
+                       if (Prefs.NODE.getBoolean(preference, def)) {
+                               return trueDesc;
+                       } else {
+                               return falseDesc;
+                       }
+               }
+               
+               @Override
+               public void setSelectedItem(Object item) {
+                       if (!(item instanceof String)) {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+                       
+                       if (trueDesc.equals(item)) {
+                               Prefs.NODE.putBoolean(preference, true);
+                       } else if (falseDesc.equals(item)) {
+                               Prefs.NODE.putBoolean(preference, false);
+                       } else {
+                               throw new IllegalArgumentException("Illegal argument "+item);
+                       }
+               }
+               
+               @Override
+               public Object getElementAt(int index) {
+                       switch (index) {
+                       case 0:
+                               return def ? trueDesc : falseDesc;
+
+                       case 1:
+                               return def ? falseDesc: trueDesc;
+                               
+                       default:
+                               throw new IndexOutOfBoundsException("Boolean asked for index="+index);
+                       }
+               }
+               @Override
+               public int getSize() {
+                       return 2;
+               }
+       }
+       
+       
+       
+       ////////  Singleton implementation  ////////
+       
+       private static PreferencesDialog dialog = null;
+       
+       public static void showPreferences() {
+               if (dialog != null) {
+                       dialog.dispose();
+               }
+               dialog = new PreferencesDialog();
+               dialog.setVisible(true);
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/Resettable.java b/src/net/sf/openrocket/gui/Resettable.java
new file mode 100644 (file)
index 0000000..1f30d2b
--- /dev/null
@@ -0,0 +1,15 @@
+package net.sf.openrocket.gui;
+
+/**
+ * An interface for GUI elements with a resettable model.  The resetModel() method in 
+ * this interface resets the model to some default model, releasing the old model 
+ * listening connections.
+ * 
+ * Some components that don't have a settable model simply release the current model.
+ * These components cannot therefore be reused after calling resetModel().
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface Resettable {
+       public void resetModel();
+}
diff --git a/src/net/sf/openrocket/gui/ResizeLabel.java b/src/net/sf/openrocket/gui/ResizeLabel.java
new file mode 100644 (file)
index 0000000..6951d62
--- /dev/null
@@ -0,0 +1,47 @@
+package net.sf.openrocket.gui;
+
+import java.awt.Font;
+import javax.swing.JLabel;
+
+/**
+ * A resizeable JLabel.  The method resizeFont(float) changes the current font size by the
+ * given (positive or negative) amount.  The change is relative to the current font size.
+ * <p>
+ * A nice small text is achievable by  <code>new ResizeLabel("My text", -2);</code>
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ResizeLabel extends JLabel {
+       
+       public ResizeLabel() {
+               super();
+       }
+       
+       public ResizeLabel(String text) {
+               super(text);
+       }
+       
+       public ResizeLabel(float size) {
+               super();
+               resizeFont(size);
+       }
+       
+       public ResizeLabel(String text, float size) {
+               super(text);
+               resizeFont(size);
+       }
+       
+       public ResizeLabel(String text, int horizontalAlignment, float size) {
+               super(text, horizontalAlignment);
+               resizeFont(size);
+       }
+       
+       
+       public void resizeFont(float size) {
+               Font font = this.getFont();
+               font = font.deriveFont(font.getSize2D()+size);
+               this.setFont(font);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/SpinnerEditor.java b/src/net/sf/openrocket/gui/SpinnerEditor.java
new file mode 100644 (file)
index 0000000..843eed6
--- /dev/null
@@ -0,0 +1,21 @@
+package net.sf.openrocket.gui;
+
+import javax.swing.JSpinner;
+
+/**
+ * Editable editor for a JSpinner.  Simply uses JSpinner.DefaultEditor, which has been made
+ * editable.  Why the f*** isn't this possible in the normal API?
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class SpinnerEditor extends JSpinner.NumberEditor {
+//public class SpinnerEditor extends JSpinner.DefaultEditor {
+
+       public SpinnerEditor(JSpinner spinner) {
+               //super(spinner);
+               super(spinner,"0.0##");
+               //getTextField().setEditable(true);
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/StageSelector.java b/src/net/sf/openrocket/gui/StageSelector.java
new file mode 100644 (file)
index 0000000..a0ad437
--- /dev/null
@@ -0,0 +1,108 @@
+package net.sf.openrocket.gui;
+
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.JPanel;
+import javax.swing.JToggleButton;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.rocketcomponent.Configuration;
+
+
+public class StageSelector extends JPanel implements ChangeListener {
+
+       private final Configuration configuration;
+       
+       private List<JToggleButton> buttons = new ArrayList<JToggleButton>();
+       
+       public StageSelector(Configuration configuration) {
+               super(new MigLayout("gap 0!"));
+               this.configuration = configuration;
+               
+               JToggleButton button = new JToggleButton(new StageAction(0));
+               this.add(button);
+               buttons.add(button);
+               
+               updateButtons();
+               configuration.addChangeListener(this);
+       }
+       
+       private void updateButtons() {
+               int stages = configuration.getStageCount();
+               if (buttons.size() == stages)
+                       return;
+               
+               while (buttons.size() > stages) {
+                       JToggleButton button = buttons.remove(buttons.size()-1);
+                       this.remove(button);
+               }
+               
+               while (buttons.size() < stages) {
+                       JToggleButton button = new JToggleButton(new StageAction(buttons.size()));
+                       this.add(button);
+                       buttons.add(button);
+               }
+               
+               this.revalidate();
+       }
+       
+
+
+
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               updateButtons();
+       }
+       
+       
+       private class StageAction extends AbstractAction implements ChangeListener {
+               private final int stage;
+
+               public StageAction(final int stage) {
+                       this.stage = stage;
+                       configuration.addChangeListener(this);
+                       stateChanged(null);
+               }
+               
+               @Override
+               public Object getValue(String key) {
+                       if (key.equals(NAME)) {
+                               return "Stage "+(stage+1);
+                       }
+                       return super.getValue(key);
+               }
+               
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       configuration.setToStage(stage);
+                       
+//                     boolean state = (Boolean)getValue(SELECTED_KEY);
+//                     if (state == true) {
+//                             // Was disabled, now enabled
+//                             configuration.setToStage(stage);
+//                     } else {
+//                             // Was enabled, check what to do
+//                             if (configuration.isStageActive(stage + 1)) {
+//                                     configuration.setToStage(stage);
+//                             } else {
+//                                     if (stage == 0)
+//                                             configuration.setAllStages();
+//                                     else 
+//                                             configuration.setToStage(stage-1);
+//                             }
+//                     }
+//                     stateChanged(null);
+               }
+               
+
+               @Override
+               public void stateChanged(ChangeEvent e) {
+                       this.putValue(SELECTED_KEY, configuration.isStageActive(stage));
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/StorageOptionChooser.java b/src/net/sf/openrocket/gui/StorageOptionChooser.java
new file mode 100644 (file)
index 0000000..9b8f1aa
--- /dev/null
@@ -0,0 +1,204 @@
+package net.sf.openrocket.gui;
+
+import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+
+public class StorageOptionChooser extends JPanel {
+       
+       public static final double DEFAULT_SAVE_TIME_SKIP = 0.20;
+
+       private JRadioButton allButton;
+       private JRadioButton someButton;
+       private JRadioButton noneButton;
+       
+       private JSpinner timeSpinner;
+       
+       private JCheckBox compressButton;
+       
+       
+       private boolean artificialEvent = false;
+       
+       public StorageOptionChooser(StorageOptions opts) {
+               super(new MigLayout());
+
+               ButtonGroup buttonGroup = new ButtonGroup();
+               String tip;
+               
+               this.add(new JLabel("Simulated data to store:"), "spanx, wrap unrel");
+
+               allButton = new JRadioButton("All simulated data");
+               allButton.setToolTipText("<html>Store all simulated data.<br>" +
+                               "This can result in very large files!");
+               buttonGroup.add(allButton);
+               this.add(allButton, "spanx, wrap rel");
+               
+               
+               someButton = new JRadioButton("Every");
+               tip = "<html>Store plottable values approximately this far apart.<br>" +
+                               "Larger values result in smaller files.";
+               someButton.setToolTipText(tip);
+               buttonGroup.add(someButton);
+               this.add(someButton, "");
+               
+               timeSpinner = new JSpinner(new SpinnerNumberModel(0.0, 0.0, 5.0, 0.1));
+               timeSpinner.setToolTipText(tip);
+               timeSpinner.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               if (artificialEvent)
+                                       return;
+                               someButton.setSelected(true);
+                       }
+               });
+               this.add(timeSpinner, "wmin 55lp");
+               
+               JLabel label = new JLabel("seconds");
+               label.setToolTipText(tip);
+               this.add(label, "wrap rel");
+               
+               
+               noneButton = new JRadioButton("Only primary figures");
+               noneButton.setToolTipText("<html>Store only the values shown in the summary table.<br>" +
+                               "This results in the smallest files.");
+               buttonGroup.add(noneButton);
+
+               this.add(noneButton, "spanx, wrap 20lp");
+               
+               
+               compressButton = new JCheckBox("Compress file");
+               compressButton.setToolTipText("Using compression reduces the file size significantly.");
+               this.add(compressButton, "spanx");
+               
+               
+               this.setBorder(BorderFactory.createCompoundBorder(
+                               BorderFactory.createEmptyBorder(0, 10, 0, 0),
+                               BorderFactory.createTitledBorder("Save options")));
+               
+               loadOptions(opts);
+       }
+       
+       
+       public void loadOptions(StorageOptions opts) {
+               double t;
+               
+               // Data storage radio button
+               t = opts.getSimulationTimeSkip();
+               if (t == StorageOptions.SIMULATION_DATA_ALL) {
+                       allButton.setSelected(true);
+                       t = DEFAULT_SAVE_TIME_SKIP;
+               } else if (t == StorageOptions.SIMULATION_DATA_NONE) {
+                       noneButton.setSelected(true);
+                       t = DEFAULT_SAVE_TIME_SKIP;
+               } else {
+                       someButton.setSelected(true);
+               }
+               
+               // Time skip spinner
+               artificialEvent = true;
+               timeSpinner.setValue(t);
+               artificialEvent = false;
+               
+               // Compression checkbox
+               compressButton.setSelected(opts.isCompressionEnabled());
+       }
+       
+       
+       public void storeOptions(StorageOptions opts) {
+               double t;
+               
+               if (allButton.isSelected()) {
+                       t = StorageOptions.SIMULATION_DATA_ALL;
+               } else if (noneButton.isSelected()) {
+                       t = StorageOptions.SIMULATION_DATA_NONE;
+               } else {
+                       t = (Double)timeSpinner.getValue();
+               }
+               
+               opts.setSimulationTimeSkip(t);
+               
+               opts.setCompressionEnabled(compressButton.isSelected());
+               
+               opts.setExplicitlySet(true);
+       }
+       
+       
+       
+       /**
+        * Asks the user the storage options using a modal dialog window if the document
+        * contains simulated data and the user has not explicitly set how to store the data.
+        * 
+        * @param document      the document to check.
+        * @param parent        the parent frame for the dialog.
+        * @return                      <code>true</code> to continue, <code>false</code> if the user cancelled.
+        */
+       public static boolean verifyStorageOptions(OpenRocketDocument document, JFrame parent) {
+               StorageOptions options = document.getDefaultStorageOptions();
+               
+               if (options.isExplicitlySet()) {
+                       // User has explicitly set the values, save as is
+                       return true;
+               }
+               
+               
+               boolean hasData = false;
+               
+               simulationLoop:
+                       for (Simulation s: document.getSimulations()) {
+                               if (s.getStatus() == Simulation.Status.NOT_SIMULATED ||
+                                               s.getStatus() == Simulation.Status.EXTERNAL)
+                                       continue;
+                               
+                               FlightData data = s.getSimulatedData();
+                               if (data == null)
+                                       continue;
+                               
+                               for (int i=0; i < data.getBranchCount(); i++) {
+                                       FlightDataBranch branch = data.getBranch(i);
+                                       if (branch == null)
+                                               continue;
+                                       if (branch.getLength() > 0) {
+                                               hasData = true;
+                                               break simulationLoop;
+                                       }
+                               }
+                       }
+               
+
+               if (!hasData) {
+                       // No data to store, do not ask only about compression
+                       return true;
+               }
+               
+               
+               StorageOptionChooser chooser = new StorageOptionChooser(options);
+               
+               if (JOptionPane.showConfirmDialog(parent, chooser, "Save options", 
+                               JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) !=
+                                       JOptionPane.OK_OPTION) {
+                       // User cancelled
+                       return false;
+               }
+               
+               chooser.storeOptions(options);
+               return true;
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/TextFieldListener.java b/src/net/sf/openrocket/gui/TextFieldListener.java
new file mode 100644 (file)
index 0000000..d077300
--- /dev/null
@@ -0,0 +1,35 @@
+package net.sf.openrocket.gui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+
+import javax.swing.JTextField;
+
+public abstract class TextFieldListener implements ActionListener, FocusListener {
+       private JTextField field;
+               
+       public void listenTo(JTextField newField) {
+               if (field != null) {
+                       field.removeActionListener(this);
+                       field.removeFocusListener(this);
+               }
+               field = newField;
+               if (field != null) {
+                       field.addActionListener(this);
+                       field.addFocusListener(this);
+               }
+       }
+
+       public abstract void setText(String text);
+
+       public void actionPerformed(ActionEvent e) {
+               setText(field.getText());
+       }
+       public void focusGained(FocusEvent e) { }
+       public void focusLost(FocusEvent e) {
+               setText(field.getText());
+       }
+       
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/UnitSelector.java b/src/net/sf/openrocket/gui/UnitSelector.java
new file mode 100644 (file)
index 0000000..37453ef
--- /dev/null
@@ -0,0 +1,314 @@
+package net.sf.openrocket.gui;
+
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.ItemSelectable;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.Action;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.border.Border;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+/**
+ * A Swing component that allows one to choose a unit from a UnitGroup within
+ * a DoubleModel model.  The current unit of the model is shown as a JLabel, and
+ * the unit can be changed by clicking on the label.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class UnitSelector extends ResizeLabel implements ChangeListener, MouseListener,
+               ItemSelectable {
+
+       private final DoubleModel model;
+       private final Action[] extraActions;
+
+       private UnitGroup unitGroup;
+       private Unit currentUnit;
+
+       private final boolean showValue;
+
+       private final Border normalBorder;
+       private final Border withinBorder;
+
+
+       private final List<ItemListener> itemListeners = new ArrayList<ItemListener>();
+
+
+       /**
+        * Common private constructor that sets the values and sets up the borders.
+        * Either model or group must be null.
+        * 
+        * @param model
+        * @param showValue
+        * @param group
+        * @param actions
+        */
+       private UnitSelector(DoubleModel model, boolean showValue, UnitGroup group,
+                       Action[] actions) {
+               super();
+
+               this.model = model;
+               this.showValue = showValue;
+
+               if (model != null) {
+                       this.unitGroup = model.getUnitGroup();
+                       this.currentUnit = model.getCurrentUnit();
+               } else {
+                       this.unitGroup = group;
+                       this.currentUnit = group.getDefaultUnit();
+               }
+
+               this.extraActions = actions;
+
+               addMouseListener(this);
+
+               // Define borders to use:
+
+               normalBorder = new CompoundBorder(
+                               new LineBorder(new Color(0f, 0f, 0f, 0.08f), 1), new EmptyBorder(1, 1, 1,
+                                               1));
+               withinBorder = new CompoundBorder(new LineBorder(new Color(0f, 0f, 0f, 0.6f)),
+                               new EmptyBorder(1, 1, 1, 1));
+
+               setBorder(normalBorder);
+               updateText();
+       }
+
+
+
+       public UnitSelector(DoubleModel model, Action... actions) {
+               this(model, false, actions);
+       }
+
+       public UnitSelector(DoubleModel model, boolean showValue, Action... actions) {
+               this(model, showValue, null, actions);
+
+               // Add model listener
+               this.model.addChangeListener(this);
+       }
+
+
+       public UnitSelector(UnitGroup group, Action... actions) {
+               this(null, false, group, actions);
+       }
+
+
+
+
+       /**
+        * Return the DoubleModel that is backing this selector up, or <code>null</code>.
+        * Either this method or {@link #getUnitGroup()} always returns <code>null</code>.
+        * 
+        * @return              the DoubleModel being used, or <code>null</code>.
+        */
+       public DoubleModel getModel() {
+               return model;
+       }
+
+
+       /**
+        * Return the unit group that is being shown, or <code>null</code>.  Either this method
+        * or {@link #getModel()} always returns <code>null</code>.
+        * 
+        * @return              the UnitGroup being used, or <code>null</code>.
+        */
+       public UnitGroup getUnitGroup() {
+               return unitGroup;
+       }
+
+
+       public void setUnitGroup(UnitGroup group) {
+               if (model != null) {
+                       throw new IllegalStateException(
+                                       "UnitGroup cannot be set when backed up with model.");
+               }
+
+               if (this.unitGroup == group)
+                       return;
+
+               this.unitGroup = group;
+               this.currentUnit = group.getDefaultUnit();
+               updateText();
+       }
+
+
+       /**
+        * Return the currently selected unit.  Works both when backup up with a DoubleModel
+        * and UnitGroup.
+        * 
+        * @return              the currently selected unit.
+        */
+       public Unit getSelectedUnit() {
+               return currentUnit;
+       }
+
+
+       /**
+        * Set the currently selected unit.  Sets it to the DoubleModel if it is backed up
+        * by it.
+        * 
+        * @param unit          the unit to select.
+        */
+       public void setSelectedUnit(Unit unit) {
+               if (!unitGroup.contains(unit)) {
+                       throw new IllegalArgumentException("unit " + unit
+                                       + " not contained in group " + unitGroup);
+               }
+
+               this.currentUnit = unit;
+               if (model != null) {
+                       model.setCurrentUnit(unit);
+               }
+               updateText();
+               fireItemEvent();
+       }
+
+
+
+       /**
+        * Updates the text of the label
+        */
+       private void updateText() {
+               if (model != null) {
+
+                       Unit unit = model.getCurrentUnit();
+                       if (showValue) {
+                               setText(unit.toStringUnit(model.getValue()));
+                       } else {
+                               setText(unit.getUnit());
+                       }
+
+               } else if (unitGroup != null) {
+
+                       setText(currentUnit.getUnit());
+
+               } else {
+                       throw new IllegalStateException("Both model and unitGroup are null.");
+               }
+       }
+
+
+       /**
+        * Update the component when the DoubleModel changes.
+        */
+       public void stateChanged(ChangeEvent e) {
+               updateText();
+       }
+
+
+
+       ////////  ItemListener handling  ////////
+
+       public void addItemListener(ItemListener listener) {
+               itemListeners.add(listener);
+       }
+
+       public void removeItemListener(ItemListener listener) {
+               itemListeners.remove(listener);
+       }
+
+       protected void fireItemEvent() {
+               ItemEvent event = null;
+               ItemListener[] listeners = itemListeners.toArray(new ItemListener[0]);
+               for (ItemListener l: listeners) {
+                       if (event == null) {
+                               event = new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, getSelectedUnit(),
+                                               ItemEvent.SELECTED);
+                       }
+                       l.itemStateChanged(event);
+               }
+       }
+
+
+
+       ////////  Popup  ////////
+
+       private void popup() {
+               JPopupMenu popup = new JPopupMenu();
+
+               for (int i = 0; i < unitGroup.getUnitCount(); i++) {
+                       Unit unit = unitGroup.getUnit(i);
+                       JMenuItem item = new JMenuItem(unit.getUnit());
+                       item.addActionListener(new UnitSelectorItem(unit));
+                       popup.add(item);
+               }
+
+               for (int i = 0; i < extraActions.length; i++) {
+                       if (extraActions[i] == null && i < extraActions.length - 1) {
+                               popup.addSeparator();
+                       } else {
+                               popup.add(new JMenuItem(extraActions[i]));
+                       }
+               }
+
+               Dimension d = getSize();
+               popup.show(this, 0, d.height);
+       }
+
+
+       /**
+        * ActionListener class that sets the currently selected unit.
+        */
+       private class UnitSelectorItem implements ActionListener {
+               private final Unit unit;
+
+               public UnitSelectorItem(Unit u) {
+                       unit = u;
+               }
+
+               public void actionPerformed(ActionEvent e) {
+                       setSelectedUnit(unit);
+               }
+       }
+
+
+       @Override
+       public Object[] getSelectedObjects() {
+               return new Object[]{ getSelectedUnit() };
+       }
+
+
+
+       ////////  Mouse handling ////////
+
+       public void mouseClicked(MouseEvent e) {
+               if (unitGroup.getUnitCount() > 1)
+                       popup();
+       }
+
+       public void mouseEntered(MouseEvent e) {
+               if (unitGroup.getUnitCount() > 1)
+                       setBorder(withinBorder);
+       }
+
+       public void mouseExited(MouseEvent e) {
+               setBorder(normalBorder);
+       }
+
+       public void mousePressed(MouseEvent e) {
+       } // Ignore
+
+       public void mouseReleased(MouseEvent e) {
+       } // Ignore
+
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/BooleanModel.java b/src/net/sf/openrocket/gui/adaptors/BooleanModel.java
new file mode 100644 (file)
index 0000000..ab68c33
--- /dev/null
@@ -0,0 +1,241 @@
+package net.sf.openrocket.gui.adaptors;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+
+
+/**
+ * A class that adapts an isXXX/setXXX boolean variable.  It functions as an Action suitable
+ * for usage in JCheckBox or JToggleButton.  You can create a suitable button with
+ * <code>
+ *   check = new JCheckBox(new BooleanModel(component,"Value"))
+ *   check.setText("Label");
+ * </code>
+ * This will produce a button that uses isValue() and setValue(boolean) of the corresponding
+ * component.
+ * <p>
+ * Additionally a number of component enabled states may be controlled by this class using
+ * the method {@link #addEnableComponent(Component, boolean)}.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BooleanModel extends AbstractAction implements ChangeListener {
+
+       private final ChangeSource source;
+       private final String valueName;
+       
+       private final Method getMethod;
+       private final Method setMethod;
+       private final Method getEnabled;
+       
+       private final List<Component> components = new ArrayList<Component>();
+       private final List<Boolean> componentEnableState = new ArrayList<Boolean>();
+       
+       private int firing = 0;
+       
+       private boolean oldValue;
+       private boolean oldEnabled;
+       
+       public BooleanModel(ChangeSource source, String valueName) {
+               this.source = source;
+               this.valueName = valueName;
+               
+               Method getter=null, setter=null;
+               
+               
+               // Try get/is and set
+               try {
+                       getter = source.getClass().getMethod("is" + valueName);
+               } catch (NoSuchMethodException ignore) { }
+               if (getter == null) {
+                       try {
+                               getter = source.getClass().getMethod("get" + valueName);
+                       } catch (NoSuchMethodException ignore) { }
+               }
+               try {
+                       setter = source.getClass().getMethod("set" + valueName,boolean.class);
+               } catch (NoSuchMethodException ignore) { }
+               
+               if (getter==null || setter==null) {
+                       throw new IllegalArgumentException("get/is methods for boolean '"+valueName+
+                                       "' not present in class "+source.getClass().getCanonicalName());
+               }
+
+               getMethod = getter;
+               setMethod = setter;
+               
+               Method e = null;
+               try {
+                       e = source.getClass().getMethod("is" + valueName + "Enabled");
+               } catch (NoSuchMethodException ignore) { }
+               getEnabled = e;
+               
+               oldValue = getValue();
+               oldEnabled = getIsEnabled();
+               
+               this.setEnabled(oldEnabled);
+               this.putValue(SELECTED_KEY, oldValue);
+               
+               source.addChangeListener(this);
+       }
+       
+       public boolean getValue() {
+               try {
+                       return (Boolean)getMethod.invoke(source);
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException("getMethod execution error for source "+source,e);
+               } catch (InvocationTargetException e) {
+                       throw new RuntimeException("getMethod execution error for source "+source,e);
+               }
+       }
+       
+       public void setValue(boolean b) {
+               try {
+                       setMethod.invoke(source, new Object[] { (Boolean)b });
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException("setMethod execution error for source "+source,e);
+               } catch (InvocationTargetException e) {
+                       throw new RuntimeException("setMethod execution error for source "+source,e);
+               }
+       }
+       
+       
+       /**
+        * Add a component the enabled status of which will be controlled by the value
+        * of this boolean.  The <code>component</code> will be enabled exactly when
+        * the state of this model is equal to that of <code>enableState</code>.
+        * 
+        * @param component             the component to control.
+        * @param enableState   the state in which the component should be enabled.
+        */
+       public void addEnableComponent(Component component, boolean enableState) {
+               components.add(component);
+               componentEnableState.add(enableState);
+               updateEnableStatus();
+       }
+       
+       /**
+        * Add a component which will be enabled when this boolean is <code>true</code>.
+        * This is equivalent to <code>booleanModel.addEnableComponent(component, true)</code>.
+        * 
+        * @param component             the component to control.
+        * @see #addEnableComponent(Component, boolean)
+        */
+       public void addEnableComponent(Component component) {
+               addEnableComponent(component, true);
+       }
+       
+       private void updateEnableStatus() {
+               boolean state = getValue();
+               
+               for (int i=0; i < components.size(); i++) {
+                       Component c = components.get(i);
+                       boolean b = componentEnableState.get(i);
+                       c.setEnabled(state == b);
+               }
+       }
+       
+       
+//     @Override
+//     public boolean isEnabled() {
+//             if (getEnabled == null)
+//                     return true;
+//             try {
+//                     return (Boolean)getEnabled.invoke(source);
+//             } catch (IllegalAccessException e) {
+//                     throw new RuntimeException("getEnabled execution error for source "+source,e);
+//             } catch (InvocationTargetException e) {
+//                     throw new RuntimeException("getEnabled execution error for source "+source,e);
+//             }
+//     }
+
+
+       private boolean getIsEnabled() {
+               if (getEnabled == null)
+                       return true;
+               try {
+                       return (Boolean)getEnabled.invoke(source);
+               } catch (IllegalAccessException e) {
+                       throw new RuntimeException("getEnabled execution error for source "+source,e);
+               } catch (InvocationTargetException e) {
+                       throw new RuntimeException("getEnabled execution error for source "+source,e);
+               }
+       }
+       
+//     @Override
+//     public Object getValue(String key) {
+//             if (key.equals(SELECTED_KEY)) {
+//                     return getValue();
+//             }
+//             return super.getValue(key);
+//     }
+//
+//     @Override
+//     public void putValue(String key, Object value) {
+//             if (firing > 0)  // Ignore if currently firing event
+//                     return;
+//             if (key.equals(SELECTED_KEY) && (value instanceof Boolean)) {
+//                     setValue((Boolean)value);
+//             } else {
+//                     super.putValue(key, value);
+//             }
+//             updateEnableStatus();
+//     }
+       
+       
+       @Override
+       public void stateChanged(ChangeEvent event) {
+               if (firing > 0)
+                       return;
+               
+               boolean v = getValue();
+               boolean e = getIsEnabled();
+               if (oldValue != v) {
+                       oldValue = v;
+                       firing++;
+                       this.putValue(SELECTED_KEY, getValue());
+//                     this.firePropertyChange(SELECTED_KEY, !v, v);
+                       updateEnableStatus();
+                       firing--;
+               }
+               if (oldEnabled != e) {
+                       oldEnabled = e;
+                       setEnabled(e);
+               }
+       }
+
+
+       @Override
+       public void actionPerformed(ActionEvent e) {
+               if (firing > 0)
+                       return;
+               
+               boolean v = (Boolean)this.getValue(SELECTED_KEY);
+               if (v != oldValue) {
+                       firing++;
+                       setValue(v);
+                       oldValue = getValue();
+                       // Update all states
+                       this.putValue(SELECTED_KEY, oldValue);
+                       this.setEnabled(getIsEnabled());
+                       updateEnableStatus();
+                       firing--;
+               }
+       }
+       
+       @Override
+       public String toString() {
+               return "BooleanModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+       }
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/Column.java b/src/net/sf/openrocket/gui/adaptors/Column.java
new file mode 100644 (file)
index 0000000..3b22d11
--- /dev/null
@@ -0,0 +1,60 @@
+package net.sf.openrocket.gui.adaptors;
+
+import javax.swing.table.TableColumnModel;
+
+public abstract class Column {
+       private final String name;
+       
+       /**
+        * Create a new column with specified name.  Additionally, the {@link #getValueAt(int)}
+        * method must be implemented.
+        * 
+        * @param name  the caption of the column.
+        */
+       public Column(String name) {
+               this.name = name;
+       }
+
+       /**
+        * Return the caption of the column.
+        */
+       @Override
+       public String toString() {
+               return name;
+       }
+       
+       /**
+        * Return the default width of the column.  This is used by the method
+        * {@link #ColumnTableModel.setColumnWidth(TableColumnModel)}.  The default width is
+        * 100, the method may be overridden to return other values relative to this value.
+        * 
+        * @return              the relative width of the column (default 100).
+        */
+       public int getDefaultWidth() {
+               return 100;
+       }
+       
+       
+       /**
+        * Returns the exact width of this column.  If the return value is positive,
+        * both the minimum and maximum widths of this column are set to this value
+        * 
+        * @return              the absolute exact width of the column (default 0).
+        */
+       public int getExactWidth() {
+               return 0;
+       }
+       
+       
+       public Class<?> getColumnClass() {
+               return Object.class;
+       }
+
+       /**
+        * Return the value in this column at the specified row.
+        * 
+        * @param row   the row of the data.
+        * @return              the value at the specified position.
+        */
+       public abstract Object getValueAt(int row);
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java b/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java
new file mode 100644 (file)
index 0000000..651738c
--- /dev/null
@@ -0,0 +1,54 @@
+package net.sf.openrocket.gui.adaptors;
+
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableColumnModel;
+
+public abstract class ColumnTableModel extends AbstractTableModel {
+       private final Column[] columns;
+       
+       public ColumnTableModel(Column... columns) {
+               this.columns = columns;
+       }
+       
+       public void setColumnWidths(TableColumnModel model) {
+               for (int i=0; i < columns.length; i++) {
+                       if (columns[i].getExactWidth() > 0) {
+                               TableColumn col = model.getColumn(i);
+                               int w = columns[i].getExactWidth();
+                               col.setResizable(false);
+                               col.setMinWidth(w);
+                               col.setMaxWidth(w);
+                               col.setPreferredWidth(w);
+                       } else {
+                               model.getColumn(i).setPreferredWidth(columns[i].getDefaultWidth());
+                       }
+               }
+       }
+
+       @Override
+       public int getColumnCount() {
+               return columns.length;
+       }
+       
+       @Override
+       public String getColumnName(int col) {
+               return columns[col].toString();
+       }
+       
+       @Override
+       public Class<?> getColumnClass(int col) {
+               return columns[col].getColumnClass();
+       }
+
+       @Override
+       public Object getValueAt(int row, int col) {
+               if ((row < 0) || (row >= getRowCount()) ||
+                               (col < 0) || (col >= columns.length)) {
+                       System.err.println("Error:  Requested illegal column/row = "+col+"/"+row+".");
+                       assert(false);
+                       return null;
+               }
+               return columns[col].getValueAt(row);
+       }
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/DoubleModel.java b/src/net/sf/openrocket/gui/adaptors/DoubleModel.java
new file mode 100644 (file)
index 0000000..0004dbc
--- /dev/null
@@ -0,0 +1,778 @@
+package net.sf.openrocket.gui.adaptors;
+
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BoundedRangeModel;
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A model connector that can read and modify any value of any ChangeSource that
+ * has the appropriate get/set methods defined.  
+ * 
+ * The variable is defined in the constructor by providing the variable name as a string
+ * (e.g. "Radius" -> getRadius()/setRadius()).  Additional scaling may be applied, e.g. a 
+ * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
+ * 
+ * Sub-models suitable for JSpinners and other components are available from the appropriate
+ * methods.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class DoubleModel implements ChangeListener, ChangeSource {
+       private static final boolean DEBUG_LISTENERS = false;
+
+       //////////// JSpinner Model ////////////
+       
+       /**
+        * Model suitable for JSpinner using JSpinner.NumberEditor.  It extends SpinnerNumberModel
+        * to be compatible with the NumberEditor, but only has the necessary methods defined.
+        */
+       private class ValueSpinnerModel extends SpinnerNumberModel {
+               
+               @Override
+               public Object getValue() {
+                       return currentUnit.toUnit(DoubleModel.this.getValue());
+//                     return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
+               }
+
+               @Override
+               public void setValue(Object value) {
+                       
+                       System.out.println("setValue("+value+") called, valueName="+valueName+
+                                       " firing="+firing);
+                       
+                       if (firing > 0)   // Ignore, if called when model is sending events
+                               return;
+                       Number num = (Number)value;
+                       double newValue = num.doubleValue();
+                       DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
+                       
+                       
+//                     try {
+//                             double newValue = Double.parseDouble((String)value);
+//                             DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
+//                     } catch (NumberFormatException e) { 
+//                             DoubleModel.this.fireStateChanged();
+//                     };
+               }
+               
+               @Override
+               public Object getNextValue() {
+                       double d = currentUnit.toUnit(DoubleModel.this.getValue());
+                       double max = currentUnit.toUnit(maxValue);
+                       if (MathUtil.equals(d,max))
+                               return null;
+                       d = currentUnit.getNextValue(d);
+                       if (d > max)
+                               d = max;
+                       return d;
+//                     return makeString(d);
+               }
+
+               @Override
+               public Object getPreviousValue() {
+                       double d = currentUnit.toUnit(DoubleModel.this.getValue());
+                       double min = currentUnit.toUnit(minValue);
+                       if (MathUtil.equals(d,min))
+                               return null;
+                       d = currentUnit.getPreviousValue(d);
+                       if (d < min)
+                               d = min;
+                       return d;
+//                     return makeString(d);
+               }
+
+               
+               @Override
+               public Comparable<Double> getMinimum() {
+                       return currentUnit.toUnit(minValue);
+               }
+               
+               @Override
+               public Comparable<Double> getMaximum() {
+                       return currentUnit.toUnit(maxValue);
+               }
+               
+               
+               @Override
+               public void addChangeListener(ChangeListener l) {
+                       DoubleModel.this.addChangeListener(l);
+               }
+
+               @Override
+               public void removeChangeListener(ChangeListener l) {
+                       DoubleModel.this.removeChangeListener(l);
+               }
+       }
+       
+       /**
+        * Returns a new SpinnerModel with the same base as the DoubleModel.
+        * The values given to the JSpinner are in the currently selected units.
+        * 
+        * @return  A compatibility layer for a SpinnerModel.
+        */
+       public SpinnerModel getSpinnerModel() {
+               return new ValueSpinnerModel();
+       }
+       
+       
+       
+       
+       
+       ////////////  JSlider model  ////////////
+       
+       private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
+               private static final int MAX = 1000;
+               
+               /*
+                * Use linear scale  value = linear1 * x + linear0  when x < linearPosition
+                * Use quadratic scale  value = quad2 * x^2 + quad1 * x + quad0  otherwise
+                */
+               
+               // Linear in range x <= linearPosition
+               private final double linearPosition;
+               
+               // May be changing DoubleModels when using linear model
+               private final DoubleModel min, mid, max;
+               
+               // Linear multiplier and constant
+               //private final double linear1;
+               //private final double linear0;
+               
+               // Non-linear multiplier, exponent and constant
+               private final double quad2,quad1,quad0;
+               
+               
+               
+               public ValueSliderModel(DoubleModel min, DoubleModel max) {
+                       linearPosition = 1.0;
+
+                       this.min = min;
+                       this.mid = max;  // Never use exponential scale
+                       this.max = max;
+                       
+                       min.addChangeListener(this);
+                       max.addChangeListener(this);
+
+                       quad2 = quad1 = quad0 = 0;  // Not used
+               }
+               
+               
+               
+               /**
+                * Generate a linear model from min to max.
+                */
+               public ValueSliderModel(double min, double max) {
+                       linearPosition = 1.0;
+
+                       this.min = new DoubleModel(min);
+                       this.mid = new DoubleModel(max);  // Never use exponential scale
+                       this.max = new DoubleModel(max);
+
+                       quad2 = quad1 = quad0 = 0;  // Not used
+               }
+               
+               public ValueSliderModel(double min, double mid, double max) {
+                       this(min,0.5,mid,max);
+               }
+               
+               /*
+                * v(x)  = mul * x^exp + add
+                * 
+                * v(pos)  = mul * pos^exp + add = mid
+                * v(1)    = mul + add = max
+                * v'(pos) = mul*exp * pos^(exp-1) = linearMul
+                */
+               public ValueSliderModel(double min, double pos, double mid, double max) {
+                       this.min = new DoubleModel(min);
+                       this.mid = new DoubleModel(mid);
+                       this.max = new DoubleModel(max);
+
+                       
+                       linearPosition = pos;
+                       //linear0 = min;
+                       //linear1 = (mid-min)/pos;
+                       
+                       if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
+                               throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
+                                               "min="+min+" mid="+mid+" max="+max+" pos="+pos);
+                       }
+                       
+                       /*
+                        * quad2..0 are calculated such that
+                        *   f(pos)  = mid      - continuity
+                        *   f(1)    = max      - end point
+                        *   f'(pos) = linear1  - continuity of derivative
+                        */
+                       
+                       double delta = (mid-min)/pos;
+                       quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
+                       quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
+                       quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
+                       
+               }
+               
+               private double pow2(double x) {
+                       return x*x;
+               }
+               
+               public int getValue() {
+                       double value = DoubleModel.this.getValue();
+                       if (value <= min.getValue())
+                               return 0;
+                       if (value >= max.getValue())
+                               return MAX;
+                       
+                       double x;
+                       if (value <= mid.getValue()) {
+                               // Use linear scale
+                               //linear0 = min;
+                               //linear1 = (mid-min)/pos;
+                               
+                               x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
+                       } else {
+                               // Use quadratic scale
+                               // Further solution of the quadratic equation
+                               //   a*x^2 + b*x + c-value == 0
+                               x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
+                       }
+                       return (int)(x*MAX);
+               }
+
+
+               public void setValue(int newValue) {
+                       if (firing > 0)   // Ignore loops
+                               return;
+                       
+                       double x = (double)newValue/MAX;
+                       double value;
+                       
+                       if (x <= linearPosition) {
+                               // Use linear scale
+                               //linear0 = min;
+                               //linear1 = (mid-min)/pos;
+
+                               value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
+                       } else {
+                               // Use quadratic scale
+                               value = quad2*x*x + quad1*x + quad0;
+                       }
+                       
+                       DoubleModel.this.setValue(currentUnit.fromUnit(
+                                       currentUnit.round(currentUnit.toUnit(value))));
+               }
+
+               
+               // Static get-methods
+               private boolean isAdjusting;
+               public int getExtent() { return 0; }
+               public int getMaximum() { return MAX; }
+               public int getMinimum() { return 0; }
+               public boolean getValueIsAdjusting() { return isAdjusting; }
+               
+               // Ignore set-values
+               public void setExtent(int newExtent) { }
+               public void setMaximum(int newMaximum) { }
+               public void setMinimum(int newMinimum) { }
+               public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
+
+               public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
+                       setValueIsAdjusting(adjusting);
+                       setValue(value);
+               }
+
+               // Pass change listeners to the underlying model
+               public void addChangeListener(ChangeListener l) {
+                       DoubleModel.this.addChangeListener(l);
+               }
+
+               public void removeChangeListener(ChangeListener l) {
+                       DoubleModel.this.removeChangeListener(l);
+               }
+
+
+
+               public void stateChanged(ChangeEvent e) {
+                       // Min or max range has changed.
+                       // Fire if not already firing
+                       if (firing == 0)
+                               fireStateChanged();
+               }
+       }
+       
+       
+       public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
+               return new ValueSliderModel(min,max);
+       }
+       
+       public BoundedRangeModel getSliderModel(double min, double max) {
+               return new ValueSliderModel(min,max);
+       }
+       
+       public BoundedRangeModel getSliderModel(double min, double mid, double max) {
+               return new ValueSliderModel(min,mid,max);
+       }
+       
+       public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
+               return new ValueSliderModel(min,pos,mid,max);
+       }
+       
+       
+       
+       
+
+       ////////////  Action model  ////////////
+       
+       private class AutomaticActionModel extends AbstractAction implements ChangeListener {
+               private boolean oldValue = false;
+               
+               public AutomaticActionModel() {
+                       oldValue = isAutomatic();
+                       addChangeListener(this);
+               }
+               
+
+               @Override
+               public boolean isEnabled() {
+                       // TODO: LOW: does not reflect if component is currently able to support automatic setting
+                       return isAutomaticAvailable();
+               }
+               
+               @Override
+               public Object getValue(String key) {
+                       if (key.equals(Action.SELECTED_KEY)) {
+                               oldValue = isAutomatic();
+                               return oldValue;
+                       }
+                       return super.getValue(key);
+               }
+
+               @Override
+               public void putValue(String key, Object value) {
+                       if (firing > 0)
+                               return;
+                       if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
+                               oldValue = (Boolean)value;
+                               setAutomatic((Boolean)value);
+                       } else {
+                               super.putValue(key, value);
+                       }
+               }
+
+               // Implement a wrapper to the ChangeListeners
+               ArrayList<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
+               @Override
+               public void addPropertyChangeListener(PropertyChangeListener listener) {
+                       listeners.add(listener);
+                       DoubleModel.this.addChangeListener(this);
+               }
+               @Override
+               public void removePropertyChangeListener(PropertyChangeListener listener) {
+                       listeners.remove(listener);
+                       if (listeners.isEmpty())
+                               DoubleModel.this.removeChangeListener(this);
+               }
+               // If the value has changed, generate an event to the listeners
+               public void stateChanged(ChangeEvent e) {
+                       boolean newValue = isAutomatic();
+                       if (oldValue == newValue)
+                               return;
+                       PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
+                                       oldValue,newValue);
+                       oldValue = newValue;
+                       Object[] l = listeners.toArray();
+                       for (int i=0; i<l.length; i++) {
+                               ((PropertyChangeListener)l[i]).propertyChange(event);
+                       }
+               }
+
+               public void actionPerformed(ActionEvent e) {
+                       // Setting performed in putValue
+               }
+
+       }
+       
+       /**
+        * Returns a new Action corresponding to the changes of the automatic setting
+        * property of the value model.  This may be used directly with e.g. check buttons.
+        * 
+        * @return  A compatibility layer for an Action.
+        */
+       public Action getAutomaticAction() {
+               return new AutomaticActionModel();
+       }
+       
+       
+       
+       
+
+
+       ////////////  Main model  /////////////
+
+       /*
+        * The main model handles all values in SI units, i.e. no conversion is made within the model.
+        */
+       
+       private final ChangeSource source;
+       private final String valueName;
+       private final double multiplier;
+       
+       private final Method getMethod;
+       private final Method setMethod;
+       
+       private final Method getAutoMethod;
+       private final Method setAutoMethod;
+       
+       private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
+       
+       private final UnitGroup units;
+       private Unit currentUnit;
+
+       private final double minValue;
+       private final double maxValue;
+
+       
+       private int firing = 0;  //  >0 when model itself is sending events
+       
+       
+       // Used to differentiate changes in valueName and other changes in the component:
+       private double lastValue = 0;
+       private boolean lastAutomatic = false;
+               
+       
+       public DoubleModel(double value) {
+               this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(double value, UnitGroup unit) {
+               this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(double value, UnitGroup unit, double min) {
+               this(value,unit,min,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(double value, UnitGroup unit, double min, double max) {
+               this.lastValue = value;
+               this.minValue = min;
+               this.maxValue = max;
+
+               source = null;
+               valueName = "Constant value";
+               multiplier = 1;
+               
+               getMethod = setMethod = null;
+               getAutoMethod = setAutoMethod = null;
+               units = unit;
+               currentUnit = units.getDefaultUnit();
+       }
+
+       
+       /**
+        * Generates a new DoubleModel that changes the values of the specified component.
+        * The double value is read and written using the methods "get"/"set" + valueName.
+        *  
+        * @param source Component whose parameter to use.
+        * @param valueName Name of metods used to get/set the parameter.
+        * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
+        * @param min Minimum value allowed (in SI units)
+        * @param max Maximum value allowed (in SI units)
+        */
+       public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
+                       double min, double max) {
+               this.source = source;
+               this.valueName = valueName;
+               this.multiplier = multiplier;
+
+               this.units = unit;
+               currentUnit = units.getDefaultUnit();
+               
+               this.minValue = min;
+               this.maxValue = max;
+               
+               try {
+                       getMethod = source.getClass().getMethod("get" + valueName);
+               } catch (NoSuchMethodException e) {
+                       throw new IllegalArgumentException("get method for value '"+valueName+
+                                       "' not present in class "+source.getClass().getCanonicalName());
+               }
+
+               Method s=null;
+               try {
+                       s = source.getClass().getMethod("set" + valueName,double.class);
+               } catch (NoSuchMethodException e1) { }  // Ignore
+               setMethod = s;
+               
+               // Automatic selection methods
+               
+               Method set=null,get=null;
+               
+               try {
+                       get = source.getClass().getMethod("is" + valueName + "Automatic");
+                       set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
+               } catch (NoSuchMethodException e) { } // ignore
+               
+               if (set!=null && get!=null) {
+                       getAutoMethod = get;
+                       setAutoMethod = set;
+               } else {
+                       getAutoMethod = null;
+                       setAutoMethod = null;
+               }
+               
+       }
+
+       public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
+                       double min) {
+               this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
+               this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, 
+                       double min, double max) {
+               this(source,valueName,1.0,unit,min,max);
+       }
+       
+       public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
+               this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
+               this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+       }
+
+       public DoubleModel(ChangeSource source, String valueName) {
+               this(source,valueName,1.0,UnitGroup.UNITS_NONE,
+                               Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+       }
+
+       public DoubleModel(ChangeSource source, String valueName, double min) {
+               this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
+       }
+       
+       public DoubleModel(ChangeSource source, String valueName, double min, double max) {
+               this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
+       }
+       
+       
+       
+       /**
+        * Returns the value of the variable (in SI units).
+        */
+       public double getValue() {
+               if (getMethod==null)  // Constant value
+                       return lastValue;
+
+               try {
+                       return (Double)getMethod.invoke(source)*multiplier;
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace();
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               return lastValue;  // Should not occur
+       }
+       
+       /**
+        * Sets the value of the variable.
+        * @param v New value for parameter in SI units.
+        */
+       public void setValue(double v) {
+               if (setMethod==null) {
+                       if (getMethod != null) {
+                               throw new RuntimeException("setMethod not available for variable '"+valueName+
+                                               "' in class "+source.getClass().getCanonicalName());
+                       }
+                       lastValue = v;
+                       fireStateChanged();
+                       return;
+               }
+
+               try {
+                       setMethod.invoke(source, v/multiplier);
+                       return;
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace();
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               fireStateChanged();  // Should not occur
+       }
+
+       
+       /**
+        * Returns whether setting the value automatically is available.
+        */
+       public boolean isAutomaticAvailable() {
+               return (getAutoMethod != null) && (setAutoMethod != null);
+       }
+
+       /**
+        * Returns whether the value is currently being set automatically.
+        * Returns false if automatic setting is not available at all.
+        */
+       public boolean isAutomatic() {
+               if (getAutoMethod == null)
+                       return false;
+               
+               try {
+                       return (Boolean)getAutoMethod.invoke(source);
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace();
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               return false;  // Should not occur
+       }
+       
+       /**
+        * Sets whether the value should be set automatically.  Simply fires a
+        * state change event if automatic setting is not available.
+        */
+       public void setAutomatic(boolean auto) {
+               if (setAutoMethod == null) {
+                       fireStateChanged();  // in case something is out-of-sync
+                       return;
+               }
+               
+               try {
+                       lastAutomatic = auto;
+                       setAutoMethod.invoke(source, auto);
+                       return;
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace();
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               fireStateChanged();  // Should not occur
+       }
+       
+
+       /**
+        * Returns the current Unit.  At the beginning it is the default unit of the UnitGroup.
+        * @return The most recently set unit.
+        */
+       public Unit getCurrentUnit() {
+               return currentUnit;
+       }
+       
+       /**
+        * Sets the current Unit.  The unit must be one of those included in the UnitGroup.
+        * @param u  The unit to set active.
+        */
+       public void setCurrentUnit(Unit u) {
+               if (currentUnit == u)
+                       return;
+               currentUnit = u;
+               fireStateChanged();
+       }
+       
+       
+       /**
+        * Returns the UnitGroup associated with the parameter value.
+        *
+        * @return The UnitGroup given to the constructor.
+        */
+       public UnitGroup getUnitGroup() {
+               return units;
+       }
+       
+       
+       
+       /**
+        * Add a listener to the model.  Adds the model as a listener to the Component if this
+        * is the first listener.
+        * @param l Listener to add.
+        */
+       public void addChangeListener(ChangeListener l) {
+               if (listeners.isEmpty()) {
+                       if (source != null) {
+                               source.addChangeListener(this);
+                               lastValue = getValue();
+                               lastAutomatic = isAutomatic();
+                       }
+               }
+
+               listeners.add(l);
+               if (DEBUG_LISTENERS)
+                       System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
+       }
+
+       /**
+        * Remove a listener from the model.  Removes the model from being a listener to the Component
+        * if this was the last listener of the model.
+        * @param l Listener to remove.
+        */
+       public void removeChangeListener(ChangeListener l) {
+               listeners.remove(l);
+               if (listeners.isEmpty() && source != null) {
+                       source.removeChangeListener(this);
+               }
+               if (DEBUG_LISTENERS)
+                       System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
+       }
+       
+       /**
+        * Fire a ChangeEvent to all listeners.
+        */
+       protected void fireStateChanged() {
+               Object[] l = listeners.toArray();
+               ChangeEvent event = new ChangeEvent(this);
+               firing++;
+               for (int i=0; i<l.length; i++)
+                       ((ChangeListener)l[i]).stateChanged(event);
+               firing--;
+       }
+
+       /**
+        * Called when the component changes.  Checks whether the modeled value has changed, and if
+        * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
+        */
+       public void stateChanged(ChangeEvent e) {
+               double v = getValue();
+               boolean b = isAutomatic();
+               if (lastValue == v && lastAutomatic == b)
+                       return;
+               lastValue = v;
+               lastAutomatic = b;
+               fireStateChanged();
+       }
+
+       /**
+        * Explain the DoubleModel as a String.
+        */
+       @Override
+       public String toString() {
+               if (source == null)
+                       return "DoubleModel[constant="+lastValue+"]";
+               return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+       }
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/EnumModel.java b/src/net/sf/openrocket/gui/adaptors/EnumModel.java
new file mode 100644 (file)
index 0000000..f6e7d67
--- /dev/null
@@ -0,0 +1,128 @@
+package net.sf.openrocket.gui.adaptors;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Reflection;
+
+
+public class EnumModel<T extends Enum<T>> extends AbstractListModel 
+               implements ComboBoxModel, ChangeListener {
+
+       private final ChangeSource source;
+       private final String valueName;
+       private final String nullText;
+       
+       private final Enum<T>[] values;
+       private Enum<T> currentValue = null;
+       
+       private final Reflection.Method getMethod;
+       private final Reflection.Method setMethod;
+       
+       
+       
+       public EnumModel(ChangeSource source, String valueName) {
+               this(source,valueName,null,null);
+       }
+       
+       public EnumModel(ChangeSource source, String valueName, Enum<T>[] values) {
+               this(source, valueName, values, null);
+       }
+       
+       @SuppressWarnings("unchecked")
+       public EnumModel(ChangeSource source, String valueName, Enum<T>[] values, String nullText) {
+               Class<? extends Enum<T>> enumClass;
+               this.source = source;
+               this.valueName = valueName;
+               
+               try {
+                       java.lang.reflect.Method getM = source.getClass().getMethod("get" + valueName);
+                       enumClass = (Class<? extends Enum<T>>) getM.getReturnType();
+                       if (!enumClass.isEnum()) {
+                               throw new IllegalArgumentException("Return type of get" + valueName +
+                                               " not an enum type");
+                       }
+                       
+                       getMethod = new Reflection.Method(getM);
+                       setMethod = new Reflection.Method(source.getClass().getMethod("set" + valueName,
+                                       enumClass));
+               } catch (NoSuchMethodException e) {
+                       throw new IllegalArgumentException("get/is methods for enum '"+valueName+
+                                       "' not present in class "+source.getClass().getCanonicalName());
+               }
+               
+               if (values != null)
+                       this.values = values;
+               else 
+                       this.values = enumClass.getEnumConstants();
+               
+               this.nullText = nullText;
+               
+               stateChanged(null);  // Update current value
+               source.addChangeListener(this);
+       }
+
+       
+               
+       @Override
+       public Object getSelectedItem() {
+               if (currentValue==null)
+                       return nullText;
+               return currentValue;
+       }
+
+       @Override
+       public void setSelectedItem(Object item) {
+               if (item instanceof String) {
+                       if (currentValue != null)
+                               setMethod.invoke(source, (Object)null);
+                       return;
+               }
+               
+               if (!(item instanceof Enum<?>)) {
+                       throw new IllegalArgumentException("Not String or Enum");
+               }
+               
+               // Comparison with == ok, since both are enums
+               if (currentValue == item)
+                       return;
+               setMethod.invoke(source, item);
+       }
+
+       @Override
+       public Object getElementAt(int index) {
+               if (values[index] == null)
+                       return nullText;
+               return values[index];
+       }
+
+       @Override
+       public int getSize() {
+               return values.length;
+       }
+
+
+
+
+
+       @SuppressWarnings("unchecked")
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               Enum<T> value = (Enum<T>) getMethod.invoke(source);
+               if (value != currentValue) {
+                       currentValue = value;
+                       this.fireContentsChanged(this, 0, values.length);
+               }
+       }
+       
+       
+
+       @Override
+       public String toString() {
+               return "EnumModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/IntegerModel.java b/src/net/sf/openrocket/gui/adaptors/IntegerModel.java
new file mode 100644 (file)
index 0000000..6800e0d
--- /dev/null
@@ -0,0 +1,234 @@
+package net.sf.openrocket.gui.adaptors;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+
+
+public class IntegerModel implements ChangeListener {
+
+
+       //////////// JSpinner Model ////////////
+       
+       private class IntegerSpinnerModel extends SpinnerNumberModel {
+               @Override
+               public Object getValue() {
+                       return IntegerModel.this.getValue();
+               }
+
+               @Override
+               public void setValue(Object value) {
+                       if (firing > 0)   // Ignore, if called when model is sending events
+                               return;
+                       Number num = (Number)value;
+                       int newValue = num.intValue();
+                       IntegerModel.this.setValue(newValue);
+                       
+//                     try {
+//                             int newValue = Integer.parseInt((String)value);
+//                             IntegerModel.this.setValue(newValue);
+//                     } catch (NumberFormatException e) { 
+//                             IntegerModel.this.fireStateChanged();
+//                     };
+               }
+                       
+               @Override
+               public Object getNextValue() {
+                       int d = IntegerModel.this.getValue();
+                       if (d >= maxValue)
+                               return null;
+                       return (d+1);
+               }
+
+               @Override
+               public Object getPreviousValue() {
+                       int d = IntegerModel.this.getValue();
+                       if (d <= minValue)
+                               return null;
+                       return (d-1);
+               }
+               
+               @Override
+               public void addChangeListener(ChangeListener l) {
+                       IntegerModel.this.addChangeListener(l);
+               }
+
+               @Override
+               public void removeChangeListener(ChangeListener l) {
+                       IntegerModel.this.removeChangeListener(l);
+               }
+       }
+       
+       /**
+        * Returns a new SpinnerModel with the same base as the DoubleModel.
+        * The values given to the JSpinner are in the currently selected units.
+        * 
+        * @return  A compatibility layer for a SpinnerModel.
+        */
+       public SpinnerModel getSpinnerModel() {
+               return new IntegerSpinnerModel();
+       }
+       
+       
+
+
+       ////////////  Main model  /////////////
+
+       /*
+        * The main model handles all values in SI units, i.e. no conversion is made within the model.
+        */
+       
+       private final ChangeSource source;
+       private final String valueName;
+       
+       private final Method getMethod;
+       private final Method setMethod;
+       
+       private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
+
+       private final int minValue;
+       private final int maxValue;
+
+       
+       private int firing = 0;  //  >0 when model itself is sending events
+       
+       
+       // Used to differentiate changes in valueName and other changes in the source:
+       private int lastValue = 0;
+               
+
+       
+       /**
+        * Generates a new DoubleModel that changes the values of the specified source.
+        * The double value is read and written using the methods "get"/"set" + valueName.
+        *  
+        * @param source Component whose parameter to use.
+        * @param valueName Name of metods used to get/set the parameter.
+        * @param multiplier Value shown by the model is the value from source.getXXX * multiplier
+        * @param min Minimum value allowed (in SI units)
+        * @param max Maximum value allowed (in SI units)
+        */
+       public IntegerModel(ChangeSource source, String valueName, int min, int max) {
+               this.source = source;
+               this.valueName = valueName;
+               
+               this.minValue = min;
+               this.maxValue = max;
+               
+               try {
+                       getMethod = source.getClass().getMethod("get" + valueName);
+                       setMethod = source.getClass().getMethod("set" + valueName,int.class);
+               } catch (NoSuchMethodException e) {
+                       throw new IllegalArgumentException("get/set methods for value '"+valueName+
+                                       "' not present in class "+source.getClass().getCanonicalName());
+               }
+       }
+
+       public IntegerModel(ChangeSource source, String valueName, int min) {
+               this(source,valueName,min,Integer.MAX_VALUE);
+       }
+       
+       public IntegerModel(ChangeSource source, String valueName) {
+               this(source,valueName,Integer.MIN_VALUE,Integer.MAX_VALUE);
+       }
+       
+
+       
+       
+       /**
+        * Returns the value of the variable.
+        */
+       public int getValue() {
+               try {
+                       return (Integer)getMethod.invoke(source);
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace();
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               return lastValue;  // Should not occur
+       }
+       
+       /**
+        * Sets the value of the variable.
+        */
+       public void setValue(int v) {
+               try {
+                       setMethod.invoke(source, v);
+                       return;
+               } catch (IllegalArgumentException e) {
+                       e.printStackTrace();
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               } catch (InvocationTargetException e) {
+                       e.printStackTrace();
+               }
+               fireStateChanged();  // Should not occur
+       }
+
+       
+       /**
+        * Add a listener to the model.  Adds the model as a listener to the Component if this
+        * is the first listener.
+        * @param l Listener to add.
+        */
+       public void addChangeListener(ChangeListener l) {
+               if (listeners.isEmpty()) {
+                       source.addChangeListener(this);
+                       lastValue = getValue();
+               }
+
+               listeners.add(l);
+       }
+
+       /**
+        * Remove a listener from the model.  Removes the model from being a listener to the Component
+        * if this was the last listener of the model.
+        * @param l Listener to remove.
+        */
+       public void removeChangeListener(ChangeListener l) {
+               listeners.remove(l);
+               if (listeners.isEmpty()) {
+                       source.removeChangeListener(this);
+               }
+       }
+       
+       public void fireStateChanged() {
+               Object[] l = listeners.toArray();
+               ChangeEvent event = new ChangeEvent(this);
+               firing++;
+               for (int i=0; i<l.length; i++)
+                       ((ChangeListener)l[i]).stateChanged(event);
+               firing--;
+       }
+
+       /**
+        * Called when the source changes.  Checks whether the modeled value has changed, and if
+        * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
+        */
+       public void stateChanged(ChangeEvent e) {
+               int v = getValue();
+               if (lastValue == v)
+                       return;
+               lastValue = v;
+               fireStateChanged();
+       }
+
+       /**
+        * Explain the DoubleModel as a String.
+        */
+       @Override
+       public String toString() {
+               return "IntegerModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/MaterialModel.java b/src/net/sf/openrocket/gui/adaptors/MaterialModel.java
new file mode 100644 (file)
index 0000000..1af6f2a
--- /dev/null
@@ -0,0 +1,206 @@
+package net.sf.openrocket.gui.adaptors;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.database.Database;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Reflection;
+
+public class MaterialModel extends AbstractListModel implements
+               ComboBoxModel, ChangeListener {
+       
+       private static final String CUSTOM = "Custom";
+
+       
+       private final RocketComponent component;
+       private final Material.Type type;
+       private final Database<Material> database;
+       
+       private final Reflection.Method getMethod;
+       private final Reflection.Method setMethod;
+       
+       
+       public MaterialModel(RocketComponent component, Material.Type type) {
+               this(component, type, "Material");
+       }       
+
+       public MaterialModel(RocketComponent component, Material.Type type, String name) {
+               this.component = component;
+               this.type = type;
+               
+               switch (type) {
+               case LINE:
+                       this.database = Databases.LINE_MATERIAL;
+                       break;
+                       
+               case BULK:
+                       this.database = Databases.BULK_MATERIAL;
+                       break;
+                       
+               case SURFACE:
+                       this.database = Databases.SURFACE_MATERIAL;
+                       break;
+                       
+               default:
+                       throw new IllegalArgumentException("Unknown material type:"+type);
+               }
+               
+               try {
+                       getMethod = new Reflection.Method(component.getClass().getMethod("get"+name));
+                       setMethod = new Reflection.Method(component.getClass().getMethod("set"+name,
+                                       Material.class));
+               } catch (NoSuchMethodException e) {
+                       throw new IllegalArgumentException("get/is methods for material " +
+                                       "not present in class "+component.getClass().getCanonicalName());
+               }
+               
+               component.addChangeListener(this);
+               database.addChangeListener(this);
+       }
+       
+       @Override
+       public Object getSelectedItem() {
+               return getMethod.invoke(component);
+       }
+
+       @Override
+       public void setSelectedItem(Object item) {
+               if (item == CUSTOM) {
+                       
+                       // Open custom material dialog in the future, after combo box has closed
+                       SwingUtilities.invokeLater(new Runnable() {
+                               @Override
+                               public void run() {
+                                       AddMaterialDialog dialog = new AddMaterialDialog();
+                                       dialog.setVisible(true);
+                                       
+                                       if (!dialog.okClicked)
+                                               return;
+                                       
+                                       Material material = Material.newMaterial(type, 
+                                                       dialog.nameField.getText().trim(),
+                                                       dialog.density.getValue());
+                                       setMethod.invoke(component, material);
+                                       
+                                       // TODO: HIGH: Allow saving added material to database
+//                                     if (dialog.addBox.isSelected()) {
+//                                             database.add(material);
+//                                     }
+                               }
+                       });
+                       
+               } else if (item instanceof Material) {
+                       
+                       setMethod.invoke(component, item);
+                       
+               } else {
+                       assert(false): "Should not occur";
+               }
+       }
+
+       @Override
+       public Object getElementAt(int index) {
+               if (index == database.size()) {
+                       return CUSTOM;
+               } else if (index >= database.size()+1) {
+                       return null;
+               }
+               return database.get(index);
+       }
+
+       @Override
+       public int getSize() {
+               return database.size() + 1;
+       }
+
+
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               if (e instanceof ComponentChangeEvent) {
+                       if (((ComponentChangeEvent)e).isMassChange()) {
+                               this.fireContentsChanged(this, 0, 0);
+                       }
+               } else {
+                       this.fireContentsChanged(this, 0, database.size());
+               }
+       }
+       
+       
+       
+       
+       private class AddMaterialDialog extends JDialog {
+               
+               private boolean okClicked = false;
+               private JTextField nameField;
+               private DoubleModel density;
+//             private JCheckBox addBox;
+               
+               public AddMaterialDialog() {
+                       super((JFrame)null, "Custom material", true);
+                       
+                       Material material = (Material) getSelectedItem();
+                       
+                       JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]"));
+                       
+                       panel.add(new JLabel("Material name:"));
+                       nameField = new JTextField(15);
+                       nameField.setText(material.getName());
+                       panel.add(nameField,"span 2, growx, wrap");
+                       
+                       panel.add(new JLabel("Material density:"));
+                       density = new DoubleModel(material.getDensity(),UnitGroup.UNITS_DENSITY_BULK,0);
+                       JSpinner spinner = new JSpinner(density.getSpinnerModel());
+                       panel.add(spinner, "growx");
+                       panel.add(new UnitSelector(density),"wrap");
+                       
+//                     addBox = new JCheckBox("Add material to database");
+//                     panel.add(addBox,"span, wrap");
+                       
+                       JButton button = new JButton("OK");
+                       button.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       okClicked = true;
+                                       AddMaterialDialog.this.setVisible(false);
+                               }
+                       });
+                       panel.add(button,"span, split, tag ok");
+                       
+                       button = new JButton("Cancel");
+                       button.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       AddMaterialDialog.this.setVisible(false);
+                               }
+                       });
+                       panel.add(button,"tag cancel");
+                       
+                       this.setContentPane(panel);
+                       this.pack();
+                       this.setAlwaysOnTop(true);
+                       this.setLocationRelativeTo(null);
+               }
+               
+       }
+}
diff --git a/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java b/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java
new file mode 100644 (file)
index 0000000..fbf367b
--- /dev/null
@@ -0,0 +1,382 @@
+package net.sf.openrocket.gui.adaptors;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.TextFieldListener;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.GUIUtil;
+
+public class MotorConfigurationModel implements ComboBoxModel, ChangeListener {
+
+       private static final String EDIT = "Edit configurations";
+       
+       
+       private EventListenerList listenerList = new EventListenerList();
+       
+       private final Configuration config;
+       private final Rocket rocket;
+       
+       private Map<String, ID> map = new HashMap<String, ID>();
+       
+
+       public MotorConfigurationModel(Configuration config) {
+               this.config = config;
+               this.rocket = config.getRocket();
+               config.addChangeListener(this);
+       }
+       
+       
+       
+       @Override
+       public Object getElementAt(int index) {
+               String[] ids = rocket.getMotorConfigurationIDs();
+               if (index < 0  ||  index > ids.length)
+                       return null;
+               
+               if (index == ids.length)
+                       return EDIT;
+               
+               return get(ids[index]);
+       }
+
+       @Override
+       public int getSize() {
+               return rocket.getMotorConfigurationIDs().length + 1;
+       }
+
+       @Override
+       public Object getSelectedItem() {
+               return get(config.getMotorConfigurationID());
+       }
+
+       @Override
+       public void setSelectedItem(Object item) {
+               if (item == EDIT) {
+                       
+                       // Open edit dialog in the future, after combo box has closed
+                       SwingUtilities.invokeLater(new Runnable() {
+                               @Override
+                               public void run() {
+                                       EditConfigurationDialog dialog = new EditConfigurationDialog();
+                                       dialog.setVisible(true);
+                                       
+                                       if (dialog.isRowSelected()) {
+                                               rocket.getDefaultConfiguration().setMotorConfigurationID(
+                                                               dialog.getSelectedID());
+                                       }
+                               }
+                       });
+
+                       return;
+               }
+               if (!(item instanceof ID))
+                       return;
+               
+               ID idObject = (ID) item;
+               config.setMotorConfigurationID(idObject.getID());
+       }
+
+
+       
+       ////////////////  Event/listener handling  ////////////////
+       
+       
+       @Override
+       public void addListDataListener(ListDataListener l) {
+               listenerList.add(ListDataListener.class, l);
+       }
+
+       @Override
+       public void removeListDataListener(ListDataListener l) {
+               listenerList.remove(ListDataListener.class, l);
+       }
+
+       protected void fireListDataEvent() {
+               Object[] listeners = listenerList.getListenerList();
+               ListDataEvent e = null;
+
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i] == ListDataListener.class) {
+                               if (e == null)
+                                       e = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize());
+                               ((ListDataListener) listeners[i+1]).contentsChanged(e);
+                       }
+               }
+       }
+        
+       
+       @Override
+       public void stateChanged(ChangeEvent e) {
+               if (e instanceof ComponentChangeEvent) {
+                       // Ignore unnecessary changes
+                       if (!((ComponentChangeEvent)e).isMotorChange())
+                               return;
+               }
+               fireListDataEvent();
+       }
+       
+       
+       
+       /*
+        * The ID class is an adapter, that contains the actual configuration ID,
+        * but gives the configuration description as its String representation.
+        * The get(id) method retrieves ID objects and caches them for reuse.
+        */
+       
+       private ID get(String id) {
+               ID idObject = map.get(id);
+               if (idObject != null)
+                       return idObject;
+               
+               idObject = new ID(id);
+               map.put(id, idObject);
+               return idObject;
+       }
+       
+       
+       private class ID {
+               private final String id;
+               
+               public ID(String id) {
+                       this.id = id;
+               }
+               
+               public String getID() {
+                       return id;
+               }
+               
+               @Override
+               public String toString() {
+                       return rocket.getMotorConfigurationDescription(id);
+               }
+       }
+       
+       
+       private class EditConfigurationDialog extends JDialog {
+               private final ColumnTableModel tableModel;
+               private String[] ids;
+               int selection = -1;
+               
+               private final JButton addButton;
+               private final JButton removeButton;
+               private final JTextField nameField;
+               
+               private final JTable table;
+               
+               
+               public boolean isRowSelected() {
+                       return selection >= 0;
+               }
+               
+               public String getSelectedID() {
+                       if (selection >= 0)
+                               return ids[selection];
+                       return null;
+               }
+               
+               
+               public EditConfigurationDialog() {
+                       super((JFrame)null, "Edit configurations", true);
+
+                       ids = rocket.getMotorConfigurationIDs();
+                       
+                       // Create columns
+                       ArrayList<Column> columnList = new ArrayList<Column>();
+                       columnList.add(new Column("Name") {
+                               @Override
+                               public Object getValueAt(int row) {
+                                       return rocket.getMotorConfigurationDescription(ids[row]);
+                               }
+                       });
+                       
+                       // Create columns from the motor mounts
+                       Iterator<RocketComponent> iterator = rocket.deepIterator();
+                       while (iterator.hasNext()) {
+                               RocketComponent c = iterator.next();
+                               if (!(c instanceof MotorMount))
+                                       continue;
+                               
+                               final MotorMount mount = (MotorMount)c;
+                               if (!mount.isMotorMount())
+                                       continue;
+                               
+                               Column col = new Column(c.getName()) {
+                                       @Override
+                                       public Object getValueAt(int row) {
+                                               Motor motor = mount.getMotor(ids[row]);
+                                               if (motor == null)
+                                                       return "";
+                                               return motor.getDesignation(mount.getMotorDelay(ids[row]));
+                                       }
+                               };
+                               columnList.add(col);
+                       }
+                       tableModel = new ColumnTableModel(columnList.toArray(new Column[0])) {
+                               @Override
+                               public int getRowCount() {
+                                       return ids.length;
+                               }
+                       };
+
+                       
+                       
+                       // Create the panel
+                       JPanel panel = new JPanel(new MigLayout("fill","[shrink][grow]"));
+                       
+                       
+                       panel.add(new JLabel("Configuration name:"), "gapright para");
+                       nameField = new JTextField();
+                       new TextFieldListener() {
+                               @Override
+                               public void setText(String text) {
+                                       if (selection < 0 || ids[selection] == null)
+                                               return;
+                                       rocket.setMotorConfigurationName(ids[selection], text);
+                                       fireChange();
+                               }
+                       }.listenTo(nameField);
+                       panel.add(nameField, "growx, wrap");
+                       
+                       panel.add(new ResizeLabel("Leave empty for default description", -2), 
+                                       "skip, growx, wrap para");
+                       
+                       
+                       table = new JTable(tableModel);
+                       table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+                       table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+                               @Override
+                               public void valueChanged(ListSelectionEvent e) {
+                                       updateSelection();
+                               }
+                       });
+
+                       // Mouse listener to act on double-clicks
+                       table.addMouseListener(new MouseAdapter() {
+                               @Override
+                               public void mouseClicked(MouseEvent e) {
+                                       if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
+                                               EditConfigurationDialog.this.dispose();
+                                       }
+                               }
+                       });
+                        
+                       
+                       
+                       JScrollPane scrollpane = new JScrollPane(table);
+                       panel.add(scrollpane, "spanx, height 150lp, width 400lp, grow, wrap");
+                       
+                       
+                       addButton = new JButton("New");
+                       addButton.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       String id = rocket.newMotorConfigurationID();
+                                       ids = rocket.getMotorConfigurationIDs();
+                                       tableModel.fireTableDataChanged();
+                                       int sel;
+                                       for (sel=0; sel < ids.length; sel++) {
+                                               if (id.equals(ids[sel]))
+                                                       break;
+                                       }
+                                       table.getSelectionModel().addSelectionInterval(sel, sel);
+                               }
+                       });
+                       panel.add(addButton, "growx, spanx, split 2");
+                       
+                       removeButton = new JButton("Remove");
+                       removeButton.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       int sel = table.getSelectedRow();
+                                       if (sel < 0 || sel >= ids.length || ids[sel] == null)
+                                               return;
+                                       rocket.removeMotorConfigurationID(ids[sel]);
+                                       ids = rocket.getMotorConfigurationIDs();
+                                       tableModel.fireTableDataChanged();
+                                       if (sel >= ids.length)
+                                               sel--;
+                                       table.getSelectionModel().addSelectionInterval(sel, sel);
+                               }
+                       });
+                       panel.add(removeButton, "growx, wrap para");
+                       
+                       
+                       JButton close = new JButton("Close");
+                       close.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       EditConfigurationDialog.this.dispose();
+                               }
+                       });
+                       panel.add(close, "spanx, alignx 100%");
+                       
+                       this.getRootPane().setDefaultButton(close);
+                       
+                       
+                       this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+                       GUIUtil.installEscapeCloseOperation(this);
+                       this.setLocationByPlatform(true);
+                       
+                       updateSelection();
+                       
+                       this.add(panel);
+                       this.validate();
+                       this.pack();
+               }
+               
+               private void fireChange() {
+                       int sel = table.getSelectedRow();
+                       tableModel.fireTableDataChanged();
+                       table.getSelectionModel().addSelectionInterval(sel, sel);
+               }
+               
+               private void updateSelection() {
+                       selection = table.getSelectedRow();
+                       if (selection < 0  ||  ids[selection] == null) {
+                               removeButton.setEnabled(false);
+                               nameField.setEnabled(false);
+                               nameField.setText("");
+                       } else {
+                               removeButton.setEnabled(true);
+                               nameField.setEnabled(true);
+                               nameField.setText(rocket.getMotorConfigurationName(ids[selection]));
+                       }
+               }
+       }
+       
+}
+
diff --git a/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java b/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java
new file mode 100644 (file)
index 0000000..7c5776a
--- /dev/null
@@ -0,0 +1,111 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class BodyTubeConfig extends RocketComponentConfig {
+
+       private MotorConfig motorConfigPane = null;
+
+       public BodyTubeConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+               
+               ////  Body tube length
+               panel.add(new JLabel("Body tube length:"));
+               
+               DoubleModel m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.5, 2.0)),"w 100lp, wrap");
+               
+               
+               //// Body tube diameter
+               panel.add(new JLabel("Outer diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+               JCheckBox check = new JCheckBox(od.getAutomaticAction());
+               check.setText("Automatic");
+               panel.add(check,"skip, span 2, wrap");
+               
+               
+               ////  Inner diameter
+               panel.add(new JLabel("Inner diameter:"));
+
+               // Diameter = 2*Radius
+               m = new DoubleModel(component,"InnerRadius",2,UnitGroup.UNITS_LENGTH,0);
+               
+
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)),"w 100lp, wrap");
+
+               
+               ////  Wall thickness
+               panel.add(new JLabel("Wall thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 0px");
+               
+               
+               check = new JCheckBox(new BooleanModel(component,"Filled"));
+               check.setText("Filled");
+               panel.add(check,"skip, span 2, wrap");
+               
+               
+               //// Material
+               panel.add(materialPanel(new JPanel(new MigLayout()), Material.Type.BULK),
+                               "cell 4 0, gapleft paragraph, aligny 0%, spany");
+               
+
+               tabbedPane.insertTab("General", null, panel, "General properties", 0);
+               motorConfigPane = new MotorConfig((BodyTube)c);
+               tabbedPane.insertTab("Motor", null, motorConfigPane, "Motor mount configuration", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       @Override
+       public void updateFields() {
+               super.updateFields();
+               if (motorConfigPane != null)
+                       motorConfigPane.updateFields();
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java b/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java
new file mode 100644 (file)
index 0000000..c813042
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class BulkheadConfig extends RingComponentConfig {
+
+       public BulkheadConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel tab;
+               
+               tab = generalTab("Radius:", null, null, "Thickness:");
+               tabbedPane.insertTab("General", null, tab, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java b/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java
new file mode 100644 (file)
index 0000000..7abedfa
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class CenteringRingConfig extends RingComponentConfig {
+
+       public CenteringRingConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel tab;
+               
+               tab = generalTab("Outer diameter:", "Inner diameter:", null, "Thickness:");
+               tabbedPane.insertTab("General", null, tab, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java b/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java
new file mode 100644 (file)
index 0000000..e785e45
--- /dev/null
@@ -0,0 +1,281 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Point;
+import java.awt.Window;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.DefaultBoundedRangeModel;
+import javax.swing.JDialog;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.Resettable;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * A JFrame dialog that contains the configuration elements of one component.
+ * The contents of the dialog are instantiated from CONFIGDIALOGPACKAGE according
+ * to the current component.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentConfigDialog extends JDialog implements ComponentChangeListener {
+       private static final long serialVersionUID = 1L;
+       private static final String CONFIGDIALOGPACKAGE = "net.sf.openrocket.gui.configdialog";
+       private static final String CONFIGDIALOGPOSTFIX = "Config";
+       
+       
+       private static ComponentConfigDialog dialog = null;
+
+       
+       private OpenRocketDocument document = null;
+       private RocketComponent component = null;
+       private RocketComponentConfig configurator = null;
+       
+       private final Window parent;
+       
+       private ComponentConfigDialog(Window parent, OpenRocketDocument document, 
+                       RocketComponent component) {
+               super(parent);
+               this.parent = parent;
+               
+               setComponent(document, component);
+               
+               // Set window position according to preferences, and set prefs when moving
+               Point position = Prefs.getWindowPosition(this.getClass());
+               if (position == null)
+                       this.setLocationByPlatform(true);
+               else
+                       this.setLocation(position);
+
+               this.addComponentListener(new ComponentAdapter() {
+                       @Override
+                       public void componentMoved(ComponentEvent e) {
+                               Prefs.setWindowPosition(ComponentConfigDialog.this.getClass(), 
+                                               ComponentConfigDialog.this.getLocation());
+                       }
+               });
+               
+               
+               // Install ESC listener
+               GUIUtil.installEscapeCloseOperation(this);
+       }
+       
+
+       /**
+        * Set the component being configured.  The listening connections of the old configurator
+        * will be removed and the new ones created.
+        * 
+        * @param component  Component to configure.
+        */
+       private void setComponent(OpenRocketDocument document, RocketComponent component) {
+               if (this.document != null) {
+                       this.document.getRocket().removeComponentChangeListener(this);
+               }
+
+               if (configurator != null) {
+                       // Remove listeners by setting all applicable models to null
+                       setNullModels(configurator);  // null-safe
+
+//                     mainPanel.remove(configurator);
+               }
+               
+               this.document = document;
+               this.component = component;
+               this.document.getRocket().addComponentChangeListener(this);
+               
+               configurator = getDialogContents();
+               this.setContentPane(configurator);
+               configurator.updateFields();
+//             mainPanel.add(configurator,"cell 0 0, growx, growy");
+               
+               setTitle(component.getComponentName()+" configuration");
+
+//             Dimension pref = getPreferredSize();
+//             Dimension real = getSize();
+//             if (pref.width > real.width || pref.height > real.height)
+               pack();
+       }
+       
+       /**
+        * Traverses recursively the component tree, and sets all applicable component 
+        * models to null, so as to remove the listener connections.
+        * 
+        * NOTE:  All components in the configuration dialogs that use custom models must be added
+        * to this method.
+        */
+       private void setNullModels(Component c) {
+               if (c==null)
+                       return;
+               
+               // Remove models for known components
+               //  Why the FSCK must this be so hard?!?!?
+
+               if (c instanceof JSpinner) {
+                       ((JSpinner)c).setModel(new SpinnerNumberModel());
+               } else if (c instanceof JSlider) {
+                       ((JSlider)c).setModel(new DefaultBoundedRangeModel());
+               } else if (c instanceof Resettable) {
+                       ((Resettable)c).resetModel();
+               }
+
+               
+               if (c instanceof Container) {
+                       Component[] cs = ((Container)c).getComponents();
+                       for (Component sub: cs)
+                               setNullModels(sub);
+               }
+
+       }
+       
+       
+       /**
+        * Return the configurator panel of the current component.
+        */
+       private RocketComponentConfig getDialogContents() {
+               Constructor<? extends RocketComponentConfig> c = 
+                       findDialogContentsConstructor(component);
+               if (c != null) {
+                       try {
+                               return (RocketComponentConfig) c.newInstance(component);
+                       } catch (InstantiationException e) {
+                               throw new RuntimeException("BUG in constructor reflection",e);
+                       } catch (IllegalAccessException e) {
+                               throw new RuntimeException("BUG in constructor reflection",e);
+                       } catch (InvocationTargetException e) {
+                               throw new RuntimeException("BUG in constructor reflection",e);
+                       }
+               }
+               
+               // Should never be reached, since RocketComponentConfig should catch all
+               // components without their own configurator.
+               throw new RuntimeException("Unable to find any configurator for "+component);
+       }
+
+       /**
+        * Finds the Constructor of the given component's config dialog panel in 
+        * CONFIGDIALOGPACKAGE.
+        */
+       @SuppressWarnings("unchecked")
+       private static Constructor<? extends RocketComponentConfig> 
+                       findDialogContentsConstructor(RocketComponent component) {
+               Class<?> currentclass;
+               String currentclassname;
+               String configclassname;
+               
+               Class<?> configclass;
+               Constructor<? extends RocketComponentConfig> c;
+               
+               currentclass = component.getClass();
+               while ((currentclass != null) && (currentclass != Object.class)) {
+                       currentclassname = currentclass.getCanonicalName();
+                       int index = currentclassname.lastIndexOf('.');
+                       if (index >= 0)
+                               currentclassname = currentclassname.substring(index + 1);
+                       configclassname = CONFIGDIALOGPACKAGE + "." + currentclassname + 
+                               CONFIGDIALOGPOSTFIX;
+                       
+                       try {
+                               configclass = Class.forName(configclassname);
+                               c = (Constructor<? extends RocketComponentConfig>)
+                                       configclass.getConstructor(RocketComponent.class);
+                               return c;
+                       } catch (Exception ignore) { }
+
+                       currentclass = currentclass.getSuperclass();
+               }
+               return null;
+       }
+       
+       
+       
+
+       //////////  Static dialog  /////////
+       
+       /**
+        * A singleton configuration dialog.  Will create and show a new dialog if one has not 
+        * previously been used, or update the dialog and show it if a previous one exists.
+        * 
+        * @param document              the document to configure.
+        * @param component             the component to configure.
+        */
+       public static void showDialog(Window parent, OpenRocketDocument document, 
+                       RocketComponent component) {
+               if (dialog != null)
+                       dialog.dispose();
+               
+               dialog = new ComponentConfigDialog(parent, document, component);
+               dialog.setVisible(true);
+               
+               document.addUndoPosition("Modify "+component.getComponentName());
+       }
+       
+       
+       /* package */ 
+       static void showDialog(RocketComponent component) {
+               showDialog(dialog.parent, dialog.document, component);
+       }
+       
+       /**
+        * Hides the configuration dialog.  May be used even if not currently visible.
+        */
+       public static void hideDialog() {
+               if (dialog != null)
+                       dialog.setVisible(false);
+       }
+
+       
+       /**
+        * Add an undo position for the current document.  This is intended for use only
+        * by the currently open dialog.
+        * 
+        * @param description  Description of the undoable action
+        */
+       /*package*/ static void addUndoPosition(String description) {
+               if (dialog == null) {
+                       throw new IllegalStateException("Dialog not open, report bug!");
+               }
+               dialog.document.addUndoPosition(description);
+       }
+       
+       /*package*/
+       static String getUndoDescription() {
+               if (dialog == null) {
+                       throw new IllegalStateException("Dialog not open, report bug!");
+               }
+               return dialog.document.getUndoDescription();
+       }
+       
+       /**
+        * Returns whether the singleton configuration dialog is currently visible or not.
+        */
+       public static boolean isDialogVisible() {
+               return (dialog!=null) && (dialog.isVisible());
+       }
+
+
+       public void componentChanged(ComponentChangeEvent e) {
+               if (e.isTreeChange() || e.isUndoChange()) {
+                       
+                       // Hide dialog in case of tree or undo change
+                       dialog.setVisible(false);
+
+               } else {
+                       configurator.updateFields();
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java
new file mode 100644 (file)
index 0000000..8092ae4
--- /dev/null
@@ -0,0 +1,195 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class EllipticalFinSetConfig extends FinSetConfig {
+
+       public EllipticalFinSetConfig(final RocketComponent component) {
+               super(component);
+
+               DoubleModel m;
+               JSpinner spin;
+               JComboBox combo;
+               
+               JPanel mainPanel = new JPanel(new MigLayout());
+               
+               
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               ////  Number of fins
+               panel.add(new JLabel("Number of fins:"));
+               
+               IntegerModel im = new IntegerModel(component,"FinCount",1,8);
+               
+               spin = new JSpinner(im.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx, wrap");
+               
+               
+               ////  Base rotation
+               panel.add(new JLabel("Rotation:"));
+               
+               m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE,-Math.PI,Math.PI);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI,Math.PI)),"w 100lp, wrap");
+               
+               
+               ////  Root chord
+               panel.add(new JLabel("Root chord:"));
+               
+               m  = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+               ////  Height
+               panel.add(new JLabel("Height:"));
+               
+               m = new DoubleModel(component,"Height",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+       
+               
+               ////  Position
+               
+               panel.add(new JLabel("Position relative to:"));
+
+               combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+
+               
+               //// Right portion
+               mainPanel.add(panel,"aligny 20%");
+               
+               mainPanel.add(new JSeparator(SwingConstants.VERTICAL),"growy");
+               
+               
+               
+               panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+
+               ////  Cross section
+               panel.add(new JLabel("Fin cross section:"),"span, split");
+               combo = new JComboBox(
+                               new EnumModel<FinSet.CrossSection>(component,"CrossSection"));
+               panel.add(combo,"growx, wrap unrel");
+               
+
+               ////  Thickness
+               panel.add(new JLabel("Thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 30lp");
+               
+               
+
+               //// Material
+               materialPanel(panel, Material.Type.BULK);
+               
+               
+               
+               //// Convert button
+               
+               JButton button = new JButton("Convert to freeform fin set");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               // Do change in future for overall safety
+                               SwingUtilities.invokeLater(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               FreeformFinSet freeform = new FreeformFinSet((FinSet)component);
+                                               RocketComponent parent = component.getParent();
+                                               int index = parent.getChildPosition(component);
+                                               
+                                               ComponentConfigDialog.addUndoPosition("Convert fin set");
+                                               parent.removeChild(index);
+                                               parent.addChild(freeform, index);
+                                               ComponentConfigDialog.showDialog(freeform);
+                                       }
+                               });
+
+                               ComponentConfigDialog.hideDialog();
+                       }
+               });
+               panel.add(button,"span, growx, gaptop paragraph");
+               
+               
+               
+               mainPanel.add(panel,"aligny 20%");
+               
+               addFinSetButtons();
+
+
+               tabbedPane.insertTab("General", null, mainPanel, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java
new file mode 100644 (file)
index 0000000..fb8e76c
--- /dev/null
@@ -0,0 +1,108 @@
+package net.sf.openrocket.gui.configdialog;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.SwingUtilities;
+
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+public abstract class FinSetConfig extends RocketComponentConfig {
+
+       private JButton split = null;
+       
+       public FinSetConfig(RocketComponent component) {
+               super(component);
+       }
+
+       
+       
+       protected void addFinSetButtons() {
+               JButton convert=null;
+               
+               //// Convert buttons
+               if (!(component instanceof FreeformFinSet)) {
+                       convert = new JButton("Convert to freeform");
+                       convert.setToolTipText("Convert this fin set into a freeform fin set");
+                       convert.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       // Do change in future for overall safety
+                                       SwingUtilities.invokeLater(new Runnable() {
+                                               @Override
+                                               public void run() {
+                                                       FreeformFinSet freeform = new FreeformFinSet((FinSet)component);
+                                                       String name = component.getComponentName();
+                                                       
+                                                       if (freeform.getName().startsWith(name)) {
+                                                               freeform.setName(freeform.getComponentName() + 
+                                                                               freeform.getName().substring(name.length()));
+                                                       }
+                                                       
+                                                       RocketComponent parent = component.getParent();
+                                                       int index = parent.getChildPosition(component);
+
+                                                       ComponentConfigDialog.addUndoPosition("Convert fin set");
+                                                       parent.removeChild(index);
+                                                       parent.addChild(freeform, index);
+                                                       ComponentConfigDialog.showDialog(freeform);
+                                               }
+                                       });
+
+                                       ComponentConfigDialog.hideDialog();
+                               }
+                       });
+               }
+
+               split = new JButton("Split fins");
+               split.setToolTipText("Split the fin set into separate fins");
+               split.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               // Do change in future for overall safety
+                               SwingUtilities.invokeLater(new Runnable() {
+                                       @Override
+                                       public void run() {
+                                               RocketComponent parent = component.getParent();
+                                               int index = parent.getChildPosition(component);
+                                               int count = ((FinSet)component).getFinCount();
+                                               double base = ((FinSet)component).getBaseRotation();
+                                               if (count <= 1)
+                                                       return;
+                                               
+                                               ComponentConfigDialog.addUndoPosition("Split fin set");
+                                               parent.removeChild(index);
+                                               for (int i=0; i<count; i++) {
+                                                       FinSet copy = (FinSet)component.copy();
+                                                       copy.setFinCount(1);
+                                                       copy.setBaseRotation(base + i*2*Math.PI/count);
+                                                       copy.setName(copy.getName() + " #" + (i+1));
+                                                       parent.addChild(copy, index+i);
+                                               }
+                                       }
+                               });
+
+                               ComponentConfigDialog.hideDialog();
+                       }
+               });
+               split.setEnabled(((FinSet)component).getFinCount() > 1);
+               
+               if (convert==null)
+                       addButtons(split);
+               else
+                       addButtons(split,convert);
+
+       }
+
+
+       @Override
+       public void updateFields() {
+               super.updateFields();
+               if (split != null)
+                       split.setEnabled(((FinSet)component).getFinCount() > 1);
+       }
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java
new file mode 100644 (file)
index 0000000..de59d7a
--- /dev/null
@@ -0,0 +1,470 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Point;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingConstants;
+import javax.swing.table.AbstractTableModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.gui.scalefigure.FinPointFigure;
+import net.sf.openrocket.gui.scalefigure.ScaleScrollPane;
+import net.sf.openrocket.gui.scalefigure.ScaleSelector;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+
+public class FreeformFinSetConfig extends FinSetConfig {
+
+       private final FreeformFinSet finset;
+       private JTable table = null;
+       private FinPointTableModel tableModel = null;
+       
+       private FinPointFigure figure = null;
+       
+       
+       public FreeformFinSetConfig(RocketComponent component) {
+               super(component);
+               this.finset = (FreeformFinSet)component;
+
+
+               tabbedPane.insertTab("General", null, generalPane(), "General properties", 0);
+               tabbedPane.insertTab("Shape", null, shapePane(), "Fin shape", 1);
+               tabbedPane.setSelectedIndex(0);
+               
+               addFinSetButtons();
+       }
+       
+       
+       
+       private JPanel generalPane() {
+
+               DoubleModel m;
+               JSpinner spin;
+               JComboBox combo;
+               
+               JPanel mainPanel = new JPanel(new MigLayout("fill"));
+               
+               JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel","[][65lp::][30lp::]",""));
+               
+               
+               
+               ////  Number of fins
+               panel.add(new JLabel("Number of fins:"));
+               
+               IntegerModel im = new IntegerModel(component,"FinCount",1,8);
+               
+               spin = new JSpinner(im.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx, wrap");
+               
+               
+               ////  Base rotation
+               panel.add(new JLabel("Fin rotation:"));
+               
+               m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE,-Math.PI,Math.PI);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI,Math.PI)),"w 100lp, wrap");
+               
+               
+
+               ////  Fin cant
+               JLabel label = new JLabel("Fin cant:");
+               label.setToolTipText("The angle that the fins are canted with respect to the rocket " +
+                               "body.");
+               panel.add(label);
+               
+               m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE,
+                               -FinSet.MAX_CANT,FinSet.MAX_CANT);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT,FinSet.MAX_CANT)),
+                               "w 100lp, wrap 40lp");
+               
+               
+               
+               ////  Position
+               panel.add(new JLabel("Position relative to:"));
+
+               combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx 3, growx, wrap");
+                               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+               
+               
+               
+
+               mainPanel.add(panel, "aligny 20%");
+               mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy, height 150lp");
+               
+               
+               panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               
+               
+               
+               ////  Cross section
+               panel.add(new JLabel("Fin cross section:"),"span, split");
+               combo = new JComboBox(
+                               new EnumModel<FinSet.CrossSection>(component,"CrossSection"));
+               panel.add(combo,"growx, wrap unrel");
+               
+
+               ////  Thickness
+               panel.add(new JLabel("Thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 30lp");
+               
+
+               //// Material
+               materialPanel(panel, Material.Type.BULK);
+               
+               
+               
+               mainPanel.add(panel, "aligny 20%");
+               
+               return mainPanel;
+       }
+       
+       
+       
+       private JPanel shapePane() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               
+               // Create the figure
+               figure = new FinPointFigure(finset);
+               ScaleScrollPane figurePane = new FinPointScrollPane();
+               figurePane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+               figurePane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
+
+               // Create the table
+               tableModel = new FinPointTableModel();
+               table = new JTable(tableModel);
+               table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               for (int i=0; i < Columns.values().length; i++) {
+                       table.getColumnModel().getColumn(i).
+                               setPreferredWidth(Columns.values()[i].getWidth());
+               }
+               JScrollPane tablePane = new JScrollPane(table);
+               
+               
+//             panel.add(new JLabel("Coordinates:"), "aligny bottom, alignx 50%");
+//             panel.add(new JLabel("    View:"), "wrap, aligny bottom");
+               
+               
+               panel.add(tablePane,"growy, width :100lp:, height 100lp:250lp:");
+               panel.add(figurePane,"gap unrel, spanx, growx, growy 1000, height 100lp:250lp:, wrap");
+               
+               panel.add(new ResizeLabel("Double-click", -2), "alignx 50%");
+               
+               panel.add(new ScaleSelector(figurePane),"spany 2");
+               panel.add(new ResizeLabel("Click+drag: Add and move points   " +
+                               "Ctrl+click: Remove point", -2), "spany 2, right, wrap");
+               
+               
+               panel.add(new ResizeLabel("to edit", -2), "alignx 50%");
+               
+               return panel;
+       }
+       
+       
+       
+       
+       
+       @Override
+       public void updateFields() {
+               super.updateFields();
+               
+               if (tableModel != null) {
+                       tableModel.fireTableDataChanged();
+               }
+               if (figure != null) {
+                       figure.updateFigure();
+               }
+       }
+       
+       
+       
+       
+       private class FinPointScrollPane extends ScaleScrollPane {
+               private static final int ANY_MASK = 
+                       (MouseEvent.ALT_DOWN_MASK | MouseEvent.ALT_GRAPH_DOWN_MASK | 
+                                       MouseEvent.META_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK | 
+                                       MouseEvent.SHIFT_DOWN_MASK);
+               
+               private int dragIndex = -1;
+               
+               public FinPointScrollPane() {
+                       super(figure, false);   // Disallow fitting as it's buggy
+               }
+
+               @Override
+               public void mousePressed(MouseEvent event) {
+                       int mods = event.getModifiersEx();
+                       
+                       if (event.getButton() != MouseEvent.BUTTON1 ||
+                                       (mods & ANY_MASK) != 0) {
+                               super.mousePressed(event);
+                               return;
+                       }
+                       
+                       int index = getPoint(event);
+                       if (index >= 0) {
+                               dragIndex = index;
+                               return;
+                       }
+                       index = getSegment(event);
+                       if (index >= 0) {
+                               Point2D.Double point = getCoordinates(event);
+                               finset.addPoint(index);
+                               try {
+                                       finset.setPoint(index, point.x, point.y);
+                               } catch (IllegalArgumentException ignore) { }
+                               dragIndex = index;
+                               
+                               return;
+                       }
+                       
+                       super.mousePressed(event);
+                       return;
+               }
+
+               
+               @Override
+               public void mouseDragged(MouseEvent event) {
+                       int mods = event.getModifiersEx();
+                       if (dragIndex < 0 ||
+                                       (mods & (ANY_MASK | MouseEvent.BUTTON1_DOWN_MASK)) != 
+                                               MouseEvent.BUTTON1_DOWN_MASK) {
+                               super.mouseDragged(event);
+                               return;
+                       }
+                       Point2D.Double point = getCoordinates(event);
+                       
+                       try {
+                               finset.setPoint(dragIndex, point.x, point.y);
+                       } catch (IllegalArgumentException ignore) {
+                               System.out.println("IAE:"+ignore);
+                       }
+               }
+               
+               
+               @Override
+               public void mouseReleased(MouseEvent event) {
+                       dragIndex = -1;
+                       super.mouseReleased(event);
+               }
+               
+               @Override
+               public void mouseClicked(MouseEvent event) {
+                       int mods = event.getModifiersEx();
+                       if (event.getButton() != MouseEvent.BUTTON1 ||
+                                       (mods & ANY_MASK) != MouseEvent.CTRL_DOWN_MASK) {
+                               super.mouseClicked(event);
+                               return;
+                       }
+                       
+                       int index = getPoint(event);
+                       if (index < 0) {
+                               super.mouseClicked(event);
+                               return;
+                       }
+
+                       try {
+                               finset.removePoint(index);
+                       } catch (IllegalArgumentException ignore) {
+                       }
+               }
+               
+               
+               private int getPoint(MouseEvent event) {
+                       Point p0 = event.getPoint();
+                       Point p1 = this.getViewport().getViewPosition();
+                       int x = p0.x + p1.x;
+                       int y = p0.y + p1.y;
+                       
+                       return figure.getIndexByPoint(x, y);
+               }
+               
+               private int getSegment(MouseEvent event) {
+                       Point p0 = event.getPoint();
+                       Point p1 = this.getViewport().getViewPosition();
+                       int x = p0.x + p1.x;
+                       int y = p0.y + p1.y;
+                       
+                       return figure.getSegmentByPoint(x, y);
+               }
+               
+               private Point2D.Double getCoordinates(MouseEvent event) {
+                       Point p0 = event.getPoint();
+                       Point p1 = this.getViewport().getViewPosition();
+                       int x = p0.x + p1.x;
+                       int y = p0.y + p1.y;
+                       
+                       return figure.convertPoint(x, y);
+               }
+               
+               
+       }
+       
+       
+
+       
+       
+       private enum Columns {
+//             NUMBER {
+//                     @Override
+//                     public String toString() {
+//                             return "#";
+//                     }
+//                     @Override
+//                     public String getValue(FreeformFinSet finset, int row) {
+//                             return "" + (row+1) + ".";
+//                     }
+//                     @Override
+//                     public int getWidth() {
+//                             return 10;
+//                     }
+//             }, 
+               X {
+                       @Override
+                       public String toString() {
+                               return "X / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString();
+                       }
+                       @Override
+                       public String getValue(FreeformFinSet finset, int row) {
+                               return UnitGroup.UNITS_LENGTH.getDefaultUnit()
+                                       .toString(finset.getFinPoints()[row].x);
+                       }
+               }, 
+               Y {
+                       @Override
+                       public String toString() {
+                               return "Y / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString();
+                       }
+                       @Override
+                       public String getValue(FreeformFinSet finset, int row) {
+                               return UnitGroup.UNITS_LENGTH.getDefaultUnit()
+                                       .toString(finset.getFinPoints()[row].y);
+                       }
+               };
+               
+               public abstract String getValue(FreeformFinSet finset, int row);
+               @Override
+               public abstract String toString();
+               public int getWidth() {
+                       return 20;
+               }
+       }
+       
+       private class FinPointTableModel extends AbstractTableModel {
+               
+               @Override
+               public int getColumnCount() {
+                       return Columns.values().length;
+               }
+
+               @Override
+               public int getRowCount() {
+                       return finset.getPointCount();
+               }
+
+               @Override
+               public Object getValueAt(int rowIndex, int columnIndex) {
+                       return Columns.values()[columnIndex].getValue(finset, rowIndex);
+               }
+               
+               @Override
+               public String getColumnName(int columnIndex) {
+                       return Columns.values()[columnIndex].toString();
+               }
+               
+               @Override
+               public boolean isCellEditable(int rowIndex, int columnIndex) {
+                       if (rowIndex == 0 || rowIndex == getRowCount()-1) {
+                               return (columnIndex == Columns.X.ordinal());
+                       }
+                       
+                       return (columnIndex == Columns.X.ordinal() || columnIndex == Columns.Y.ordinal());
+               }
+               
+               @Override
+               public void setValueAt(Object o, int rowIndex, int columnIndex) {
+                       if (!(o instanceof String))
+                               return;
+                       
+                       String str = (String)o;
+                       try {
+                               
+                               double value = UnitGroup.UNITS_LENGTH.fromString(str);
+                               Coordinate c = finset.getFinPoints()[rowIndex];
+                               if (columnIndex == Columns.X.ordinal())
+                                       c = c.setX(value);
+                               else
+                                       c = c.setY(value);
+                               
+                               finset.setPoint(rowIndex, c.x, c.y);
+                       
+                       } catch (NumberFormatException ignore) {
+                       }
+               }
+               
+               
+       }
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java b/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java
new file mode 100644 (file)
index 0000000..b8d9534
--- /dev/null
@@ -0,0 +1,237 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.geom.Ellipse2D;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.border.BevelBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.Resettable;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.rocketcomponent.ClusterConfiguration;
+import net.sf.openrocket.rocketcomponent.Clusterable;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class InnerTubeConfig extends ThicknessRingComponentConfig {
+
+
+       public InnerTubeConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel tab;
+               
+               tab = positionTab();
+               tabbedPane.insertTab("Radial position", null, tab, "Radial position", 1);
+               
+               tab = clusterTab();
+               tabbedPane.insertTab("Cluster", null, tab, "Cluster configuration", 2);
+               
+               tab = new MotorConfig((MotorMount)c);
+               tabbedPane.insertTab("Motor", null, tab, "Motor mount configuration", 3);
+
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       
+       private JPanel clusterTab() {
+               JPanel panel = new JPanel(new MigLayout());
+               
+               JPanel subPanel = new JPanel(new MigLayout());
+               
+               // Cluster type selection
+               subPanel.add(new JLabel("Select cluster configuration:"),"spanx, wrap");
+               subPanel.add(new ClusterSelectionPanel((InnerTube)component),"spanx, wrap");
+//             JPanel clusterSelection = new ClusterSelectionPanel((InnerTube)component);
+//             clusterSelection.setBackground(Color.blue);
+//             subPanel.add(clusterSelection);
+               
+               panel.add(subPanel);
+               
+               
+               subPanel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]"));
+
+               // Tube separation scale
+               JLabel l = new JLabel("Tube separation:");
+               l.setToolTipText("The separation of the tubes, 1.0 = touching each other");
+               subPanel.add(l);
+               DoubleModel dm  = new DoubleModel(component,"ClusterScale",1,UnitGroup.UNITS_NONE,0);
+
+               JSpinner spin = new JSpinner(dm.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText("The separation of the tubes, 1.0 = touching each other");
+               subPanel.add(spin,"growx");
+               
+               BasicSlider bs = new BasicSlider(dm.getSliderModel(0, 1, 4));
+               bs.setToolTipText("The separation of the tubes, 1.0 = touching each other");
+               subPanel.add(bs,"skip,w 100lp, wrap");
+
+               // Rotation
+               l = new JLabel("Rotation:");
+               l.setToolTipText("Rotation angle of the cluster configuration");
+               subPanel.add(l);
+               dm  = new DoubleModel(component,"ClusterRotation",1,UnitGroup.UNITS_ANGLE,0);
+
+               spin = new JSpinner(dm.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText("Rotation angle of the cluster configuration");
+               subPanel.add(spin,"growx");
+               
+               subPanel.add(new UnitSelector(dm),"growx");
+               bs = new BasicSlider(dm.getSliderModel(-Math.PI, 0, Math.PI));
+               bs.setToolTipText("Rotation angle of the cluster configuration");
+               subPanel.add(bs,"w 100lp, wrap");
+
+               // Reset button
+               JButton reset = new JButton("Reset");
+               reset.setToolTipText("Reset the separation and rotation to the default values");
+               reset.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent arg0) {
+                               ((InnerTube)component).setClusterScale(1.0);
+                               ((InnerTube)component).setClusterRotation(0.0);
+                       }
+               });
+               subPanel.add(reset,"spanx,right");
+               
+               panel.add(subPanel,"grow");
+               
+               
+               return panel;
+       }
+}
+
+
+class ClusterSelectionPanel extends JPanel {
+       private static final int BUTTON_SIZE = 50;
+       private static final int MOTOR_DIAMETER = 10;
+       
+       private static final Color SELECTED_COLOR = Color.RED;
+       private static final Color UNSELECTED_COLOR = Color.WHITE;
+       private static final Color MOTOR_FILL_COLOR = Color.GREEN;
+       private static final Color MOTOR_BORDER_COLOR = Color.BLACK;
+       
+       public ClusterSelectionPanel(Clusterable component) {
+               super(new MigLayout("gap 0 0",
+                               "["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]",
+                               "["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]"));
+               
+               for (int i=0; i<ClusterConfiguration.CONFIGURATIONS.length; i++) {
+                       ClusterConfiguration config = ClusterConfiguration.CONFIGURATIONS[i];
+                       
+                       JComponent button = new ClusterButton(component,config);
+                       if (i%4 == 3) 
+                               add(button,"wrap");
+                       else
+                               add(button);
+               }
+
+       }
+       
+       
+       private class ClusterButton extends JPanel implements ChangeListener, MouseListener,
+                                                                                                                 Resettable {
+               private Clusterable component;
+               private ClusterConfiguration config;
+               
+               public ClusterButton(Clusterable c, ClusterConfiguration config) {
+                       component = c;
+                       this.config = config;
+                       setMinimumSize(new Dimension(BUTTON_SIZE,BUTTON_SIZE));
+                       setPreferredSize(new Dimension(BUTTON_SIZE,BUTTON_SIZE));
+                       setMaximumSize(new Dimension(BUTTON_SIZE,BUTTON_SIZE));
+                       setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
+//                     setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+                       component.addChangeListener(this);
+                       addMouseListener(this);
+               }
+               
+
+               @Override
+               public void paintComponent(Graphics g) {
+                       super.paintComponent(g);
+                       Graphics2D g2 = (Graphics2D)g;
+                       Rectangle area = g2.getClipBounds();
+                       
+                       if (component.getClusterConfiguration() == config)
+                               g2.setColor(SELECTED_COLOR);
+                       else
+                               g2.setColor(UNSELECTED_COLOR);
+                       
+                       g2.fillRect(area.x, area.y, area.width, area.height);
+                       
+                       g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
+                                       RenderingHints.VALUE_STROKE_NORMALIZE);
+                       g2.setRenderingHint(RenderingHints.KEY_RENDERING, 
+                                       RenderingHints.VALUE_RENDER_QUALITY);
+                       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
+                                       RenderingHints.VALUE_ANTIALIAS_ON);
+                       
+                       List<Double> points = config.getPoints();
+                       Ellipse2D.Float circle = new Ellipse2D.Float();
+                       for (int i=0; i < points.size()/2; i++) {
+                               double x = points.get(i*2);
+                               double y = points.get(i*2+1);
+                               
+                               double px = BUTTON_SIZE/2 + x*MOTOR_DIAMETER;
+                               double py = BUTTON_SIZE/2 - y*MOTOR_DIAMETER;
+                               circle.setFrameFromCenter(px,py,px+MOTOR_DIAMETER/2,py+MOTOR_DIAMETER/2);
+                               
+                               g2.setColor(MOTOR_FILL_COLOR);
+                               g2.fill(circle);
+                               g2.setColor(MOTOR_BORDER_COLOR);
+                               g2.draw(circle);
+                       }
+               }
+
+
+               public void stateChanged(ChangeEvent e) {
+                       repaint();
+               }
+
+
+               public void mouseClicked(MouseEvent e) {
+                       if (e.getButton() == MouseEvent.BUTTON1) {
+                               component.setClusterConfiguration(config);
+                       }
+               }
+               
+               public void mouseEntered(MouseEvent e) { }
+               public void mouseExited(MouseEvent e) { }
+               public void mousePressed(MouseEvent e) { }
+               public void mouseReleased(MouseEvent e) { }
+
+
+               public void resetModel() {
+                       component.removeChangeListener(this);
+                       removeMouseListener(this);
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java b/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java
new file mode 100644 (file)
index 0000000..9ef7203
--- /dev/null
@@ -0,0 +1,153 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class LaunchLugConfig extends RocketComponentConfig {
+
+       private MotorConfig motorConfigPane = null;
+
+       public LaunchLugConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel primary = new JPanel(new MigLayout("fill"));
+               
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+               
+               ////  Body tube length
+               panel.add(new JLabel("Length:"));
+               
+               DoubleModel m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.1)),"w 100lp, wrap para");
+               
+               
+               //// Body tube diameter
+               panel.add(new JLabel("Outer diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap rel");
+
+               
+               ////  Inner diameter
+               panel.add(new JLabel("Inner diameter:"));
+
+               // Diameter = 2*Radius
+               m = new DoubleModel(component,"InnerRadius",2,UnitGroup.UNITS_LENGTH,0);
+               
+
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)),"w 100lp, wrap rel");
+
+               
+               ////  Wall thickness
+               panel.add(new JLabel("Thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 20lp");
+               
+
+               ////  Radial direction
+               panel.add(new JLabel("Radial position:"));
+               
+               m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,
+                               -Math.PI, Math.PI);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+               
+               
+               
+               
+               primary.add(panel, "grow, gapright 20lp");
+               panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+               
+               
+               
+
+               panel.add(new JLabel("Position relative to:"));
+
+               JComboBox combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap para");
+
+               
+               
+               //// Material
+               materialPanel(panel, Material.Type.BULK);
+               
+               
+               primary.add(panel,"grow");
+               
+
+               tabbedPane.insertTab("General", null, primary, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       @Override
+       public void updateFields() {
+               super.updateFields();
+               if (motorConfigPane != null)
+                       motorConfigPane.updateFields();
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java
new file mode 100644 (file)
index 0000000..93a3d47
--- /dev/null
@@ -0,0 +1,152 @@
+package net.sf.openrocket.gui.configdialog;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class MassComponentConfig extends RocketComponentConfig {
+
+       public MassComponentConfig(RocketComponent component) {
+               super(component);
+               
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               
+               
+               ////  Mass
+               panel.add(new JLabel("Mass"));
+               
+               DoubleModel m = new DoubleModel(component,"ComponentMass",UnitGroup.UNITS_MASS,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.5)),"w 100lp, wrap");
+               
+               
+               
+               ////  Mass length
+               panel.add(new JLabel("Length"));
+               
+               m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+               
+               
+               //// Tube diameter
+               panel.add(new JLabel("Diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap");
+
+               
+               ////  Position
+               
+               panel.add(new JLabel("Position relative to:"));
+
+               JComboBox combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+
+               tabbedPane.insertTab("General", null, panel, "General properties", 0);
+               tabbedPane.insertTab("Radial position", null, positionTab(), 
+                               "Radial position configuration", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       
+       protected JPanel positionTab() {
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               ////  Radial position
+               panel.add(new JLabel("Radial distance:"));
+               
+               DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+               
+               
+               //// Radial direction
+               panel.add(new JLabel("Radial direction:"));
+               
+               m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+               
+               //// Reset button
+               JButton button = new JButton("Reset");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               ((MassComponent) component).setRadialDirection(0.0);
+                               ((MassComponent) component).setRadialPosition(0.0);
+                       }
+               });
+               panel.add(button,"spanx, right");
+               
+               return panel;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/MotorConfig.java b/src/net/sf/openrocket/gui/configdialog/MotorConfig.java
new file mode 100644 (file)
index 0000000..47b9419
--- /dev/null
@@ -0,0 +1,203 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.main.MotorChooserDialog;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class MotorConfig extends JPanel {
+
+       private final Rocket rocket;
+       private final MotorMount mount;
+       private final Configuration configuration;
+       private JPanel panel;
+       private JLabel motorLabel;
+       
+       public MotorConfig(MotorMount motorMount) {
+               super(new MigLayout("fill"));
+
+               this.rocket = ((RocketComponent)motorMount).getRocket();
+               this.mount = motorMount;
+               this.configuration = ((RocketComponent)motorMount).getRocket()
+                       .getDefaultConfiguration();
+               
+               BooleanModel model;
+               
+               model = new BooleanModel(motorMount, "MotorMount");
+               JCheckBox check = new JCheckBox(model);
+               check.setText("This component is a motor mount");
+               this.add(check,"wrap");
+               
+               
+               panel = new JPanel(new MigLayout("fill"));
+               this.add(panel,"grow, wrap");
+               
+
+               // Motor configuration selector
+               panel.add(new JLabel("Motor configuration:"), "shrink");
+               
+               JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration));
+               panel.add(combo,"growx");
+               
+               configuration.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               updateFields();
+                       }
+               });
+               
+               JButton button = new JButton("New");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               String id = rocket.newMotorConfigurationID();
+                               configuration.setMotorConfigurationID(id);
+                       }
+               });
+               panel.add(button, "wrap unrel");
+               
+               
+               // Current motor
+               panel.add(new JLabel("Current motor:"), "shrink");
+               
+               motorLabel = new JLabel();
+               motorLabel.setFont(motorLabel.getFont().deriveFont(Font.BOLD));
+               updateFields();
+               panel.add(motorLabel,"wrap unrel");
+
+               
+               
+               //  Overhang
+               panel.add(new JLabel("Motor overhang:"));
+               
+               DoubleModel m = new DoubleModel(motorMount, "MotorOverhang", UnitGroup.UNITS_LENGTH);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"span, split, width :65lp:");
+               
+               panel.add(new UnitSelector(m),"width :30lp:");
+               panel.add(new BasicSlider(m.getSliderModel(-0.02,0.06)),"w 100lp, wrap unrel");
+
+
+               
+               // Select ignition event
+               panel.add(new JLabel("Ignition at:"),"");
+               
+               combo = new JComboBox(new EnumModel<IgnitionEvent>(mount, "IgnitionEvent"));
+               panel.add(combo,"growx, wrap");
+               
+               // ... and delay
+               panel.add(new JLabel("plus"),"gap indent, skip 1, span, split");
+               
+               m = new DoubleModel(mount,"IgnitionDelay",0);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"gap rel rel");
+               
+               panel.add(new JLabel("seconds"),"wrap paragraph");
+
+
+               
+               
+               // Select etc. buttons
+               button = new JButton("Select motor");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               String id = configuration.getMotorConfigurationID();
+                               
+                               MotorChooserDialog dialog = new MotorChooserDialog(mount.getMotor(id),
+                                               mount.getMotorDelay(id), mount.getMotorMountDiameter());
+                               dialog.setVisible(true);
+                               Motor m = dialog.getSelectedMotor();
+                               double d = dialog.getSelectedDelay();
+                               
+                               if (m != null) {
+                                       if (id == null) {
+                                               id = rocket.newMotorConfigurationID();
+                                               configuration.setMotorConfigurationID(id);
+                                       }
+                                       mount.setMotor(id, m);
+                                       mount.setMotorDelay(id, d);
+                               }
+                               updateFields();
+                       }
+               });
+               panel.add(button,"span, split, grow");
+               
+               button = new JButton("Remove motor");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               mount.setMotor(configuration.getMotorConfigurationID(), null);
+                               updateFields();
+                       }
+               });
+               panel.add(button,"grow, wrap");
+               
+               
+               
+               
+               
+               // Set enabled status
+               
+               setDeepEnabled(panel, motorMount.isMotorMount());
+               check.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               setDeepEnabled(panel, mount.isMotorMount());
+                       }
+               });
+               
+       }
+       
+       public void updateFields() {
+               String id = configuration.getMotorConfigurationID();
+               Motor m = mount.getMotor(id);
+               if (m == null)
+                       motorLabel.setText("None");
+               else
+                       motorLabel.setText(m.getManufacturer() + " " +
+                                       m.getDesignation(mount.getMotorDelay(id)));
+       }
+       
+       
+       private static void setDeepEnabled(Component component, boolean enabled) {
+               component.setEnabled(enabled);
+               if (component instanceof Container) {
+                       for (Component c: ((Container) component).getComponents()) {
+                               setDeepEnabled(c,enabled);
+                       }
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java b/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java
new file mode 100644 (file)
index 0000000..1c75f8c
--- /dev/null
@@ -0,0 +1,172 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.DescriptionArea;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class NoseConeConfig extends RocketComponentConfig {
+
+       private JComboBox typeBox;
+       
+       private DescriptionArea description;
+       
+       private JLabel shapeLabel;
+       private JSpinner shapeSpinner;
+       private JSlider shapeSlider;
+       
+       // Prepended to the description from NoseCone.DESCRIPTIONS
+       private static final String PREDESC = "<html><p style=\"font-size: x-small\">";
+       
+       public NoseConeConfig(RocketComponent c) {
+               super(c);
+               
+               DoubleModel m;
+               JPanel panel = new JPanel(new MigLayout("","[][65lp::][30lp::]"));
+
+               
+               
+
+               ////  Shape selection
+               
+               panel.add(new JLabel("Nose cone shape:"));
+
+               Transition.Shape selected = ((NoseCone)component).getType();
+               Transition.Shape[] typeList = Transition.Shape.values();
+               
+               typeBox = new JComboBox(typeList);
+               typeBox.setEditable(false);
+               typeBox.setSelectedItem(selected);
+               typeBox.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               Transition.Shape s = (Transition.Shape)typeBox.getSelectedItem();
+                               ((NoseCone)component).setType(s);
+                               description.setText(PREDESC + s.getNoseConeDescription());
+                               updateEnabled();
+                       }
+               });
+               panel.add(typeBox,"span, wrap rel");
+
+               
+               
+
+               ////  Shape parameter
+               shapeLabel = new JLabel("Shape parameter:");
+               panel.add(shapeLabel);
+               
+               m = new DoubleModel(component,"ShapeParameter");
+               
+               shapeSpinner = new JSpinner(m.getSpinnerModel());
+               shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner));
+               panel.add(shapeSpinner,"growx");
+               
+               DoubleModel min = new DoubleModel(component,"ShapeParameterMin");
+               DoubleModel max = new DoubleModel(component,"ShapeParameterMax");
+               shapeSlider = new BasicSlider(m.getSliderModel(min,max)); 
+               panel.add(shapeSlider,"skip, w 100lp, wrap para");
+               
+               updateEnabled();
+
+               
+               ////  Length
+               
+               panel.add(new JLabel("Nose cone length:"));
+
+               m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.7)),"w 100lp, wrap");
+               
+               ////  Diameter
+               
+               panel.add(new JLabel("Base diameter:"));
+
+               m = new DoubleModel(component,"AftRadius",2.0,UnitGroup.UNITS_LENGTH,0);  // Diameter = 2*Radius
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+               
+               JCheckBox check = new JCheckBox(m.getAutomaticAction());
+               check.setText("Automatic");
+               panel.add(check,"skip, span 2, wrap");
+               
+
+               ////  Wall thickness
+               panel.add(new JLabel("Wall thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 0px");
+               
+
+               check = new JCheckBox(new BooleanModel(component,"Filled"));
+               check.setText("Filled");
+               panel.add(check,"skip, span 2, wrap");
+
+               
+               panel.add(new JLabel(""), "growy");
+               
+               
+               
+               ////  Description
+               
+               JPanel panel2 = new JPanel(new MigLayout("ins 0"));
+               
+               description = new DescriptionArea(5);
+               description.setText(PREDESC + ((NoseCone)component).getType().getNoseConeDescription());
+               panel2.add(description, "wmin 250lp, spanx, growx, wrap para");
+               
+
+               //// Material
+               
+               
+               materialPanel(panel2, Material.Type.BULK);
+               panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany");
+               
+
+               
+               tabbedPane.insertTab("General", null, panel, "General properties", 0);
+               tabbedPane.insertTab("Shoulder", null, shoulderTab(), "Shoulder properties", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+
+       private void updateEnabled() {
+               boolean e = ((NoseCone)component).getType().usesParameter();
+               shapeLabel.setEnabled(e);
+               shapeSpinner.setEnabled(e);
+               shapeSlider.setEnabled(e);
+       }
+
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java b/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java
new file mode 100644 (file)
index 0000000..340dfb8
--- /dev/null
@@ -0,0 +1,274 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.gui.adaptors.MaterialModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class ParachuteConfig extends RecoveryDeviceConfig {
+
+       public ParachuteConfig(final RocketComponent component) {
+               super(component);
+
+               JPanel primary = new JPanel(new MigLayout());
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+               
+               
+               //// Canopy
+               panel.add(new JLabel("<html><b>Canopy:</b>"), "wrap unrel");
+               
+
+               panel.add(new JLabel("Diameter:"));
+               
+               DoubleModel m = new DoubleModel(component,"Diameter",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)),"w 100lp, wrap");
+
+               
+               panel.add(new JLabel("Material:"));
+               
+               JComboBox combo = new JComboBox(new MaterialModel(component, Material.Type.SURFACE));
+               combo.setToolTipText("The component material affects the weight of the component.");
+               panel.add(combo,"spanx 3, growx, wrap paragraph");
+
+//             materialPanel(panel, Material.Type.SURFACE, "Material:", null);
+               
+               
+               
+               // CD
+               JLabel label = new JLabel("<html>Drag coefficient C<sub>D</sub>:");
+               String tip = "<html>The drag coefficient relative to the total area of the parachute.<br>" +
+                               "A larger drag coefficient yields a slowed descent rate.  " +
+                               "A typical value for parachutes is 0.8.";
+               label.setToolTipText(tip);
+               panel.add(label);
+               
+               m = new DoubleModel(component,"CD",UnitGroup.UNITS_COEFFICIENT,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setToolTipText(tip);
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               JButton button = new JButton("Reset");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               Parachute p = (Parachute)component;
+                               p.setCD(Parachute.DEFAULT_CD);
+                       }
+               });
+               panel.add(button,"spanx, wrap 30lp");
+
+               
+               
+               ////  Shroud lines
+               panel.add(new JLabel("<html><b>Shroud lines:</b>"), "wrap unrel");
+
+
+               panel.add(new JLabel("Number of lines:"));
+               IntegerModel im = new IntegerModel(component,"LineCount",0);
+               
+               spin = new JSpinner(im.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx, wrap");
+               
+               
+               panel.add(new JLabel("Line length:"));
+
+               m = new DoubleModel(component,"LineLength",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)),"w 100lp, wrap");
+
+               
+               panel.add(new JLabel("Material:"));
+               
+               combo = new JComboBox(new MaterialModel(component, Material.Type.LINE, 
+                               "LineMaterial"));
+               panel.add(combo,"spanx 3, growx, wrap");
+
+               
+               
+               primary.add(panel, "grow, gapright 20lp");
+               panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+               
+               
+               
+               //// Position
+
+               panel.add(new JLabel("Position relative to:"));
+
+               combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+
+               ////  Spatial length
+               panel.add(new JLabel("Packed length:"));
+               
+               m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+               
+               
+               //// Tube diameter
+               panel.add(new JLabel("Packed diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 30lp");
+               
+               
+               //// Deployment
+
+               panel.add(new JLabel("Deploys at:"),"");
+               
+               combo = new JComboBox(new EnumModel<IgnitionEvent>(component, "DeployEvent"));
+               panel.add(combo,"spanx 3, growx, wrap");
+               
+               // ... and delay
+               panel.add(new JLabel("plus"),"right");
+               
+               m = new DoubleModel(component,"DeployDelay",0);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"spanx, split");
+               
+               panel.add(new JLabel("seconds"),"wrap paragraph");
+
+               // Altitude
+               label = new JLabel("Altitude:");
+               altitudeComponents.add(label);
+               panel.add(label);
+               
+               m = new DoubleModel(component,"DeployAltitude",UnitGroup.UNITS_DISTANCE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               altitudeComponents.add(spin);
+               panel.add(spin,"growx");
+               UnitSelector unit = new UnitSelector(m);
+               altitudeComponents.add(unit);
+               panel.add(unit,"growx");
+               BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000));
+               altitudeComponents.add(slider);
+               panel.add(slider,"w 100lp, wrap");
+
+               
+               primary.add(panel, "grow");
+               
+               updateFields();
+
+               tabbedPane.insertTab("General", null, primary, "General properties", 0);
+               tabbedPane.insertTab("Radial position", null, positionTab(), 
+                               "Radial position configuration", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       
+       
+
+
+       protected JPanel positionTab() {
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               ////  Radial position
+               panel.add(new JLabel("Radial distance:"));
+               
+               DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+               
+               
+               //// Radial direction
+               panel.add(new JLabel("Radial direction:"));
+               
+               m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+               
+               //// Reset button
+               JButton button = new JButton("Reset");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               ((MassComponent) component).setRadialDirection(0.0);
+                               ((MassComponent) component).setRadialPosition(0.0);
+                       }
+               });
+               panel.add(button,"spanx, right");
+               
+               return panel;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java b/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java
new file mode 100644 (file)
index 0000000..a6b2ef8
--- /dev/null
@@ -0,0 +1,36 @@
+package net.sf.openrocket.gui.configdialog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JComponent;
+
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+public abstract class RecoveryDeviceConfig extends RocketComponentConfig {
+
+       protected final List<JComponent> altitudeComponents = new ArrayList<JComponent>();
+       
+       public RecoveryDeviceConfig(RocketComponent component) {
+               super(component);
+       }
+
+       
+       
+       @Override
+       public void updateFields() {
+               super.updateFields();
+               
+               if (altitudeComponents == null)
+                       return;
+               
+               boolean enabled = (((RecoveryDevice)component).getDeployEvent() 
+                               == RecoveryDevice.DeployEvent.ALTITUDE); 
+               
+               for (JComponent c: altitudeComponents) {
+                       c.setEnabled(enabled);
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java
new file mode 100644 (file)
index 0000000..9acbbda
--- /dev/null
@@ -0,0 +1,208 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class RingComponentConfig extends RocketComponentConfig {
+
+       public RingComponentConfig(RocketComponent component) {
+               super(component);
+       }
+       
+       
+       protected JPanel generalTab(String outer, String inner, String thickness, String length) {
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               DoubleModel m;
+               JSpinner spin;
+               DoubleModel od=null;
+               
+               
+               //// Outer diameter
+               if (outer != null) {
+                       panel.add(new JLabel(outer));
+                       
+                       od  = new DoubleModel(component,"OuterRadius",2,UnitGroup.UNITS_LENGTH,0);
+                       // Diameter = 2*Radius
+                       
+                       spin = new JSpinner(od.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       panel.add(spin,"growx");
+                       
+                       panel.add(new UnitSelector(od),"growx");
+                       panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap");
+                       
+                       if (od.isAutomaticAvailable()) {
+                               JCheckBox check = new JCheckBox(od.getAutomaticAction());
+                               check.setText("Automatic");
+                               panel.add(check,"skip, span 2, wrap");
+                       }
+               }
+
+               
+               ////  Inner diameter
+               if (inner != null) {
+                       panel.add(new JLabel(inner));
+                       
+                       m = new DoubleModel(component,"InnerRadius",2,UnitGroup.UNITS_LENGTH,0);
+                       
+                       spin = new JSpinner(m.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       panel.add(spin,"growx");
+                       
+                       panel.add(new UnitSelector(m),"growx");
+                       if (od == null)
+                               panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap");
+                       else
+                               panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)),
+                                               "w 100lp, wrap");
+                       
+                       if (m.isAutomaticAvailable()) {
+                               JCheckBox check = new JCheckBox(m.getAutomaticAction());
+                               check.setText("Automatic");
+                               panel.add(check,"skip, span 2, wrap");
+                       }
+               }
+               
+               
+               ////  Wall thickness
+               if (thickness != null) {
+                       panel.add(new JLabel(thickness));
+                       
+                       m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+                       
+                       spin = new JSpinner(m.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       panel.add(spin,"growx");
+                       
+                       panel.add(new UnitSelector(m),"growx");
+                       panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap");
+               }
+
+               
+               ////  Inner tube length
+               if (length != null) {
+                       panel.add(new JLabel(length));
+                       
+                       m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+                       
+                       spin = new JSpinner(m.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       panel.add(spin,"growx");
+                       
+                       panel.add(new UnitSelector(m),"growx");
+                       panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+               }
+               
+               
+               ////  Position
+               
+               panel.add(new JLabel("Position relative to:"));
+
+               JComboBox combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx 3, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+               
+               //// Material
+               panel.add(materialPanel(new JPanel(new MigLayout()), Material.Type.BULK),
+                               "cell 4 0, gapleft paragraph, aligny 0%, spany");
+               
+               return panel;
+       }
+       
+       
+       protected JPanel positionTab() {
+               JPanel panel = new JPanel(new MigLayout("align 20% 20%, gap rel unrel",
+                               "[][65lp::][30lp::]",""));
+               
+               ////  Radial position
+               JLabel l = new JLabel("Radial distance:");
+               l.setToolTipText("Distance from the rocket centerline");
+               panel.add(l);
+               
+               DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText("Distance from the rocket centerline");
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               BasicSlider bs = new BasicSlider(m.getSliderModel(0, 0.1, 1.0));
+               bs.setToolTipText("Distance from the rocket centerline");
+               panel.add(bs,"w 100lp, wrap");
+               
+               
+               //// Radial direction
+               l = new JLabel("Radial direction:");
+               l.setToolTipText("The radial direction from the rocket centerline");
+               panel.add(l);
+               
+               m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText("The radial direction from the rocket centerline");
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               bs = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI));
+               bs.setToolTipText("The radial direction from the rocket centerline");
+               panel.add(bs,"w 100lp, wrap");
+
+               
+               //// Reset button
+               JButton button = new JButton("Reset");
+               button.setToolTipText("Reset the component to the rocket centerline");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               ((RingComponent) component).setRadialDirection(0.0);
+                               ((RingComponent) component).setRadialPosition(0.0);
+                       }
+               });
+               panel.add(button,"spanx, right");
+               
+               
+               return panel;
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java
new file mode 100644 (file)
index 0000000..9b19bcb
--- /dev/null
@@ -0,0 +1,595 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.Iterator;
+
+import javax.swing.BorderFactory;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.MaterialModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.ComponentAssembly;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.Prefs;
+
+public class RocketComponentConfig extends JPanel {
+
+       protected final RocketComponent component;
+       protected final JTabbedPane tabbedPane;
+       
+       
+       protected final JTextField componentNameField;
+       protected JTextArea commentTextArea;
+       private final TextFieldListener textFieldListener;
+       private JButton colorButton;
+       private JCheckBox colorDefault;
+       private JPanel buttonPanel;
+
+       private JLabel massLabel;
+       
+       
+       public RocketComponentConfig(RocketComponent component) {
+               setLayout(new MigLayout("fill","[grow, fill]"));
+               this.component = component;
+               
+               JLabel label = new JLabel("Component name:");
+               label.setToolTipText("The component name.");
+               this.add(label,"split, gapright 10");
+               
+               componentNameField = new JTextField(15);
+               textFieldListener = new TextFieldListener();
+               componentNameField.addActionListener(textFieldListener);
+               componentNameField.addFocusListener(textFieldListener);
+               componentNameField.setToolTipText("The component name.");
+               this.add(componentNameField,"growx, growy 0, wrap");
+               
+               
+               tabbedPane = new JTabbedPane();
+               this.add(tabbedPane,"growx, growy 1, wrap");
+               
+               tabbedPane.addTab("Override", null, overrideTab(), "Mass and CG override options");
+               if (component.isMassive())
+                       tabbedPane.addTab("Figure", null, figureTab(), "Figure style options");
+               tabbedPane.addTab("Comment", null, commentTab(), "Specify a comment for the component");
+               
+               addButtons();
+               
+               updateFields();
+       }
+       
+       
+       protected void addButtons(JButton... buttons) {
+               if (buttonPanel != null) {
+                       this.remove(buttonPanel);
+               }
+               
+               buttonPanel = new JPanel(new MigLayout("fill, ins 0"));
+               
+               massLabel = new ResizeLabel("Mass: ", -1);
+               buttonPanel.add(massLabel, "growx");
+               
+               for (JButton b: buttons) {
+                       buttonPanel.add(b, "right, gap para");
+               }
+               
+               JButton closeButton = new JButton("Close");
+               closeButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent arg0) {
+                               ComponentConfigDialog.hideDialog();
+                       }
+               });
+               buttonPanel.add(closeButton, "right, gap 30lp");
+               
+               updateFields();
+               
+               this.add(buttonPanel, "spanx, growx");
+       }
+       
+       
+       /**
+        * Called when a change occurs, so that the fields can be updated if necessary.
+        * When overriding this method, the supermethod must always be called.
+        */
+       public void updateFields() {
+               // Component name
+               componentNameField.setText(component.getName());
+               
+               // Component color and "Use default color" checkbox
+               if (colorButton != null && colorDefault != null) {
+                       colorButton.setIcon(new ColorIcon(component.getColor()));
+               
+                       if ((component.getColor()==null) != colorDefault.isSelected())
+                               colorDefault.setSelected(component.getColor()==null);
+               }
+               
+               // Mass label
+               if (component.isMassive()) {
+                       String text = "Component mass: ";
+                       text += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(
+                                       component.getComponentMass());
+                       
+                       String overridetext = null;
+                       if (component.isMassOverridden()) {
+                               overridetext = "(overridden to " + UnitGroup.UNITS_MASS.getDefaultUnit().
+                                       toStringUnit(component.getOverrideMass()) + ")";
+                       }
+                       
+                       for (RocketComponent c = component.getParent(); c != null; c = c.getParent()) {
+                               if (c.isMassOverridden() && c.getOverrideSubcomponents()) {
+                                       overridetext = "(overridden by " + c.getName() + ")";
+                               }
+                       }
+                       
+                       if (overridetext != null)
+                               text = text + " " + overridetext;
+                       
+                       massLabel.setText(text);
+               } else {
+                       massLabel.setText("");
+               }
+       }
+       
+       
+       protected JPanel materialPanel(JPanel panel, Material.Type type) {
+               return materialPanel(panel, type, "Component material:", "Component finish:");
+       }
+       
+       protected JPanel materialPanel(JPanel panel, Material.Type type,
+                       String materialString, String finishString) {
+               JLabel label = new JLabel(materialString);
+               label.setToolTipText("The component material affects the weight of the component.");
+               panel.add(label,"spanx 4, wrap rel");
+               
+               JComboBox combo = new JComboBox(new MaterialModel(component,type));
+               combo.setToolTipText("The component material affects the weight of the component.");
+               panel.add(combo,"spanx 4, growx, wrap paragraph");
+               
+               
+               if (component instanceof ExternalComponent) {
+                       label = new JLabel(finishString);
+                       String tip = "<html>The component finish affects the aerodynamic drag of the " 
+                               +"component.<br>" 
+                               + "The value indicated is the average roughness height of the surface.";
+                       label.setToolTipText(tip);
+                       panel.add(label,"spanx 4, wmin 220lp, wrap rel");
+                       
+                       combo = new JComboBox(new EnumModel<ExternalComponent.Finish>(component,"Finish"));
+                       combo.setToolTipText(tip);
+                       panel.add(combo,"spanx 4, growx, split");
+                       
+                       JButton button = new JButton("Set for all");
+                       button.setToolTipText("Set this finish for all components of the rocket.");
+                       button.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       Finish f = ((ExternalComponent)component).getFinish();
+                                       Rocket rocket = component.getRocket();
+                                       try {
+                                               rocket.freeze();
+                                               // Store previous undo description
+                                               String desc = ComponentConfigDialog.getUndoDescription();
+                                               ComponentConfigDialog.addUndoPosition("Set rocket finish");
+                                               // Do changes
+                                               Iterator<RocketComponent> iter = rocket.deepIterator();
+                                               while (iter.hasNext()) {
+                                                       RocketComponent c = iter.next();
+                                                       if (c instanceof ExternalComponent) {
+                                                               ((ExternalComponent)c).setFinish(f);
+                                                       }
+                                               }
+                                               // Restore undo description
+                                               ComponentConfigDialog.addUndoPosition(desc);
+                                       } finally {
+                                               rocket.thaw();
+                                       }
+                               }
+                       });
+                       panel.add(button, "wrap paragraph");
+               }
+               
+               return panel;
+       }
+       
+       
+       private JPanel overrideTab() {
+               JPanel panel = new JPanel(new MigLayout("align 50% 20%, fillx, gap rel unrel",
+                               "[][65lp::][30lp::][]",""));
+               
+               panel.add(new JLabel("Override the mass or center of gravity of the " +
+                               component.getComponentName() + ":"),"spanx, wrap 20lp");
+
+               JCheckBox check;
+               BooleanModel bm;
+               UnitSelector us;
+               BasicSlider bs;
+
+               ////  Mass
+               bm = new BooleanModel(component, "MassOverridden");
+               check = new JCheckBox(bm);
+               check.setText("Override mass:");
+               panel.add(check, "growx 1, gapright 20lp");
+               
+               DoubleModel m = new DoubleModel(component,"OverrideMass",UnitGroup.UNITS_MASS,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               bm.addEnableComponent(spin, true);
+               panel.add(spin,"growx 1");
+               
+               us = new UnitSelector(m);
+               bm.addEnableComponent(us, true);
+               panel.add(us,"growx 1");
+               
+               bs = new BasicSlider(m.getSliderModel(0, 0.03, 1.0));
+               bm.addEnableComponent(bs);
+               panel.add(bs,"growx 5, w 100lp, wrap");
+               
+               
+               ////  CG override
+               bm = new BooleanModel(component, "CGOverridden");
+               check = new JCheckBox(bm);
+               check.setText("Override center of gravity:");
+               panel.add(check, "growx 1, gapright 20lp");
+               
+               m = new DoubleModel(component,"OverrideCGX",UnitGroup.UNITS_LENGTH,0);
+               // Calculate suitable length for slider
+               DoubleModel length;
+               if (component instanceof ComponentAssembly) {
+                       double l=0;
+                       
+                       Iterator<RocketComponent> iterator = component.deepIterator();
+                       while (iterator.hasNext()) {
+                               RocketComponent c = iterator.next();
+                               if (c.getRelativePosition() == RocketComponent.Position.AFTER)
+                                       l += c.getLength();
+                       }
+                       length = new DoubleModel(l);
+               } else {
+                       length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH,0);
+               }
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               bm.addEnableComponent(spin, true);
+               panel.add(spin,"growx 1");
+               
+               us = new UnitSelector(m);
+               bm.addEnableComponent(us, true);
+               panel.add(us,"growx 1");
+               
+               bs = new BasicSlider(m.getSliderModel(new DoubleModel(0), length));
+               bm.addEnableComponent(bs);
+               panel.add(bs,"growx 5, w 100lp, wrap 35lp");
+               
+               
+               // Override subcomponents checkbox
+               bm = new BooleanModel(component, "OverrideSubcomponents");
+               check = new JCheckBox(bm);
+               check.setText("Override mass and CG of all subcomponents");
+               panel.add(check, "gap para, spanx, wrap para");
+               
+
+               panel.add(new ResizeLabel("<html>The overridden mass does not include motors.<br>" +
+                               "The center of gravity is measured from the front end of the " +
+                               component.getComponentName().toLowerCase()+".", -1),
+                               "spanx, wrap, gap para, height 0::30lp");
+               
+               return panel;
+       }
+       
+       
+       private JPanel commentTab() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new JLabel("Comments on the "+component.getComponentName()+":"), "wrap");
+               
+               // TODO: LOW:  Changes in comment from other sources not reflected in component
+               commentTextArea = new JTextArea(component.getComment());
+               commentTextArea.setLineWrap(true);
+               commentTextArea.setWrapStyleWord(true);
+               commentTextArea.setEditable(true);
+               GUIUtil.setTabToFocusing(commentTextArea);
+               commentTextArea.addFocusListener(textFieldListener);
+               
+               panel.add(new JScrollPane(commentTextArea), "growx, growy");
+               
+               return panel;
+       }
+       
+
+
+       private JPanel figureTab() {
+               JPanel panel = new JPanel(new MigLayout("align 20% 20%"));
+               
+               panel.add(new JLabel("Figure style:"), "wrap para");
+               
+               
+               panel.add(new JLabel("Component color:"), "gapleft para, gapright 10lp");
+               
+               colorButton = new JButton(new ColorIcon(component.getColor()));
+               colorButton.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               Color c = component.getColor();
+                               if (c == null) {
+                                       c = Prefs.getDefaultColor(component.getClass());
+                               }
+                               
+                               c = JColorChooser.showDialog(tabbedPane, "Choose color", c);
+                               if (c!=null) {
+                                       component.setColor(c);
+                               }
+                       }
+               });
+               panel.add(colorButton, "gapright 10lp");
+               
+               colorDefault = new JCheckBox("Use default color");
+               if (component.getColor()==null)
+                       colorDefault.setSelected(true);
+               colorDefault.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (colorDefault.isSelected())
+                                       component.setColor(null);
+                               else
+                                       component.setColor(Prefs.getDefaultColor(component.getClass()));
+                       }
+               });
+               panel.add(colorDefault, "wrap para");
+               
+               
+               panel.add(new JLabel("Component line style:"), "gapleft para, gapright 10lp");
+
+               LineStyle[] list = new LineStyle[LineStyle.values().length+1];
+               System.arraycopy(LineStyle.values(), 0, list, 1, LineStyle.values().length);
+
+               JComboBox combo = new JComboBox(new EnumModel<LineStyle>(component, "LineStyle",
+                               list, "Default style"));
+               panel.add(combo, "spanx 2, growx, wrap 50lp");
+               
+               
+               JButton button = new JButton("Save as default style");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (component.getColor() != null) {
+                                       Prefs.setDefaultColor(component.getClass(), component.getColor());
+                                       component.setColor(null);
+                               }
+                               if (component.getLineStyle() != null) {
+                                       Prefs.setDefaultLineStyle(component.getClass(), component.getLineStyle());
+                                       component.setLineStyle(null);
+                               }
+                       }
+               });
+               panel.add(button, "gapleft para, spanx 3, growx, wrap");
+               
+               return panel;
+       }
+
+       
+       
+
+       protected JPanel shoulderTab() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               JPanel sub;
+               DoubleModel m, m2;
+               DoubleModel m0 = new DoubleModel(0);
+               BooleanModel bm;
+               JCheckBox check;
+               JSpinner spin;
+               
+               
+               ////  Fore shoulder, not for NoseCone
+               
+               if (!(component instanceof NoseCone)) {
+                       sub = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+                       
+                       sub.setBorder(BorderFactory.createTitledBorder("Fore shoulder"));
+
+                       
+                       ////  Radius
+                       sub.add(new JLabel("Diameter:"));
+                       
+                       m = new DoubleModel(component,"ForeShoulderRadius",2,UnitGroup.UNITS_LENGTH,0);
+                       m2 = new DoubleModel(component,"ForeRadius",2,UnitGroup.UNITS_LENGTH);
+                       
+                       spin = new JSpinner(m.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       sub.add(spin,"growx");
+                       
+                       sub.add(new UnitSelector(m),"growx");
+                       sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+                       
+                       
+                       ////  Length
+                       sub.add(new JLabel("Length:"));
+                       
+                       m = new DoubleModel(component,"ForeShoulderLength",UnitGroup.UNITS_LENGTH,0);
+                       
+                       spin = new JSpinner(m.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       sub.add(spin,"growx");
+                       
+                       sub.add(new UnitSelector(m),"growx");
+                       sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)),"w 100lp, wrap");
+                       
+
+                       ////  Thickness
+                       sub.add(new JLabel("Thickness:"));
+                       
+                       m = new DoubleModel(component,"ForeShoulderThickness",UnitGroup.UNITS_LENGTH,0);
+                       m2 = new DoubleModel(component,"ForeShoulderRadius",UnitGroup.UNITS_LENGTH);
+
+                       spin = new JSpinner(m.getSpinnerModel());
+                       spin.setEditor(new SpinnerEditor(spin));
+                       sub.add(spin,"growx");
+                       
+                       sub.add(new UnitSelector(m),"growx");
+                       sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+                       
+                       
+                       ////  Capped
+                       bm = new BooleanModel(component, "ForeShoulderCapped");
+                       check = new JCheckBox(bm);
+                       check.setText("End capped");
+                       check.setToolTipText("Whether the end of the shoulder is capped.");
+                       sub.add(check, "spanx");
+
+                       
+                       panel.add(sub);
+               }
+               
+               
+               ////  Aft shoulder
+               sub = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               if (component instanceof NoseCone)
+                       sub.setBorder(BorderFactory.createTitledBorder("Nose cone shoulder"));
+               else
+                       sub.setBorder(BorderFactory.createTitledBorder("Aft shoulder"));
+
+               
+               ////  Radius
+               sub.add(new JLabel("Diameter:"));
+               
+               m = new DoubleModel(component,"AftShoulderRadius",2,UnitGroup.UNITS_LENGTH,0);
+               m2 = new DoubleModel(component,"AftRadius",2,UnitGroup.UNITS_LENGTH);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               sub.add(spin,"growx");
+               
+               sub.add(new UnitSelector(m),"growx");
+               sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+               
+               
+               ////  Length
+               sub.add(new JLabel("Length:"));
+               
+               m = new DoubleModel(component,"AftShoulderLength",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               sub.add(spin,"growx");
+               
+               sub.add(new UnitSelector(m),"growx");
+               sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)),"w 100lp, wrap");
+               
+
+               ////  Thickness
+               sub.add(new JLabel("Thickness:"));
+               
+               m = new DoubleModel(component,"AftShoulderThickness",UnitGroup.UNITS_LENGTH,0);
+               m2 = new DoubleModel(component,"AftShoulderRadius",UnitGroup.UNITS_LENGTH);
+
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               sub.add(spin,"growx");
+               
+               sub.add(new UnitSelector(m),"growx");
+               sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+               
+               
+               ////  Capped
+               bm = new BooleanModel(component, "AftShoulderCapped");
+               check = new JCheckBox(bm);
+               check.setText("End capped");
+               check.setToolTipText("Whether the end of the shoulder is capped.");
+               sub.add(check, "spanx");
+
+               
+               panel.add(sub);
+
+               
+               return panel;
+       }
+       
+       
+       
+       
+       /*
+        * Private inner class to handle events in componentNameField.
+        */
+       private class TextFieldListener implements ActionListener, FocusListener {
+               public void actionPerformed(ActionEvent e) {
+                       setName();
+               }
+               public void focusGained(FocusEvent e) { }
+               public void focusLost(FocusEvent e) {
+                       setName();
+               }
+               private void setName() {
+                       if (!component.getName().equals(componentNameField.getText())) {
+                               component.setName(componentNameField.getText());
+                       }
+                       if (!component.getComment().equals(commentTextArea.getText())) {
+                               component.setComment(commentTextArea.getText());
+                       }
+               }
+       }
+       
+       
+       private class ColorIcon implements Icon {
+               private final Color color;
+               
+               public ColorIcon(Color c) {
+                       this.color = c;
+               }
+               
+               @Override
+               public int getIconHeight() {
+                       return 15;
+               }
+
+               @Override
+               public int getIconWidth() {
+                       return 25;
+               }
+
+               @Override
+               public void paintIcon(Component c, Graphics g, int x, int y) {
+                       if (color==null) {
+                               g.setColor(Prefs.getDefaultColor(component.getClass()));
+                       } else {
+                               g.setColor(color);
+                       }
+                       g.fill3DRect(x, y, getIconWidth(), getIconHeight(), false);
+               }
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/RocketConfig.java b/src/net/sf/openrocket/gui/configdialog/RocketConfig.java
new file mode 100644 (file)
index 0000000..2e31316
--- /dev/null
@@ -0,0 +1,92 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+
+import javax.swing.JLabel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.rocketcomponent.Rocket;
+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;
+               
+               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();
+               designerTextArea = new JTextArea(rocket.getDesigner());
+               designerTextArea.setLineWrap(true);
+               designerTextArea.setWrapStyleWord(true);
+               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 JLabel("Comments:"), "top, pad 4lp, gapright 10lp");
+               this.add(new JScrollPane(commentTextArea), "wmin 300lp, hmin 105lp, grow 100, wrap para");
+               
+               
+               this.add(new JLabel("Revision history:"), "top, pad 4lp, gapright 10lp");
+               revisionTextArea = new JTextArea(rocket.getRevision());
+               revisionTextArea.setLineWrap(true);
+               revisionTextArea.setWrapStyleWord(true);
+               revisionTextArea.setEditable(true);
+               GUIUtil.setTabToFocusing(revisionTextArea);
+               revisionTextArea.addFocusListener(textFieldListener);
+               
+               this.add(new JScrollPane(revisionTextArea), "wmin 300lp, hmin 45lp, grow 30, wrap para");
+
+               
+               addButtons();
+       }
+       
+       
+
+       private class TextFieldListener implements ActionListener, FocusListener {
+               public void actionPerformed(ActionEvent e) {
+                       setName();
+               }
+               public void focusGained(FocusEvent e) { }
+               public void focusLost(FocusEvent e) {
+                       setName();
+               }
+               private void setName() {
+                       if (!rocket.getDesigner().equals(designerTextArea.getText())) {
+                               rocket.setDesigner(designerTextArea.getText());
+                       }
+                       if (!rocket.getRevision().equals(revisionTextArea.getText())) {
+                               rocket.setRevision(revisionTextArea.getText());
+                       }
+               }
+       }
+       
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java b/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java
new file mode 100644 (file)
index 0000000..9b82ea9
--- /dev/null
@@ -0,0 +1,122 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class ShockCordConfig extends RocketComponentConfig {
+
+
+       public ShockCordConfig(RocketComponent component) {
+               super(component);
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               JLabel label;
+               DoubleModel m;
+               JSpinner spin;
+               String tip;
+               
+               
+               //////  Left side
+               
+               // Cord length
+               label = new JLabel("Shock cord length");
+               panel.add(label);
+               
+               m = new DoubleModel(component,"CordLength",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 1, 10)),"w 100lp, wrap");
+               
+
+               // Material
+               materialPanel(panel, Material.Type.LINE, "Shock cord material:", null);
+               
+
+               
+               /////  Right side
+               JPanel panel2 = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany");
+               
+               
+               ////  Position
+               
+               panel2.add(new JLabel("Position relative to:"));
+
+               JComboBox combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel2.add(combo,"spanx, growx, wrap");
+               
+               panel2.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel2.add(spin,"growx");
+               
+               panel2.add(new UnitSelector(m),"growx");
+               panel2.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+
+               ////  Spatial length
+               panel2.add(new JLabel("Packed length:"));
+               
+               m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel2.add(spin,"growx");
+               
+               panel2.add(new UnitSelector(m),"growx");
+               panel2.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+               
+               
+               //// Tube diameter
+               panel2.add(new JLabel("Packed diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel2.add(spin,"growx");
+               
+               panel2.add(new UnitSelector(od),"growx");
+               panel2.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap");
+
+               
+               
+               
+               tabbedPane.insertTab("General", null, panel, "General properties", 0);
+//             tabbedPane.insertTab("Radial position", null, positionTab(), 
+//                             "Radial position configuration", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java b/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java
new file mode 100644 (file)
index 0000000..fbb2480
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class SleeveConfig extends RingComponentConfig {
+
+       public SleeveConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel tab;
+               
+               tab = generalTab("Outer diameter:", "Inner diameter:", "Wall thickness:", "Length:");
+               tabbedPane.insertTab("General", null, tab, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java b/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java
new file mode 100644 (file)
index 0000000..951969e
--- /dev/null
@@ -0,0 +1,270 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.MaterialModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class StreamerConfig extends RecoveryDeviceConfig {
+
+       public StreamerConfig(final RocketComponent component) {
+               super(component);
+
+               JPanel primary = new JPanel(new MigLayout());
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+               
+               
+               
+               panel.add(new JLabel("Strip length:"));
+               
+               DoubleModel m = new DoubleModel(component,"StripLength",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.6, 1.5)),"w 100lp, wrap");
+
+               
+               panel.add(new JLabel("Strip width:"));
+               
+               m = new DoubleModel(component,"StripWidth",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.2)),"w 100lp, wrap 20lp");
+
+               
+               
+
+               panel.add(new JLabel("Strip area:"));
+               
+               m = new DoubleModel(component,"Area",UnitGroup.UNITS_AREA,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.25)),"w 100lp, wrap");
+
+               
+               panel.add(new JLabel("Aspect ratio:"));
+               
+               m = new DoubleModel(component,"AspectRatio",UnitGroup.UNITS_NONE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+//             panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(2, 15)),"skip, w 100lp, wrap 20lp");
+
+               
+               
+               panel.add(new JLabel("Material:"));
+               
+               JComboBox combo = new JComboBox(new MaterialModel(component, Material.Type.SURFACE));
+               combo.setToolTipText("The component material affects the weight of the component.");
+               panel.add(combo,"spanx 3, growx, wrap 20lp");
+
+               
+               
+               // CD
+               JLabel label = new JLabel("<html>Drag coefficient C<sub>D</sub>:");
+               String tip = "<html>The drag coefficient relative to the total area of the streamer.<br>" +
+                               "A larger drag coefficient yields a slowed descent rate.";
+               label.setToolTipText(tip);
+               panel.add(label);
+               
+               m = new DoubleModel(component,"CD",UnitGroup.UNITS_COEFFICIENT,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setToolTipText(tip);
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               JCheckBox check = new JCheckBox(m.getAutomaticAction());
+               check.setText("Automatic");
+               panel.add(check,"skip, span, wrap");
+               
+               panel.add(new ResizeLabel("The drag coefficient is relative to the area of the streamer.",
+                               -2), "span, wrap");
+               
+               
+               
+               primary.add(panel, "grow, gapright 20lp");
+               panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+               
+               
+               
+               //// Position
+
+               panel.add(new JLabel("Position relative to:"));
+
+               combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap");
+
+
+               ////  Spatial length
+               panel.add(new JLabel("Packed length:"));
+               
+               m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+               
+               
+               //// Tube diameter
+               panel.add(new JLabel("Packed diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 30lp");
+               
+               
+               //// Deployment
+
+               panel.add(new JLabel("Deploys at:"),"");
+               
+               combo = new JComboBox(new EnumModel<IgnitionEvent>(component, "DeployEvent"));
+               panel.add(combo,"spanx 3, growx, wrap");
+               
+               // ... and delay
+               panel.add(new JLabel("plus"),"right");
+               
+               m = new DoubleModel(component,"DeployDelay",0);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"spanx, split");
+               
+               panel.add(new JLabel("seconds"),"wrap paragraph");
+
+               // Altitude
+               label = new JLabel("Altitude:");
+               altitudeComponents.add(label);
+               panel.add(label);
+               
+               m = new DoubleModel(component,"DeployAltitude",UnitGroup.UNITS_DISTANCE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               altitudeComponents.add(spin);
+               panel.add(spin,"growx");
+               UnitSelector unit = new UnitSelector(m);
+               altitudeComponents.add(unit);
+               panel.add(unit,"growx");
+               BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000));
+               altitudeComponents.add(slider);
+               panel.add(slider,"w 100lp, wrap");
+
+               
+               primary.add(panel, "grow");
+               
+               updateFields();
+
+               tabbedPane.insertTab("General", null, primary, "General properties", 0);
+               tabbedPane.insertTab("Radial position", null, positionTab(), 
+                               "Radial position configuration", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       
+       
+
+
+       protected JPanel positionTab() {
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               ////  Radial position
+               panel.add(new JLabel("Radial distance:"));
+               
+               DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+               
+               JSpinner spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+               
+               
+               //// Radial direction
+               panel.add(new JLabel("Radial direction:"));
+               
+               m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+               
+               //// Reset button
+               JButton button = new JButton("Reset");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               ((MassComponent) component).setRadialDirection(0.0);
+                               ((MassComponent) component).setRadialPosition(0.0);
+                       }
+               });
+               panel.add(button,"spanx, right");
+               
+               return panel;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java
new file mode 100644 (file)
index 0000000..2230979
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class ThicknessRingComponentConfig extends RingComponentConfig {
+
+       public ThicknessRingComponentConfig(RocketComponent c) {
+               super(c);
+               
+               JPanel tab;
+               
+               tab = generalTab("Outer diameter:", "Inner diameter:", "Wall thickness:", "Length:");
+               tabbedPane.insertTab("General", null, tab, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java b/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java
new file mode 100644 (file)
index 0000000..61a6ff2
--- /dev/null
@@ -0,0 +1,196 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.DescriptionArea;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class TransitionConfig extends RocketComponentConfig {
+
+       private JComboBox typeBox;
+       //private JLabel description;
+       
+       private JLabel shapeLabel;
+       private JSpinner shapeSpinner;
+       private BasicSlider shapeSlider;
+       private DescriptionArea description;
+       
+
+       // Prepended to the description from Transition.DESCRIPTIONS
+       private static final String PREDESC = "<html><p style=\"font-size: x-small\">";
+       
+       
+       public TransitionConfig(RocketComponent c) {
+               super(c);
+               
+               DoubleModel m;
+               JSpinner spin;
+               JCheckBox checkbox;
+
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+
+
+               ////  Shape selection
+               
+               panel.add(new JLabel("Transition shape:"));
+
+               Transition.Shape selected = ((Transition)component).getType();
+               Transition.Shape[] typeList = Transition.Shape.values();
+               
+               typeBox = new JComboBox(typeList);
+               typeBox.setEditable(false);
+               typeBox.setSelectedItem(selected);
+               typeBox.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               Transition.Shape s = (Transition.Shape)typeBox.getSelectedItem();
+                               ((Transition)component).setType(s);
+                               description.setText(PREDESC + s.getTransitionDescription());
+                               updateEnabled();
+                       }
+               });
+               panel.add(typeBox,"span, split 2");
+
+
+               checkbox = new JCheckBox(new BooleanModel(component,"Clipped"));
+               checkbox.setText("Clipped");
+               panel.add(checkbox,"wrap");
+               
+               
+               ////  Shape parameter
+               shapeLabel = new JLabel("Shape parameter:");
+               panel.add(shapeLabel);
+               
+               m = new DoubleModel(component,"ShapeParameter");
+               
+               shapeSpinner = new JSpinner(m.getSpinnerModel());
+               shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner));
+               panel.add(shapeSpinner,"growx");
+               
+               DoubleModel min = new DoubleModel(component,"ShapeParameterMin");
+               DoubleModel max = new DoubleModel(component,"ShapeParameterMax");
+               shapeSlider = new BasicSlider(m.getSliderModel(min,max)); 
+               panel.add(shapeSlider,"skip, w 100lp, wrap");
+               
+               updateEnabled();
+               
+               
+               ////  Length
+               panel.add(new JLabel("Transition length:"));
+               
+               m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.3)),"w 100lp, wrap");
+               
+               
+               //// Transition diameter 1
+               panel.add(new JLabel("Fore diameter:"));
+
+               DoubleModel od  = new DoubleModel(component,"ForeRadius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+               checkbox = new JCheckBox(od.getAutomaticAction());
+               checkbox.setText("Automatic");
+               panel.add(checkbox,"skip, span 2, wrap");
+               
+               
+               //// Transition diameter 2
+               panel.add(new JLabel("Aft diameter:"));
+
+               od  = new DoubleModel(component,"AftRadius",2,UnitGroup.UNITS_LENGTH,0);
+               // Diameter = 2*Radius
+
+               spin = new JSpinner(od.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(od),"growx");
+               panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+               checkbox = new JCheckBox(od.getAutomaticAction());
+               checkbox.setText("Automatic");
+               panel.add(checkbox,"skip, span 2, wrap");
+               
+               
+               ////  Wall thickness
+               panel.add(new JLabel("Wall thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 0px");
+               
+               
+               checkbox = new JCheckBox(new BooleanModel(component,"Filled"));
+               checkbox.setText("Filled");
+               panel.add(checkbox,"skip, span 2, wrap");
+
+               
+               
+               ////  Description
+               
+               JPanel panel2 = new JPanel(new MigLayout("ins 0"));
+               
+               description = new DescriptionArea(5);
+               description.setText(PREDESC + ((Transition)component).getType().
+                               getTransitionDescription());
+               panel2.add(description, "wmin 250lp, spanx, growx, wrap para");
+               
+
+               //// Material
+               
+               
+               materialPanel(panel2, Material.Type.BULK);
+               panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany");
+               
+
+               tabbedPane.insertTab("General", null, panel, "General properties", 0);
+               tabbedPane.insertTab("Shoulder", null, shoulderTab(), "Shoulder properties", 1);
+               tabbedPane.setSelectedIndex(0);
+       }
+       
+       
+       
+       
+       
+       private void updateEnabled() {
+               boolean e = ((Transition)component).getType().usesParameter();
+               shapeLabel.setEnabled(e);
+               shapeSpinner.setEnabled(e);
+               shapeSlider.setEnabled(e);
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java
new file mode 100644 (file)
index 0000000..4a640ec
--- /dev/null
@@ -0,0 +1,235 @@
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.SwingConstants;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class TrapezoidFinSetConfig extends FinSetConfig {
+
+       public TrapezoidFinSetConfig(final RocketComponent component) {
+               super(component);
+               
+               DoubleModel m;
+               JSpinner spin;
+               JComboBox combo;
+               
+               JPanel mainPanel = new JPanel(new MigLayout());
+               
+               
+               JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+               
+               ////  Number of fins
+               JLabel label = new JLabel("Number of fins:");
+               label.setToolTipText("The number of fins in the fin set.");
+               panel.add(label);
+               
+               IntegerModel im = new IntegerModel(component,"FinCount",1,8);
+               
+               spin = new JSpinner(im.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText("The number of fins in the fin set.");
+               panel.add(spin,"growx, wrap");
+               
+               
+               ////  Base rotation
+               label = new JLabel("Fin rotation:");
+               label.setToolTipText("The angle of the first fin in the fin set.");
+               panel.add(label);
+               
+               m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE,-Math.PI,Math.PI);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI,Math.PI)),"w 100lp, wrap");
+               
+               
+               ////  Fin cant
+               label = new JLabel("Fin cant:");
+               label.setToolTipText("The angle that the fins are canted with respect to the rocket " +
+                               "body.");
+               panel.add(label);
+               
+               m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE,
+                               -FinSet.MAX_CANT, FinSet.MAX_CANT);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT,FinSet.MAX_CANT)),
+                               "w 100lp, wrap");
+               
+               
+               ////  Root chord
+               panel.add(new JLabel("Root chord:"));
+               
+               m  = new DoubleModel(component,"RootChord",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+               
+               ////  Tip chord
+               panel.add(new JLabel("Tip chord:"));
+               
+               m = new DoubleModel(component,"TipChord",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+               
+               ////  Height
+               panel.add(new JLabel("Height:"));
+               
+               m = new DoubleModel(component,"Height",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+       
+               
+               
+               ////  Sweep
+               panel.add(new JLabel("Sweep length:"));
+               
+               m = new DoubleModel(component,"Sweep",UnitGroup.UNITS_LENGTH);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+
+               // sweep slider from -1.1*TipChord to 1.1*RootChord
+               DoubleModel tc = new DoubleModel(component,"TipChord",-1.1,UnitGroup.UNITS_LENGTH);
+               DoubleModel rc = new DoubleModel(component,"RootChord",1.1,UnitGroup.UNITS_LENGTH);
+               panel.add(new BasicSlider(m.getSliderModel(tc,rc)),"w 100lp, wrap");
+
+               
+               ////  Sweep angle
+               panel.add(new JLabel("Sweep angle:"));
+               
+               m = new DoubleModel(component, "SweepAngle",UnitGroup.UNITS_ANGLE,
+                               -TrapezoidFinSet.MAX_SWEEP_ANGLE,TrapezoidFinSet.MAX_SWEEP_ANGLE);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(-Math.PI/4,Math.PI/4)),
+                               "w 100lp, wrap paragraph");
+
+               
+
+               
+
+               mainPanel.add(panel,"aligny 20%");
+               
+               mainPanel.add(new JSeparator(SwingConstants.VERTICAL),"growy");
+               
+               
+               
+               panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+               
+               
+               ////  Cross section
+               panel.add(new JLabel("Fin cross section:"));
+               combo = new JComboBox(
+                               new EnumModel<FinSet.CrossSection>(component,"CrossSection"));
+               panel.add(combo,"span, growx, wrap");
+               
+
+               ////  Thickness
+               panel.add(new JLabel("Thickness:"));
+               
+               m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap para");
+               
+               
+               ////  Position
+               
+               panel.add(new JLabel("Position relative to:"));
+
+               combo = new JComboBox(
+                               new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+                                               new RocketComponent.Position[] {
+                                               RocketComponent.Position.TOP,
+                                               RocketComponent.Position.MIDDLE,
+                                               RocketComponent.Position.BOTTOM,
+                                               RocketComponent.Position.ABSOLUTE
+                               }));
+               panel.add(combo,"spanx, growx, wrap");
+               
+               panel.add(new JLabel("plus"),"right");
+
+               m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               panel.add(spin,"growx");
+               
+               panel.add(new UnitSelector(m),"growx");
+               panel.add(new BasicSlider(m.getSliderModel(
+                               new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+                               new DoubleModel(component.getParent(), "Length"))),
+                               "w 100lp, wrap para");
+
+
+
+               //// Material
+               materialPanel(panel, Material.Type.BULK);
+               
+               
+               
+               
+               mainPanel.add(panel,"aligny 20%");
+               
+
+               tabbedPane.insertTab("General", null, mainPanel, "General properties", 0);
+               tabbedPane.setSelectedIndex(0);
+               
+               addFinSetButtons();
+               
+       }
+}
diff --git a/src/net/sf/openrocket/gui/figureelements/CGCaret.java b/src/net/sf/openrocket/gui/figureelements/CGCaret.java
new file mode 100644 (file)
index 0000000..14a8a67
--- /dev/null
@@ -0,0 +1,62 @@
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Color;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * A mark indicating the position of the center of gravity.  It is a blue circle with every
+ * second quarter filled with blue.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class CGCaret extends Caret {
+       private static final float RADIUS = 7;
+       
+       private static Area caret = null;
+       
+       /**
+        * Create a new CGCaret at the specified coordinates.
+        */
+       public CGCaret(double x, double y) {
+               super(x,y);
+       }
+
+       /**
+        * Returns the Area corresponding to the caret.  The Area object is created only once,
+        * after which the object is cloned for new copies.
+        */
+       @Override
+       protected Area getCaret() {
+               if (caret != null) {
+                       return (Area)caret.clone();
+               }
+
+               Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS);
+               caret = new Area(e);
+               
+               Area a;
+               a = new Area(new Rectangle2D.Float(-RADIUS,-RADIUS,RADIUS,RADIUS));
+               caret.subtract(a);
+               a = new Area(new Rectangle2D.Float(0,0,RADIUS,RADIUS));
+               caret.subtract(a);
+               
+               a = new Area(new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS));
+               a.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f,
+                               2*0.9f*RADIUS,2*0.9f*RADIUS)));
+               caret.add(a);
+               
+               return (Area) caret.clone();
+       }
+
+       /**
+        * Return the color of the caret (blue).
+        */
+       @Override
+       protected Color getColor() {
+               return Color.BLUE;
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/figureelements/CPCaret.java b/src/net/sf/openrocket/gui/figureelements/CPCaret.java
new file mode 100644 (file)
index 0000000..09e9cce
--- /dev/null
@@ -0,0 +1,56 @@
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Color;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+
+/**
+ * A mark indicating the position of the center of pressure.  It is a red filled circle
+ * inside a slightly larger red circle.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class CPCaret extends Caret {
+       private static final float RADIUS = 7;
+       
+       private static Area caret = null;
+       
+       /**
+        * Create a new CPCaret at the specified coordinates.
+        */
+       public CPCaret(double x, double y) {
+               super(x,y);
+       }
+
+       /**
+        * Returns the Area object of the caret.  The Area object is created only once,
+        * after which new copies are cloned from it.
+        */
+       @Override
+       protected Area getCaret() {
+               if (caret != null) {
+                       return (Area)caret.clone();
+               }
+
+               Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS);
+               caret = new Area(e);
+
+               caret.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f,
+                               2*0.9f*RADIUS,2*0.9f*RADIUS)));
+               
+               caret.add(new Area(new Ellipse2D.Float(-RADIUS*0.75f,-RADIUS*0.75f,
+                               2*0.75f*RADIUS,2*0.75f*RADIUS)));
+               
+               return (Area) caret.clone();
+       }
+
+       
+       /**
+        * Return the color of the caret (red).
+        */
+       @Override
+       protected Color getColor() {
+               return Color.RED;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/figureelements/Caret.java b/src/net/sf/openrocket/gui/figureelements/Caret.java
new file mode 100644 (file)
index 0000000..b82731f
--- /dev/null
@@ -0,0 +1,55 @@
+package net.sf.openrocket.gui.figureelements;
+
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Area;
+
+public abstract class Caret implements FigureElement {
+       private double x,y;
+       
+       /**
+        * Creates a new caret at the specified coordinates.
+        */
+       public Caret(double x, double y) {
+               this.x = x;
+               this.y = y;
+       }
+
+       /**
+        * Sets the position of the caret to the new coordinates.
+        */
+       public void setPosition(double x, double y) {
+               this.x = x;
+               this.y = y;
+       }
+
+       /**
+        * Paints the caret to the Graphics2D element.
+        */
+       public void paint(Graphics2D g2, double scale) {
+               Area caret = getCaret();
+               AffineTransform t = new AffineTransform(1.0/scale, 0, 0, 1.0/scale, x, y);
+               caret.transform(t);
+
+               g2.setColor(getColor());
+               g2.fill(caret);
+       }
+
+       
+       public void paint(Graphics2D g2, double scale, Rectangle visible) {
+               throw new UnsupportedOperationException("paint() with rectangle unsupported.");
+       }
+
+       /**
+        * Return the Area object corresponding to the mark.
+        */
+       protected abstract Area getCaret();
+       
+       /**
+        * Return the color to be used when drawing the mark.
+        */
+       protected abstract Color getColor();
+}
diff --git a/src/net/sf/openrocket/gui/figureelements/FigureElement.java b/src/net/sf/openrocket/gui/figureelements/FigureElement.java
new file mode 100644 (file)
index 0000000..953d191
--- /dev/null
@@ -0,0 +1,12 @@
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+
+public interface FigureElement {
+
+       public void paint(Graphics2D g2, double scale);
+       
+       public void paint(Graphics2D g2, double scale, Rectangle visible);
+       
+}
diff --git a/src/net/sf/openrocket/gui/figureelements/RocketInfo.java b/src/net/sf/openrocket/gui/figureelements/RocketInfo.java
new file mode 100644 (file)
index 0000000..a70e873
--- /dev/null
@@ -0,0 +1,343 @@
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.font.GlyphVector;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+
+
+/**
+ * A <code>FigureElement</code> that draws text at different positions in the figure
+ * with general data about the rocket.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketInfo implements FigureElement {
+       
+       // Margin around the figure edges, pixels
+       private static final int MARGIN = 8;
+
+       // Font to use
+       private static final Font FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 11);
+       private static final Font SMALLFONT = new Font(Font.SANS_SERIF, Font.PLAIN, 9);
+
+       
+       private final Caret cpCaret = new CPCaret(0,0);
+       private final Caret cgCaret = new CGCaret(0,0);
+       
+       private final Configuration configuration;
+       private final UnitGroup stabilityUnits;
+       
+       private double cg = 0, cp = 0;
+       private double length = 0, diameter = 0;
+       private double mass = 0;
+       private double aoa = Double.NaN, theta = Double.NaN, mach = Prefs.getDefaultMach();
+       
+       private WarningSet warnings = null;
+       
+       private boolean calculatingData = false;
+       private FlightData flightData = null;
+       
+       private Graphics2D g2 = null;
+       private float line = 0;
+       private float x1, x2, y1, y2;
+       
+       
+       
+       
+       
+       public RocketInfo(Configuration configuration) {
+               this.configuration = configuration;
+               this.stabilityUnits = UnitGroup.stabilityUnits(configuration);
+       }
+       
+       
+       @Override
+       public void paint(Graphics2D g2, double scale) {
+               throw new UnsupportedOperationException("paint() must be called with coordinates");
+       }
+
+       @Override
+       public void paint(Graphics2D g2, double scale, Rectangle visible) {
+               this.g2 = g2;
+               this.line = FONT.getLineMetrics("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+                               g2.getFontRenderContext()).getHeight();
+               
+               x1 = visible.x + MARGIN;
+               x2 = visible.x + visible.width - MARGIN;
+               y1 = visible.y + line ;
+               y2 = visible.y + visible.height - MARGIN;
+
+               drawMainInfo();
+               drawStabilityInfo();
+               drawWarnings();
+               drawFlightInformation();
+       }
+       
+       
+       public void setCG(double cg) {
+               this.cg = cg;
+       }
+       
+       public void setCP(double cp) {
+               this.cp = cp;
+       }
+       
+       public void setLength(double length) {
+               this.length = length;
+       }
+       
+       public void setDiameter(double diameter) {
+               this.diameter = diameter;
+       }
+       
+       public void setMass(double mass) {
+               this.mass = mass;
+       }
+       
+       public void setWarnings(WarningSet warnings) {
+               this.warnings = warnings.clone();
+       }
+       
+       public void setAOA(double aoa) {
+               this.aoa = aoa;
+       }
+       
+       public void setTheta(double theta) {
+               this.theta = theta;
+       }
+       
+       public void setMach(double mach) {
+               this.mach = mach;
+       }
+       
+       
+       public void setFlightData(FlightData data) {
+               this.flightData = data;
+       }
+       
+       public void setCalculatingData(boolean calc) {
+               this.calculatingData = calc;
+       }
+       
+       
+       
+       
+       private void drawMainInfo() {
+               GlyphVector name = createText(configuration.getRocket().getName());
+               GlyphVector lengthLine = createText(
+                               "Length " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(length) +
+                               ", max. diameter " + 
+                               UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(diameter));
+               
+               String massText;
+               if (configuration.hasMotors())
+                       massText = "Mass with motors ";
+               else
+                       massText = "Mass with no motors ";
+               
+               massText += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(mass);
+               
+               GlyphVector massLine = createText(massText);
+
+               
+               g2.setColor(Color.BLACK);
+
+               g2.drawGlyphVector(name, x1, y1);
+               g2.drawGlyphVector(lengthLine, x1, y1+line);
+               g2.drawGlyphVector(massLine, x1, y1+2*line);
+
+       }
+       
+       
+       private void drawStabilityInfo() {
+               String at;
+               
+               at = "at M="+UnitGroup.UNITS_COEFFICIENT.getDefaultUnit().toStringUnit(mach);
+               if (!Double.isNaN(aoa)) {
+                       at += " \u03b1=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(aoa);
+               }
+               if (!Double.isNaN(theta)) {
+                       at += " \u0398=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(theta);
+               }
+               
+               GlyphVector cgValue = createText(
+                               UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(cg));
+               GlyphVector cpValue = createText(
+                               UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(cp));
+               GlyphVector stabValue = createText(
+                               stabilityUnits.getDefaultUnit().toStringUnit(cp-cg));
+                               
+               GlyphVector cgText = createText("CG:  ");
+               GlyphVector cpText = createText("CP:  ");
+               GlyphVector stabText = createText("Stability:  ");
+               GlyphVector atText = createSmallText(at);
+
+               Rectangle2D cgRect = cgValue.getVisualBounds();
+               Rectangle2D cpRect = cpValue.getVisualBounds();
+               Rectangle2D cgTextRect = cgText.getVisualBounds();
+               Rectangle2D cpTextRect = cpText.getVisualBounds();
+               Rectangle2D stabRect = stabValue.getVisualBounds();
+               Rectangle2D stabTextRect = stabText.getVisualBounds();
+               Rectangle2D atTextRect = atText.getVisualBounds();
+               
+               double unitWidth = MathUtil.max(cpRect.getWidth(), cgRect.getWidth(),
+                               stabRect.getWidth());
+               double textWidth = Math.max(cpTextRect.getWidth(), cgTextRect.getWidth());
+               
+
+               g2.setColor(Color.BLACK);
+
+               g2.drawGlyphVector(stabValue, (float)(x2-stabRect.getWidth()), y1);
+               g2.drawGlyphVector(cgValue, (float)(x2-cgRect.getWidth()), y1+line);
+               g2.drawGlyphVector(cpValue, (float)(x2-cpRect.getWidth()), y1+2*line);
+
+               g2.drawGlyphVector(stabText, (float)(x2-unitWidth-stabTextRect.getWidth()), y1);
+               g2.drawGlyphVector(cgText, (float)(x2-unitWidth-cgTextRect.getWidth()), y1+line);
+               g2.drawGlyphVector(cpText, (float)(x2-unitWidth-cpTextRect.getWidth()), y1+2*line);
+                               
+               cgCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+line-0.3*line);
+               cgCaret.paint(g2, 1.7);
+
+               cpCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+2*line-0.3*line);
+               cpCaret.paint(g2, 1.7);
+               
+               float atPos;
+               if (unitWidth + textWidth + 10 > atTextRect.getWidth()) {
+                       atPos = (float)(x2-(unitWidth+textWidth+10+atTextRect.getWidth())/2);
+               } else {
+                       atPos = (float)(x2 - atTextRect.getWidth());
+               }
+               
+               g2.setColor(Color.GRAY);
+               g2.drawGlyphVector(atText, atPos, y1 + 3*line);
+
+       }
+
+       
+       private void drawWarnings() {
+               if (warnings == null || warnings.isEmpty())
+                       return;
+               
+               GlyphVector[] texts = new GlyphVector[warnings.size()+1];
+               double max = 0;
+               
+               texts[0] = createText("Warning:");
+               int i=1;
+               for (Warning w: warnings) {
+                       texts[i] = createText(w.toString());
+                       i++;
+               }
+               
+               for (GlyphVector v: texts) {
+                       Rectangle2D rect = v.getVisualBounds();
+                       if (rect.getWidth() > max)
+                               max = rect.getWidth();
+               }
+               
+
+               float y = y2 - line * warnings.size();
+               g2.setColor(new Color(255,0,0,130));
+
+               for (GlyphVector v: texts) {
+                       Rectangle2D rect = v.getVisualBounds();
+                       g2.drawGlyphVector(v, (float)(x2 - max/2 - rect.getWidth()/2), y);
+                       y += line;
+               }
+       }
+       
+       
+       private void drawFlightInformation() {
+               double height = drawFlightData();
+               
+               if (calculatingData) {
+                       GlyphVector calculating = createText("Calculating...");
+                       g2.setColor(Color.BLACK);
+                       g2.drawGlyphVector(calculating, x1, (float)(y2-height));
+               }
+       }
+       
+       
+       private double drawFlightData() {
+               if (flightData == null)
+                       return 0;
+               
+               double width=0;
+               
+               GlyphVector apogee = createText("Apogee: ");
+               GlyphVector maxVelocity = createText("Max. velocity: ");
+               GlyphVector maxAcceleration = createText("Max. acceleration: ");
+
+               GlyphVector apogeeValue, velocityValue, accelerationValue;
+               if (!Double.isNaN(flightData.getMaxAltitude())) {
+                       apogeeValue = createText(
+                                       UnitGroup.UNITS_DISTANCE.toStringUnit(flightData.getMaxAltitude()));
+               } else {
+                       apogeeValue = createText("N/A");
+               }
+               if (!Double.isNaN(flightData.getMaxVelocity())) {
+                       velocityValue = createText(
+                                       UnitGroup.UNITS_VELOCITY.toStringUnit(flightData.getMaxVelocity()) +
+                                       "  (Mach " + 
+                                       UnitGroup.UNITS_COEFFICIENT.toString(flightData.getMaxMachNumber()) + ")");
+               } else {
+                       velocityValue = createText("N/A");
+               }
+               if (!Double.isNaN(flightData.getMaxAcceleration())) {
+                       accelerationValue = createText(
+                                       UnitGroup.UNITS_ACCELERATION.toStringUnit(flightData.getMaxAcceleration()));
+               } else {
+                       accelerationValue = createText("N/A");
+               }
+               
+               Rectangle2D rect;
+               rect = apogee.getVisualBounds();
+               width = MathUtil.max(width, rect.getWidth());
+               
+               rect = maxVelocity.getVisualBounds();
+               width = MathUtil.max(width, rect.getWidth());
+               
+               rect = maxAcceleration.getVisualBounds();
+               width = MathUtil.max(width, rect.getWidth());
+               
+               width += 5;
+
+               if (!calculatingData) 
+                       g2.setColor(new Color(0,0,127));
+               else
+                       g2.setColor(new Color(0,0,127,127));
+
+               
+               g2.drawGlyphVector(apogee, (float)x1, (float)(y2-2*line));
+               g2.drawGlyphVector(maxVelocity, (float)x1, (float)(y2-line));
+               g2.drawGlyphVector(maxAcceleration, (float)x1, (float)(y2));
+
+               g2.drawGlyphVector(apogeeValue, (float)(x1+width), (float)(y2-2*line));
+               g2.drawGlyphVector(velocityValue, (float)(x1+width), (float)(y2-line));
+               g2.drawGlyphVector(accelerationValue, (float)(x1+width), (float)(y2));
+               
+               return 3*line;
+       }
+       
+       
+       
+       private GlyphVector createText(String text) {
+               return FONT.createGlyphVector(g2.getFontRenderContext(), text);
+       }
+
+       private GlyphVector createSmallText(String text) {
+               return SMALLFONT.createGlyphVector(g2.getFontRenderContext(), text);
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/main/AboutDialog.java b/src/net/sf/openrocket/gui/main/AboutDialog.java
new file mode 100644 (file)
index 0000000..ca302bc
--- /dev/null
@@ -0,0 +1,88 @@
+package net.sf.openrocket.gui.main;
+
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class AboutDialog extends JDialog {
+       
+       public static final String OPENROCKET_URL = "http://openrocket.sourceforge.net/";
+       
+
+       public AboutDialog(JFrame parent) {
+               super(parent, true);
+               
+               final String version = Prefs.getVersion();
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new ResizeLabel("OpenRocket", 20), "ax 50%, wrap para");
+               panel.add(new ResizeLabel("Version " + version, 3), "ax 50%, wrap 30lp");
+               
+               panel.add(new ResizeLabel("Copyright \u00A9 2007-2009 Sampo Niskanen"), "ax 50%, wrap para");
+               
+               JLabel link;
+               
+               if (Desktop.isDesktopSupported()) {
+                       
+                       link = new JLabel("<html><a href=\"" + OPENROCKET_URL + "\">" +
+                                       OPENROCKET_URL + "</a>");
+                       link.addMouseListener(new MouseAdapter() {
+                               @Override
+                               public void mouseClicked(MouseEvent e) {
+                                       Desktop d = Desktop.getDesktop();
+                                       try {
+                                               d.browse(new URI(OPENROCKET_URL));
+                                               
+                                       } catch (URISyntaxException e1) {
+                                               throw new RuntimeException("BUG: Illegal OpenRocket URL: "+OPENROCKET_URL,
+                                                               e1);
+                                       } catch (IOException e1) {
+                                               System.err.println("Unable to launch browser:");
+                                               e1.printStackTrace();
+                                       }
+                               }
+                       });
+                       
+               } else {
+                       link = new JLabel(OPENROCKET_URL);
+               }
+               panel.add(link, "ax 50%, wrap para");
+               
+
+               JButton close = new JButton("Close");
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               AboutDialog.this.dispose();
+                       }
+               });
+               panel.add(close, "right");
+               
+               this.add(panel);
+               this.setTitle("OpenRocket " + version);
+               this.pack();
+               this.setResizable(false);
+               this.setLocationRelativeTo(null);
+               GUIUtil.setDefaultButton(close);
+               GUIUtil.installEscapeCloseOperation(this);
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java b/src/net/sf/openrocket/gui/main/BareComponentTreeModel.java
new file mode 100644 (file)
index 0000000..d2071bc
--- /dev/null
@@ -0,0 +1,184 @@
+package net.sf.openrocket.gui.main;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+/**
+ * A TreeModel that implements viewing of the rocket tree structure.
+ * This class shows the internal structure of the tree, as opposed to the regular
+ * user-side view.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BareComponentTreeModel implements TreeModel, ComponentChangeListener {
+       ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
+
+       private final RocketComponent root;
+       private final JTree tree;
+
+       public BareComponentTreeModel(RocketComponent root, JTree tree) {
+               this.root = root;
+               this.tree = tree;
+               root.addComponentChangeListener(this);
+       }
+       
+       
+       public Object getChild(Object parent, int index) {
+               RocketComponent component = (RocketComponent)parent;
+               try {
+                       return component.getChild(index);
+               } catch (IndexOutOfBoundsException e) {
+                       return null;
+               }
+       }
+
+       public int getChildCount(Object parent) {
+               return ((RocketComponent)parent).getChildCount();
+       }
+
+       public int getIndexOfChild(Object parent, Object child) {
+               RocketComponent p = (RocketComponent)parent;
+               RocketComponent c = (RocketComponent)child;
+               return p.getChildPosition(c);
+       }
+
+       public Object getRoot() {
+               return root;
+       }
+
+       public boolean isLeaf(Object node) {
+               RocketComponent c = (RocketComponent)node;
+               return (c.getChildCount()==0);
+       }
+
+       public void addTreeModelListener(TreeModelListener l) {
+               listeners.add(l);
+       }
+
+       public void removeTreeModelListener(TreeModelListener l) {
+               listeners.remove(l);
+       }
+       
+       private void fireTreeNodesChanged() {
+               Object[] path = { root };
+               TreeModelEvent e = new TreeModelEvent(this,path);
+               Object[] l = listeners.toArray();
+               for (int i=0; i<l.length; i++)
+                       ((TreeModelListener)l[i]).treeNodesChanged(e);
+       }
+       
+       
+       private void printStructure(TreePath p, int level) {
+               String indent="";
+               for (int i=0; i<level; i++)
+                       indent += "  ";
+               System.out.println(indent+p+
+                               ": isVisible:"+tree.isVisible(p)+
+                               " isCollapsed:"+tree.isCollapsed(p)+
+                               " isExpanded:"+tree.isExpanded(p));
+               Object parent = p.getLastPathComponent();
+               for (int i=0; i<getChildCount(parent); i++) {
+                       Object child = getChild(parent,i);
+                       TreePath path = makeTreePath((RocketComponent)child);
+                       printStructure(path,level+1);
+               }
+       }
+       
+       
+       private void fireTreeStructureChanged(RocketComponent source) {
+               Object[] path = { root };
+               
+               
+               // Get currently expanded path IDs
+               Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
+               ArrayList<String> expanded = new ArrayList<String>();
+               if (enumer != null) {
+                       while (enumer.hasMoreElements()) {
+                               TreePath p = enumer.nextElement();
+                               expanded.add(((RocketComponent)p.getLastPathComponent()).getID());
+                       }
+               }
+               
+               // Send structure change event
+               TreeModelEvent e = new TreeModelEvent(this,path);
+               Object[] l = listeners.toArray();
+               for (int i=0; i<l.length; i++)
+                       ((TreeModelListener)l[i]).treeStructureChanged(e);
+               
+               // Re-expand the paths
+               Iterator<String> iter = expanded.iterator();
+               while (iter.hasNext()) {
+                       RocketComponent c = root.findComponent(iter.next());
+                       if (c==null)
+                               continue;
+                       tree.expandPath(makeTreePath(c));
+               }
+               if (source != null) {
+                       TreePath p = makeTreePath(source);
+                       tree.makeVisible(p);
+                       tree.expandPath(p);
+               }
+       }
+       
+       public void valueForPathChanged(TreePath path, Object newValue) {
+               System.err.println("ERROR: valueForPathChanged called?!");
+       }
+
+
+       public void componentChanged(ComponentChangeEvent e) {
+               if (e.isTreeChange() || e.isUndoChange()) {
+                       // Tree must be fully updated also in case of an undo change 
+                       fireTreeStructureChanged((RocketComponent)e.getSource());
+                       if (e.isTreeChange() && e.isUndoChange()) {
+                               // If the undo has changed the tree structure, some elements may be hidden unnecessarily
+                               // TODO: LOW: Could this be performed better?
+                               expandAll();
+                       }
+               } else if (e.isOtherChange()) {
+                       fireTreeNodesChanged();
+               }
+       }
+
+       public void expandAll() {
+               Iterator<RocketComponent> iterator = root.deepIterator();
+               while (iterator.hasNext()) {
+                       tree.makeVisible(makeTreePath(iterator.next()));
+               }
+       }
+       
+       public static TreePath makeTreePath(RocketComponent component) {
+               int count = 0;
+               RocketComponent c = component;
+               
+               while (c != null) {
+                       count++;
+                       c = c.getParent();
+               }
+               
+               Object[] list = new Object[count];
+               
+               count--;
+               c=component;
+               while (c!=null) {
+                       list[count] = c;
+                       count--;
+                       c = c.getParent();
+               }
+               
+               return new TreePath(list);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java
new file mode 100644 (file)
index 0000000..4bd73ca
--- /dev/null
@@ -0,0 +1,840 @@
+package net.sf.openrocket.gui.main;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import javax.swing.Action;
+import javax.swing.InputMap;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSplitPane;
+import javax.swing.JTabbedPane;
+import javax.swing.KeyStroke;
+import javax.swing.LookAndFeel;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.ToolTipManager;
+import javax.swing.UIManager;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.tree.DefaultTreeSelectionModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.file.GeneralRocketLoader;
+import net.sf.openrocket.file.OpenRocketSaver;
+import net.sf.openrocket.file.RocketLoadException;
+import net.sf.openrocket.file.RocketLoader;
+import net.sf.openrocket.file.RocketSaver;
+import net.sf.openrocket.gui.ComponentAnalysisDialog;
+import net.sf.openrocket.gui.PreferencesDialog;
+import net.sf.openrocket.gui.StorageOptionChooser;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.gui.scalefigure.RocketPanel;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Prefs;
+
+public class BasicFrame extends JFrame {
+       private static final long serialVersionUID = 1L;
+
+       /**
+        * The RocketLoader instance used for loading all rocket designs.
+        */
+       private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
+
+       
+       /**
+        * File filter for filtering only rocket designs.
+        */
+       private static final FileFilter ROCKET_DESIGN_FILTER = new FileFilter() {
+               @Override
+               public String getDescription() {
+                       return "OpenRocket designs (*.ork)";
+               }
+               @Override
+               public boolean accept(File f) {
+                       String name = f.getName().toLowerCase();
+                       return name.endsWith(".ork") || name.endsWith(".ork.gz");
+               }
+    };
+    
+    
+
+       /**
+        * List of currently open frames.  When the list goes empty
+        * it is time to exit the application.
+        */
+       private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>();
+       
+       
+       
+       
+       
+       /**
+        * Whether "New" and "Open" should replace this frame.
+        * Should be set to false on the first rocket modification.
+        */
+       private boolean replaceable = false;
+       
+       
+       
+       private final OpenRocketDocument document;
+       private final Rocket rocket;
+       
+       private RocketPanel rocketpanel;
+       private ComponentTree tree = null;
+       private final TreeSelectionModel selectionModel;
+       
+       /** Actions available for rocket modifications */
+       private final RocketActions actions;
+       
+       
+       
+       /**
+        * Sole constructor.  Creates a new frame based on the supplied document
+        * and adds it to the current frames list.
+        * 
+        * @param document      the document to show.
+        */
+       public BasicFrame(OpenRocketDocument document) {
+
+               this.document = document;
+               this.rocket = document.getRocket();
+               this.rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               // Set replaceable flag to false at first modification
+               rocket.addComponentChangeListener(new ComponentChangeListener() {
+                       public void componentChanged(ComponentChangeEvent e) {
+                               replaceable = false;
+                               BasicFrame.this.rocket.removeComponentChangeListener(this);
+                       }
+               });
+               
+               
+               // Create the selection model that will be used
+               selectionModel = new DefaultTreeSelectionModel();
+               selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+               
+               actions = new RocketActions(document, selectionModel, this);
+               
+               
+               // The main vertical split pane         
+               JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
+               vertical.setResizeWeight(0.5);
+               this.add(vertical);
+
+               
+               // The top tabbed pane
+               JTabbedPane tabbed = new JTabbedPane();
+               tabbed.addTab("Rocket design", null, designTab());
+               tabbed.addTab("Flight simulations", null, simulationsTab());
+               
+               vertical.setTopComponent(tabbed);
+               
+
+
+               //  Bottom segment, rocket figure
+               
+               rocketpanel = new RocketPanel(document);
+               vertical.setBottomComponent(rocketpanel);
+
+               rocketpanel.setSelectionModel(tree.getSelectionModel());
+
+                                       
+               createMenu();
+               
+               
+               rocket.addComponentChangeListener(new ComponentChangeListener() {
+                       public void componentChanged(ComponentChangeEvent e) {
+                               setTitle();
+                       }
+               });
+               
+               setTitle();
+               this.pack();
+
+               Dimension size = Prefs.getWindowSize(this.getClass());
+               if (size == null) {
+                       size = Toolkit.getDefaultToolkit().getScreenSize();
+                       size.width = size.width*9/10;
+                       size.height = size.height*9/10;
+               }
+               this.setSize(size);
+               this.addComponentListener(new ComponentAdapter() {
+                       @Override
+                       public void componentResized(ComponentEvent e) {
+                               Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize());
+                       }
+               });
+               this.setLocationByPlatform(true);
+                               
+               this.validate();
+               vertical.setDividerLocation(0.4);
+               setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+               addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosing(WindowEvent e) {
+                               closeAction();
+                       }
+               });
+               frames.add(this);
+               
+       }
+       
+       
+       /**
+        * Construct the "Rocket design" tab.  This contains a horizontal split pane
+        * with the left component the design tree and the right component buttons
+        * for adding components.
+        */
+       private JComponent designTab() {
+               JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true);
+               horizontal.setResizeWeight(0.5);
+
+
+               //  Upper-left segment, component tree
+
+               JPanel panel = new JPanel(new MigLayout("fill, flowy","","[grow]"));
+
+               tree = new ComponentTree(rocket);
+               tree.setSelectionModel(selectionModel);
+
+               // Remove JTree key events that interfere with menu accelerators
+               InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null);
+               im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null);
+
+
+               
+               // Double-click opens config dialog
+               MouseListener ml = new MouseAdapter() {
+                       @Override
+                       public void mousePressed(MouseEvent e) {
+                               int selRow = tree.getRowForLocation(e.getX(), e.getY());
+                               TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
+                               if(selRow != -1) {
+                                       if(e.getClickCount() == 2) {
+                                               // Double-click
+                                               RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
+                                               ComponentConfigDialog.showDialog(BasicFrame.this, 
+                                                               BasicFrame.this.document, c);
+                                       }
+                               }
+                       }
+               };
+               tree.addMouseListener(ml);
+
+               // Update dialog when selection is changed
+               selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
+                       public void valueChanged(TreeSelectionEvent e) {
+                               // Scroll tree to the selected item
+                               TreePath path = selectionModel.getSelectionPath();
+                               if (path == null)
+                                       return;
+                               tree.scrollPathToVisible(path);
+                               
+                               if (!ComponentConfigDialog.isDialogVisible())
+                                       return;
+                               RocketComponent c = (RocketComponent)path.getLastPathComponent();
+                               ComponentConfigDialog.showDialog(BasicFrame.this, 
+                                               BasicFrame.this.document, c);
+                       }
+               });
+
+               // Place tree inside scroll pane
+               JScrollPane scroll = new JScrollPane(tree);
+               panel.add(scroll,"spany, grow, wrap");
+               
+               
+               // Buttons
+               JButton button = new JButton(actions.getMoveUpAction());
+               panel.add(button,"sizegroup buttons, aligny 65%");
+               
+               button = new JButton(actions.getMoveDownAction());
+               panel.add(button,"sizegroup buttons, aligny 0%");
+               
+               button = new JButton(actions.getEditAction());
+               panel.add(button, "sizegroup buttons");
+               
+               button = new JButton(actions.getNewStageAction());
+               panel.add(button,"sizegroup buttons");
+               
+               button = new JButton(actions.getDeleteAction());
+               button.setIcon(null);
+               button.setMnemonic(0);
+               panel.add(button,"sizegroup buttons");
+
+               horizontal.setLeftComponent(panel);
+
+
+               //  Upper-right segment, component addition buttons
+
+               panel = new JPanel(new MigLayout("fill, insets 0","[0::]"));
+
+               scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
+                               ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+               scroll.setViewportView(new ComponentAddButtons(document, selectionModel,
+                               scroll.getViewport()));
+               scroll.setBorder(null);
+               scroll.setViewportBorder(null);
+
+               TitledBorder border = new TitledBorder("Add new component");
+               border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD));
+               scroll.setBorder(border);
+
+               panel.add(scroll,"grow");
+
+               horizontal.setRightComponent(panel);
+
+               return horizontal;
+       }
+       
+       
+       /**
+        * Construct the "Flight simulations" tab.
+        * @return
+        */
+       private JComponent simulationsTab() {
+               return new SimulationPanel(document);
+       }
+       
+       
+       
+       /**
+        * Creates the menu for the window.
+        */
+       private void createMenu() {
+               JMenuBar menubar = new JMenuBar();
+               JMenu menu;
+               JMenuItem item;
+               
+               ////  File
+               menu = new JMenu("File");
+               menu.setMnemonic(KeyEvent.VK_F);
+               menu.getAccessibleContext().setAccessibleDescription("File-handling related tasks");
+               menubar.add(menu);
+               
+               item = new JMenuItem("New",KeyEvent.VK_N);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
+               item.setMnemonic(KeyEvent.VK_N);
+               item.getAccessibleContext().setAccessibleDescription("Create a new rocket design");
+               item.setIcon(Icons.FILE_NEW);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               newAction();
+                               if (replaceable)
+                                       closeAction();
+                       }
+               });
+               menu.add(item);
+               
+               item = new JMenuItem("Open...",KeyEvent.VK_O);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
+               item.getAccessibleContext().setAccessibleDescription("Open a rocket design");
+               item.setIcon(Icons.FILE_OPEN);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               openAction();
+                       }
+               });
+               menu.add(item);
+               
+               menu.addSeparator();
+               
+               item = new JMenuItem("Save",KeyEvent.VK_S);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
+               item.getAccessibleContext().setAccessibleDescription("Save the current rocket design");
+               item.setIcon(Icons.FILE_SAVE);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               saveAction();
+                       }
+               });
+               menu.add(item);
+               
+               item = new JMenuItem("Save as...",KeyEvent.VK_A);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 
+                               ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
+               item.getAccessibleContext().setAccessibleDescription("Save the current rocket design "+
+                               "to a new file");
+               item.setIcon(Icons.FILE_SAVE_AS);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               saveAsAction();
+                       }
+               });
+               menu.add(item);
+               
+//             menu.addSeparator();
+               menu.add(new JSeparator());
+               
+               item = new JMenuItem("Close",KeyEvent.VK_C);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK));
+               item.getAccessibleContext().setAccessibleDescription("Close the current rocket design");
+               item.setIcon(Icons.FILE_CLOSE);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               closeAction();
+                       }
+               });
+               menu.add(item);
+               
+               menu.addSeparator();
+
+               item = new JMenuItem("Quit",KeyEvent.VK_Q);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
+               item.getAccessibleContext().setAccessibleDescription("Quit the program");
+               item.setIcon(Icons.FILE_QUIT);
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               quitAction();
+                       }
+               });
+               menu.add(item);
+               
+               
+
+               ////  Edit
+               menu = new JMenu("Edit");
+               menu.setMnemonic(KeyEvent.VK_E);
+               menu.getAccessibleContext().setAccessibleDescription("Rocket editing");
+               menubar.add(menu);
+               
+               
+               Action action = document.getUndoAction();
+               item = new JMenuItem(action);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
+               item.setMnemonic(KeyEvent.VK_U);
+               item.getAccessibleContext().setAccessibleDescription("Undo the previous operation");
+
+               menu.add(item);
+
+               action = document.getRedoAction();
+               item = new JMenuItem(action);
+               item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
+               item.setMnemonic(KeyEvent.VK_R);
+               item.getAccessibleContext().setAccessibleDescription("Redo the previously undone " +
+                               "operation");
+               menu.add(item);
+               
+               menu.addSeparator();
+               
+               
+               item = new JMenuItem(actions.getCutAction());
+               menu.add(item);
+       
+               item = new JMenuItem(actions.getCopyAction());
+               menu.add(item);
+       
+               item = new JMenuItem(actions.getPasteAction());
+               menu.add(item);
+               
+               item = new JMenuItem(actions.getDeleteAction());
+               menu.add(item);
+               
+               menu.addSeparator();
+               
+               item = new JMenuItem("Preferences");
+               item.setIcon(Icons.PREFERENCES);
+               item.getAccessibleContext().setAccessibleDescription("Setup the application "+
+                               "preferences");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               PreferencesDialog.showPreferences();
+                       }
+               });
+               menu.add(item);
+       
+               
+
+
+               ////  Analyze
+               menu = new JMenu("Analyze");
+               menu.setMnemonic(KeyEvent.VK_A);
+               menu.getAccessibleContext().setAccessibleDescription("Analyzing the rocket");
+               menubar.add(menu);
+               
+               item = new JMenuItem("Component analysis",KeyEvent.VK_C);
+               item.getAccessibleContext().setAccessibleDescription("Analyze the rocket components " +
+                               "separately");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               ComponentAnalysisDialog.showDialog(rocketpanel);
+                       }
+               });
+               menu.add(item);
+               
+               
+               
+               ////  Help
+               
+               menu = new JMenu("Help");
+               menu.setMnemonic(KeyEvent.VK_H);
+               menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
+               menubar.add(menu);
+               
+               item = new JMenuItem("License",KeyEvent.VK_L);
+               item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               new LicenseDialog(BasicFrame.this).setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               item = new JMenuItem("About",KeyEvent.VK_A);
+               item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
+               item.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               new AboutDialog(BasicFrame.this).setVisible(true);
+                       }
+               });
+               menu.add(item);
+               
+               
+               this.setJMenuBar(menubar);
+       }
+       
+       
+       
+       // TODO: HIGH: Remember last directory on open/save
+       
+       private void openAction() {
+           JFileChooser chooser = new JFileChooser();
+           chooser.setFileFilter(ROCKET_DESIGN_FILTER);
+           chooser.setMultiSelectionEnabled(true);
+           chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
+           if (chooser.showOpenDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
+               return;
+           
+           Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
+
+           File[] files = chooser.getSelectedFiles();
+           boolean opened = false;
+           
+           for (File file: files) {
+               System.out.println("Opening file: " + file);
+               if (open(file)) {
+                       opened = true;
+               }
+           }
+
+           // Close this frame if replaceable and file opened successfully
+               if (replaceable && opened) {
+                       closeAction();
+               }
+       }
+       
+       
+       /**
+        * Open the specified file in a new design frame.  If an error occurs, an error dialog
+        * is shown and <code>false</code> is returned.
+        * 
+        * @param file  the file to open.
+        * @return              whether the file was successfully loaded and opened.
+        */
+       private static boolean open(File file) {
+           OpenRocketDocument doc = null;
+               try {
+                       doc = ROCKET_LOADER.load(file);
+               } catch (RocketLoadException e) {
+                       JOptionPane.showMessageDialog(null, "Unable to open file '" + file.getName() 
+                                       +"': " + e.getMessage(), "Error opening file", JOptionPane.ERROR_MESSAGE);
+                       e.printStackTrace();
+                       return false;
+               }
+               
+           if (doc == null) {
+               throw new RuntimeException("BUG: Rocket loader returned null");
+           }       
+           
+           // Show warnings
+           Iterator<Warning> warns = ROCKET_LOADER.getWarnings().iterator();
+           System.out.println("Warnings:");
+           while (warns.hasNext()) {
+               System.out.println("  "+warns.next());
+               // TODO: HIGH: dialog
+           }
+           
+           // Set document state
+           doc.setFile(file);
+           doc.setSaved(true);
+
+           // Open the frame
+           BasicFrame frame = new BasicFrame(doc);
+           frame.setVisible(true);
+
+           return true;
+       }
+       
+       
+       
+       private boolean saveAction() {
+               File file = document.getFile();
+               if (file==null) {
+                       return saveAsAction();
+               } else {
+                       return saveAs(file);
+               }
+       }
+       
+       private boolean saveAsAction() {
+               File file = null;
+               while (file == null) {
+                       StorageOptionChooser storageChooser = 
+                               new StorageOptionChooser(document.getDefaultStorageOptions());
+                       JFileChooser chooser = new JFileChooser();
+                       chooser.setFileFilter(ROCKET_DESIGN_FILTER);
+                       chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
+                       chooser.setAccessory(storageChooser);
+                       if (document.getFile() != null)
+                               chooser.setSelectedFile(document.getFile());
+                       
+                       if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
+                               return false;
+                       
+                       file = chooser.getSelectedFile();
+                       if (file == null)
+                               return false;
+
+                       Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
+                       storageChooser.storeOptions(document.getDefaultStorageOptions());
+                       
+                       if (file.getName().indexOf('.') < 0) {
+                               String name = file.getAbsolutePath();
+                               name = name + ".ork";
+                               file = new File(name);
+                       }
+                       
+                       if (file.exists()) {
+                               int result = JOptionPane.showConfirmDialog(this, 
+                                               "File '"+file.getName()+"' exists.  Do you want to overwrite it?", 
+                                               "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+                               if (result != JOptionPane.YES_OPTION)
+                                       return false;
+                       }
+               }
+           saveAs(file);
+           return true;
+       }
+       
+       
+       private boolean saveAs(File file) {
+           System.out.println("Saving to file: " + file.getName());
+           boolean saved = false;
+           
+           if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
+               // User cancelled the dialog
+               return false;
+           }
+
+           RocketSaver saver = new OpenRocketSaver();
+           try {
+               saver.save(file, document);
+               document.setFile(file);
+               document.setSaved(true);
+               saved = true;
+           } catch (IOException e) {
+               JOptionPane.showMessageDialog(this, new String[] { 
+                               "An I/O error occurred while saving:",
+                               e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
+           }
+           setTitle();
+           return saved;
+       }
+       
+       
+       private boolean closeAction() {
+               if (!document.isSaved()) {
+                       ComponentConfigDialog.hideDialog();
+                       int result = JOptionPane.showConfirmDialog(this, 
+                                       "Design '"+rocket.getName()+"' has not been saved.  " +
+                                                       "Do you want to save it?", 
+                                       "Design not saved", JOptionPane.YES_NO_CANCEL_OPTION, 
+                                       JOptionPane.QUESTION_MESSAGE);
+                       if (result == JOptionPane.YES_OPTION) {
+                               // Save
+                               if (!saveAction())
+                                       return false;  // If save was interrupted
+                       } else if (result == JOptionPane.NO_OPTION) {
+                               // Don't save: No-op
+                       } else {
+                               // Cancel or close
+                               return false;
+                       }
+               }
+               
+               // Rocket has been saved or discarded
+               this.dispose();
+
+               // TODO: LOW: Close only dialogs that have this frame as their parent
+               ComponentConfigDialog.hideDialog();
+               ComponentAnalysisDialog.hideDialog();
+               
+               frames.remove(this);
+               if (frames.isEmpty())
+                       System.exit(0);
+               return true;
+       }
+       
+       /**
+        * Open a new design window with a basic rocket+stage.
+        */
+       public static void newAction() {
+               Rocket rocket = new Rocket();
+               Stage stage = new Stage();
+               stage.setName("Sustainer");
+               rocket.addChild(stage);
+               OpenRocketDocument doc = new OpenRocketDocument(rocket);
+               doc.setSaved(true);
+               
+               BasicFrame frame = new BasicFrame(doc);
+               frame.replaceable = true;
+               frame.setVisible(true);
+               ComponentConfigDialog.showDialog(frame, doc, rocket);
+       }
+       
+       /**
+        * Quit the application.  Confirms saving unsaved designs.  The action of File->Quit.
+        */
+       public static void quitAction() {
+               for (int i=frames.size()-1; i>=0; i--) {
+                       if (!frames.get(i).closeAction()) {
+                               // Close canceled
+                               return;
+                       }
+               }
+               // Should not be reached, but just in case
+               System.exit(0);
+       }
+       
+       
+       /**
+        * Set the title of the frame, taking into account the name of the rocket, file it 
+        * has been saved to (if any) and saved status.
+        */
+       private void setTitle() {
+               File file = document.getFile();
+               boolean saved = document.isSaved();
+               String title;
+               
+               title = rocket.getName();
+               if (file!=null) {
+                       title = title + " ("+file.getName()+")";
+               }
+               if (!saved)
+                       title = "*" + title;
+               
+               setTitle(title);
+       }
+       
+       
+       
+       
+       
+       
+       public static void main(String[] args) {
+               
+               /*
+                * Set the look-and-feel.  On Linux, Motif/Metal is sometimes incorrectly used 
+                * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
+                * other alternatives.
+                */
+               try {
+                       UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
+//                     System.out.println("Available look-and-feels:");
+//                     for (int i=0; i<info.length; i++) {
+//                             System.out.println("  "+info[i]);
+//                     }
+
+                       // Set system L&F
+                       UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+                       
+                       // Check whether we have an ugly L&F
+                       LookAndFeel laf = UIManager.getLookAndFeel();
+                       if (laf == null ||
+                                       laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
+                                       laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
+                               
+                               // Search for better LAF
+                               for (UIManager.LookAndFeelInfo l: info) {
+                                       if (l.getName().matches(".*[gG][tT][kK].*")) {
+                                               UIManager.setLookAndFeel(l.getClassName());
+                                               break;
+                                       }
+                                       if (l.getName().contains(".*[wW][iI][nN].*")) {
+                                               UIManager.setLookAndFeel(l.getClassName());
+                                               break;
+                                       }
+                                       if (l.getName().contains(".*[mM][aA][cC].*")) {
+                                               UIManager.setLookAndFeel(l.getClassName());
+                                               break;
+                                       }
+                               }
+                       }
+               } catch (Exception e) {
+                       System.err.println("Error setting LAF: " + e);
+               }
+
+               // Set tooltip delay time.  Tooltips are used in MotorChooserDialog extensively.
+               ToolTipManager.sharedInstance().setDismissDelay(30000);
+               
+               
+               // Load defaults
+               Prefs.loadDefaultUnits();
+
+               
+               // Check command-line for files
+               boolean opened = false;
+               for (String file: args) {
+                       if (open(new File(file))) {
+                               opened = true;
+                       }
+               }
+               
+               if (!opened) {
+                       newAction();
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/src/net/sf/openrocket/gui/main/ComponentAddButtons.java
new file mode 100644 (file)
index 0000000..7d0308c
--- /dev/null
@@ -0,0 +1,619 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.lang.reflect.Constructor;
+
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JViewport;
+import javax.swing.Scrollable;
+import javax.swing.SwingConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.ShockCord;
+import net.sf.openrocket.rocketcomponent.Streamer;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * A component that contains addition buttons to add different types of rocket components
+ * to a rocket.  It enables and disables buttons according to the current selection of a 
+ * TreeSelectionModel. 
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentAddButtons extends JPanel implements Scrollable {
+
+       private static final int ROWS = 3;
+       private static final int MAXCOLS = 6;
+       private static final String BUTTONPARAM = "grow, sizegroup buttons";
+
+       private static final int GAP = 5;
+       private static final int EXTRASPACE = 0;
+       
+       private final ComponentButton[][] buttons;
+       
+       private final OpenRocketDocument document;
+       private final TreeSelectionModel selectionModel;
+       private final JViewport viewport;
+       private final MigLayout layout;
+       
+       private final int width, height;
+       
+       
+       public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model, 
+                       JViewport viewport) {
+               
+               super();
+               String constaint = "[min!]";
+               for (int i=1; i<MAXCOLS; i++)
+                       constaint = constaint + GAP + "[min!]";
+               
+               layout = new MigLayout("fill",constaint);
+               setLayout(layout);
+               this.document = document;
+               this.selectionModel = model;
+               this.viewport = viewport;
+               
+               buttons = new ComponentButton[ROWS][];
+               int row = 0;
+               
+               ////////////////////////////////////////////
+               
+//             addButtonRow("Body components",row,
+//                             new ComponentButton(NoseCone.class,"Nose cone") {
+//                                     @Override
+//                                     public boolean isAddable(RocketComponent c) {
+//                                             if (!(c instanceof ComponentAssembly))
+//                                                     return false;
+//                                             if (c.getSiblingCount() == 0)
+//                                                     return true;
+//                                             return false;
+//                                     }
+//                             },
+//                             new BodyComponentButton(BodyTube.class,"Body tube"),
+//                             new BodyComponentButton(null,"Transition"));
+               
+               
+               addButtonRow("Body components and fin sets",row,
+                               new BodyComponentButton(NoseCone.class,"Nose cone"),
+                               new BodyComponentButton(BodyTube.class,"Body tube"),
+                               new BodyComponentButton(Transition.class,"Transition"),
+                               new FinButton(TrapezoidFinSet.class,"Trapezoidal"),  // TODO: MEDIUM: freer fin placing
+                               new FinButton(EllipticalFinSet.class,"Elliptical"),
+                               new FinButton(FreeformFinSet.class,"Freeform"),
+                               new FinButton(LaunchLug.class,"Launch lug")
+               );
+               
+               row++;
+/////
+               
+               
+               /////////////////////////////////////////////
+               
+               addButtonRow("Inner component",row,
+                               new ComponentButton(InnerTube.class, "Inner tube"),
+                               new ComponentButton(TubeCoupler.class, "Coupler"),
+                               new ComponentButton(CenteringRing.class, "Centering\nring"),
+                               new ComponentButton(Bulkhead.class, "Bulkhead"),
+                               new ComponentButton(EngineBlock.class, "Engine\nblock"));
+
+               row++;
+               
+               ////////////////////////////////////////////
+               
+               addButtonRow("Mass objects",row,
+                               new ComponentButton(Parachute.class, "Parachute"),
+                               new ComponentButton(Streamer.class, "Streamer"),
+                               new ComponentButton(ShockCord.class, "Shock cord"),
+//                             new ComponentButton("Motor clip"),
+//                             new ComponentButton("Payload"),
+                               new ComponentButton(MassComponent.class,"Mass\ncomponent")
+               );
+               
+               
+               // Get maximum button size
+               int w=0, h=0;
+               
+               for (row=0; row < buttons.length; row++) {
+                       for (int col=0; col < buttons[row].length; col++) {
+                               Dimension d = buttons[row][col].getPreferredSize();
+                               if (d.width > w)
+                                       w = d.width;
+                               if (d.height > h)
+                                       h = d.height;
+                       }
+               }
+               
+               // Set all buttons to maximum size
+               System.out.println("Setting w="+w+" h="+h);
+               width=w;
+               height=h;
+               Dimension d = new Dimension(width,height);
+               for (row=0; row < buttons.length; row++) {
+                       for (int col=0; col < buttons[row].length; col++) {
+                               buttons[row][col].setMinimumSize(d);
+                               buttons[row][col].setPreferredSize(d);
+                               buttons[row][col].getComponent(0).validate();
+                       }
+               }
+               
+               // Add viewport listener if viewport provided
+               if (viewport != null) {
+                       viewport.addChangeListener(new ChangeListener() {
+                               private int oldWidth = -1;
+                               public void stateChanged(ChangeEvent e) {
+                                       Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
+                                       if (d.width != oldWidth) {
+                                               oldWidth = d.width;
+                                               flowButtons();
+                                       }
+                               }
+                       });
+               }
+               
+               add(new JPanel(),"grow");
+       }
+       
+       
+       /**
+        * Adds a row of buttons to the panel.
+        * @param label  Label placed before the row
+        * @param row    Row number
+        * @param b      List of ComponentButtons to place on the row
+        */
+       private void addButtonRow(String label, int row, ComponentButton ... b) {
+               if (row>0)
+                       add(new JLabel(label),"span, gaptop unrel, wrap");
+               else 
+                       add(new JLabel(label),"span, gaptop 0, wrap");
+               
+               int col=0;
+               buttons[row] = new ComponentButton[b.length];
+
+               for (int i=0; i<b.length; i++) {
+                       buttons[row][col] = b[i];
+                       if (i < b.length-1)
+                               add(b[i],BUTTONPARAM);
+                       else
+                               add(b[i],BUTTONPARAM+", wrap");
+                       col++;
+               }
+       }
+       
+
+       /**
+        * Flows the buttons in all rows of the panel.  If a button would come too close
+        * to the right edge of the viewport, "newline" is added to its constraints flowing 
+        * it to the next line.
+        */
+       private void flowButtons() {
+               if (viewport==null)
+                       return;
+               
+               int w;
+               
+               Dimension d = viewport.getExtentSize();
+
+               for (int row=0; row < buttons.length; row++) {
+                       w=0;
+                       for (int col=0; col < buttons[row].length; col++) {
+                               w += GAP+width;
+                               String param = BUTTONPARAM+",width "+width+"!,height "+height+"!";
+
+                               if (w+EXTRASPACE > d.width) {
+                                       param = param + ",newline";
+                                       w = GAP+width;
+                               }
+                               if (col == buttons[row].length-1)
+                                       param = param + ",wrap";
+                               layout.setComponentConstraints(buttons[row][col], param);
+                       }
+               }
+               revalidate();
+       }
+       
+       
+       
+       /**
+        * Class for a component button.
+        */
+       private class ComponentButton extends JButton implements TreeSelectionListener {
+               protected Class<? extends RocketComponent> componentClass = null;
+               private Constructor<? extends RocketComponent> constructor = null;
+               
+               /** Only label, no icon. */
+               public ComponentButton(String text) {
+                       this(text,null,null);
+               }
+               
+               /**
+                * Constructor with icon and label.  The icon and label are placed into the button.
+                * The label may contain "\n" as a newline.
+                */
+               public ComponentButton(String text, Icon enabled, Icon disabled) {
+                       super();
+                       setLayout(new MigLayout("fill, flowy, insets 0, gap 0","",""));
+                       
+                       add(new JLabel(),"push, sizegroup spacing");
+                       
+                       // Add Icon
+                       if (enabled != null) {
+                               JLabel label = new JLabel(enabled);
+                               if (disabled != null)
+                                       label.setDisabledIcon(disabled);
+                               add(label,"growx");
+                       }
+                               
+                       // Add labels
+                       String[] l = text.split("\n");
+                       for (int i=0; i<l.length; i++) {
+                               add(new ResizeLabel(l[i],SwingConstants.CENTER,-3.0f),"growx");
+                       }
+                       
+                       add(new JLabel(),"push, sizegroup spacing");
+                       
+                       valueChanged(null);  // Update enabled status
+                       selectionModel.addTreeSelectionListener(this);
+               }
+
+               
+               /**
+                * Main constructor that should be used.  The generated component type is specified
+                * and the text.  The icons are fetched based on the component type.
+                */
+               public ComponentButton(Class<? extends RocketComponent> c, String text) {
+                       this(text,ComponentIcons.getLargeIcon(c),ComponentIcons.getLargeDisabledIcon(c));
+                       
+                       if (c==null)
+                               return;
+                       
+                       componentClass = c; 
+
+                       try {
+                               constructor = c.getConstructor();
+                       } catch (NoSuchMethodException e) {
+                               throw new IllegalArgumentException("Unable to get default "+
+                                               "constructor for class "+c,e);
+                       }
+               }
+               
+               
+               /**
+                * Return whether the current component is addable when the component c is selected.
+                * c is null if there is no selection.  The default is to use c.isCompatible(class).
+                */
+               public boolean isAddable(RocketComponent c) {
+                       if (c==null)
+                               return false;
+                       if (componentClass==null)
+                               return false;
+                       return c.isCompatible(componentClass);
+               }
+               
+               /**
+                * Return the position to add the component if component c is selected currently.
+                * The first element of the returned array is the RocketComponent to add the component
+                * to, and the second (in any) an Integer telling the position of the component.
+                * A return value of null means that the user cancelled addition of the component.
+                * If the array has only one element, the component is added at the end of the sibling 
+                * list.  By default returns the end of the currently selected component.
+                * 
+                * @param c  The component currently selected
+                * @return   The position to add the new component to, or null if should not add.
+                */
+               public Object[] getAdditionPosition(RocketComponent c) {
+                       return new Object[] { c };
+               }
+               
+               /**
+                * Updates the enabled status of the button.
+                * TODO: LOW: What about updates to the rocket tree?
+                */
+               public void valueChanged(TreeSelectionEvent e) {
+                       updateEnabled();
+               }
+               
+               /**
+                * Sets the enabled status of the button and all subcomponents.
+                */
+               @Override
+               public void setEnabled(boolean enabled) {
+                       super.setEnabled(enabled);
+                       Component[] c = getComponents();
+                       for (int i=0; i<c.length; i++)
+                               c[i].setEnabled(enabled);
+               }
+               
+
+               /**
+                * Update the enabled status of the button.
+                */
+               private void updateEnabled() {
+                       RocketComponent c=null;
+                       TreePath p = selectionModel.getSelectionPath();
+                       if (p!=null)
+                               c = (RocketComponent)p.getLastPathComponent();
+                       setEnabled(isAddable(c));
+               }
+
+               
+               @Override
+               protected void fireActionPerformed(ActionEvent event) {
+                       super.fireActionPerformed(event);
+                       RocketComponent c = null;
+                       Integer position = null;
+                       
+                       TreePath p = selectionModel.getSelectionPath();
+                       if (p!= null)
+                               c = (RocketComponent)p.getLastPathComponent();
+                       if (c != null) {
+                               Object[] pos = getAdditionPosition(c);
+                               if (pos==null || pos.length==0) {
+                                       // Cancel addition
+                                       return;
+                               }
+
+                               c = (RocketComponent)pos[0];
+                               if (pos.length>1)
+                                       position = (Integer)pos[1];
+                       }
+                       
+                       if (c == null) {
+                               // Should not occur
+                               System.err.println("ERROR:  Could not place new component.");
+                               Thread.dumpStack();
+                               updateEnabled();
+                               return;
+                       }
+                       
+                       if (constructor == null) {
+                               System.err.println("ERROR:  Construction of type not supported yet.");
+                               return;
+                       }
+                       
+                       RocketComponent component;
+                       try {
+                               component = (RocketComponent)constructor.newInstance();
+                       } catch (Exception e) {
+                               throw new RuntimeException("Could not construct new instance of class "+
+                                               constructor,e);
+                       }
+                       
+                       // Next undo position is set by opening the configuration dialog
+                       document.addUndoPosition("Add " + component.getComponentName());
+                       
+                       
+                       if (position == null)
+                               c.addChild(component);
+                       else
+                               c.addChild(component, position);
+                       
+                       // Select new component and open config dialog
+                       selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
+                       
+                       JFrame parent = null;
+                       for (Component comp = ComponentAddButtons.this; comp != null; 
+                                comp = comp.getParent()) {
+                               if (comp instanceof JFrame) {
+                                       parent = (JFrame) comp;
+                                       break;
+                               }
+                       }
+                               
+                       ComponentConfigDialog.showDialog(parent, document, component);
+               }
+       }
+       
+       /**
+        * A class suitable for BodyComponents.  Addition is allowed ...  
+        */
+       private class BodyComponentButton extends ComponentButton {
+               
+               public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
+                       super(c, text);
+               }
+
+               public BodyComponentButton(String text, Icon enabled, Icon disabled) {
+                       super(text, enabled, disabled);
+               }
+
+               public BodyComponentButton(String text) {
+                       super(text);
+               }
+
+               @Override
+               public boolean isAddable(RocketComponent c) {
+                       if (super.isAddable(c))
+                               return true;
+                       if (c instanceof BodyComponent)  // Handled separately
+                               return true;
+                       return false;
+               }
+               
+               @Override
+               public Object[] getAdditionPosition(RocketComponent c) {
+                       if (super.isAddable(c))     // Handled automatically
+                               return super.getAdditionPosition(c);
+                       
+                       // Handle BodyComponent separately
+                       if (!(c instanceof BodyComponent))
+                               return null;
+                       RocketComponent parent = c.getParent();
+                       assert(parent != null);
+                       
+                       // Check whether to insert between or at the end.
+                       // 0 = ask, 1 = in between, 2 = at the end
+                       int pos = Prefs.getChoise(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0);
+                       if (pos==0) {
+                               if (parent.getChildPosition(c) == parent.getChildCount()-1)
+                                       pos = 2;  // Selected component is the last component
+                               else
+                                       pos = askPosition();
+                       }
+                       
+                       switch (pos) {
+                       case 0:
+                               // Cancel
+                               return null;
+                       case 1:
+                               // Insert after current position
+                               return new Object[] { parent, new Integer(parent.getChildPosition(c)+1) };
+                       case 2:
+                               // Insert at the end of the parent
+                               return new Object[] { parent };
+                       default:
+                               System.err.println("ERROR:  Bad position type: "+pos);
+                               Thread.dumpStack();
+                               return null;
+                       }
+               }
+               
+               private int askPosition() {
+                       Object[] options = { "Insert here", "Add to the end", "Cancel" };
+                       
+                       JPanel panel = new JPanel(new MigLayout());
+                       JCheckBox check = new JCheckBox("Do not ask me again");
+                       panel.add(check,"wrap");
+                       panel.add(new ResizeLabel("You can change the default operation in the " +
+                                       "preferences.",-2));
+                       
+                       int sel = JOptionPane.showOptionDialog(null,  // parent component 
+                                       new Object[] {
+                                       "Insert the component after the current component or as the last " +
+                                       "component?",
+                                       panel },
+                                       "Select component position",   // title
+                                       JOptionPane.DEFAULT_OPTION,    // default selections
+                                       JOptionPane.QUESTION_MESSAGE,  // dialog type
+                                       null,         // icon
+                                       options,      // options
+                                       options[0]);  // initial value
+
+                       switch (sel) {
+                       case JOptionPane.CLOSED_OPTION:
+                       case 2:
+                               // Cancel
+                               return 0;
+                       case 0:
+                               // Insert
+                               sel = 1;
+                               break;
+                       case 1:
+                               // Add
+                               sel = 2;
+                               break;
+                       default:
+                               System.err.println("ERROR:  JOptionPane returned "+sel);
+                               Thread.dumpStack();
+                               return 0;
+                       }
+                       
+                       if (check.isSelected()) {
+                               // Save the preference
+                               Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
+                       }
+                       return sel;
+               }
+               
+       }
+       
+
+
+       /**
+        * Class for fin sets, that attach only to BodyTubes.
+        */
+       private class FinButton extends ComponentButton {
+               public FinButton(Class<? extends RocketComponent> c, String text) {
+                       super(c, text);
+               }
+
+               public FinButton(String text, Icon enabled, Icon disabled) {
+                       super(text, enabled, disabled);
+               }
+
+               public FinButton(String text) {
+                       super(text);
+               }
+
+               @Override
+               public boolean isAddable(RocketComponent c) {
+                       if (c==null)
+                               return false;
+                       return (c.getClass().equals(BodyTube.class));
+               }
+       }
+
+
+       
+       /////////  Scrolling functionality
+
+       @Override
+       public Dimension getPreferredScrollableViewportSize() {
+               return getPreferredSize();
+       }
+
+
+       @Override
+       public int getScrollableBlockIncrement(Rectangle visibleRect,
+                       int orientation, int direction) {
+               if (orientation == SwingConstants.VERTICAL)
+                       return visibleRect.height * 8 / 10;
+               return 10;
+       }
+
+
+       @Override
+       public boolean getScrollableTracksViewportHeight() {
+               return false;
+       }
+
+
+       @Override
+       public boolean getScrollableTracksViewportWidth() {
+               return true;
+       }
+
+
+       @Override
+       public int getScrollableUnitIncrement(Rectangle visibleRect,
+                       int orientation, int direction) {
+               return 10;
+       }
+       
+}
+
diff --git a/src/net/sf/openrocket/gui/main/ComponentIcons.java b/src/net/sf/openrocket/gui/main/ComponentIcons.java
new file mode 100644 (file)
index 0000000..0c140b2
--- /dev/null
@@ -0,0 +1,145 @@
+package net.sf.openrocket.gui.main;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+
+import javax.imageio.ImageIO;
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.ShockCord;
+import net.sf.openrocket.rocketcomponent.Streamer;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+
+
+public class ComponentIcons {
+       
+       private static final String ICON_DIRECTORY = "pix/componenticons/";
+       private static final String SMALL_SUFFIX = "-small.png";
+       private static final String LARGE_SUFFIX = "-large.png";
+       
+       private static final HashMap<Class<?>,ImageIcon> SMALL_ICONS = 
+               new HashMap<Class<?>,ImageIcon>();
+       private static final HashMap<Class<?>,ImageIcon> LARGE_ICONS = 
+               new HashMap<Class<?>,ImageIcon>();
+       private static final HashMap<Class<?>,ImageIcon> DISABLED_ICONS = 
+               new HashMap<Class<?>,ImageIcon>();
+
+       static {
+               load("nosecone", "Nose cone", NoseCone.class);
+               load("bodytube", "Body tube", BodyTube.class);
+               load("transition", "Transition", Transition.class);
+               load("trapezoidfin", "Trapezoidal fin set", TrapezoidFinSet.class);
+               load("ellipticalfin", "Elliptical fin set", EllipticalFinSet.class);
+               load("freeformfin", "Freeform fin set", FreeformFinSet.class);
+               load("launchlug", "Launch lug", LaunchLug.class);
+               load("innertube", "Inner tube", InnerTube.class);
+               load("tubecoupler", "Tube coupler", TubeCoupler.class);
+               load("centeringring", "Centering ring", CenteringRing.class);
+               load("bulkhead", "Bulk head", Bulkhead.class);
+               load("engineblock", "Engine block", EngineBlock.class);
+               load("parachute", "Parachute", Parachute.class);
+               load("streamer", "Streamer", Streamer.class);
+               load("shockcord", "Shock cord", ShockCord.class);
+               load("mass", "Mass component", MassComponent.class);
+       }
+       
+       private static void load(String filename, String name, Class<?> componentClass) {
+               ImageIcon icon = loadSmall(ICON_DIRECTORY + filename + SMALL_SUFFIX, name);
+               SMALL_ICONS.put(componentClass, icon);
+               
+               ImageIcon[] icons = loadLarge(ICON_DIRECTORY + filename + LARGE_SUFFIX, name);
+               LARGE_ICONS.put(componentClass, icons[0]);
+               DISABLED_ICONS.put(componentClass, icons[1]);
+       }
+       
+       
+       
+       public static Icon getSmallIcon(Class<?> c) {
+               return SMALL_ICONS.get(c);
+       }
+       public static Icon getLargeIcon(Class<?> c) {
+               return LARGE_ICONS.get(c);
+       }
+       public static Icon getLargeDisabledIcon(Class<?> c) {
+               return DISABLED_ICONS.get(c);
+       }
+       
+       
+       
+       
+       private static ImageIcon loadSmall(String file, String desc) {
+               URL url = ClassLoader.getSystemResource(file);
+               if (url==null) {
+               System.err.println("ERROR:  Couldn't find file: " + file);
+                       return null;
+               }
+               return new ImageIcon(url, desc);
+       }
+       
+       
+       private static ImageIcon[] loadLarge(String file, String desc) {
+               ImageIcon[] icons = new ImageIcon[2];
+               
+               URL url = ClassLoader.getSystemResource(file);
+           if (url != null) {
+               BufferedImage bi,bi2;
+               try {
+                               bi = ImageIO.read(url);
+                               bi2 = ImageIO.read(url);   //  How the fsck can one duplicate a BufferedImage???
+                       } catch (IOException e) {
+                               System.err.println("ERROR:  Couldn't read file: "+file);
+                               e.printStackTrace();
+                       return new ImageIcon[]{null,null};
+                       }
+                       
+                       icons[0] = new ImageIcon(bi,desc);
+                       
+                       // Create disabled icon
+                       if (false) {   // Fade using alpha 
+                               
+                               int rgb[] = bi2.getRGB(0,0,bi2.getWidth(),bi2.getHeight(),null,0,bi2.getWidth());
+                               for (int i=0; i<rgb.length; i++) {
+                                       final int alpha = (rgb[i]>>24)&0xFF;
+                                       rgb[i] = (rgb[i]&0xFFFFFF) | (alpha/3)<<24;
+                                       
+                                       //rgb[i] = (rgb[i]&0xFFFFFF) | ((rgb[i]>>1)&0x3F000000);
+                               }
+                               bi2.setRGB(0, 0, bi2.getWidth(), bi2.getHeight(), rgb, 0, bi2.getWidth());
+
+                       } else {   // Raster alpha
+
+                               for (int x=0; x < bi.getWidth(); x++) {
+                                       for (int y=0; y < bi.getHeight(); y++) {
+                                               if ((x+y)%2 == 0) {
+                                                       bi2.setRGB(x, y, 0);
+                                               }
+                                       }
+                               }
+                               
+                       }
+                       
+                       icons[1] = new ImageIcon(bi2,desc + " (disabled)");
+               
+               return icons;
+           } else {
+               System.err.println("ERROR:  Couldn't find file: " + file);
+               return new ImageIcon[]{null,null};
+           }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/main/ComponentTree.java b/src/net/sf/openrocket/gui/main/ComponentTree.java
new file mode 100644 (file)
index 0000000..8b4c034
--- /dev/null
@@ -0,0 +1,92 @@
+package net.sf.openrocket.gui.main;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.Icon;
+import javax.swing.JTree;
+
+import net.sf.openrocket.rocketcomponent.*;
+
+
+public class ComponentTree extends JTree {
+       private static final long serialVersionUID = 1L;
+
+       public ComponentTree(RocketComponent root) {
+               super();
+               this.setModel(new ComponentTreeModel(root,this));
+//             this.setModel(new BareComponentTreeModel(root,this));
+               setToggleClickCount(0);
+               
+               javax.swing.plaf.basic.BasicTreeUI ui = new javax.swing.plaf.basic.BasicTreeUI();
+               this.setUI(ui);
+
+               ui.setExpandedIcon(TreeIcon.MINUS);
+               ui.setCollapsedIcon(TreeIcon.PLUS);
+               
+               ui.setLeftChildIndent(15);
+               
+
+               setBackground(Color.WHITE);
+               setShowsRootHandles(false);
+               
+               setCellRenderer(new ComponentTreeRenderer());
+               
+               // Expand whole tree by default
+               expandTree();
+       }
+       
+       
+       public void expandTree() {
+               for (int i=0; i<getRowCount(); i++)
+                       expandRow(i);
+               
+       }
+       
+       private static class TreeIcon implements Icon{
+               public static final Icon PLUS = new TreeIcon(true);
+               public static final Icon MINUS = new TreeIcon(false);
+               
+               // Implementation:
+               
+           private final static int width = 9;
+           private final static int height = 9;
+           private final static BasicStroke stroke = new BasicStroke(2);
+           private boolean plus;
+           
+           private TreeIcon(boolean plus) {
+               this.plus = plus;
+           }
+           
+           public void paintIcon(Component c, Graphics g, int x, int y) {
+               Graphics2D g2 = (Graphics2D)g.create();
+               
+               g2.setColor(Color.WHITE);
+               g2.fillRect(x,y,width,height);
+               
+               g2.setColor(Color.DARK_GRAY);
+               g2.drawRect(x,y,width,height);
+               
+               g2.setStroke(stroke);
+               g2.drawLine(x+3, y+(height+1)/2, x+width-2, y+(height+1)/2);
+               if (plus)
+                       g2.drawLine(x+(width+1)/2, y+3, x+(width+1)/2, y+height-2);
+               
+               g2.dispose();
+           }
+           
+           public int getIconWidth() {
+               return width;
+           }
+           
+           public int getIconHeight() {
+               return height;
+           }
+       }
+       
+}
+
+
diff --git a/src/net/sf/openrocket/gui/main/ComponentTreeModel.java b/src/net/sf/openrocket/gui/main/ComponentTreeModel.java
new file mode 100644 (file)
index 0000000..f4428bf
--- /dev/null
@@ -0,0 +1,195 @@
+package net.sf.openrocket.gui.main;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+/**
+ * A TreeModel that implements viewing of the rocket tree structure.
+ * This transforms the internal view (which has nested Stages) into the user-view
+ * (which has parallel Stages).
+ * 
+ * To view with the internal structure, switch to using BareComponentTreeModel in
+ * ComponentTree.java.  NOTE: This class's makeTreePath will still be used, which
+ * will create illegal paths, which results in problems with selections. 
+ * 
+ * TODO: MEDIUM: When converting a component to another component this model given 
+ * outdated information, since it uses the components themselves as the nodes.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
+       ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
+
+       private final RocketComponent root;
+       private final JTree tree;
+
+       public ComponentTreeModel(RocketComponent root, JTree tree) {
+               this.root = root;
+               this.tree = tree;
+               root.addComponentChangeListener(this);
+       }
+       
+       
+       public Object getChild(Object parent, int index) {
+               RocketComponent component = (RocketComponent)parent;
+               
+               try {
+                       return component.getChild(index);
+               } catch (IndexOutOfBoundsException e) {
+                       return null;
+               }
+       }
+       
+
+       public int getChildCount(Object parent) {
+               RocketComponent c = (RocketComponent)parent;
+
+               return c.getChildCount();
+       }
+
+       
+       public int getIndexOfChild(Object parent, Object child) {
+               if (parent==null || child==null)
+                       return -1;
+               
+               RocketComponent p = (RocketComponent)parent;
+               RocketComponent c = (RocketComponent)child;
+               
+               return p.getChildPosition(c);
+       }
+
+       public Object getRoot() {
+               return root;
+       }
+
+       public boolean isLeaf(Object node) {
+               RocketComponent c = (RocketComponent)node;
+
+               return (c.getChildCount() == 0);
+       }
+
+       public void addTreeModelListener(TreeModelListener l) {
+               listeners.add(l);
+       }
+
+       public void removeTreeModelListener(TreeModelListener l) {
+               listeners.remove(l);
+       }
+       
+       private void fireTreeNodesChanged() {
+               Object[] path = { root };
+               TreeModelEvent e = new TreeModelEvent(this,path);
+               Object[] l = listeners.toArray();
+               for (int i=0; i<l.length; i++)
+                       ((TreeModelListener)l[i]).treeNodesChanged(e);
+       }
+       
+       
+       @SuppressWarnings("unused")
+       private void printStructure(TreePath p, int level) {
+               String indent="";
+               for (int i=0; i<level; i++)
+                       indent += "  ";
+               System.out.println(indent+p+
+                               ": isVisible:"+tree.isVisible(p)+
+                               " isCollapsed:"+tree.isCollapsed(p)+
+                               " isExpanded:"+tree.isExpanded(p));
+               Object parent = p.getLastPathComponent();
+               for (int i=0; i<getChildCount(parent); i++) {
+                       Object child = getChild(parent,i);
+                       TreePath path = makeTreePath((RocketComponent)child);
+                       printStructure(path,level+1);
+               }
+       }
+       
+       
+       private void fireTreeStructureChanged(RocketComponent source) {
+               Object[] path = { root };
+               
+               
+               // Get currently expanded path IDs
+               Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
+               ArrayList<String> expanded = new ArrayList<String>();
+               if (enumer != null) {
+                       while (enumer.hasMoreElements()) {
+                               TreePath p = enumer.nextElement();
+                               expanded.add(((RocketComponent)p.getLastPathComponent()).getID());
+                       }
+               }
+               
+               // Send structure change event
+               TreeModelEvent e = new TreeModelEvent(this,path);
+               Object[] l = listeners.toArray();
+               for (int i=0; i<l.length; i++)
+                       ((TreeModelListener)l[i]).treeStructureChanged(e);
+               
+               // Re-expand the paths
+               for (String id: expanded) {
+                       RocketComponent c = root.findComponent(id);
+                       if (c==null)
+                               continue;
+                       tree.expandPath(makeTreePath(c));
+               }
+               if (source != null) {
+                       TreePath p = makeTreePath(source);
+                       tree.makeVisible(p);
+                       tree.expandPath(p);
+               }
+       }
+       
+       public void valueForPathChanged(TreePath path, Object newValue) {
+               System.err.println("ERROR: valueForPathChanged called?!");
+       }
+
+
+       public void componentChanged(ComponentChangeEvent e) {
+               if (e.isTreeChange() || e.isUndoChange()) {
+                       // Tree must be fully updated also in case of an undo change 
+                       fireTreeStructureChanged((RocketComponent)e.getSource());
+                       if (e.isTreeChange() && e.isUndoChange()) {
+                               // If the undo has changed the tree structure, some elements may be hidden
+                               // unnecessarily
+                               // TODO: LOW: Could this be performed better?
+                               expandAll();
+                       }
+               } else if (e.isOtherChange()) {
+                       fireTreeNodesChanged();
+               }
+       }
+
+       public void expandAll() {
+               Iterator<RocketComponent> iterator = root.deepIterator();
+               while (iterator.hasNext()) {
+                       tree.makeVisible(makeTreePath(iterator.next()));
+               }
+       }
+
+       
+       public static TreePath makeTreePath(RocketComponent component) {
+               RocketComponent c = component;
+       
+               List<RocketComponent> list = new ArrayList<RocketComponent>();
+               
+               while (c != null) {
+                       list.add(0,c);
+                       c = c.getParent();
+               }
+               
+               return new TreePath(list.toArray());
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/ComponentTreeRenderer.java b/src/net/sf/openrocket/gui/main/ComponentTreeRenderer.java
new file mode 100644 (file)
index 0000000..e505d7a
--- /dev/null
@@ -0,0 +1,29 @@
+package net.sf.openrocket.gui.main;
+
+
+
+import java.awt.Component;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultTreeCellRenderer;
+
+public class ComponentTreeRenderer extends DefaultTreeCellRenderer {
+
+    @Override
+       public Component getTreeCellRendererComponent(
+            JTree tree,
+            Object value,
+            boolean sel,
+            boolean expanded,
+            boolean leaf,
+            int row,
+            boolean hasFocus) {
+
+       super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+       
+       setIcon(ComponentIcons.getSmallIcon(value.getClass()));
+       
+       return this;
+    }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/LicenseDialog.java b/src/net/sf/openrocket/gui/main/LicenseDialog.java
new file mode 100644 (file)
index 0000000..77690b9
--- /dev/null
@@ -0,0 +1,79 @@
+package net.sf.openrocket.gui.main;
+
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.util.GUIUtil;
+
+public class LicenseDialog extends JDialog {
+       private static final String LICENSE_FILENAME = "LICENSE.TXT";
+       
+       private static final String DEFAULT_LICENSE_TEXT =
+               "\n" +
+               "Error:  Unable to load " + LICENSE_FILENAME + "!\n" +
+               "\n" +
+               "OpenRocket is licensed under the GNU GPL version 3, with additional permissions.\n" +
+               "See http://openrocket.sourceforge.net/ for details.";
+
+       public LicenseDialog(JFrame parent) {
+               super(parent, true);
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new ResizeLabel("OpenRocket license", 10), "ax 50%, wrap para");
+
+               String licenseText;
+               try {
+                       
+                       BufferedReader reader = new BufferedReader(
+                                       new InputStreamReader(ClassLoader.getSystemResourceAsStream(LICENSE_FILENAME)));
+                       StringBuffer sb = new StringBuffer();
+                       for (String s = reader.readLine(); s != null; s = reader.readLine()) {
+                               sb.append(s);
+                               sb.append('\n');
+                       }
+                       licenseText = sb.toString();
+                       
+               } catch (Exception e) {
+
+                       licenseText = DEFAULT_LICENSE_TEXT;
+                       
+               }
+               
+               JTextArea text = new JTextArea(licenseText);
+               text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+               text.setRows(20);
+               text.setColumns(80);
+               text.setEditable(false);
+               panel.add(new JScrollPane(text),"grow, wrap para");
+               
+               JButton close = new JButton("Close");
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               LicenseDialog.this.dispose();
+                       }
+               });
+               panel.add(close, "right");
+               
+               this.add(panel);
+               this.setTitle("OpenRocket license");
+               this.pack();
+               this.setLocationByPlatform(true);
+               GUIUtil.setDefaultButton(close);
+               GUIUtil.installEscapeCloseOperation(this);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/MotorChooserDialog.java b/src/net/sf/openrocket/gui/main/MotorChooserDialog.java
new file mode 100644 (file)
index 0000000..c6e0f0f
--- /dev/null
@@ -0,0 +1,642 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.text.Collator;
+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 javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class MotorChooserDialog extends JDialog {
+       
+       private static final int SHOW_ALL = 0;
+       private static final int SHOW_SMALLER = 1;
+       private static final int SHOW_EXACT = 2;
+       private static final String[] SHOW_DESCRIPTIONS = {
+               "Show all motors",
+               "Show motors with diameter less than that of the motor mount",
+               "Show motors with diameter equal to that of the motor mount"
+       };
+       private static final int SHOW_MAX = 2;
+       
+
+       private final double diameter;
+
+       private Motor selectedMotor = null;
+       private double selectedDelay = 0;
+
+       private JTable table;
+       private TableRowSorter<TableModel> sorter;
+       private JComboBox delayBox;
+       private MotorDatabaseModel model;
+       
+       private boolean okClicked = false;
+
+       
+       public MotorChooserDialog(double diameter) {
+               this(null,5,diameter,null);
+       }
+       
+       public MotorChooserDialog(Motor current, double delay, double diameter) {
+               this(current,delay,diameter,null);
+       }
+       
+       public MotorChooserDialog(Motor current, double delay, double diameter, Frame owner) {
+               super(owner, "Select a rocket motor", true);
+               
+               JButton button;
+
+               this.selectedMotor = current;
+               this.selectedDelay = delay;
+               this.diameter = diameter;
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+
+               // Label
+               JLabel label = new JLabel("Select a rocket motor:");
+               label.setFont(label.getFont().deriveFont(Font.BOLD));
+               panel.add(label,"split 2, growx");
+               
+               label = new JLabel("Motor mount diameter: " +
+                               UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter));
+               panel.add(label,"alignx 100%, wrap paragraph");
+               
+               
+               // Diameter selection
+               JComboBox combo = new JComboBox(SHOW_DESCRIPTIONS);
+               combo.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               JComboBox cb = (JComboBox) e.getSource();
+                               int sel = cb.getSelectedIndex();
+                               if ((sel < 0) || (sel > SHOW_MAX))
+                                       sel = SHOW_ALL;
+                               switch (sel) {
+                               case SHOW_ALL:
+                                       System.out.println("Setting filter: all");
+                                       sorter.setRowFilter(new MotorRowFilterAll());
+                                       break;
+                                       
+                               case SHOW_SMALLER:
+                                       System.out.println("Setting filter: smaller");
+                                       sorter.setRowFilter(new MotorRowFilterSmaller());
+                                       break;
+                                       
+                               case SHOW_EXACT:
+                                       System.out.println("Setting filter: exact");
+                                       sorter.setRowFilter(new MotorRowFilterExact());
+                                       break;
+                                       
+                               default:
+                                       assert(false) : "Should not occur.";    
+                               }
+                               Prefs.putChoise("MotorDiameterMatch", sel);
+                               setSelectionVisible();
+                       }
+               });
+               panel.add(combo,"growx, wrap");
+
+               
+               // Table, overridden to show meaningful tooltip texts
+               model = new MotorDatabaseModel(current);
+               table = new JTable(model) {
+                       @Override
+                       public String getToolTipText(MouseEvent e) {
+                       java.awt.Point p = e.getPoint();
+                       int colIndex = columnAtPoint(p);
+                       int viewRow = rowAtPoint(p);
+                       if (viewRow < 0)
+                               return null;
+                       int rowIndex = convertRowIndexToModel(viewRow);
+                       Motor motor = model.getMotor(rowIndex);
+
+                       if (colIndex < 0 || colIndex >= MotorColumns.values().length)
+                               return null;
+
+                       return MotorColumns.values()[colIndex].getToolTipText(motor);
+                       }
+               };
+               
+               // Set comparators and widths
+               table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+               sorter = new TableRowSorter<TableModel>(model);
+               for (int i=0; i < MotorColumns.values().length; i++) {
+                       MotorColumns column = MotorColumns.values()[i];
+                       sorter.setComparator(i, column.getComparator());
+                       table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth());
+               }
+               table.setRowSorter(sorter);
+
+               // Set selection and double-click listeners
+               table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+                       @Override
+                       public void valueChanged(ListSelectionEvent e) {
+                               int row = table.getSelectedRow();
+                               if (row >= 0) {
+                                       row = table.convertRowIndexToModel(row);
+                                       Motor m = model.getMotor(row);
+                                       if (!m.equals(selectedMotor)) {
+                                               selectedMotor = model.getMotor(row);
+                                               setDelays(true);  // Reset delay times
+                                       }
+                               }
+                       }
+               });
+               table.addMouseListener(new MouseAdapter() {
+                       @Override
+                       public void mouseClicked(MouseEvent e) {
+                               if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
+                                       okClicked = true;
+                                       MotorChooserDialog.this.setVisible(false);
+                               }
+                       }
+               });
+               // (Current selection and scrolling performed later)
+               
+               JScrollPane scrollpane = new JScrollPane();
+               scrollpane.setViewportView(table);
+               panel.add(scrollpane,"grow, width :700:, height :300:, wrap paragraph");
+               
+               
+               // Ejection delay
+               panel.add(new JLabel("Select ejection charge delay:"), "split 3, gap rel");
+               
+               delayBox = new JComboBox();
+               delayBox.setEditable(true);
+               delayBox.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               JComboBox cb = (JComboBox) e.getSource();
+                               String sel = (String)cb.getSelectedItem();
+                               if (sel.equalsIgnoreCase("None")) {
+                                       selectedDelay = Motor.PLUGGED;
+                               } else {
+                                       try {
+                                               selectedDelay = Double.parseDouble(sel);
+                                       } catch (NumberFormatException ignore) { }
+                               }
+                               setDelays(false);
+                       }
+               });
+               panel.add(delayBox,"gapright unrel");
+               panel.add(new ResizeLabel("(Number of seconds or \"None\")", -1), "wrap para");
+               setDelays(false);
+               
+               
+               JButton okButton = new JButton("OK");
+               okButton.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               okClicked = true;
+                               MotorChooserDialog.this.setVisible(false);
+                       }
+               });
+               panel.add(okButton,"split, tag ok");
+
+               button = new JButton("Cancel");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               MotorChooserDialog.this.setVisible(false);
+                       }
+               });
+               panel.add(button,"tag cancel");
+
+                               
+               // Sets the filter:
+               int showMode = Prefs.getChoise("MotorDiameterMatch", SHOW_MAX, SHOW_EXACT);
+               combo.setSelectedIndex(showMode);
+               
+               
+               this.add(panel);
+               this.pack();
+//             this.setAlwaysOnTop(true);
+
+               GUIUtil.setDefaultButton(okButton);
+               GUIUtil.installEscapeCloseOperation(this);
+               
+               // Table can be scrolled only after pack() has been called
+               setSelectionVisible();
+       }
+       
+       private void setSelectionVisible() {
+               if (selectedMotor != null) {
+                       int index = table.convertRowIndexToView(model.getIndex(selectedMotor));
+                       table.getSelectionModel().setSelectionInterval(index, index);
+                       Rectangle rect = table.getCellRect(index, 0, true);
+                       rect = new Rectangle(rect.x,rect.y-100,rect.width,rect.height+200);
+                       table.scrollRectToVisible(rect);
+               }
+       }
+       
+       
+       /**
+        * Set the values in the delay combo box.  If <code>reset</code> is <code>true</code>
+        * then sets the selected value as the value closest to selectedDelay, otherwise
+        * leaves selection alone.
+        */
+       private void setDelays(boolean reset) {
+               if (selectedMotor == null) {
+                       
+                       delayBox.setModel(new DefaultComboBoxModel(new String[] { "None" }));
+                       delayBox.setSelectedIndex(0);
+                       
+               } else {
+                       
+                       double[] delays = selectedMotor.getStandardDelays();
+                       String[] delayStrings = new String[delays.length];
+                       double currentDelay = selectedDelay;  // Store current setting locally
+                       
+                       for (int i=0; i < delays.length; i++) {
+                               delayStrings[i] = Motor.getDelayString(delays[i], "None");
+                       }
+                       delayBox.setModel(new DefaultComboBoxModel(delayStrings));
+                       
+                       if (reset) {
+
+                               // Find and set the closest value
+                               double closest = Double.NaN;
+                               for (int i=0; i < delays.length; i++) {
+                                       // if-condition to always become true for NaN
+                                       if (!(Math.abs(delays[i] - currentDelay) > 
+                                                 Math.abs(closest - currentDelay))) {
+                                               closest = delays[i];
+                                       }
+                               }
+                               if (!Double.isNaN(closest)) {
+                                       selectedDelay = closest;
+                                       delayBox.setSelectedItem(Motor.getDelayString(closest, "None"));
+                               } else {
+                                       delayBox.setSelectedItem("None");
+                               }
+
+                       } else {
+                               
+                               selectedDelay = currentDelay;
+                               delayBox.setSelectedItem(Motor.getDelayString(currentDelay, "None"));
+                               
+                       }
+                       
+               }
+       }
+
+       
+       
+       public Motor getSelectedMotor() {
+               if (!okClicked)
+                       return null;
+               return selectedMotor;
+       }
+       
+       
+       public double getSelectedDelay() {
+               return selectedDelay;
+       }
+       
+
+       
+       
+       ////////////////  JTable elements  ////////////////
+       
+       
+       /**
+        * Enum defining the table columns.
+        */
+       private enum MotorColumns {
+               MANUFACTURER("Manufacturer",100) {
+                       @Override
+                       public String getValue(Motor m) {
+                               return m.getManufacturer();
+                       }
+//                     @Override
+//                     public String getToolTipText(Motor m) {
+//                             return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
+//                     }
+                       @Override
+                       public Comparator<?> getComparator() {
+                               return Collator.getInstance();
+                       }
+               },
+               DESIGNATION("Designation") {
+                       @Override
+                       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();
+                       }
+               },
+               TYPE("Type") {
+                       @Override
+                       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());
+                       }
+                       @Override
+                       public Comparator<?> getComparator() {
+                               return getNumericalComparator();
+                       }
+               },
+               LENGTH("Length") {
+                       @Override
+                       public String getValue(Motor m) {
+                               return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
+                                               m.getLength());
+                       }
+                       @Override
+                       public Comparator<?> getComparator() {
+                               return getNumericalComparator();
+                       }
+               },
+               IMPULSE("Impulse") {
+                       @Override
+                       public String getValue(Motor m) {
+                               return UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(
+                                               m.getTotalImpulse());
+                       }
+                       @Override
+                       public Comparator<?> getComparator() {
+                               return getNumericalComparator();
+                       }
+               },
+               TIME("Burn time") {
+                       @Override
+                       public String getValue(Motor m) {
+                               return UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit(
+                                               m.getAverageTime());
+                       }
+                       @Override
+                       public Comparator<?> getComparator() {
+                               return getNumericalComparator();
+                       }
+               };
+               
+               
+               private final String title;
+               private final int width;
+               
+               MotorColumns(String title) {
+                       this(title, 50);
+               }
+               
+               MotorColumns(String title, int width) {
+                       this.title = title;
+                       this.width = width;
+               }
+               
+               
+               public abstract String getValue(Motor m);
+               public abstract Comparator<?> getComparator();
+
+               public String getTitle() {
+                       return title;
+               }
+               
+               public int getWidth() {
+                       return width;
+               }
+               
+               public String getToolTipText(Motor m) {
+                       String tip = "<html>";
+                       tip += "<b>" + m.toString() + "</b>";
+                       tip += " (" + m.getMotorType().getDescription() + ")<br><hr>";
+                       
+                       String desc = m.getDescription().trim();
+                       if (desc.length() > 0) {
+                               tip += "<i>" + desc.replace("\n", "<br>") + "</i><br><hr>";
+                       }
+                       
+                       tip += ("Diameter: " + 
+                                       UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getDiameter()) +
+                                       "<br>");
+                       tip += ("Length: " + 
+                                       UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getLength()) +
+                                       "<br>");
+                       tip += ("Maximum thrust: " + 
+                                       UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getMaxThrust()) +
+                                       "<br>");
+                       tip += ("Average thrust: " + 
+                                       UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getAverageThrust()) +
+                                       "<br>");
+                       tip += ("Burn time: " + 
+                                       UnitGroup.UNITS_SHORT_TIME.getDefaultUnit()
+                                       .toStringUnit(m.getAverageTime()) + "<br>");
+                       tip += ("Total impulse: " +
+                                       UnitGroup.UNITS_IMPULSE.getDefaultUnit()
+                                       .toStringUnit(m.getTotalImpulse()) + "<br>");
+                       tip += ("Launch mass: " + 
+                                       UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(m.getMass(0)) +
+                                       "<br>");
+                       tip += ("Empty mass: " + 
+                                       UnitGroup.UNITS_MASS.getDefaultUnit()
+                                       .toStringUnit(m.getMass(Double.MAX_VALUE)));
+                       return tip;
+               }
+               
+       }
+       
+       
+       /**
+        * The JTable model.  Includes an extra motor, given in the constructor,
+        * if it is not already in the database.
+        */
+       private class MotorDatabaseModel extends AbstractTableModel {
+               private final Motor extra;
+               
+               public MotorDatabaseModel(Motor current) {
+                       if (Databases.MOTOR.contains(current))
+                               extra = null;
+                       else
+                               extra = current;
+               }
+               
+               @Override
+               public int getColumnCount() {
+                       return MotorColumns.values().length;
+               }
+
+               @Override
+               public int getRowCount() {
+                       if (extra == null)
+                               return Databases.MOTOR.size();
+                       else
+                               return Databases.MOTOR.size()+1;
+               }
+
+               @Override
+               public Object getValueAt(int rowIndex, int columnIndex) {
+                       MotorColumns column = getColumn(columnIndex);
+                       if (extra == null) {
+                               return column.getValue(Databases.MOTOR.get(rowIndex));
+                       } else {
+                               if (rowIndex == 0)
+                                       return column.getValue(extra);
+                               else
+                                       return column.getValue(Databases.MOTOR.get(rowIndex - 1));
+                       }
+               }
+               
+               @Override
+               public String getColumnName(int columnIndex) {
+                       return getColumn(columnIndex).getTitle();
+               }
+               
+               
+               public Motor getMotor(int rowIndex) {
+                       if (extra == null) {
+                               return Databases.MOTOR.get(rowIndex);
+                       } else {
+                               if (rowIndex == 0)
+                                       return extra;
+                               else
+                                       return Databases.MOTOR.get(rowIndex-1);
+                       }
+               }
+               
+               public int getIndex(Motor m) {
+                       if (extra == null) {
+                               return Databases.MOTOR.indexOf(m);
+                       } else {
+                               if (extra.equals(m))
+                                       return 0;
+                               else
+                                       return Databases.MOTOR.indexOf(m)+1;
+                       }
+               }
+               
+               private MotorColumns getColumn(int index) {
+                       return MotorColumns.values()[index];
+               }
+       }
+
+       
+       ////////  Row filters
+       
+       /**
+        * Abstract adapter class.
+        */
+       private abstract class MotorRowFilter extends RowFilter<TableModel,Integer> {
+               @Override
+               public boolean include(
+                               RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
+                       int index = entry.getIdentifier();
+                       Motor m = model.getMotor(index);
+                       return include(m);
+               }
+               
+               public abstract boolean include(Motor m);
+       }
+       
+       /**
+        * Show all motors.
+        */
+       private class MotorRowFilterAll extends MotorRowFilter {
+               @Override
+               public boolean include(Motor m) {
+                       return true;
+               }
+       }
+       
+       /**
+        * Show motors smaller than the mount.
+        */
+       private class MotorRowFilterSmaller extends MotorRowFilter {
+               @Override
+               public boolean include(Motor m) {
+                       return (m.getDiameter() <= diameter + 0.0004);
+               }
+       }
+       
+       /**
+        * Show motors that fit the mount.
+        */
+       private class MotorRowFilterExact extends MotorRowFilter {
+               @Override
+               public boolean include(Motor m) {
+                       return ((m.getDiameter() <= diameter + 0.0004) &&
+                                       (m.getDiameter() >= diameter - 0.0015));
+               }
+       }
+       
+       
+       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);
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/main/RocketActions.java b/src/net/sf/openrocket/gui/main/RocketActions.java
new file mode 100644 (file)
index 0000000..05b31e4
--- /dev/null
@@ -0,0 +1,551 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JFrame;
+import javax.swing.KeyStroke;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Pair;
+
+
+
+/**
+ * A class that holds Actions for common rocket operations such as
+ * cut/copy/paste/delete etc.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketActions {
+
+       private static RocketComponent clipboard = null;
+       private static List<RocketAction> clipboardListeners = new ArrayList<RocketAction>();
+
+       private final OpenRocketDocument document;
+       private final Rocket rocket;
+       private final JFrame parentFrame;
+       private final TreeSelectionModel selectionModel;
+
+
+       private final RocketAction deleteAction;
+       private final RocketAction cutAction;
+       private final RocketAction copyAction;
+       private final RocketAction pasteAction;
+       private final RocketAction editAction;
+       private final RocketAction newStageAction;
+       private final RocketAction moveUpAction;
+       private final RocketAction moveDownAction;
+       
+
+       public RocketActions(OpenRocketDocument document, TreeSelectionModel selectionModel,
+                       JFrame parentFrame) {
+               this.document = document;
+               this.rocket = document.getRocket();
+               this.selectionModel = selectionModel;
+               this.parentFrame = parentFrame;
+
+               // Add action also to updateActions()
+               this.deleteAction = new DeleteAction();
+               this.cutAction = new CutAction();
+               this.copyAction = new CopyAction();
+               this.pasteAction = new PasteAction();
+               this.editAction = new EditAction();
+               this.newStageAction = new NewStageAction();
+               this.moveUpAction = new MoveUpAction();
+               this.moveDownAction = new MoveDownAction();
+
+               updateActions();
+
+               // Update all actions when tree selection or rocket changes
+
+               selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
+                       @Override
+                       public void valueChanged(TreeSelectionEvent e) {
+                               updateActions();
+                       }
+               });
+               document.getRocket().addComponentChangeListener(new ComponentChangeListener() {
+                       @Override
+                       public void componentChanged(ComponentChangeEvent e) {
+                               updateActions();
+                       }
+               });
+       }
+
+       /**
+        * Update the state of all of the actions.
+        */
+       private void updateActions() {
+               deleteAction.update();
+               cutAction.update();
+               copyAction.update();
+               pasteAction.update();
+               editAction.update();
+               newStageAction.update();
+               moveUpAction.update();
+               moveDownAction.update();
+       }
+       
+       
+       /**
+        * Update the state of all actions that depend on the clipboard.
+        */
+       private void updateClipboardActions() {
+               RocketAction[] array = clipboardListeners.toArray(new RocketAction[0]);
+               for (RocketAction a: array) {
+                       a.update();
+               }
+       }
+
+       
+
+       public Action getDeleteAction() {
+               return deleteAction;
+       }
+
+       public Action getCutAction() {
+               return cutAction;
+       }
+       
+       public Action getCopyAction() {
+               return copyAction;
+       }
+       
+       public Action getPasteAction() {
+               return pasteAction;
+       }
+       
+       public Action getEditAction() {
+               return editAction;
+       }
+       
+       public Action getNewStageAction() {
+               return newStageAction;
+       }
+       
+       public Action getMoveUpAction() {
+               return moveUpAction;
+       }
+       
+       public Action getMoveDownAction() {
+               return moveDownAction;
+       }
+
+       
+       
+       ////////  Helper methods for the actions
+
+       /**
+        * Return the currently selected rocket component, or null if none selected.
+        * 
+        * @return      the currently selected component.
+        */
+       private RocketComponent getSelectedComponent() {
+               RocketComponent c = null;
+               TreePath p = selectionModel.getSelectionPath();
+               if (p != null)
+                       c = (RocketComponent) p.getLastPathComponent();
+
+               if (c != null && c.getRocket() != rocket) {
+                       throw new IllegalStateException("Selection not same as document rocket, "
+                                       + "report bug!");
+               }
+               return c;
+       }
+       
+       private void setSelectedComponent(RocketComponent component) {
+               TreePath path = ComponentTreeModel.makeTreePath(component);
+               selectionModel.setSelectionPath(path);
+       }
+
+
+       private boolean isDeletable(RocketComponent c) {
+               // Sanity check
+               if (c == null || c.getParent() == null)
+                       return false;
+
+               // Cannot remove Rocket
+               if (c instanceof Rocket)
+                       return false;
+
+               // Cannot remove last stage
+               if ((c instanceof Stage) && (c.getParent().getChildCount() == 1)) {
+                       return false;
+               }
+
+               return true;
+       }
+
+       private void delete(RocketComponent c) {
+               if (!isDeletable(c)) {
+                       throw new IllegalArgumentException("Report bug!  Component " + c + " not deletable.");
+               }
+
+               RocketComponent parent = c.getParent();
+               parent.removeChild(c);
+       }
+
+       private boolean isCopyable(RocketComponent c) {
+               if (c==null)
+                       return false;
+               if (c instanceof Rocket)
+                       return false;
+               return true;
+       }
+
+       
+
+
+       /**
+        * Return the component and position to which the current clipboard
+        * should be pasted.  Returns null if the clipboard is empty or if the
+        * clipboard cannot be pasted to the current selection.
+        * 
+        * @return  a Pair with both components defined, or null.
+        */
+       private Pair<RocketComponent, Integer> getPastePosition() {
+               RocketComponent selected = getSelectedComponent();
+               if (selected == null)
+                       return null;
+
+               if (clipboard == null)
+                       return null;
+
+               if (selected.isCompatible(clipboard))
+                       return new Pair<RocketComponent, Integer>(selected, selected.getChildCount());
+
+               RocketComponent parent = selected.getParent();
+               if (parent != null && parent.isCompatible(clipboard)) {
+                       int index = parent.getChildPosition(selected) + 1;
+                       return new Pair<RocketComponent, Integer>(parent, index);
+               }
+
+               return null;
+       }
+       
+       
+       
+       
+
+       ///////  Action classes
+
+       private abstract class RocketAction extends AbstractAction {
+               public abstract void update();
+       }
+
+
+       /**
+        * Action that deletes the selected component.
+        */
+       private class DeleteAction extends RocketAction {
+               public DeleteAction() {
+                       this.putValue(NAME, "Delete");
+                       this.putValue(SHORT_DESCRIPTION, "Delete the selected component and subcomponents.");
+                       this.putValue(MNEMONIC_KEY, KeyEvent.VK_D);
+                       this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));
+                       this.putValue(SMALL_ICON, Icons.EDIT_DELETE);
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       RocketComponent c = getSelectedComponent();
+                       if (!isDeletable(c))
+                               return;
+
+                       ComponentConfigDialog.hideDialog();
+
+                       document.addUndoPosition("Delete " + c.getComponentName());
+                       delete(c);
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(isDeletable(getSelectedComponent()));
+               }
+       }
+
+
+       
+       /**
+        * Action the cuts the selected component (copies to clipboard and deletes).
+        */
+       private class CutAction extends RocketAction {
+               public CutAction() {
+                       this.putValue(NAME, "Cut");
+                       this.putValue(MNEMONIC_KEY, KeyEvent.VK_T);
+                       this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X,
+                                       ActionEvent.CTRL_MASK));
+                       this.putValue(SHORT_DESCRIPTION, "Cut this component (and subcomponents) to "
+                                       + "the clipboard and remove from this design");
+                       this.putValue(SMALL_ICON, Icons.EDIT_CUT);
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       RocketComponent c = getSelectedComponent();
+                       if (!isDeletable(c) || !isCopyable(c))
+                               return;
+
+                       ComponentConfigDialog.hideDialog();
+
+                       document.addUndoPosition("Cut " + c.getComponentName());
+                       clipboard = c.copy();
+                       delete(c);
+                       updateClipboardActions();
+               }
+
+               @Override
+               public void update() {
+                       RocketComponent c = getSelectedComponent();
+                       this.setEnabled(isDeletable(c) && isCopyable(c));
+               }
+       }
+
+
+
+       /**
+        * Action that copies the selected component to the clipboard.
+        */
+       private class CopyAction extends RocketAction {
+               public CopyAction() {
+                       this.putValue(NAME, "Copy");
+                       this.putValue(MNEMONIC_KEY, KeyEvent.VK_C);
+                       this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C,
+                                       ActionEvent.CTRL_MASK));
+                       this.putValue(SHORT_DESCRIPTION, "Copy this component (and subcomponents) to "
+                                       + "the clipboard.");
+                       this.putValue(SMALL_ICON, Icons.EDIT_COPY);
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       RocketComponent c = getSelectedComponent();
+                       if (!isCopyable(c))
+                               return;
+
+                       clipboard = c.copy();
+                       updateClipboardActions();
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(isCopyable(getSelectedComponent()));
+               }
+               
+       }
+
+
+
+       /**
+        * Action that pastes the current clipboard to the selected position.
+        * It first tries to paste the component to the end of the selected component
+        * as a child, and after that as a sibling after the selected component. 
+        */
+       private class PasteAction extends RocketAction {
+               public PasteAction() {
+                       this.putValue(NAME, "Paste");
+                       this.putValue(MNEMONIC_KEY, KeyEvent.VK_P);
+                       this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V,
+                                       ActionEvent.CTRL_MASK));
+                       this.putValue(SHORT_DESCRIPTION, "Paste the component (and subcomponents) on "
+                                       + "the clipboard to the design.");
+                       this.putValue(SMALL_ICON, Icons.EDIT_PASTE);
+                       update();
+                       
+                       // Listen to when the clipboard changes
+                       clipboardListeners.add(this);
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       Pair<RocketComponent, Integer> position = getPastePosition();
+                       if (position == null)
+                               return;
+
+                       ComponentConfigDialog.hideDialog();
+
+                       RocketComponent pasted = clipboard.copy();
+                       document.addUndoPosition("Paste " + pasted.getComponentName());
+                       position.getU().addChild(pasted, position.getV());
+                       setSelectedComponent(pasted);
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(getPastePosition() != null);
+               }
+       }
+       
+       
+       
+       
+
+       
+       /**
+        * Action to edit the currently selected component.
+        */
+       private class EditAction extends RocketAction {
+               public EditAction() {
+                       this.putValue(NAME, "Edit");
+                       this.putValue(SHORT_DESCRIPTION, "Edit the selected component.");
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       RocketComponent c = getSelectedComponent();
+                       if (c == null)
+                               return;
+                       
+                       ComponentConfigDialog.showDialog(parentFrame, document, c);
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(getSelectedComponent() != null);
+               }
+       }
+
+
+       
+       
+       
+       
+       
+       /**
+        * Action to add a new stage to the rocket.
+        */
+       private class NewStageAction extends RocketAction {
+               public NewStageAction() {
+                       this.putValue(NAME, "New stage");
+                       this.putValue(SHORT_DESCRIPTION, "Add a new stage to the rocket design.");
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       
+                       ComponentConfigDialog.hideDialog();
+
+                       RocketComponent stage = new Stage();
+                       stage.setName("Booster stage");
+                       document.addUndoPosition("Add stage");
+                       rocket.addChild(stage);
+                       rocket.getDefaultConfiguration().setAllStages();
+                       setSelectedComponent(stage);
+                       ComponentConfigDialog.showDialog(parentFrame, document, stage);
+                       
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(true);
+               }
+       }
+
+
+
+       
+       /**
+        * Action to move the selected component upwards in the parent's child list.
+        */
+       private class MoveUpAction extends RocketAction {
+               public MoveUpAction() {
+                       this.putValue(NAME, "Move up");
+                       this.putValue(SHORT_DESCRIPTION, "Move this component upwards.");
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       RocketComponent selected = getSelectedComponent();
+                       if (!canMove(selected))
+                               return;
+                       
+                       ComponentConfigDialog.hideDialog();
+
+                       RocketComponent parent = selected.getParent();
+                       document.addUndoPosition("Move "+selected.getComponentName());
+                       parent.moveChild(selected, parent.getChildPosition(selected)-1);
+                       setSelectedComponent(selected);
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(canMove(getSelectedComponent()));
+               }
+               
+               private boolean canMove(RocketComponent c) {
+                       if (c == null || c.getParent() == null)
+                               return false;
+                       RocketComponent parent = c.getParent();
+                       if (parent.getChildPosition(c) > 0)
+                               return true;
+                       return false;
+               }
+       }
+
+
+
+       /**
+        * Action to move the selected component down in the parent's child list.
+        */
+       private class MoveDownAction extends RocketAction {
+               public MoveDownAction() {
+                       this.putValue(NAME, "Move down");
+                       this.putValue(SHORT_DESCRIPTION, "Move this component downwards.");
+                       update();
+               }
+
+               @Override
+               public void actionPerformed(ActionEvent e) {
+                       RocketComponent selected = getSelectedComponent();
+                       if (!canMove(selected))
+                               return;
+                       
+                       ComponentConfigDialog.hideDialog();
+
+                       RocketComponent parent = selected.getParent();
+                       document.addUndoPosition("Move "+selected.getComponentName());
+                       parent.moveChild(selected, parent.getChildPosition(selected)+1);
+                       setSelectedComponent(selected);
+               }
+
+               @Override
+               public void update() {
+                       this.setEnabled(canMove(getSelectedComponent()));
+               }
+               
+               private boolean canMove(RocketComponent c) {
+                       if (c == null || c.getParent() == null)
+                               return false;
+                       RocketComponent parent = c.getParent();
+                       if (parent.getChildPosition(c) < parent.getChildCount()-1)
+                               return true;
+                       return false;
+               }
+       }
+
+
+
+}
diff --git a/src/net/sf/openrocket/gui/main/SimulationEditDialog.java b/src/net/sf/openrocket/gui/main/SimulationEditDialog.java
new file mode 100644 (file)
index 0000000..f9c1c4c
--- /dev/null
@@ -0,0 +1,1008 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.ListCellRenderer;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.ExtendedISAModel;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.DescriptionArea;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.plot.Axis;
+import net.sf.openrocket.gui.plot.PlotConfiguration;
+import net.sf.openrocket.gui.plot.PlotPanel;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.RK4Simulator;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.listeners.CSVSaveListener;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Prefs;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+
+public class SimulationEditDialog extends JDialog {
+
+       public static final int DEFAULT = -1;
+       public static final int EDIT = 1;
+       public static final int PLOT = 2;
+       
+       
+       private final Window parentWindow;
+       private final Simulation simulation;
+       private final SimulationConditions conditions;
+       private final Configuration configuration;
+       
+       
+       public SimulationEditDialog(Window parent, Simulation s) {
+               this(parent, s, 0);
+       }       
+       
+       public SimulationEditDialog(Window parent, Simulation s, int tab) {
+               super(parent, "Edit simulation", JDialog.ModalityType.DOCUMENT_MODAL);
+               
+               this.parentWindow = parent;
+               this.simulation = s;
+               this.conditions = simulation.getConditions();
+               configuration = simulation.getConfiguration();
+               
+               JPanel mainPanel = new JPanel(new MigLayout("fill","[grow, fill]"));
+               
+               mainPanel.add(new JLabel("Simulation name: "), "span, split 2, shrink");
+               final JTextField field = new JTextField(simulation.getName());
+               field.getDocument().addDocumentListener(new DocumentListener() {
+                       @Override
+                       public void changedUpdate(DocumentEvent e) {
+                               setText();
+                       }
+                       @Override
+                       public void insertUpdate(DocumentEvent e) {
+                               setText();
+                       }
+                       @Override
+                       public void removeUpdate(DocumentEvent e) {
+                               setText();
+                       }
+                       private void setText() {
+                               String name = field.getText();
+                               if (name == null || name.equals(""))
+                                       return;
+                               System.out.println("Setting name:"+name);
+                               simulation.setName(name);
+                               
+                       }
+               });
+               mainPanel.add(field, "shrinky, growx, wrap");
+               
+               JTabbedPane tabbedPane = new JTabbedPane();
+               
+               
+               tabbedPane.addTab("Launch conditions", flightConditionsTab());
+               tabbedPane.addTab("Simulation options", simulationOptionsTab());
+               tabbedPane.addTab("Plot data", plotTab());
+//             tabbedPane.addTab("Export data", exportTab());
+               
+               // Select the initial tab
+               if (tab == EDIT) {
+                       tabbedPane.setSelectedIndex(0);
+               } else if (tab == PLOT) {
+                       tabbedPane.setSelectedIndex(2);
+               } else {
+                       FlightData data = s.getSimulatedData();
+                       if (data == null || data.getBranchCount() == 0)
+                               tabbedPane.setSelectedIndex(0);
+                       else
+                               tabbedPane.setSelectedIndex(2);
+               }
+               
+               mainPanel.add(tabbedPane, "spanx, grow, wrap");
+
+               
+               // Buttons
+               mainPanel.add(new JPanel(), "spanx, split, growx");
+
+               JButton button;
+               button = new JButton("Run simulation");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               SimulationEditDialog.this.dispose();
+                               SimulationRunDialog.runSimulations(parentWindow, simulation);
+                       }
+               });
+               mainPanel.add(button, "gapright para");
+               
+               
+               JButton close = new JButton("Close");
+               close.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               SimulationEditDialog.this.dispose();
+                       }
+               });
+               mainPanel.add(close, "");
+               
+               
+               this.add(mainPanel);
+               this.validate();
+               this.pack();
+               this.setLocationByPlatform(true);
+               GUIUtil.setDefaultButton(button);
+               GUIUtil.installEscapeCloseOperation(this);
+       }
+       
+       
+       
+       
+       
+       private JPanel flightConditionsTab() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               JPanel sub;
+               String tip;
+               UnitSelector unit;
+               BasicSlider slider;
+               DoubleModel m;
+               JSpinner spin;
+               
+               //// Motor selector
+               JLabel label = new JLabel("Motor configuration:");
+               label.setToolTipText("Select the motor configuration to use.");
+               panel.add(label, "shrinkx, spanx, split 2");
+               
+               JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration));
+               combo.setToolTipText("Select the motor configuration to use.");
+               combo.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               conditions.setMotorConfigurationID(configuration.getMotorConfigurationID());
+                       }
+               });
+               panel.add(combo, "growx, wrap para");
+               
+               
+               //// Wind settings:  Average wind speed, turbulence intensity, std. deviation
+               sub = new JPanel(new MigLayout("fill, gap rel unrel",
+                               "[grow][65lp!][30lp!][75lp!]",""));
+               sub.setBorder(BorderFactory.createTitledBorder("Wind"));
+               panel.add(sub, "growx, split 2, aligny 0, flowy, gapright para");
+
+
+               // Wind average
+               label = new JLabel("Average windspeed:");
+               tip = "The average windspeed relative to the ground.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"WindSpeedAverage", UnitGroup.UNITS_VELOCITY,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0, 10.0));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+
+               // Wind std. deviation
+               label = new JLabel("Standard deviation:");
+               tip = "<html>The standard deviation of the windspeed.<br>" +
+                               "The windspeed is within twice the standard deviation from the average for " +
+                               "95% of the time.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"WindSpeedDeviation", UnitGroup.UNITS_VELOCITY,0);
+               DoubleModel m2 = new DoubleModel(conditions,"WindSpeedAverage", 0.25, 
+                               UnitGroup.UNITS_COEFFICIENT,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(new DoubleModel(0), m2));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+                               
+
+               // Wind turbulence intensity
+               label = new JLabel("Turbulence intensity:");
+               tip = "<html>The turbulence intensity is the standard deviation " +
+                       "divided by the average windspeed.<br>" +
+                       "Typical values range from "+
+                       UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.05) +
+                       " to " +
+                       UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.20) + ".";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"WindTurbulenceIntensity", UnitGroup.UNITS_RELATIVE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               
+               final JLabel intensityLabel = new JLabel(
+                               getIntensityDescription(conditions.getWindTurbulenceIntensity()));
+               intensityLabel.setToolTipText(tip);
+               sub.add(intensityLabel,"w 75lp, wrap");
+               m.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               intensityLabel.setText(
+                                               getIntensityDescription(conditions.getWindTurbulenceIntensity()));
+                       }
+               });
+               
+
+               
+               
+               
+               //// Temperature and pressure
+               sub = new JPanel(new MigLayout("fill, gap rel unrel",
+                               "[grow][65lp!][30lp!][75lp!]",""));
+               sub.setBorder(BorderFactory.createTitledBorder("Atmospheric conditions"));
+               panel.add(sub, "growx, aligny 0, gapright para");
+
+               
+               BooleanModel isa = new BooleanModel(conditions, "ISAAtmosphere");
+               JCheckBox check = new JCheckBox(isa);
+               check.setText("Use International Standard Atmosphere");
+               check.setToolTipText("<html>Select to use the International Standard Atmosphere model."+
+                               "<br>This model has a temperature of " + 
+                               UnitGroup.UNITS_TEMPERATURE.toStringUnit(ExtendedISAModel.STANDARD_TEMPERATURE)+
+                               " and a pressure of " +
+                               UnitGroup.UNITS_PRESSURE.toStringUnit(ExtendedISAModel.STANDARD_PRESSURE) +
+                               " at sea level.");
+               sub.add(check, "spanx, wrap unrel");
+               
+               // Temperature
+               label = new JLabel("Temperature:");
+               tip = "The temperature at the launch site.";
+               label.setToolTipText(tip);
+               isa.addEnableComponent(label, false);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchTemperature", UnitGroup.UNITS_TEMPERATURE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               isa.addEnableComponent(spin, false);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               isa.addEnableComponent(unit, false);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(253.15, 308.15));  // -20 ... 35
+               slider.setToolTipText(tip);
+               isa.addEnableComponent(slider, false);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+               
+               // Pressure
+               label = new JLabel("Pressure:");
+               tip = "The atmospheric pressure at the launch site.";
+               label.setToolTipText(tip);
+               isa.addEnableComponent(label, false);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchPressure", UnitGroup.UNITS_PRESSURE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               isa.addEnableComponent(spin, false);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               isa.addEnableComponent(unit, false);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0.950e5, 1.050e5));
+               slider.setToolTipText(tip);
+               isa.addEnableComponent(slider, false);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+               
+               
+               
+               //// Launch site conditions
+               sub = new JPanel(new MigLayout("fill, gap rel unrel",
+                               "[grow][65lp!][30lp!][75lp!]",""));
+               sub.setBorder(BorderFactory.createTitledBorder("Launch site"));
+               panel.add(sub, "growx, split 2, aligny 0, flowy");
+
+               
+               // Latitude
+               label = new JLabel("Latitude:");
+               tip = "<html>The launch site latitude affects the gravitational pull of Earth.<br>" +
+                       "Positive values are on the Northern hemisphere, negative values on the " +
+                       "Southern hemisphere.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchLatitude", UnitGroup.UNITS_NONE, -90, 90);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               label = new JLabel("\u00b0 N");
+               label.setToolTipText(tip);
+               sub.add(label,"growx");
+               slider = new BasicSlider(m.getSliderModel(-90, 90));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+               
+               // Altitude
+               label = new JLabel("Altitude:");
+               tip = "<html>The launch altitude above mean sea level.<br>" +
+                               "This affects the position of the rocket in the atmospheric model.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchAltitude", UnitGroup.UNITS_DISTANCE,0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0, 250, 1000));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+               
+
+               
+               //// Launch rod
+               sub = new JPanel(new MigLayout("fill, gap rel unrel",
+                               "[grow][65lp!][30lp!][75lp!]",""));
+               sub.setBorder(BorderFactory.createTitledBorder("Launch rod"));
+               panel.add(sub, "growx, aligny 0, wrap");
+
+               
+               // Length
+               label = new JLabel("Length:");
+               tip = "The length of the launch rod.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchRodLength", UnitGroup.UNITS_LENGTH, 0);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0, 1, 5));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+               
+               // Angle
+               label = new JLabel("Angle:");
+               tip = "The angle of the launch rod from vertical.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchRodAngle", UnitGroup.UNITS_ANGLE,
+                               0, SimulationConditions.MAX_LAUNCH_ROD_ANGLE);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0, Math.PI/9, 
+                               SimulationConditions.MAX_LAUNCH_ROD_ANGLE));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+               
+               // Direction
+               label = new JLabel("Direction:");
+               tip = "<html>Direction of the launch rod relative to the wind.<br>" +
+                               UnitGroup.UNITS_ANGLE.toStringUnit(0) +
+                               " = towards the wind, "+
+                               UnitGroup.UNITS_ANGLE.toStringUnit(Math.PI) +
+                               " = downwind.";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"LaunchRodDirection", UnitGroup.UNITS_ANGLE,
+                               -Math.PI, Math.PI);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+               return panel;
+       }
+
+       
+       private String getIntensityDescription(double i) {
+               if (i < 0.001)
+                       return "None";
+               if (i < 0.05)
+                       return "Very low";
+               if (i < 0.10)
+                       return "Low";
+               if (i < 0.15)
+                       return "Medium";
+               if (i < 0.20)
+                       return "High";
+               if (i < 0.25)
+                       return "Very high";
+               return "Extreme";
+       }
+
+       
+       
+       private JPanel simulationOptionsTab() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               JPanel sub;
+               String tip;
+               JLabel label;
+               DoubleModel m;
+               JSpinner spin;
+               UnitSelector unit;
+               BasicSlider slider;
+
+               
+               //// Simulation options
+               sub = new JPanel(new MigLayout("fill, gap rel unrel",
+                               "[grow][65lp!][30lp!][75lp!]",""));
+               sub.setBorder(BorderFactory.createTitledBorder("Simulator options"));
+               panel.add(sub, "w 330lp!, growy, aligny 0");
+
+               
+               //  Calculation method
+               tip = "<html>" +
+                               "The Extended Barrowman method calculates aerodynamic forces according <br>" +
+                               "to the Barrowman equations extended to accommodate more components.";
+
+               label = new JLabel("Calculation method:");
+               label.setToolTipText(tip);
+               sub.add(label, "gaptop unrel, gapright para, spanx, split 2, w 150lp!");
+               
+               label = new JLabel("Extended Barrowman");
+               label.setToolTipText(tip);
+               sub.add(label, "growx, wrap para");
+               
+               
+               //  Simulation method
+               tip = "<html>" +
+                               "The six degree-of-freedom simulator allows the rocket total freedom during " +
+                               "flight.<br>" +
+                               "Integration is performed using a 4<sup>th</sup> order Runge-Kutta 4 " +
+                               "numerical integration.";
+
+               label = new JLabel("Simulation method:");
+               label.setToolTipText(tip);
+               sub.add(label, "gaptop unrel, gapright para, spanx, split 2, w 150lp!");
+               
+               label = new JLabel("6-DOF Runge-Kutta 4");
+               label.setToolTipText(tip);
+               sub.add(label, "growx, wrap 35lp");
+               
+               
+               // Wind average
+               label = new JLabel("Time step:");
+               tip = "<html>The time between simulation steps.<br>" +
+               "A smaller time step results in a more accurate but slower simulation.<br>" +
+                               "The 4<sup>th</sup> order simulation method is quite accurate with a time " +
+                               "step of " +
+                               UnitGroup.UNITS_TIME_STEP.toStringUnit(RK4Simulator.RECOMMENDED_TIME_STEP) +
+                               ".";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0, 0.2));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap");
+               
+
+
+               // Maximum angle step
+               /*
+               label = new JLabel("Max. angle step:");
+               tip = "<html>" +
+                               "This defines the maximum angle the rocket will turn during one time step.<br>"+
+                               "Smaller values result in a more accurate but possibly slower simulation.<br>"+
+                               "A recommended value is " +
+                               UnitGroup.UNITS_ANGLE.toStringUnit(RK4Simulator.RECOMMENDED_ANGLE_STEP) + ".";
+               label.setToolTipText(tip);
+               sub.add(label);
+               
+               m = new DoubleModel(conditions,"MaximumStepAngle", UnitGroup.UNITS_ANGLE, 
+                               1*Math.PI/180, Math.PI/9);
+               
+               spin = new JSpinner(m.getSpinnerModel());
+               spin.setEditor(new SpinnerEditor(spin));
+               spin.setToolTipText(tip);
+               sub.add(spin,"w 65lp!");
+               
+               unit = new UnitSelector(m);
+               unit.setToolTipText(tip);
+               sub.add(unit,"growx");
+               slider = new BasicSlider(m.getSliderModel(0, Math.toRadians(10)));
+               slider.setToolTipText(tip);
+               sub.add(slider,"w 75lp, wrap para");
+               */
+
+               JButton button = new JButton("Reset to default");
+               button.setToolTipText("Reset the time step to its default value (" +
+                               UnitGroup.UNITS_SHORT_TIME.toStringUnit(RK4Simulator.RECOMMENDED_TIME_STEP) +
+                               ").");
+                               
+//             button.setToolTipText("<html>Reset the step value to its default:<br>" +
+//                             "Time step " +
+//                             UnitGroup.UNITS_SHORT_TIME.toStringUnit(RK4Simulator.RECOMMENDED_TIME_STEP) +
+//                             "; maximum angle step " +
+//                             UnitGroup.UNITS_ANGLE.toStringUnit(RK4Simulator.RECOMMENDED_ANGLE_STEP) + ".");
+               sub.add(button, "spanx, tag right, wrap para");
+               
+               
+               
+               
+               //// Simulation listeners
+               sub = new JPanel(new MigLayout("fill, gap 0 0"));
+               sub.setBorder(BorderFactory.createTitledBorder("Simulator listeners"));
+               panel.add(sub, "growx, growy");
+               
+               
+               DescriptionArea desc = new DescriptionArea(5, -1);
+               desc.setText("<html><p>" +
+                               "<i>Simulation listeners</i> is an advanced feature that allows "+
+                               "user-written code to listen to and interact with the simulation.  " +
+                               "For details on writing simulation listeners, see the OpenRocket " +
+                               "technical documentation.</p>");
+               sub.add(desc, "aligny 0, growx, wrap para");
+               
+               
+               label = new JLabel("Current listeners:");
+               sub.add(label, "spanx, wrap rel");
+
+               final ListenerListModel listenerModel = new ListenerListModel();
+               final JList list = new JList(listenerModel);
+               list.setCellRenderer(new ListenerCellRenderer());
+               JScrollPane scroll = new JScrollPane(list);
+//             scroll.setPreferredSize(new Dimension(1,1));
+               sub.add(scroll, "height 1px, grow, wrap rel");
+               
+               
+               button = new JButton("Add");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               String previous = Prefs.NODE.get("previousListenerName", "");
+                               String input = (String)JOptionPane.showInputDialog(SimulationEditDialog.this,
+                                               new Object[] {
+                                               "Type the full Java class name of the simulation listener, for example:",
+                                               "<html><tt>" + CSVSaveListener.class.getName() + "</tt>" },
+                                               "Add simulation listener",
+                                               JOptionPane.QUESTION_MESSAGE,
+                                               null, null,
+                                               previous
+                               );
+                               if (input == null || input.equals(""))
+                                       return;
+
+                               Prefs.NODE.put("previousListenerName", input);
+                               simulation.getSimulationListeners().add(input);
+                               listenerModel.fireContentsChanged();
+                       }
+               });
+               sub.add(button, "split 2, sizegroup buttons, alignx 50%, gapright para");
+               
+               button = new JButton("Remove");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               int[] selected = list.getSelectedIndices();
+                               Arrays.sort(selected);
+                               for (int i=selected.length-1; i>=0; i--) {
+                                       simulation.getSimulationListeners().remove(selected[i]);
+                               }
+                               listenerModel.fireContentsChanged();
+                       }
+               });
+               sub.add(button, "sizegroup buttons, alignx 50%");
+               
+
+               return panel;
+       }
+
+
+       private class ListenerListModel extends AbstractListModel {
+               @Override
+               public String getElementAt(int index) {
+                       if (index < 0 || index >= getSize())
+                               return null;
+                       return simulation.getSimulationListeners().get(index);
+               }
+               @Override
+               public int getSize() {
+                       return simulation.getSimulationListeners().size();
+               }
+               public void fireContentsChanged() {
+                       super.fireContentsChanged(this, 0, getSize());
+               }
+       }
+       
+       
+       
+       
+       /**
+        * A panel for plotting the previously calculated data.
+        */
+       private JPanel plotTab() {
+
+               // Check that data exists
+               if (simulation.getSimulatedData() == null  ||
+                               simulation.getSimulatedData().getBranchCount() == 0) {
+                       return noDataPanel();
+               }
+               
+               
+               if (true)
+                       return new PlotPanel(simulation);
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               
+               
+               
+               JButton button = new JButton("test");
+
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               PlotConfiguration config = new PlotConfiguration();
+                               config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
+                               config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
+                               config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
+                               config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
+                               
+                               config.setDomainAxisType(FlightDataBranch.TYPE_TIME);
+                               
+                               performPlot(config);
+                       }
+               });
+               panel.add(button);
+               
+               return panel;
+       }
+       
+       
+       
+       /**
+        * A panel for exporting the data.
+        */
+       private JPanel exportTab() {
+
+               // Check that data exists
+               if (simulation.getSimulatedData() == null  ||
+                               simulation.getSimulatedData().getBranchCount() == 0) {
+                       return noDataPanel();
+               }
+               
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               panel.add(new JLabel("Not implemented yet.")); // TODO: HIGH: Implement export
+               
+               return panel;
+       }
+       
+       
+       
+       
+
+       /**
+        * Return a panel stating that there is no data available, and that the user
+        * should run the simulation first.
+        */
+       private JPanel noDataPanel() {
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               
+               // No data available
+               panel.add(new JLabel("No flight data available."),
+               "alignx 50%, aligny 100%, wrap para");
+               panel.add(new JLabel("Please run the simulation first."),
+               "alignx 50%, aligny 0%, wrap");
+               return panel;
+       }
+       
+
+       private void performPlot(PlotConfiguration config) {
+
+               // Fill the auto-selections
+               FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
+               PlotConfiguration filled = config.fillAutoAxes(branch);
+               List<Axis> axes = filled.getAllAxes();
+
+
+               // Create the data series for both axes
+               XYSeriesCollection[] data = new XYSeriesCollection[2];
+               data[0] = new XYSeriesCollection();
+               data[1] = new XYSeriesCollection();
+               
+               
+               // Get the domain axis type
+               final FlightDataBranch.Type domainType = filled.getDomainAxisType();
+               final Unit domainUnit = filled.getDomainAxisUnit();
+               if (domainType == null) {
+                       throw new IllegalArgumentException("Domain axis type not specified.");
+               }
+               List<Double> x = branch.get(domainType);
+
+               
+               // Create the XYSeries objects from the flight data and store into the collections
+               int length = filled.getTypeCount();
+               String[] axisLabel = new String[2];
+               for (int i = 0; i < length; i++) {
+                       // Get info
+                       FlightDataBranch.Type type = filled.getType(i);
+                       Unit unit = filled.getUnit(i);
+                       int axis = filled.getAxis(i);
+                       String name = getLabel(type, unit);
+                       
+                       // Store data in provided units
+                       List<Double> y = branch.get(type);
+                       XYSeries series = new XYSeries(name, false, true);
+                       for (int j=0; j<x.size(); j++) {
+                               series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
+                       }
+                       data[axis].addSeries(series);
+
+                       // Update axis label
+                       if (axisLabel[axis] == null)
+                               axisLabel[axis] = type.getName();
+                       else
+                               axisLabel[axis] += "; " + type.getName();
+               }
+               
+               
+               // Create the chart using the factory to get all default settings
+        JFreeChart chart = ChartFactory.createXYLineChart(
+            "Simulated flight",
+            null, 
+            null, 
+            null,
+            PlotOrientation.VERTICAL,
+            true,
+            true,
+            false
+        );
+               
+        
+               // Add the data and formatting to the plot
+               XYPlot plot = chart.getXYPlot();
+               int axisno = 0;
+               for (int i=0; i<2; i++) {
+                       // Check whether axis has any data
+                       if (data[i].getSeriesCount() > 0) {
+                               // Create and set axis
+                               double min = axes.get(i).getMinValue();
+                               double max = axes.get(i).getMaxValue();
+                               NumberAxis axis = new PresetNumberAxis(min, max);
+                               axis.setLabel(axisLabel[i]);
+//                             axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
+                               plot.setRangeAxis(axisno, axis);
+                               
+                               // Add data and map to the axis
+                               plot.setDataset(axisno, data[i]);
+                               plot.setRenderer(axisno, new StandardXYItemRenderer());
+                               plot.mapDatasetToRangeAxis(axisno, axisno);
+                               axisno++;
+                       }
+               }
+               
+               plot.getDomainAxis().setLabel(getLabel(domainType,domainUnit));
+               plot.addDomainMarker(new ValueMarker(0));
+               plot.addRangeMarker(new ValueMarker(0));
+               
+               
+               // Create the dialog
+               final JDialog dialog = new JDialog(this, "Simulation results");
+               dialog.setModalityType(ModalityType.DOCUMENT_MODAL);
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               dialog.add(panel);
+               
+               ChartPanel chartPanel = new ChartPanel(chart,
+                               false, // properties
+                               true,  // save
+                               false, // print
+                               true,  // zoom
+                               true); // tooltips
+               chartPanel.setMouseWheelEnabled(true);
+               chartPanel.setEnforceFileExtensions(true);
+               chartPanel.setInitialDelay(500);
+               
+               chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+               
+               panel.add(chartPanel, "grow, wrap 20lp");
+
+               JButton button = new JButton("Close");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               dialog.setVisible(false);
+                       }
+               });
+               panel.add(button, "right");
+
+               dialog.setLocationByPlatform(true);
+               dialog.pack();
+               GUIUtil.installEscapeCloseOperation(dialog);
+               GUIUtil.setDefaultButton(button);
+
+               dialog.setVisible(true);
+       }
+
+       
+       private class PresetNumberAxis extends NumberAxis {
+               private final double min;
+               private final double max;
+               
+               public PresetNumberAxis(double min, double max) {
+                       this.min = min;
+                       this.max = max;
+                       autoAdjustRange();
+               }
+               
+               @Override
+               protected void autoAdjustRange() {
+                       this.setRange(min, max);
+               }
+       }
+       
+       
+       private String getLabel(FlightDataBranch.Type type, Unit unit) {
+               String name = type.getName();
+               if (unit != null  &&  !UnitGroup.UNITS_NONE.contains(unit)  &&
+                               !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
+                       name += " ("+unit.getUnit() + ")";
+               return name;
+       }
+       
+
+
+       private class ListenerCellRenderer extends JLabel implements ListCellRenderer {
+
+               public Component getListCellRendererComponent(JList list, Object value,
+                               int index, boolean isSelected, boolean cellHasFocus) {
+                       String s = value.toString();
+                       setText(s);
+
+                       // Attempt instantiating, catch any exceptions
+                       Exception ex = null;
+                       try {
+                               Class<?> c = Class.forName(s);
+                               @SuppressWarnings("unused")
+                               SimulationListener l = (SimulationListener)c.newInstance();
+                       } catch (Exception e) {
+                               ex = e;
+                       }
+
+                       if (ex == null) {
+                               setIcon(Icons.SIMULATION_LISTENER_OK);
+                               setToolTipText("Listener instantiated successfully.");
+                       } else {
+                               setIcon(Icons.SIMULATION_LISTENER_ERROR);
+                               setToolTipText("<html>Unable to instantiate listener due to exception:<br>" +
+                                               ex.toString());
+                       }
+
+                       if (isSelected) {
+                               setBackground(list.getSelectionBackground());
+                               setForeground(list.getSelectionForeground());
+                       } else {
+                               setBackground(list.getBackground());
+                               setForeground(list.getForeground());
+                       }
+                       setOpaque(true);
+                       return this;
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/main/SimulationPanel.java b/src/net/sf/openrocket/gui/main/SimulationPanel.java
new file mode 100644 (file)
index 0000000..bb3cd3a
--- /dev/null
@@ -0,0 +1,532 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.SwingUtilities;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Prefs;
+
+public class SimulationPanel extends JPanel {
+       
+       private static final Color WARNING_COLOR = Color.RED;
+       private static final String WARNING_TEXT = "\uFF01";    // Fullwidth exclamation mark
+       
+       private static final Color OK_COLOR = new Color(60,150,0);
+       private static final String OK_TEXT = "\u2714";                 // Heavy check mark
+       
+       private static final String NAME_PREFIX = "Simulation ";
+
+       
+       
+       private final OpenRocketDocument document;
+       
+       private final ColumnTableModel simulationTableModel;
+       private final JTable simulationTable;
+       
+       
+       public SimulationPanel(OpenRocketDocument doc) {
+               super(new MigLayout("fill","[grow][][][][][][grow]"));
+               
+               JButton button;
+               
+
+               this.document = doc;
+
+               
+               
+               ////////  The simulation action buttons
+               
+               button = new JButton("New simulation");
+               button.setToolTipText("Add a new simulation");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               
+                               // Generate unique name for the simulation
+                               int maxValue = 0;
+                               for (Simulation s: document.getSimulations()) {
+                                       String name = s.getName();
+                                       if (name.startsWith(NAME_PREFIX)) {
+                                               try {
+                                                       maxValue = Math.max(maxValue, 
+                                                                       Integer.parseInt(name.substring(NAME_PREFIX.length())));
+                                               } catch (NumberFormatException ignore) { }
+                                       }
+                               }
+
+                               Simulation sim = new Simulation(document.getRocket());
+                               sim.setName(NAME_PREFIX + (maxValue+1));
+                               
+                               int n = document.getSimulationCount();
+                               document.addSimulation(sim);
+                               simulationTableModel.fireTableDataChanged();
+                               simulationTable.clearSelection();
+                               simulationTable.addRowSelectionInterval(n, n);
+                               
+                               openDialog(sim, SimulationEditDialog.EDIT);
+                       }                       
+               });
+               this.add(button,"skip 1, gapright para");
+               
+               button = new JButton("Edit simulation");
+               button.setToolTipText("Edit the selected simulation");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               int selected = simulationTable.getSelectedRow();
+                               if (selected < 0)
+                                       return;  // TODO: MEDIUM: "None selected" dialog
+                               
+                               selected = simulationTable.convertRowIndexToModel(selected);
+                               simulationTable.clearSelection();
+                               simulationTable.addRowSelectionInterval(selected, selected);
+                               
+                               openDialog(document.getSimulations().get(selected), SimulationEditDialog.EDIT);
+                       }
+               });
+               this.add(button,"gapright para");
+               
+               button = new JButton("Run simulations");
+               button.setToolTipText("Re-run the selected simulations");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                                  int[] selection = simulationTable.getSelectedRows();
+                                  if (selection.length == 0)
+                                          return;  // TODO: LOW: "None selected" dialog
+                                  
+                                  Simulation[] sims = new Simulation[selection.length];
+                                  for (int i=0; i < selection.length; i++) {
+                                          selection[i] = simulationTable.convertRowIndexToModel(selection[i]);
+                                          sims[i] = document.getSimulation(selection[i]);
+                                  }
+                                  
+                                  long t = System.currentTimeMillis();
+                                  new SimulationRunDialog(SwingUtilities.getWindowAncestor(
+                                                  SimulationPanel.this), sims).setVisible(true);
+                                  System.err.println("Running took "+(System.currentTimeMillis()-t) + " ms");
+                                  fireMaintainSelection();
+                       }
+               });
+               this.add(button,"gapright para");
+               
+               button = new JButton("Delete simulations");
+               button.setToolTipText("Delete the selected simulations");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                                  int[] selection = simulationTable.getSelectedRows();
+                                  if (selection.length == 0)
+                                          return;  // TODO: LOW: "None selected" dialog
+                                  
+                                  // Verify deletion
+                                  boolean verify = Prefs.NODE.getBoolean(Prefs.CONFIRM_DELETE_SIMULATION, true);
+                                  if (verify) {
+
+                                               JPanel panel = new JPanel(new MigLayout());
+                                               JCheckBox dontAsk = new JCheckBox("Do not ask me again");
+                                               panel.add(dontAsk,"wrap");
+                                               panel.add(new ResizeLabel("You can change the default operation in the " +
+                                                               "preferences.",-2));
+                                               
+                                          int ret = JOptionPane.showConfirmDialog(SimulationPanel.this,
+                                                          new Object[] {
+                                                          "Delete the selected simulations?",
+                                                          "<html><i>This operation cannot be undone.</i>",
+                                                          "",
+                                                          panel },
+                               "Delete simulations",
+                               JOptionPane.OK_CANCEL_OPTION,
+                               JOptionPane.WARNING_MESSAGE);
+                                          if (ret != JOptionPane.OK_OPTION)
+                                                  return;
+                                          
+                                          if (dontAsk.isSelected()) {
+                                                  Prefs.NODE.putBoolean(Prefs.CONFIRM_DELETE_SIMULATION, false);
+                                          }
+                                  }
+                                  
+                                  // Delete simulations
+                                  for (int i=0; i < selection.length; i++) {
+                                          selection[i] = simulationTable.convertRowIndexToModel(selection[i]);
+                                  }
+                                  Arrays.sort(selection);
+                                  for (int i=selection.length-1; i>=0; i--) {
+                                          document.removeSimulation(selection[i]);
+                                  }
+                                  simulationTableModel.fireTableDataChanged();
+                       }
+               });
+               this.add(button,"gapright para");
+               
+               
+//             button = new JButton("Plot / export");
+               button = new JButton("Plot flight");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               int selected = simulationTable.getSelectedRow();
+                               if (selected < 0)
+                                       return;  // TODO: MEDIUM: "None selected" dialog
+                               
+                               selected = simulationTable.convertRowIndexToModel(selected);
+                               simulationTable.clearSelection();
+                               simulationTable.addRowSelectionInterval(selected, selected);
+                               
+                               openDialog(document.getSimulations().get(selected), SimulationEditDialog.PLOT);
+                       }
+               });
+               this.add(button, "wrap para");
+
+               
+               
+               
+               ////////  The simulation table
+               
+               simulationTableModel = new ColumnTableModel(
+                               
+                               ////  Status and warning column
+                               new Column("") {
+                                       private JLabel label = null;
+                                       @Override 
+                                       public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               // Initialize the label
+                                               if (label == null) {
+                                                       label = new ResizeLabel(2f);
+                                                       label.setIconTextGap(1);
+//                                                     label.setFont(label.getFont().deriveFont(Font.BOLD));
+                                               }
+                                               
+                                               // Set simulation status icon
+                                               Simulation.Status status = document.getSimulation(row).getStatus();
+                                               label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status));
+                                               
+
+                                               // Set warning marker
+                                               if (status == Simulation.Status.NOT_SIMULATED ||
+                                                       status == Simulation.Status.EXTERNAL) {
+                                                       
+                                                       label.setText("");
+                                                       
+                                               } else {
+                                                       
+                                                       WarningSet w = document.getSimulation(row).getSimulatedWarnings();
+                                                       if (w == null) {
+                                                               label.setText("");
+                                                       } else if (w.isEmpty()) {
+                                                               label.setForeground(OK_COLOR);
+                                                               label.setText(OK_TEXT);
+                                                       } else {
+                                                               label.setForeground(WARNING_COLOR);
+                                                               label.setText(WARNING_TEXT);
+                                                       }
+                                               }
+
+                                               return label;
+                                       }
+                                       @Override public int getExactWidth() {
+                                               return 32;
+                                       }
+                                       @Override public Class<?> getColumnClass() {
+                                               return JLabel.class;
+                                       }
+                               },
+
+                               //// Simulation name
+                               new Column("Name") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               return document.getSimulation(row).getName();
+                                       }
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 125;
+                                       }
+                               },
+                               
+                               //// Simulation motors
+                               new Column("Motors") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               return document.getSimulation(row).getConfiguration()
+                                                       .getMotorConfigurationDescription();
+                                       }
+                                       @Override
+                                       public int getDefaultWidth() {
+                                               return 125;
+                                       }
+                               },
+                               
+                               //// Apogee
+                               new Column("Apogee") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               FlightData data = document.getSimulation(row).getSimulatedData();
+                                               if (data==null)
+                                                       return null;
+                                               
+                                               return UnitGroup.UNITS_DISTANCE.getDefaultUnit().toStringUnit(
+                                                               data.getMaxAltitude());
+                                       }
+                               },
+                               
+                               //// Maximum velocity
+                               new Column("Max. velocity") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               FlightData data = document.getSimulation(row).getSimulatedData();
+                                               if (data==null)
+                                                       return null;
+                                               
+                                               return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit(
+                                                               data.getMaxVelocity());
+                                       }
+                               },
+                               
+                               //// Maximum acceleration
+                               new Column("Max. acceleration") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               FlightData data = document.getSimulation(row).getSimulatedData();
+                                               if (data==null)
+                                                       return null;
+                                               
+                                               return UnitGroup.UNITS_ACCELERATION.getDefaultUnit().toStringUnit(
+                                                               data.getMaxAcceleration());
+                                       }
+                               },
+                               
+                               //// Time to apogee
+                               new Column("Time to apogee") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               FlightData data = document.getSimulation(row).getSimulatedData();
+                                               if (data==null)
+                                                       return null;
+                                               
+                                               return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(
+                                                               data.getTimeToApogee());
+                                       }
+                               },
+                               
+                               //// Flight time
+                               new Column("Flight time") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               FlightData data = document.getSimulation(row).getSimulatedData();
+                                               if (data==null)
+                                                       return null;
+                                               
+                                               return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(
+                                                               data.getFlightTime());
+                                       }
+                               },
+                               
+                               //// Ground hit velocity
+                               new Column("Ground hit velocity") {
+                                       @Override public Object getValueAt(int row) {
+                                               if (row < 0 || row >= document.getSimulationCount())
+                                                       return null;
+                                               
+                                               FlightData data = document.getSimulation(row).getSimulatedData();
+                                               if (data==null)
+                                                       return null;
+                                               
+                                               return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit(
+                                                               data.getGroundHitVelocity());
+                                       }
+                               }
+                               
+               ) {
+                       @Override
+                       public int getRowCount() {
+                               return document.getSimulationCount();
+                       }
+               };
+               
+               simulationTable = new JTable(simulationTableModel);
+               simulationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+               simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer());
+               simulationTableModel.setColumnWidths(simulationTable.getColumnModel());
+
+               // Mouse listener to act on double-clicks
+               simulationTable.addMouseListener(new MouseAdapter() {
+                       @Override
+                       public void mouseClicked(MouseEvent e) {
+                               if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
+                                       int selected = simulationTable.getSelectedRow();
+                                       if (selected < 0)
+                                               return;
+                                       
+                                       selected = simulationTable.convertRowIndexToModel(selected);
+                                       simulationTable.clearSelection();
+                                       simulationTable.addRowSelectionInterval(selected, selected);
+                                       
+                                       openDialog(document.getSimulations().get(selected), 
+                                                       SimulationEditDialog.DEFAULT);
+                               }
+                       }
+               });
+                
+               
+               
+               
+               // Fire table change event when the rocket changes
+               document.getRocket().addComponentChangeListener(new ComponentChangeListener() {
+                       @Override
+                       public void componentChanged(ComponentChangeEvent e) {
+                               fireMaintainSelection();
+                       }
+               });
+               
+               
+               JScrollPane scrollpane = new JScrollPane(simulationTable);
+               this.add(scrollpane,"spanx, grow, wrap rel");
+               
+               
+       }
+       
+       
+       private void openDialog(final Simulation sim, int position) {
+               new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), sim, position)
+                       .setVisible(true);
+               fireMaintainSelection();
+       }
+       
+       private void fireMaintainSelection() {
+                  int[] selection = simulationTable.getSelectedRows();
+                  simulationTableModel.fireTableDataChanged();
+                  for (int row: selection) {
+                          simulationTable.addRowSelectionInterval(row, row);
+                  }
+       }
+       
+       
+       private class JLabelRenderer extends DefaultTableCellRenderer {
+
+               @Override
+               public Component getTableCellRendererComponent(JTable table,
+                               Object value, boolean isSelected, boolean hasFocus, int row,
+                               int column) {
+
+                       if (row < 0 || row >= document.getSimulationCount())
+                               return super.getTableCellRendererComponent(table, value, 
+                                               isSelected, hasFocus, row, column);
+                       
+                       // A JLabel is self-contained and has set its own tool tip
+                       if (value instanceof JLabel) {
+                               JLabel label = (JLabel)value;
+                               if (isSelected)
+                                       label.setBackground(table.getSelectionBackground());
+                               else
+                                       label.setBackground(table.getBackground());
+                               label.setOpaque(true);
+                               
+                               label.setToolTipText(getSimulationToolTip(document.getSimulation(row)));
+                               return label;
+                       }
+                       
+                       Component component = super.getTableCellRendererComponent(table, value, 
+                                       isSelected, hasFocus, row, column);
+                       
+                       if (component instanceof JComponent) {
+                               ((JComponent)component).setToolTipText(getSimulationToolTip(
+                                               document.getSimulation(row)));
+                       }
+                       return component;
+               }
+               
+               private String getSimulationToolTip(Simulation sim) {
+                       String tip;
+                       FlightData data = sim.getSimulatedData();
+                       
+                       tip = "<html><b>" + sim.getName() + "</b><br>";
+                       switch (sim.getStatus()) {
+                       case UPTODATE:
+                               tip += "<i>Up to date</i><br>";
+                               break;
+                               
+                       case LOADED:
+                               tip += "<i>Data loaded from a file</i><br>";
+                               break;
+                               
+                       case OUTDATED:
+                               tip += "<i><font color=\"red\">Data is out of date</font></i><br>";
+                               tip += "Click <i><b>Run simulations</b></i> to simulate.<br>";
+                               break;
+                               
+                       case EXTERNAL:
+                               tip += "<i>Imported data</i><br>";
+                               return tip;
+                               
+                       case NOT_SIMULATED:
+                               tip += "<i>Not simulated yet</i><br>";
+                               tip += "Click <i><b>Run simulations</b></i> to simulate.";
+                               return tip;
+                       }
+                       
+                       if (data == null) {
+                               tip += "No simulation data available.";
+                               return tip;
+                       }
+                       WarningSet warnings = data.getWarningSet();
+                       
+                       if (warnings.isEmpty()) {
+                               tip += "<font color=\"gray\">No warnings.</font>";
+                               return tip;
+                       }
+                       
+                       tip += "<font color=\"red\">Warnings:</font>";
+                       for (Warning w: warnings) {
+                               tip += "<br>" + w.toString();
+                       }
+
+                       return tip;
+               }
+               
+       }
+}
diff --git a/src/net/sf/openrocket/gui/main/SimulationRunDialog.java b/src/net/sf/openrocket/gui/main/SimulationRunDialog.java
new file mode 100644 (file)
index 0000000..fa98140
--- /dev/null
@@ -0,0 +1,479 @@
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.DetailDialog;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationLaunchException;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+
+public class SimulationRunDialog extends JDialog {
+       /** Update the dialog status every this many ms */
+       private static final long UPDATE_MS = 200;
+       
+       /** Flight progress at motor burnout */
+       private static final double BURNOUT_PROGRESS = 0.4;
+       
+       /** Flight progress at apogee */
+       private static final double APOGEE_PROGRESS = 0.7;
+       
+       
+       /*
+        * The executor service is not static since we want concurrent simulation
+        * dialogs to run in parallel, ie. they both have their own executor service.
+        */
+       private final ExecutorService executor = Executors.newFixedThreadPool(
+                       Prefs.getMaxThreadCount());
+       
+       
+       private final JLabel simLabel, timeLabel, altLabel, velLabel;
+       private final JProgressBar progressBar;
+       
+       
+       private final Simulation[] simulations;
+       private final SimulationWorker[] simulationWorkers;
+       private final SimulationStatus[] simulationStatuses;
+       private final double[] simulationMaxAltitude;
+       private final double[] simulationMaxVelocity;
+       private final boolean[] simulationDone;
+       
+       public SimulationRunDialog(Window window, Simulation ... simulations) {
+               super(window, "Running simulations...", Dialog.ModalityType.DOCUMENT_MODAL);
+               
+               if (simulations.length == 0) {
+                       throw new IllegalArgumentException("Called with no simulations to run");
+               }
+               
+               this.simulations = simulations;
+       
+               // Initialize the simulations
+               int n = simulations.length;
+               simulationWorkers = new SimulationWorker[n];
+               simulationStatuses = new SimulationStatus[n];
+               simulationMaxAltitude = new double[n];
+               simulationMaxVelocity = new double[n];
+               simulationDone = new boolean[n];
+               
+               for (int i=0; i<n; i++) {
+                       simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i);
+                       executor.execute(simulationWorkers[i]);
+               }
+               
+               // Build the dialog
+               JPanel panel = new JPanel(new MigLayout("fill", "[][grow]"));
+               
+               simLabel = new JLabel("Running ...");
+               panel.add(simLabel, "spanx, wrap para");
+               
+               panel.add(new JLabel("Simulation time: "), "gapright para");
+               timeLabel = new JLabel("");
+               panel.add(timeLabel, "growx, wrap rel");
+               
+               panel.add(new JLabel("Altitude: "));
+               altLabel = new JLabel("");
+               panel.add(altLabel, "growx, wrap rel");
+               
+               panel.add(new JLabel("Velocity: "));
+               velLabel = new JLabel("");
+               panel.add(velLabel, "growx, wrap para");
+               
+               progressBar = new JProgressBar();
+               panel.add(progressBar, "spanx, growx, wrap para");
+               
+               
+               // Add cancel button
+               JButton cancel = new JButton("Cancel");
+               cancel.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               cancelSimulations();
+                       }
+               });
+               panel.add(cancel, "spanx, tag cancel");
+               
+               
+               // Cancel simulations when user closes the window
+               this.addWindowListener(new WindowAdapter() {
+                       @Override
+                       public void windowClosing(WindowEvent e) {
+                               cancelSimulations();
+                       }
+               });
+               
+               
+               this.add(panel);
+               this.setMinimumSize(new Dimension(300, 0));
+               this.setLocationByPlatform(true);
+               this.validate();
+               this.pack();
+               GUIUtil.installEscapeCloseOperation(this);
+
+               updateProgress();
+       }
+       
+       
+       /**
+        * Cancel the currently running simulations.  This is equivalent to clicking
+        * the Cancel button on the dialog.
+        */
+       public void cancelSimulations() {
+               executor.shutdownNow();
+               for (SimulationWorker w: simulationWorkers) {
+                       w.cancel(true);
+               }
+       }
+       
+       
+       /**
+        * Static helper method to run simulations.
+        * 
+        * @param parent                the parent Window of the dialog to use.
+        * @param simulations   the simulations to run.
+        */
+       public static void runSimulations(Window parent, Simulation ... simulations) {
+               new SimulationRunDialog(parent, simulations).setVisible(true);
+       }
+       
+       
+       
+       
+       private void updateProgress() {
+               System.out.println("updateProgress() called");
+               int index;
+               for (index=0; index < simulations.length; index++) {
+                       if (!simulationDone[index])
+                               break;
+               }
+               
+               if (index >= simulations.length) {
+                       // Everything is done, close the dialog
+                       System.out.println("Everything done.");
+                       this.dispose();
+                       return;
+               }
+
+               // Update the progress bar status
+               int progress = 0;
+               for (SimulationWorker s: simulationWorkers) {
+                       progress += s.getProgress();
+               }
+               progress /= simulationWorkers.length;
+               progressBar.setValue(progress);
+               System.out.println("Progressbar value "+progress);
+               
+               // Update the simulation fields
+               simLabel.setText("Running " + simulations[index].getName());
+               if (simulationStatuses[index] == null) {
+                       timeLabel.setText("");
+                       altLabel.setText("");
+                       velLabel.setText("");
+                       System.out.println("Empty labels, how sad.");
+                       return;
+               }
+               
+               Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
+               timeLabel.setText(u.toStringUnit(simulationStatuses[index].time));
+               
+               u = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
+               altLabel.setText(u.toStringUnit(simulationStatuses[index].position.z) + " (max. " +
+                               u.toStringUnit(simulationMaxAltitude[index]) + ")");
+               
+               u = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
+               velLabel.setText(u.toStringUnit(simulationStatuses[index].velocity.z) + " (max. " +
+                               u.toStringUnit(simulationMaxVelocity[index]) + ")");
+               System.out.println("Set interesting labels.");
+       }
+
+       
+       
+       /**
+        * A SwingWorker that performs a flight simulation.  It periodically updates the
+        * simulation statuses of the parent class and calls updateProgress().
+        * The progress of the simulation is stored in the progress property of the
+        * SwingWorker.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       private class InteractiveSimulationWorker extends SimulationWorker {
+
+               private final int index;
+               private final double burnoutTimeEstimate;
+               private volatile double burnoutVelocity;
+               private volatile double apogeeAltitude;
+               
+               /*
+                * -2 = time from 0 ... burnoutTimeEstimate
+                * -1 = velocity from v(burnoutTimeEstimate) ... 0
+                *  0 ... n = stages from alt(max) ... 0
+                */
+               private volatile int simulationStage = -2;
+               
+               private int progress = 0;
+               
+               
+               public InteractiveSimulationWorker(Simulation sim, int index) {
+                       super(sim);
+                       this.index = index;
+
+                       // Calculate estimate of motor burn time
+                       double launchBurn = 0;
+                       double otherBurn = 0;
+                       Configuration config = simulation.getConfiguration();
+                       String id = simulation.getConditions().getMotorConfigurationID();
+                       Iterator<MotorMount> iterator = config.motorIterator();
+                       while (iterator.hasNext()) {
+                               MotorMount m = iterator.next();
+                               if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH)
+                                       launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getTotalTime());
+                               else
+                                       otherBurn = otherBurn + m.getMotor(id).getTotalTime();
+                       }
+                       burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1);
+                       
+               }
+
+
+               /**
+                * Return the extra listeners to use, a progress listener and cancel listener.
+                */
+               @Override
+               protected SimulationListener[] getExtraListeners() {
+                       return new SimulationListener[] {
+                                       new SimulationProgressListener()
+                       };
+               }
+
+               
+               /**
+                * Processes simulation statuses published by the simulation listener.
+                * The statuses of the parent class and the progress property are updated.
+                */
+               @Override
+               protected void process(List<SimulationStatus> chunks) {
+                       
+                       // Update max. altitude and velocity
+                       for (SimulationStatus s: chunks) {
+                               simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index], 
+                                               s.position.z);
+                               simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index], 
+                                               s.velocity.length());
+                       }
+
+                       // Calculate the progress
+                       SimulationStatus status = chunks.get(chunks.size()-1);
+                       simulationStatuses[index] = status;
+
+                       // 1. time = 0 ... burnoutTimeEstimate
+                       if (simulationStage == -2 && status.time < burnoutTimeEstimate) {
+                               System.out.println("Method 1:  t="+status.time + "  est="+burnoutTimeEstimate);
+                               setSimulationProgress(MathUtil.map(status.time, 0, burnoutTimeEstimate, 
+                                               0.0, BURNOUT_PROGRESS));
+                               updateProgress();
+                               return;
+                       }
+                       
+                       if (simulationStage == -2) {
+                               simulationStage++;
+                               burnoutVelocity = MathUtil.max(status.velocity.z, 0.1);
+                               System.out.println("CHANGING to Method 2, vel="+burnoutVelocity);
+                       }
+                       
+                       // 2. z-velocity from burnout velocity to zero
+                       if (simulationStage == -1 && status.velocity.z >= 0) {
+                               System.out.println("Method 2:  vel="+status.velocity.z + " burnout=" +
+                                               burnoutVelocity);
+                               setSimulationProgress(MathUtil.map(status.velocity.z, burnoutVelocity, 0,
+                                               BURNOUT_PROGRESS, APOGEE_PROGRESS));
+                               updateProgress();
+                               return;
+                       }
+                       
+                       if (simulationStage == -1 && status.velocity.z < 0) {
+                               simulationStage++;
+                               apogeeAltitude = status.position.z;
+                       }
+                       
+                       // 3. z-position from apogee to zero
+                       // TODO: MEDIUM: several stages
+                       System.out.println("Method 3:  alt="+status.position.z +"  apogee="+apogeeAltitude);
+                       setSimulationProgress(MathUtil.map(status.position.z, 
+                                       apogeeAltitude, 0, APOGEE_PROGRESS, 1.0));
+                       updateProgress();
+               }
+               
+               /**
+                * Marks this simulation as done and calls the progress update.
+                */
+               @Override
+               protected void simulationDone() {
+                       simulationDone[index] = true;
+                       System.out.println("DONE, setting progress");
+                       setSimulationProgress(1.0);
+                       updateProgress();
+               }
+               
+               
+               /**
+                * Marks the simulation as done and shows a dialog presenting
+                * the error, unless the simulation was cancelled.
+                */
+               @Override
+               protected void simulationInterrupted(Throwable t) {
+                       
+                       if (t instanceof SimulationCancelledException) {
+                               simulationDone();
+                               return;  // Ignore cancellations
+                       }
+                       
+                       // Retrieve the stack trace in a textual form
+                       CharArrayWriter arrayWriter = new CharArrayWriter();
+                       arrayWriter.append(t.toString() + "\n" + "\n");
+                       t.printStackTrace(new PrintWriter(arrayWriter));
+                       String stackTrace = arrayWriter.toString();
+                       
+                       // Analyze the exception type
+                       if (t instanceof SimulationLaunchException) {
+                               
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, 
+                                               new Object[] {
+                                               "Unable to simulate:",
+                                               t.getMessage()
+                                               },
+                                               null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       } else if (t instanceof SimulationException) {
+                               
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, 
+                                               new Object[] {
+                                               "A error occurred during the simulation:",
+                                               t.getMessage()
+                                               }, 
+                                               stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       } else if (t instanceof Exception) {
+                               
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, 
+                                               new Object[] {
+                                               "An exception occurred during the simulation:",
+                                               t.getMessage(),
+                                               simulation.getSimulationListeners().isEmpty() ? 
+                                               "Please report this as a bug along with the details below." : ""
+                                               }, 
+                                               stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       } else if (t instanceof AssertionError) {
+                               
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, 
+                                               new Object[] {
+                                                       "A computation error occurred during the simulation.",
+                                                       "Please report this as a bug along with the details below."
+                                               }, 
+                                               stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       } else {
+                               
+                               // Probably an Error
+                               DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, 
+                                               new Object[] {
+                                                       "An unknown error was encountered during the simulation.",
+                                                       "The program may be unstable, you should save all your designs " +
+                                                       "and restart OpenRocket now!"
+                                               }, 
+                                               stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+                               
+                       }
+                       simulationDone();
+               }
+               
+               
+
+               private void setSimulationProgress(double p) {
+                       progress = Math.max(progress, (int)(100*p+0.5));
+                       progress = MathUtil.clamp(progress, 0, 100);
+                       System.out.println("Setting progress to "+progress+ " (real " + 
+                                       ((int)(100*p+0.5)) + ")");
+                       super.setProgress(progress);
+               }
+
+
+               /**
+                * A simulation listener that regularly updates the progress property of the 
+                * SimulationWorker and publishes the simulation status for the run dialog to process.
+                * 
+                * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+                */
+               private class SimulationProgressListener extends AbstractSimulationListener {
+                       private long time = 0;
+
+                       @Override
+                       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                                       SimulationStatus status) {
+                               
+                               switch (event.getType()) {
+                               case APOGEE:
+                                       simulationStage = 0;
+                                       apogeeAltitude = status.position.z;
+                                       System.out.println("APOGEE, setting progress");
+                                       setSimulationProgress(APOGEE_PROGRESS);
+                                       publish(status);
+                                       break;
+                                       
+                               case LAUNCH:
+                                       publish(status);
+                                       break;
+                                       
+                               case SIMULATION_END:
+                                       System.out.println("END, setting progress");
+                                       setSimulationProgress(1.0);
+                                       break;
+                               }
+                               return null;
+                       }
+
+                       @Override
+                       public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+                               if (System.currentTimeMillis() >= time + UPDATE_MS) {
+                                       time = System.currentTimeMillis();
+                                       publish(status);
+                               }
+                               return null;
+                       }
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/main/SimulationWorker.java b/src/net/sf/openrocket/gui/main/SimulationWorker.java
new file mode 100644 (file)
index 0000000..e278da5
--- /dev/null
@@ -0,0 +1,129 @@
+package net.sf.openrocket.gui.main;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.swing.SwingWorker;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+
+
+
+/**
+ * A SwingWorker that runs a simulation in a background thread.  The simulation
+ * always includes a listener that checks whether this SwingWorked has been cancelled,
+ * and throws a {@link SimulationCancelledException} if it has.  This allows the
+ * {@link #cancel(boolean)} method to be used to cancel the simulation.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class SimulationWorker extends SwingWorker<FlightData, SimulationStatus> {
+
+       protected final Simulation simulation;
+       private Throwable throwable = null;
+
+       public SimulationWorker(Simulation sim) {
+               this.simulation = sim;
+       }
+
+
+       /**
+        * Runs the simulation.
+        */
+       @Override
+       protected FlightData doInBackground() {
+               if (isCancelled()) {
+                       throwable = new SimulationCancelledException("The simulation was interrupted.");
+                       return null;
+               }
+               
+               SimulationListener[] listeners = getExtraListeners();
+               
+               if (listeners != null) {
+                       listeners = Arrays.copyOf(listeners, listeners.length+1);
+               } else {
+                       listeners = new SimulationListener[1];
+               }
+               
+               listeners[listeners.length-1] = new CancelListener();
+               
+               try {
+                       simulation.simulate(listeners);
+               } catch (Throwable e) {
+//                     System.out.println("Simulation interrupted:");
+//                     e.printStackTrace();
+                       throwable = e;
+                       return null;
+               }
+               return simulation.getSimulatedData();
+       }
+       
+       
+       /**
+        * Return additional listeners to use during the simulation.  The default
+        * implementation returns an empty array.
+        * 
+        * @return      additional listeners to use, or <code>null</code>.
+        */
+       protected SimulationListener[] getExtraListeners() {
+               return new SimulationListener[0];
+       }
+
+       
+       /**
+        * Called after a simulation is successfully simulated.  This method is not
+        * called if the simulation ends in an exception.
+        * 
+        * @param sim   the simulation including the flight data
+        */
+       protected abstract void simulationDone();
+       
+       /**
+        * Called if the simulation is interrupted due to an exception.
+        * 
+        * @param t             the Throwable that caused the interruption
+        */
+       protected abstract void simulationInterrupted(Throwable t);
+
+       
+       
+       /**
+        * Marks this simulation as done and calls the progress update.
+        */
+       @Override
+       protected final void done() {
+               if (throwable == null)
+                       simulationDone();
+               else 
+                       simulationInterrupted(throwable);
+       }
+       
+
+
+       /**
+        * A simulation listener that throws a {@link SimulationCancelledException} if
+        * this SwingWorker has been cancelled.  The conditions is checked every time a step
+        * is taken.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       private class CancelListener extends AbstractSimulationListener {
+
+               @Override
+               public Collection<FlightEvent> stepTaken(SimulationStatus status) 
+               throws SimulationCancelledException {
+
+                       if (isCancelled()) {
+                               throw new SimulationCancelledException("The simulation was interrupted.");
+                       }
+
+                       return null;
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/plot/Axis.java b/src/net/sf/openrocket/gui/plot/Axis.java
new file mode 100644 (file)
index 0000000..98f8a41
--- /dev/null
@@ -0,0 +1,52 @@
+package net.sf.openrocket.gui.plot;
+
+public class Axis implements Cloneable {
+
+       private double minValue = Double.NaN;
+       private double maxValue = Double.NaN;
+       
+       
+       
+       public void addBound(double value) {
+               
+               if (value < minValue  ||  Double.isNaN(minValue)) {
+                       minValue = value;
+               }
+               if (value > maxValue  ||  Double.isNaN(maxValue)) {
+                       maxValue = value;
+               }
+               
+       }
+       
+       
+       public double getMinValue() {
+               return minValue;
+       }
+       
+       public double getMaxValue() {
+               return maxValue;
+       }
+       
+       public double getRangeLength() {
+               return maxValue - minValue;
+       }
+       
+       public void reset() {
+               minValue = Double.NaN;
+               maxValue = Double.NaN;
+       }
+       
+       
+       
+       @Override
+       public Axis clone() {
+               try {
+                       
+                       return (Axis) super.clone();
+                       
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("BUG! Could not clone().");
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/plot/PlotConfiguration.java b/src/net/sf/openrocket/gui/plot/PlotConfiguration.java
new file mode 100644 (file)
index 0000000..a22daf4
--- /dev/null
@@ -0,0 +1,665 @@
+package net.sf.openrocket.gui.plot;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightDataBranch.Type;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+
+
+public class PlotConfiguration implements Cloneable {
+       
+       public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
+       static {
+               ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
+               PlotConfiguration config;
+               
+               config = new PlotConfiguration("Vertical motion vs. time");
+               config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
+               config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
+               configs.add(config);
+               
+               config = new PlotConfiguration("Total motion vs. time");
+               config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_TOTAL);
+               config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
+               configs.add(config);
+               
+               config = new PlotConfiguration("Flight side profile", FlightDataBranch.TYPE_POSITION_X);
+               config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
+               configs.add(config);
+
+               config = new PlotConfiguration("Stability vs. time");
+               config.addPlotDataType(FlightDataBranch.TYPE_STABILITY, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_CP_LOCATION, 1);
+               config.addPlotDataType(FlightDataBranch.TYPE_CG_LOCATION, 1);
+               configs.add(config);
+               
+               config = new PlotConfiguration("Drag coefficients vs. Mach number", 
+                               FlightDataBranch.TYPE_MACH_NUMBER);
+               config.addPlotDataType(FlightDataBranch.TYPE_DRAG_COEFF, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_BASE_DRAG_COEFF, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, 0);
+               configs.add(config);
+
+               config = new PlotConfiguration("Roll characteristics");
+               config.addPlotDataType(FlightDataBranch.TYPE_ROLL_RATE, 0);
+               config.addPlotDataType(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, 1);
+               config.addPlotDataType(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, 1);
+               config.addPlotDataType(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, 1);
+               configs.add(config);
+
+               config = new PlotConfiguration("Simulation time step and computation time");
+               config.addPlotDataType(FlightDataBranch.TYPE_TIME_STEP);
+               config.addPlotDataType(FlightDataBranch.TYPE_COMPUTATION_TIME);
+               configs.add(config);
+
+               DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
+       }
+       
+       
+       
+       /** Bonus given for the first type being on the first axis */
+       private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;
+
+       /**
+        * Bonus given if the first axis includes zero (to prefer first axis having zero over 
+        * the others) 
+        */
+       private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
+       
+       /** Bonus given for a common zero point on left and right axes. */
+       private static final double BONUS_COMMON_ZERO = 40.0;
+       
+       /** Bonus given for only using a single axis. */
+       private static final double BONUS_ONLY_ONE_AXIS = 50.0;
+       
+       
+       private static final double INCLUDE_ZERO_DISTANCE = 0.3;  // 30% of total range
+       
+       
+
+       /** The data types to be plotted. */
+       private ArrayList<FlightDataBranch.Type> plotDataTypes = new ArrayList<FlightDataBranch.Type>();
+       
+       private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
+       
+       /** The corresponding Axis on which they will be plotted, or null to auto-select. */
+       private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
+
+       
+       /** The domain (x) axis. */
+       private FlightDataBranch.Type domainAxisType = null;
+       private Unit domainAxisUnit = null;
+       
+       
+       /** All available axes. */
+       private final int axesCount;
+       private ArrayList<Axis> allAxes = new ArrayList<Axis>();
+       
+       
+       
+       private String name = null;
+       
+       
+       
+       public PlotConfiguration() {
+               this(null, FlightDataBranch.TYPE_TIME);
+       }
+       
+       public PlotConfiguration(String name) {
+               this(name, FlightDataBranch.TYPE_TIME);
+       }
+       
+       public PlotConfiguration(String name, FlightDataBranch.Type domainType) {
+               this.name = name;
+               // Two axes
+               allAxes.add(new Axis());
+               allAxes.add(new Axis());
+               axesCount = 2;
+               
+               setDomainAxisType(domainType);
+       }
+       
+       
+       
+       
+       
+       public FlightDataBranch.Type getDomainAxisType() {
+               return domainAxisType;
+       }
+       
+       public void setDomainAxisType(FlightDataBranch.Type type) {
+               boolean setUnit;
+               
+               if (domainAxisType != null  &&  domainAxisType.getUnitGroup() == type.getUnitGroup())
+                       setUnit = false;
+               else
+                       setUnit = true;
+               
+               domainAxisType = type;
+               if (setUnit)
+                       domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
+       }
+       
+       public Unit getDomainAxisUnit() {
+               return domainAxisUnit;
+       }
+       
+       public void setDomainAxisUnit(Unit u) {
+               if (!domainAxisType.getUnitGroup().contains(u)) {
+                       throw new IllegalArgumentException("Setting unit "+u+" to type "+domainAxisType);
+               }
+               domainAxisUnit = u;
+       }
+       
+       
+       
+       public void addPlotDataType(FlightDataBranch.Type type) {
+               plotDataTypes.add(type);
+               plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
+               plotDataAxes.add(-1);
+       }
+
+       public void addPlotDataType(FlightDataBranch.Type type, int axis) {
+               if (axis >= axesCount) {
+                       throw new IllegalArgumentException("Axis index too large");
+               }
+               plotDataTypes.add(type);
+               plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
+               plotDataAxes.add(axis);
+       }
+
+
+       
+       
+       public void setPlotDataType(int index, FlightDataBranch.Type type) {
+               FlightDataBranch.Type origType = plotDataTypes.get(index);
+               plotDataTypes.set(index, type);
+               
+               if (origType.getUnitGroup() != type.getUnitGroup()) {
+                       plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
+               }
+       }
+       
+       public void setPlotDataUnit(int index, Unit unit) {
+               if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
+                       throw new IllegalArgumentException("Attempting to set unit "+unit+" to group "
+                                       + plotDataTypes.get(index).getUnitGroup());
+               }
+               plotDataUnits.set(index, unit);
+       }
+       
+       public void setPlotDataAxis(int index, int axis) {
+               if (axis >= axesCount) {
+                       throw new IllegalArgumentException("Axis index too large");
+               }
+               plotDataAxes.set(index, axis);
+       }
+       
+       
+       public void setPlotDataType(int index, FlightDataBranch.Type type, Unit unit, int axis) {
+               if (axis >= axesCount) {
+                       throw new IllegalArgumentException("Axis index too large");
+               }
+               plotDataTypes.set(index, type);
+               plotDataUnits.set(index, unit);
+               plotDataAxes.set(index, axis);
+       }
+       
+       public void removePlotDataType(int index) {
+               plotDataTypes.remove(index);
+               plotDataUnits.remove(index);
+               plotDataAxes.remove(index);
+       }
+       
+       
+       
+       public FlightDataBranch.Type getType (int index) {
+               return plotDataTypes.get(index);
+       }
+       public Unit getUnit(int index) {
+               return plotDataUnits.get(index);
+       }
+       public int getAxis(int index) {
+               return plotDataAxes.get(index);
+       }
+       
+       public int getTypeCount() {
+               return plotDataTypes.size();
+       }
+       
+       
+       
+       public List<Axis> getAllAxes() {
+               List<Axis> list = new ArrayList<Axis>();
+               list.addAll(allAxes);
+               return list;
+       }
+       
+       
+       public String getName() {
+               return name;
+       }
+       
+       public void setName(String name) {
+               this.name = name;
+       }
+       
+       /**
+        * Returns the name of this PlotConfiguration.
+        */
+       @Override
+       public String toString() {
+               return name;
+       }
+       
+       
+       
+       /**
+        * Find the best combination of the auto-selectable axes.
+        * 
+        * @return      a new PlotConfiguration with the best fitting auto-selected axes and
+        *                      axes ranges selected.
+        */
+       public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
+               PlotConfiguration config = recursiveFillAutoAxes(data).getU();
+               System.out.println("BEST FOUND, fitting");
+               config.fitAxes(data);
+               return config;
+       }
+       
+       
+       
+       
+       /**
+        * Recursively search for the best combination of the auto-selectable axes.
+        * This is a brute-force search method.
+        * 
+        * @return      a new PlotConfiguration with the best fitting auto-selected axes and
+        *                      axes ranges selected, and the goodness value
+        */
+       private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
+               
+               // Create copy to fill in
+               PlotConfiguration copy = this.clone();
+               
+               int autoindex; 
+               for (autoindex=0; autoindex < plotDataAxes.size(); autoindex++) {
+                       if (plotDataAxes.get(autoindex) < 0)
+                               break;
+               }
+               
+               
+               if (autoindex >= plotDataAxes.size()) {
+                       // All axes have been assigned, just return since we are already the best
+                       return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
+               }
+               
+               
+               // Set the auto-selected index one at a time and choose the best one
+               PlotConfiguration best = null;
+               double bestValue = Double.NEGATIVE_INFINITY;
+               for (int i=0; i < axesCount; i++) {
+                       copy.plotDataAxes.set(autoindex, i);
+                       Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
+                       if (result.getV() > bestValue) {
+                               best = result.getU();
+                               bestValue = result.getV();
+                       }
+               }
+               
+               return new Pair<PlotConfiguration, Double>(best, bestValue);
+       }
+       
+       
+       
+       
+       
+       /**
+        * Fit the axes to hold the provided data.  All of the plotDataAxis elements must
+        * be non-negative.
+        * <p>
+        * NOTE: This method assumes that only two axes are used.
+        */
+       protected void fitAxes(FlightDataBranch data) {
+               
+               // Reset axes
+               for (Axis a: allAxes) {
+                       a.reset();
+               }
+               
+               // Add full range to the axes
+               int length = plotDataTypes.size();
+               for (int i=0; i<length; i++) {
+                       FlightDataBranch.Type type = plotDataTypes.get(i);
+                       Unit unit = plotDataUnits.get(i);
+                       int index = plotDataAxes.get(i);
+                       if (index < 0) {
+                               throw new IllegalStateException("fitAxes called with auto-selected axis");
+                       }
+                       Axis axis = allAxes.get(index);
+                       
+                       double min = unit.toUnit(data.getMinimum(type));
+                       double max = unit.toUnit(data.getMaximum(type));
+
+                       axis.addBound(min);
+                       axis.addBound(max);
+               }
+               
+               // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
+               for (Axis a: allAxes) {
+                       if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
+                               a.addBound(a.getMinValue()-1);
+                               a.addBound(a.getMaxValue()+1);
+                       }
+                       
+                       double addition = a.getRangeLength() * 0.03;
+                       a.addBound(a.getMinValue() - addition);
+                       a.addBound(a.getMaxValue() + addition);
+                       
+                       double dist;
+                       dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
+                       if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
+                               a.addBound(0);
+                       }
+               }
+
+               
+               // Check whether to use a common zero
+               Axis left = allAxes.get(0);
+               Axis right = allAxes.get(1);
+               
+               if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
+                               right.getMinValue() > 0 || right.getMaxValue() < 0 ||
+                               Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
+                       return;
+               
+               
+               
+               //// Compute common zero
+               // TODO: MEDIUM: This algorithm may require tweaking
+
+               double min1 = left.getMinValue();
+               double max1 = left.getMaxValue();
+               double min2 = right.getMinValue();
+               double max2 = right.getMaxValue();
+               
+               // Calculate and round scaling factor
+               double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
+                                               Math.min(left.getRangeLength(), right.getRangeLength());
+               
+               System.out.println("Scale: "+scale);
+               
+               scale = roundScale(scale);
+               if (right.getRangeLength() > left.getRangeLength()) {
+                       scale = 1/scale;
+               }
+               System.out.println("Rounded scale: " + scale);
+
+               // Scale right axis, enlarge axes if necessary and scale back
+               min2 *= scale;
+               max2 *= scale;
+               min1 = Math.min(min1, min2);
+               min2 = min1;
+               max1 = Math.max(max1, max2);
+               max2 = max1;
+               min2 /= scale;
+               max2 /= scale;
+               
+               
+               
+               // Scale to unit length
+//             double scale1 = left.getRangeLength();
+//             double scale2 = right.getRangeLength();
+//             
+//             double min1 = left.getMinValue() / scale1;
+//             double max1 = left.getMaxValue() / scale1;
+//             double min2 = right.getMinValue() / scale2;
+//             double max2 = right.getMaxValue() / scale2;
+//             
+//             // Combine unit ranges
+//             min1 = MathUtil.min(min1, min2);
+//             min2 = min1;
+//             max1 = MathUtil.max(max1, max2);
+//             max2 = max1;
+//             
+//             // Scale up
+//             min1 *= scale1;
+//             max1 *= scale1;
+//             min2 *= scale2;
+//             max2 *= scale2;
+//             
+//             // Compute common scale
+//             double range1 = max1-min1;
+//             double range2 = max2-min2;
+//             
+//             double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
+//             double roundScale = roundScale(scale);
+//
+//             if (range2 < range1) {
+//                     if (roundScale < scale) {
+//                             min2 = min1 / roundScale;
+//                             max2 = max1 / roundScale;
+//                     } else {
+//                             min1 = min2 * roundScale;
+//                             max1 = max2 * roundScale;
+//                     }
+//             } else {
+//                     if (roundScale > scale) {
+//                             min2 = min1 * roundScale;
+//                             max2 = max1 * roundScale;
+//                     } else {
+//                             min1 = min2 / roundScale;
+//                             max1 = max2 / roundScale;
+//                     }
+//             }
+               
+               // Apply scale
+               left.addBound(min1);
+               left.addBound(max1);
+               right.addBound(min2);
+               right.addBound(max2);
+               
+       }
+       
+       
+
+       private double roundScale(double scale) {
+               double mul = 1;
+               while (scale >= 10) {
+                       scale /= 10;
+                       mul *= 10;
+               }
+               while (scale < 1) {
+                       scale *= 10;
+                       mul /= 10;
+               }
+               
+               // 1 2 4 5 10
+               
+               if (scale > 7.5) {
+                       scale = 10;
+               } else if (scale > 4.5) {
+                       scale = 5;
+               } else if (scale > 3) {
+                       scale = 4;
+               } else if (scale > 1.5) {
+                       scale = 2;
+               } else {
+                       scale = 1;
+               }
+               return scale*mul;
+       }
+       
+       
+       
+       private double roundScaleUp(double scale) {
+               double mul = 1;
+               while (scale >= 10) {
+                       scale /= 10;
+                       mul *= 10;
+               }
+               while (scale < 1) {
+                       scale *= 10;
+                       mul /= 10;
+               }
+               
+               if (scale > 5) {
+                       scale = 10;
+               } else if (scale > 4) {
+                       scale = 5;
+               } else if (scale > 2) {
+                       scale = 4;
+               } else if (scale > 1) {
+                       scale = 2;
+               } else {
+                       scale = 1;
+               }
+               return scale*mul;
+       }
+       
+
+       private double roundScaleDown(double scale) {
+               double mul = 1;
+               while (scale >= 10) {
+                       scale /= 10;
+                       mul *= 10;
+               }
+               while (scale < 1) {
+                       scale *= 10;
+                       mul /= 10;
+               }
+               
+               if (scale > 5) {
+                       scale = 5;
+               } else if (scale > 4) {
+                       scale = 4;
+               } else if (scale > 2) {
+                       scale = 2;
+               } else {
+                       scale = 1;
+               }
+               return scale*mul;
+       }
+       
+       
+       
+       /**
+        * Fits the axis ranges to the data and returns the "goodness value" of this 
+        * selection of axes.  All plotDataAxis elements must be non-null.
+        * <p>
+        * NOTE: This method assumes that all data can fit into the axes ranges and
+        * that only two axes are used.
+        *
+        * @return      a "goodness value", the larger the better.
+        */
+       protected double getGoodnessValue(FlightDataBranch data) {
+               double goodness = 0;
+               int length = plotDataTypes.size();
+               
+               // Fit the axes ranges to the data
+               fitAxes(data);
+               
+               /*
+                * Calculate goodness of ranges.  100 points is given if the values fill the
+                * entire range, 0 if they fill none of it.
+                */
+               for (int i = 0; i < length; i++) {
+                       FlightDataBranch.Type type = plotDataTypes.get(i);
+                       Unit unit = plotDataUnits.get(i);
+                       int index = plotDataAxes.get(i);
+                       if (index < 0) {
+                               throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
+                       }
+                       Axis axis = allAxes.get(index);
+                       
+                       double min = unit.toUnit(data.getMinimum(type));
+                       double max = unit.toUnit(data.getMaximum(type));
+                       if (Double.isNaN(min) || Double.isNaN(max))
+                               continue;
+                       if (MathUtil.equals(min, max))
+                               continue;
+                       
+                       double d = (max-min) / axis.getRangeLength();
+                       d = Math.sqrt(d);  // Prioritize small ranges
+                       goodness += d * 100.0;
+               }
+               
+               
+               /*
+                * Add extra points for specific things.
+                */
+
+               // A little for the first type being on the first axis
+               if (plotDataAxes.get(0) == 0)
+                       goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
+               
+               // A little bonus if the first axis contains zero
+               Axis left = allAxes.get(0);
+               if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
+                       goodness += BONUS_FIRST_AXIS_HAS_ZERO;
+               
+               // A boost if a common zero was used in the ranging
+               Axis right = allAxes.get(1);
+               if (left.getMinValue() <= 0 &&  left.getMaxValue() >= 0 &&
+                               right.getMinValue() <= 0 && right.getMaxValue() >= 0)
+                       goodness += BONUS_COMMON_ZERO;
+               
+               // A boost if only one axis is used
+               if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
+                       goodness += BONUS_ONLY_ONE_AXIS;
+               
+               return goodness;
+       }
+       
+       
+       
+       /**
+        * Reset the units of this configuration to the default units. Returns this
+        * PlotConfiguration.
+        * 
+        * @return   this PlotConfiguration.
+        */
+       public PlotConfiguration resetUnits() {
+               for (int i=0; i < plotDataTypes.size(); i++) {
+                       plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
+               }
+               return this;
+       }
+       
+       
+       
+       
+       @SuppressWarnings("unchecked")
+       @Override
+       public PlotConfiguration clone() {
+               try {
+                       
+                       PlotConfiguration copy = (PlotConfiguration) super.clone();
+                       
+                       // Shallow-clone all immutable lists
+                       copy.plotDataTypes = (ArrayList<Type>) this.plotDataTypes.clone();
+                       copy.plotDataAxes = (ArrayList<Integer>) this.plotDataAxes.clone();
+                       copy.plotDataUnits = (ArrayList<Unit>) this.plotDataUnits.clone();
+                       
+                       // Deep-clone all Axis since they are mutable
+                       copy.allAxes = new ArrayList<Axis>();
+                       for (Axis a: this.allAxes) {
+                               copy.allAxes.add(a.clone());
+                       }
+                       
+                       return copy;
+                       
+                       
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("BUG! Could not clone().");
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/plot/PlotDialog.java b/src/net/sf/openrocket/gui/plot/PlotDialog.java
new file mode 100644 (file)
index 0000000..e08525d
--- /dev/null
@@ -0,0 +1,201 @@
+package net.sf.openrocket.gui.plot;
+
+import java.awt.Color;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+public class PlotDialog extends JDialog {
+       
+       private PlotDialog(Window parent, Simulation simulation, PlotConfiguration config) {
+               super(parent, "Flight data plot");
+               this.setModalityType(ModalityType.DOCUMENT_MODAL);
+               
+               
+               // Fill the auto-selections
+               FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
+               PlotConfiguration filled = config.fillAutoAxes(branch);
+               List<Axis> axes = filled.getAllAxes();
+
+
+               // Create the data series for both axes
+               XYSeriesCollection[] data = new XYSeriesCollection[2];
+               data[0] = new XYSeriesCollection();
+               data[1] = new XYSeriesCollection();
+               
+               
+               // Get the domain axis type
+               final FlightDataBranch.Type domainType = filled.getDomainAxisType();
+               final Unit domainUnit = filled.getDomainAxisUnit();
+               if (domainType == null) {
+                       throw new IllegalArgumentException("Domain axis type not specified.");
+               }
+               List<Double> x = branch.get(domainType);
+
+               
+               // Create the XYSeries objects from the flight data and store into the collections
+               int length = filled.getTypeCount();
+               String[] axisLabel = new String[2];
+               for (int i = 0; i < length; i++) {
+                       // Get info
+                       FlightDataBranch.Type type = filled.getType(i);
+                       Unit unit = filled.getUnit(i);
+                       int axis = filled.getAxis(i);
+                       String name = getLabel(type, unit);
+                       
+                       // Store data in provided units
+                       List<Double> y = branch.get(type);
+                       XYSeries series = new XYSeries(name, false, true);
+                       for (int j=0; j<x.size(); j++) {
+                               series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
+                       }
+                       data[axis].addSeries(series);
+
+                       // Update axis label
+                       if (axisLabel[axis] == null)
+                               axisLabel[axis] = type.getName();
+                       else
+                               axisLabel[axis] += "; " + type.getName();
+               }
+               
+               
+               // Create the chart using the factory to get all default settings
+        JFreeChart chart = ChartFactory.createXYLineChart(
+            "Simulated flight",
+            null, 
+            null, 
+            null,
+            PlotOrientation.VERTICAL,
+            true,
+            true,
+            false
+        );
+               
+        chart.addSubtitle(new TextTitle(config.getName()));
+        
+               // Add the data and formatting to the plot
+               XYPlot plot = chart.getXYPlot();
+               int axisno = 0;
+               for (int i=0; i<2; i++) {
+                       // Check whether axis has any data
+                       if (data[i].getSeriesCount() > 0) {
+                               // Create and set axis
+                               double min = axes.get(i).getMinValue();
+                               double max = axes.get(i).getMaxValue();
+                               NumberAxis axis = new PresetNumberAxis(min, max);
+                               axis.setLabel(axisLabel[i]);
+//                             axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
+                               plot.setRangeAxis(axisno, axis);
+                               
+                               // Add data and map to the axis
+                               plot.setDataset(axisno, data[i]);
+                               plot.setRenderer(axisno, new StandardXYItemRenderer());
+                               plot.mapDatasetToRangeAxis(axisno, axisno);
+                               axisno++;
+                       }
+               }
+               
+               plot.getDomainAxis().setLabel(getLabel(domainType,domainUnit));
+               plot.addDomainMarker(new ValueMarker(0));
+               plot.addRangeMarker(new ValueMarker(0));
+               
+               
+               // Create the dialog
+               
+               JPanel panel = new JPanel(new MigLayout("fill"));
+               this.add(panel);
+               
+               ChartPanel chartPanel = new ChartPanel(chart,
+                               false, // properties
+                               true,  // save
+                               false, // print
+                               true,  // zoom
+                               true); // tooltips
+               chartPanel.setMouseWheelEnabled(true);
+               chartPanel.setEnforceFileExtensions(true);
+               chartPanel.setInitialDelay(500);
+               
+               chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+               
+               panel.add(chartPanel, "grow, wrap 20lp");
+
+               JButton button = new JButton("Close");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               PlotDialog.this.dispose();
+                       }
+               });
+               panel.add(button, "right");
+
+               this.setLocationByPlatform(true);
+               this.pack();
+               GUIUtil.installEscapeCloseOperation(this);
+               GUIUtil.setDefaultButton(button);
+       }
+       
+       
+       private String getLabel(FlightDataBranch.Type type, Unit unit) {
+               String name = type.getName();
+               if (unit != null  &&  !UnitGroup.UNITS_NONE.contains(unit)  &&
+                               !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
+                       name += " ("+unit.getUnit() + ")";
+               return name;
+       }
+       
+
+       
+       private class PresetNumberAxis extends NumberAxis {
+               private final double min;
+               private final double max;
+               
+               public PresetNumberAxis(double min, double max) {
+                       this.min = min;
+                       this.max = max;
+                       autoAdjustRange();
+               }
+               
+               @Override
+               protected void autoAdjustRange() {
+                       this.setRange(min, max);
+               }
+       }
+       
+       
+       /**
+        * Static method that shows a plot with the specified parameters.
+        * 
+        * @param parent                the parent window, which will be blocked.
+        * @param simulation    the simulation to plot.
+        * @param config                the configuration of the plot.
+        */
+       public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
+               new PlotDialog(parent, simulation, config).setVisible(true);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/gui/plot/PlotPanel.java b/src/net/sf/openrocket/gui/plot/PlotPanel.java
new file mode 100644 (file)
index 0000000..953ab4a
--- /dev/null
@@ -0,0 +1,336 @@
+package net.sf.openrocket.gui.plot;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightDataBranch.Type;
+import net.sf.openrocket.unit.Unit;
+
+public class PlotPanel extends JPanel {
+       
+       // TODO: LOW: Should these be somewhere else?
+       public static final int AUTO = -1;
+       public static final int LEFT = 0;
+       public static final int RIGHT = 1;
+       
+       public static final String AUTO_NAME = "Auto";
+       public static final String LEFT_NAME = "Left";
+       public static final String RIGHT_NAME = "Right";
+       
+       private static final String CUSTOM = "Custom";
+       
+       /** The "Custom" configuration - not to be used for anything other than the title. */
+       private static final PlotConfiguration CUSTOM_CONFIGURATION;
+       static {
+               CUSTOM_CONFIGURATION = new PlotConfiguration(CUSTOM);
+       }
+       
+       /** The array of presets for the combo box. */
+       private static final PlotConfiguration[] PRESET_ARRAY;
+       static {
+               PRESET_ARRAY = Arrays.copyOf(PlotConfiguration.DEFAULT_CONFIGURATIONS, 
+                               PlotConfiguration.DEFAULT_CONFIGURATIONS.length + 1);
+               PRESET_ARRAY[PRESET_ARRAY.length-1] = CUSTOM_CONFIGURATION;
+       }
+       
+       
+       
+       /** The current default configuration, set each time a plot is made. */
+       private static PlotConfiguration defaultConfiguration =
+               PlotConfiguration.DEFAULT_CONFIGURATIONS[0].resetUnits();
+       
+       
+       private final Simulation simulation;
+       private final FlightDataBranch.Type[] types;
+       private PlotConfiguration configuration;
+       
+
+       private JComboBox configurationSelector;
+       
+       private JComboBox domainTypeSelector;
+       private UnitSelector domainUnitSelector;
+       
+       private JPanel typeSelectorPanel;
+       
+       
+       private int modifying = 0;
+       
+
+       public PlotPanel(final Simulation simulation) {
+               super(new MigLayout("fill"));
+               
+               this.simulation = simulation;
+               if (simulation.getSimulatedData() == null  ||
+                               simulation.getSimulatedData().getBranchCount()==0) {
+                       throw new IllegalArgumentException("Simulation contains no data.");
+               }
+               FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
+               types = branch.getTypes();
+               
+               // TODO: LOW: Revert to custom if data type is not available.
+               configuration = defaultConfiguration.clone();
+               
+               
+               
+               
+               // Setup the combo box
+               configurationSelector = new JComboBox(PRESET_ARRAY);
+               for (PlotConfiguration config: PRESET_ARRAY) {
+                       if (config.getName().equals(configuration.getName())) {
+                               configurationSelector.setSelectedItem(config);
+                       }
+               }
+               configurationSelector.addItemListener(new ItemListener() {
+                       @Override
+                       public void itemStateChanged(ItemEvent e) {
+                               if (modifying > 0)
+                                       return;
+                               PlotConfiguration conf = (PlotConfiguration)configurationSelector.getSelectedItem();
+                               if (conf == CUSTOM_CONFIGURATION)
+                                       return;
+                               modifying++;
+                               configuration = conf.clone().resetUnits();
+                               updatePlots();
+                               modifying--;
+                       }
+               });
+               this.add(new JLabel("Preset plot configurations: "), "spanx, split");
+               this.add(configurationSelector,"growx, wrap 30lp");
+
+               
+               
+               this.add(new JLabel("X axis type:"), "spanx, split");
+               domainTypeSelector = new JComboBox(types);
+               domainTypeSelector.setSelectedItem(configuration.getDomainAxisType());
+               domainTypeSelector.addItemListener(new ItemListener() {
+                       @Override
+                       public void itemStateChanged(ItemEvent e) {
+                               if (modifying > 0)
+                                       return;
+                               FlightDataBranch.Type type = (Type) domainTypeSelector.getSelectedItem();
+                               configuration.setDomainAxisType(type);
+                               domainUnitSelector.setUnitGroup(type.getUnitGroup());
+                               domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit());
+                               setToCustom();
+                       }
+               });
+               this.add(domainTypeSelector, "gapright para");
+
+               
+               this.add(new JLabel("Unit:"));
+               domainUnitSelector = new UnitSelector(configuration.getDomainAxisType().getUnitGroup());
+               domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit());
+               domainUnitSelector.addItemListener(new ItemListener() {
+                       @Override
+                       public void itemStateChanged(ItemEvent e) {
+                               if (modifying > 0)
+                                       return;
+                               configuration.setDomainAxisUnit(domainUnitSelector.getSelectedUnit());
+                       }
+               });
+               this.add(domainUnitSelector, "width 40lp, gapright para");
+               
+               
+               ResizeLabel desc = new ResizeLabel("<html><p>The data will be plotted in time order " +
+                               "even if the X axis type is not time.", -2);
+               this.add(desc, "width :0px:, growx, wrap para");
+               
+               
+               
+               this.add(new JLabel("Y axis types:"), "spanx, wrap rel");
+               
+               typeSelectorPanel = new JPanel(new MigLayout("gapy rel"));
+               JScrollPane scroll = new JScrollPane(typeSelectorPanel);
+               this.add(scroll, "spanx, height :0:, grow, wrap para");
+               
+               
+               JButton button = new JButton("New Y axis plot type");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (configuration.getTypeCount() >= 15) {
+                                       JOptionPane.showMessageDialog(PlotPanel.this, 
+                                                       "A maximum of 15 plots is allowed.", "Cannot add plot", 
+                                                       JOptionPane.ERROR_MESSAGE);
+                                       return;
+                               }
+
+                               // Select new type smartly
+                               FlightDataBranch.Type type = null;
+                               for (FlightDataBranch.Type t: 
+                                       simulation.getSimulatedData().getBranch(0).getTypes()) {
+                                       
+                                       boolean used = false;
+                                       if (configuration.getDomainAxisType().equals(t)) {
+                                               used = true;
+                                       } else {
+                                               for (int i=0; i < configuration.getTypeCount(); i++) {
+                                                       if (configuration.getType(i).equals(t)) {
+                                                               used = true;
+                                                               break;
+                                                       }
+                                               }
+                                       }
+                                       
+                                       if (!used) {
+                                               type = t;
+                                               break;
+                                       }
+                               }
+                               if (type == null) {
+                                       type = simulation.getSimulatedData().getBranch(0).getTypes()[0];
+                               }
+                               
+                               // Add new type
+                               configuration.addPlotDataType(type);
+                               setToCustom();
+                               updatePlots();
+                       }
+               });
+               this.add(button, "spanx, split");
+               
+               this.add(new JPanel(), "growx");
+               
+               button = new JButton("Plot flight");
+               button.addActionListener(new ActionListener() {
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               defaultConfiguration = configuration.clone();
+                               PlotDialog.showPlot(SwingUtilities.getWindowAncestor(PlotPanel.this), 
+                                               simulation, configuration);
+                       }
+               });
+               this.add(button, "right");
+
+               
+               updatePlots();
+       }
+       
+       
+       private void setToCustom() {
+               configuration.setName(CUSTOM);
+               configurationSelector.setSelectedItem(CUSTOM_CONFIGURATION);
+       }
+       
+       
+       private void updatePlots() {
+               domainTypeSelector.setSelectedItem(configuration.getDomainAxisType());
+               domainUnitSelector.setUnitGroup(configuration.getDomainAxisType().getUnitGroup());
+               domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit());
+               
+               typeSelectorPanel.removeAll();
+               for (int i=0; i < configuration.getTypeCount(); i++) {
+                       FlightDataBranch.Type type = configuration.getType(i);
+                       Unit unit = configuration.getUnit(i);
+                       int axis = configuration.getAxis(i);
+                       
+                       typeSelectorPanel.add(new PlotTypeSelector(i, type, unit, axis), "wrap");
+               }
+               
+               typeSelectorPanel.repaint();
+       }
+       
+       
+       
+       
+       /**
+        * A JPanel which configures a single plot of a PlotConfiguration.
+        */
+       private class PlotTypeSelector extends JPanel {
+               private final String[] POSITIONS = { AUTO_NAME, LEFT_NAME, RIGHT_NAME };
+               
+               private final int index;
+               private JComboBox typeSelector;
+               private UnitSelector unitSelector;
+               private JComboBox axisSelector;
+               
+               
+               public PlotTypeSelector(int index, FlightDataBranch.Type type) {
+                       this (index, type, null, -1);
+               }
+               
+               public PlotTypeSelector(int plotIndex, FlightDataBranch.Type type, Unit unit, int position) {
+                       super(new MigLayout(""));
+                       
+                       this.index = plotIndex;
+                       
+                       typeSelector = new JComboBox(types);
+                       typeSelector.setSelectedItem(type);
+                       typeSelector.addItemListener(new ItemListener() {
+                               @Override
+                               public void itemStateChanged(ItemEvent e) {
+                                       if (modifying > 0)
+                                               return;
+                                       FlightDataBranch.Type type = (Type) typeSelector.getSelectedItem();
+                                       configuration.setPlotDataType(index, type);
+                                       unitSelector.setUnitGroup(type.getUnitGroup());
+                                       unitSelector.setSelectedUnit(configuration.getUnit(index));
+                                       setToCustom();
+                               }
+                       });
+                       this.add(typeSelector, "gapright para");
+                       
+                       this.add(new JLabel("Unit:"));
+                       unitSelector = new UnitSelector(type.getUnitGroup());
+                       if (unit != null)
+                               unitSelector.setSelectedUnit(unit);
+                       unitSelector.addItemListener(new ItemListener() {
+                               @Override
+                               public void itemStateChanged(ItemEvent e) {
+                                       if (modifying > 0)
+                                               return;
+                                       Unit unit = (Unit) unitSelector.getSelectedUnit();
+                                       configuration.setPlotDataUnit(index, unit);
+                               }
+                       });
+                       this.add(unitSelector, "width 40lp, gapright para");
+                       
+                       this.add(new JLabel("Axis:"));
+                       axisSelector = new JComboBox(POSITIONS);
+                       if (position == LEFT)
+                               axisSelector.setSelectedIndex(1);
+                       else if (position == RIGHT)
+                               axisSelector.setSelectedIndex(2);
+                       else
+                               axisSelector.setSelectedIndex(0);
+                       axisSelector.addItemListener(new ItemListener() {
+                               @Override
+                               public void itemStateChanged(ItemEvent e) {
+                                       if (modifying > 0)
+                                               return;
+                                       int axis = axisSelector.getSelectedIndex() - 1;
+                                       configuration.setPlotDataAxis(index, axis);
+                               }
+                       });
+                       this.add(axisSelector, "gapright para");
+                       
+                       
+                       JButton button = new JButton("Remove");
+                       button.addActionListener(new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       configuration.removePlotDataType(index);
+                                       setToCustom();
+                                       updatePlots();
+                               }
+                       });
+                       this.add(button);
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java b/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java
new file mode 100644 (file)
index 0000000..9cef22d
--- /dev/null
@@ -0,0 +1,46 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class BodyTubeShapes extends RocketComponentShapes {
+       
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component;
+
+               double length = tube.getLength();
+               double radius = tube.getRadius();
+               Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+               Shape[] s = new Shape[start.length];
+               for (int i=0; i < start.length; i++) {
+                       s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S,
+                                       length*S,2*radius*S);
+               }
+               return s;
+       }
+       
+
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component;
+               
+               double or = tube.getRadius();
+               
+               Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+               Shape[] s = new Shape[start.length];
+               for (int i=0; i < start.length; i++) {
+                       s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);
+               }
+               return s;
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java b/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java
new file mode 100644 (file)
index 0000000..3d874a2
--- /dev/null
@@ -0,0 +1,296 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Path2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Transformation;
+
+
+public class FinSetShapes extends RocketComponentShapes {
+
+       // TODO: LOW:  Clustering is ignored (FinSet cannot currently be clustered)
+
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component;
+
+               
+               int fins = finset.getFinCount();
+               Transformation cantRotation = finset.getCantRotation();
+               Transformation baseRotation = finset.getBaseRotationTransformation();
+               Transformation finRotation = finset.getFinRotationTransformation();
+               
+               Coordinate c[] = finset.getFinPoints();
+
+               
+               // TODO: MEDIUM: sloping radius
+               double radius = finset.getBodyRadius();
+               
+               // Translate & rotate the coordinates
+               for (int i=0; i<c.length; i++) {
+                       c[i] = cantRotation.transform(c[i]);
+                       c[i] = baseRotation.transform(c[i].add(0,radius,0));
+               }
+               
+               
+               // Generate shapes
+               Shape[] s = new Shape[fins];
+               for (int fin=0; fin<fins; fin++) {
+                       Coordinate a;
+                       Path2D.Float p;
+
+                       // Make polygon
+                       p = new Path2D.Float();
+                       for (int i=0; i<c.length; i++) {
+                               a = transformation.transform(finset.toAbsolute(c[i])[0]);
+                               if (i==0)
+                                       p.moveTo(a.x*S, a.y*S);
+                               else
+                                       p.lineTo(a.x*S, a.y*S);                 
+                       }
+                       p.closePath();
+                       s[fin] = p;
+
+                       // Rotate fin coordinates
+                       for (int i=0; i<c.length; i++)
+                               c[i] = finRotation.transform(c[i]);
+               }
+               
+               return s;
+       }
+       
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+
+               net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component;
+
+               if (MathUtil.equals(finset.getCantAngle(),0))
+                       return uncantedShapesBack(finset, transformation);
+               else
+                       return cantedShapesBack(finset, transformation);
+               
+       }
+       
+       
+       private static Shape[] uncantedShapesBack(net.sf.openrocket.rocketcomponent.FinSet finset,
+                       Transformation transformation) {
+               
+               int fins = finset.getFinCount();
+               double radius = finset.getBodyRadius();
+               double thickness = finset.getThickness();
+               double height = finset.getSpan();
+               
+               Transformation baseRotation = finset.getBaseRotationTransformation();
+               Transformation finRotation = finset.getFinRotationTransformation();
+               
+
+               // Generate base coordinates for a single fin
+               Coordinate c[] = new Coordinate[4];
+               c[0]=new Coordinate(0,radius,-thickness/2);
+               c[1]=new Coordinate(0,radius,thickness/2);
+               c[2]=new Coordinate(0,height+radius,thickness/2);
+               c[3]=new Coordinate(0,height+radius,-thickness/2);
+
+               // Apply base rotation
+               transformPoints(c,baseRotation);
+               
+               // Generate shapes
+               Shape[] s = new Shape[fins];
+               for (int fin=0; fin<fins; fin++) {
+                       Coordinate a;
+                       Path2D.Double p;
+
+                       // Make polygon
+                       p = new Path2D.Double();
+                       a = transformation.transform(finset.toAbsolute(c[0])[0]);
+                       p.moveTo(a.z*S, a.y*S);
+                       a = transformation.transform(finset.toAbsolute(c[1])[0]);
+                       p.lineTo(a.z*S, a.y*S);                 
+                       a = transformation.transform(finset.toAbsolute(c[2])[0]);
+                       p.lineTo(a.z*S, a.y*S);                 
+                       a = transformation.transform(finset.toAbsolute(c[3])[0]);
+                       p.lineTo(a.z*S, a.y*S); 
+                       p.closePath();
+                       s[fin] = p;
+
+                       // Rotate fin coordinates
+                       transformPoints(c,finRotation);
+               }
+               
+               return s;
+       }
+       
+       
+       // TODO: LOW:  Jagged shapes from back draw incorrectly.
+       private static Shape[] cantedShapesBack(net.sf.openrocket.rocketcomponent.FinSet finset,
+                       Transformation transformation) {
+               int i;
+               int fins = finset.getFinCount();
+               double radius = finset.getBodyRadius();
+               double thickness = finset.getThickness();
+               
+               Transformation baseRotation = finset.getBaseRotationTransformation();
+               Transformation finRotation = finset.getFinRotationTransformation();
+               Transformation cantRotation = finset.getCantRotation();
+
+               Coordinate[] sidePoints;
+               Coordinate[] backPoints;
+               int maxIndex;
+
+               Coordinate[] points = finset.getFinPoints();
+               for (maxIndex = points.length-1; maxIndex > 0; maxIndex--) {
+                       if (points[maxIndex-1].y < points[maxIndex].y)
+                               break;
+               }
+               
+               transformPoints(points,cantRotation);
+               transformPoints(points,new Transformation(0,radius,0));
+               transformPoints(points,baseRotation);
+               
+               
+               sidePoints = new Coordinate[points.length];
+               backPoints = new Coordinate[2*(points.length-maxIndex)];
+               double sign;
+               if (finset.getCantAngle() > 0) {
+                       sign = 1.0;
+               } else {
+                       sign = -1.0;
+               }                       
+                       
+               // Calculate points for the side panel
+               for (i=0; i < points.length; i++) {
+                       sidePoints[i] = points[i].add(0,0,sign*thickness/2);
+               }
+
+               // Calculate points for the back portion
+               i=0;
+               for (int j=points.length-1; j >= maxIndex; j--, i++) {
+                       backPoints[i] = points[j].add(0,0,sign*thickness/2);
+               }
+               for (int j=maxIndex; j <= points.length-1; j++, i++) {
+                       backPoints[i] = points[j].add(0,0,-sign*thickness/2);
+               }
+               
+               // Generate shapes
+               Shape[] s;
+               if (thickness > 0.0005) {
+                       
+                       s = new Shape[fins*2];
+                       for (int fin=0; fin<fins; fin++) {
+                               
+                               s[2*fin] = makePolygonBack(sidePoints,finset,transformation);
+                               s[2*fin+1] = makePolygonBack(backPoints,finset,transformation);
+                               
+                               // Rotate fin coordinates
+                               transformPoints(sidePoints,finRotation);
+                               transformPoints(backPoints,finRotation);
+                       }
+                       
+               } else {
+                       
+                       s = new Shape[fins];
+                       for (int fin=0; fin<fins; fin++) {
+                               s[fin] = makePolygonBack(sidePoints,finset,transformation);
+                               transformPoints(sidePoints,finRotation);
+                       }
+                       
+               }
+               
+               return s;
+       }
+       
+       
+       
+       private static void transformPoints(Coordinate[] array, Transformation t) {
+               for (int i=0; i < array.length; i++) {
+                       array[i] = t.transform(array[i]);
+               }
+       }
+       
+       private static Shape makePolygonBack(Coordinate[] array, net.sf.openrocket.rocketcomponent.FinSet finset, 
+                       Transformation t) {
+               Path2D.Float p;
+
+               // Make polygon
+               p = new Path2D.Float();
+               for (int i=0; i < array.length; i++) {
+                       Coordinate a = t.transform(finset.toAbsolute(array[i])[0]);
+                       if (i==0)
+                               p.moveTo(a.z*S, a.y*S);
+                       else
+                               p.lineTo(a.z*S, a.y*S);                 
+               }
+               p.closePath();
+               return p;
+       }
+       
+       
+       /*  Side painting with thickness:
+
+               Coordinate c[] = new Coordinate[8];
+               
+               c[0]=new Coordinate(0-position*rootChord,radius,thickness/2);
+               c[1]=new Coordinate(rootChord-position*rootChord,radius,thickness/2);
+               c[2]=new Coordinate(sweep+tipChord-position*rootChord,height+radius,thickness/2);
+               c[3]=new Coordinate(sweep-position*rootChord,height+radius,thickness/2);
+               
+               c[4]=new Coordinate(0-position*rootChord,radius,-thickness/2);
+               c[5]=new Coordinate(rootChord-position*rootChord,radius,-thickness/2);
+               c[6]=new Coordinate(sweep+tipChord-position*rootChord,height+radius,-thickness/2);
+               c[7]=new Coordinate(sweep-position*rootChord,height+radius,-thickness/2);
+               
+               if (rotation != 0) {
+                       rot = Transformation.rotate_x(rotation);
+                       for (int i=0; i<8; i++)
+                               c[i] = rot.transform(c[i]);
+               }
+               
+               Shape[] s = new Shape[fins*6];
+               rot = Transformation.rotate_x(2*Math.PI/fins);
+               
+               for (int fin=0; fin<fins; fin++) {
+                       Coordinate a,b;
+                       Path2D.Float p;
+
+                       // First polygon
+                       p = new Path2D.Float();
+                       a = finset.toAbsolute(c[0]);
+                       p.moveTo(a.x(), a.y());
+                       a = finset.toAbsolute(c[1]);
+                       p.lineTo(a.x(), a.y());                 
+                       a = finset.toAbsolute(c[2]);
+                       p.lineTo(a.x(), a.y());                 
+                       a = finset.toAbsolute(c[3]);
+                       p.lineTo(a.x(), a.y()); 
+                       p.closePath();
+                       s[fin*6] = p;
+                       
+                       // Second polygon
+                       p = new Path2D.Float();
+                       a = finset.toAbsolute(c[4]);
+                       p.moveTo(a.x(), a.y());
+                       a = finset.toAbsolute(c[5]);
+                       p.lineTo(a.x(), a.y());                 
+                       a = finset.toAbsolute(c[6]);
+                       p.lineTo(a.x(), a.y());                 
+                       a = finset.toAbsolute(c[7]);
+                       p.lineTo(a.x(), a.y()); 
+                       p.closePath();
+                       s[fin*6+1] = p;
+                       
+                       // Single lines
+                       for (int i=0; i<4; i++) {
+                               a = finset.toAbsolute(c[i]);
+                               b = finset.toAbsolute(c[i+4]);
+                               s[fin*6+2+i] = new Line2D.Float((float)a.x(),(float)a.y(),(float)b.x(),(float)b.y());
+                       }
+
+                       // Rotate fin coordinates
+                       for (int i=0; i<8; i++)
+                               c[i] = rot.transform(c[i]);
+               }
+               
+        */
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java b/src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java
new file mode 100644 (file)
index 0000000..7dfb1b7
--- /dev/null
@@ -0,0 +1,44 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class LaunchLugShapes extends RocketComponentShapes {
+       
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component;
+
+               double length = lug.getLength();
+               double radius = lug.getRadius();
+               Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0)));
+
+               Shape[] s = new Shape[start.length];
+               for (int i=0; i < start.length; i++) {
+                       s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S,
+                                       length*S,2*radius*S);
+               }
+               return s;
+       }
+       
+
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component;
+               
+               double or = lug.getRadius();
+               
+               Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0)));
+
+               Shape[] s = new Shape[start.length];
+               for (int i=0; i < start.length; i++) {
+                       s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);
+               }
+               return s;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/MassObjectShapes.java b/src/net/sf/openrocket/gui/rocketfigure/MassObjectShapes.java
new file mode 100644 (file)
index 0000000..c8ea511
--- /dev/null
@@ -0,0 +1,47 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.RoundRectangle2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class MassObjectShapes extends RocketComponentShapes {
+       
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.MassObject tube = (net.sf.openrocket.rocketcomponent.MassObject)component;
+
+               double length = tube.getLength();
+               double radius = tube.getRadius();
+               double arc = Math.min(length, 2*radius) * 0.7;
+               Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+               Shape[] s = new Shape[start.length];
+               for (int i=0; i < start.length; i++) {
+                       s[i] = new RoundRectangle2D.Double(start[i].x*S,(start[i].y-radius)*S,
+                                       length*S,2*radius*S,arc*S,arc*S);
+               }
+               return s;
+       }
+       
+
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.MassObject tube = (net.sf.openrocket.rocketcomponent.MassObject)component;
+               
+               double or = tube.getRadius();
+               
+               Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+               Shape[] s = new Shape[start.length];
+               for (int i=0; i < start.length; i++) {
+                       s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);
+               }
+               return s;
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/RingComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/RingComponentShapes.java
new file mode 100644 (file)
index 0000000..6cf6fe3
--- /dev/null
@@ -0,0 +1,77 @@
+package net.sf.openrocket.gui.rocketfigure;\r
+\r
+\r
+import java.awt.Shape;\r
+import java.awt.geom.Ellipse2D;\r
+import java.awt.geom.Rectangle2D;\r
+\r
+import net.sf.openrocket.util.Coordinate;\r
+import net.sf.openrocket.util.Transformation;\r
+\r
+\r
+public class RingComponentShapes extends RocketComponentShapes {\r
+\r
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, \r
+                       Transformation transformation) {\r
+               net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component;\r
+               Shape[] s;\r
+               \r
+               double length = tube.getLength();\r
+               double or = tube.getOuterRadius();\r
+               double ir = tube.getInnerRadius();\r
+               \r
+\r
+               Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));\r
+\r
+               if ((or-ir >= 0.0012) && (ir > 0)) {\r
+                       // Draw outer and inner\r
+                       s = new Shape[start.length*2];\r
+                       for (int i=0; i < start.length; i++) {\r
+                               s[2*i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S,\r
+                                               length*S,2*or*S);\r
+                               s[2*i+1] = new Rectangle2D.Double(start[i].x*S,(start[i].y-ir)*S,\r
+                                               length*S,2*ir*S);\r
+                       }\r
+               } else {\r
+                       // Draw only outer\r
+                       s = new Shape[start.length];\r
+                       for (int i=0; i < start.length; i++) {\r
+                               s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S,\r
+                                               length*S,2*or*S);\r
+                       }\r
+               }\r
+               return s;\r
+       }\r
+       \r
+\r
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, \r
+                       Transformation transformation) {\r
+               net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component;\r
+               Shape[] s;\r
+               \r
+               double or = tube.getOuterRadius();\r
+               double ir = tube.getInnerRadius();\r
+               \r
+\r
+               Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));\r
+\r
+               if ((ir < or) && (ir > 0)) {\r
+                       // Draw inner and outer\r
+                       s = new Shape[start.length*2];\r
+                       for (int i=0; i < start.length; i++) {\r
+                               s[2*i]   = new Ellipse2D.Double((start[i].z-or)*S, (start[i].y-or)*S,\r
+                                               2*or*S, 2*or*S);\r
+                               s[2*i+1] = new Ellipse2D.Double((start[i].z-ir)*S, (start[i].y-ir)*S,\r
+                                               2*ir*S, 2*ir*S);\r
+                       }\r
+               } else {\r
+                       // Draw only outer\r
+                       s = new Shape[start.length];\r
+                       for (int i=0; i < start.length; i++) {\r
+                               s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);\r
+                       }\r
+               }\r
+               return s;\r
+       }\r
+       \r
+}\r
diff --git a/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java
new file mode 100644 (file)
index 0000000..8311d0a
--- /dev/null
@@ -0,0 +1,32 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+
+import java.awt.Shape;
+
+import net.sf.openrocket.gui.scalefigure.RocketFigure;
+import net.sf.openrocket.util.Transformation;
+
+
+/**
+ * A catch-all, no-operation drawing component.
+ */
+public class RocketComponentShapes {
+
+       protected static final double S = RocketFigure.EXTRA_SCALE;
+       
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+                       Transformation t) {
+               // no-op
+               System.err.println("ERROR:  RocketComponent.getShapesSide called with "+component);
+               return new Shape[0];
+       }
+       
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+                       Transformation t) {
+               // no-op
+               System.err.println("ERROR:  RocketComponent.getShapesBack called with "+component);
+               return new Shape[0];
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java
new file mode 100644 (file)
index 0000000..c1b9cae
--- /dev/null
@@ -0,0 +1,107 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Path2D;
+import java.util.ArrayList;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class SymmetricComponentShapes extends RocketComponentShapes {
+       private static final int MINPOINTS = 91;
+       private static final double ACCEPTABLE_ANGLE = Math.cos(7.0*Math.PI/180.0);
+       
+       // TODO: HIGH: adaptiveness sucks, remove it.
+       
+       // TODO: LOW: Uses only first component of cluster (not currently clusterable)
+       
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.SymmetricComponent c = (net.sf.openrocket.rocketcomponent.SymmetricComponent)component;
+               int i;
+               
+               final double delta = 0.0000001;
+               double x;
+               
+               ArrayList<Coordinate> points = new ArrayList<Coordinate>();
+               x = delta;
+               points.add(new Coordinate(x,c.getRadius(x),0));
+               for (i=1; i < MINPOINTS-1; i++) {
+                       x = c.getLength()*i/(MINPOINTS-1);
+                       points.add(new Coordinate(x,c.getRadius(x),0));
+                       //System.out.println("Starting with x="+x);
+               }
+               x = c.getLength() - delta;
+               points.add(new Coordinate(x,c.getRadius(x),0));
+               
+               
+               i=0;
+               while (i < points.size()-2) {
+                       if (angleAcceptable(points.get(i),points.get(i+1),points.get(i+2)) ||
+                                       points.get(i+1).x - points.get(i).x < 0.001) { // 1mm
+                               i++;
+                               continue;
+                       }
+
+                       // Split the longer of the areas
+                       int n;
+                       if (points.get(i+2).x-points.get(i+1).x > points.get(i+1).x-points.get(i).x)
+                               n = i+1;
+                       else
+                               n = i;
+                       
+                       x = (points.get(n).x + points.get(n+1).x)/2;
+                       points.add(n+1,new Coordinate(x,c.getRadius(x),0));
+               }
+               
+
+               //System.out.println("Final points: "+points.size());
+               
+               final int len = points.size();
+
+               for (i=0; i < len; i++) {
+                       points.set(i, c.toAbsolute(points.get(i))[0]);
+               }
+
+               /*   Show points:
+               Shape[] s = new Shape[len+1];
+               final double d=0.001;
+               for (i=0; i<len; i++) {
+                       s[i] = new Ellipse2D.Double(points.get(i).x()-d/2,points.get(i).y()-d/2,d,d);
+               }
+               */
+               
+               //System.out.println("here");
+               
+               // TODO: LOW: curved path instead of linear
+               Path2D.Double path = new Path2D.Double();
+               path.moveTo(points.get(len-1).x*S, points.get(len-1).y*S);
+               for (i=len-2; i>=0; i--) {
+                       path.lineTo(points.get(i).x*S, points.get(i).y*S);
+               }
+               for (i=0; i<len; i++) {
+                       path.lineTo(points.get(i).x*S, -points.get(i).y*S);
+               }
+               path.lineTo(points.get(len-1).x*S, points.get(len-1).y*S);
+               path.closePath();
+               
+               //s[len] = path;
+               //return s;
+               return new Shape[]{ path };
+       }
+
+       private static boolean angleAcceptable(Coordinate v1, Coordinate v2, Coordinate v3) {
+               return (cosAngle(v1,v2,v3) > ACCEPTABLE_ANGLE);
+       }
+       /*
+        * cosAngle = v1.v2 / |v1|*|v2| = v1.v2 / sqrt(v1.v1*v2.v2)
+        */
+       private static double cosAngle(Coordinate v1, Coordinate v2, Coordinate v3) {
+               double cos;
+               double len;
+               cos = Coordinate.dot(v1.sub(v2), v2.sub(v3));
+               len = Math.sqrt(v1.sub(v2).length2() * v2.sub(v3).length2());
+               return cos/len;
+       }
+}
diff --git a/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java b/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java
new file mode 100644 (file)
index 0000000..205cd50
--- /dev/null
@@ -0,0 +1,96 @@
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class TransitionShapes extends RocketComponentShapes {
+
+       // TODO: LOW: Uses only first component of cluster (not currently clusterable).
+       
+       public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component;
+
+               Shape[] mainShapes;
+               
+               // Simpler shape for conical transition, others use the method from SymmetricComponent
+               if (transition.getType() == Transition.Shape.CONICAL) {
+                       double length = transition.getLength();
+                       double r1 = transition.getForeRadius();
+                       double r2 = transition.getAftRadius();
+                       Coordinate start = transformation.transform(transition.
+                                       toAbsolute(Coordinate.NUL)[0]);
+                       
+                       Path2D.Float path = new Path2D.Float();
+                       path.moveTo(start.x*S, r1*S);
+                       path.lineTo((start.x+length)*S, r2*S);
+                       path.lineTo((start.x+length)*S, -r2*S);
+                       path.lineTo(start.x*S, -r1*S);
+                       path.closePath();
+                       
+                       mainShapes = new Shape[] { path };
+               } else {
+                       mainShapes = SymmetricComponentShapes.getShapesSide(component, transformation);
+               }
+               
+               Rectangle2D.Double shoulder1=null, shoulder2=null;
+               int arrayLength = mainShapes.length;
+               
+               if (transition.getForeShoulderLength() > 0.0005) {
+                       Coordinate start = transformation.transform(transition.
+                                       toAbsolute(Coordinate.NUL)[0]);
+                       double r = transition.getForeShoulderRadius();
+                       double l = transition.getForeShoulderLength();
+                       shoulder1 = new Rectangle2D.Double((start.x-l)*S, -r*S, l*S, 2*r*S);
+                       arrayLength++;
+               }
+               if (transition.getAftShoulderLength() > 0.0005) {
+                       Coordinate start = transformation.transform(transition.
+                                       toAbsolute(new Coordinate(transition.getLength()))[0]);
+                       double r = transition.getAftShoulderRadius();
+                       double l = transition.getAftShoulderLength();
+                       shoulder2 = new Rectangle2D.Double(start.x*S, -r*S, l*S, 2*r*S);
+                       arrayLength++;
+               }
+               if (shoulder1==null && shoulder2==null)
+                       return mainShapes;
+               
+               Shape[] shapes = new Shape[arrayLength];
+               int i;
+               
+               for (i=0; i < mainShapes.length; i++) {
+                       shapes[i] = mainShapes[i];
+               }
+               if (shoulder1 != null) {
+                       shapes[i] = shoulder1;
+                       i++;
+               }
+               if (shoulder2 != null) {
+                       shapes[i] = shoulder2;
+               }
+               return shapes;
+       }
+       
+
+       public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, 
+                       Transformation transformation) {
+               net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component;
+               
+               double r1 = transition.getForeRadius();
+               double r2 = transition.getAftRadius();
+               
+               Shape[] s = new Shape[2];
+               s[0] = new Ellipse2D.Double(-r1*S,-r1*S,2*r1*S,2*r1*S);
+               s[1] = new Ellipse2D.Double(-r2*S,-r2*S,2*r2*S,2*r2*S);
+               return s;
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java b/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java
new file mode 100644 (file)
index 0000000..5a515ba
--- /dev/null
@@ -0,0 +1,112 @@
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.Prefs;
+
+
+public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure {
+
+       // Number of pixels to leave at edges when fitting figure
+       public static final int BORDER_PIXELS_WIDTH=30;
+       public static final int BORDER_PIXELS_HEIGHT=20;
+       
+       
+       protected final double dpi;
+
+       protected double scale = 1.0;
+       protected double scaling = 1.0;
+       
+       protected final List<ChangeListener> listeners = new LinkedList<ChangeListener>();
+       
+
+       public AbstractScaleFigure() {
+               this.dpi = Prefs.getDPI();
+               this.scaling = 1.0;
+               this.scale = dpi/0.0254*scaling;
+               
+               setBackground(Color.WHITE);
+               setOpaque(true);
+       }
+       
+       
+       
+       public abstract void updateFigure();
+       public abstract double getFigureWidth();
+       public abstract double getFigureHeight();
+       
+
+       @Override
+       public double getScaling() {
+               return scaling;
+       }
+
+       @Override
+       public double getAbsoluteScale() {
+               return scale;
+       }
+       
+       @Override
+       public void setScaling(double scaling) {
+               if (Double.isInfinite(scaling) || Double.isNaN(scaling))
+                       scaling = 1.0;
+               if (scaling < 0.001)
+                       scaling = 0.001;
+               if (scaling > 1000)
+                       scaling = 1000;
+               if (Math.abs(this.scaling - scaling) < 0.01)
+                       return;
+               this.scaling = scaling;
+               this.scale = dpi/0.0254*scaling;
+               updateFigure();
+       }
+       
+       @Override
+       public void setScaling(Dimension bounds) {
+               double zh = 1, zv = 1;
+               int w = bounds.width - 2*BORDER_PIXELS_WIDTH -20;
+               int h = bounds.height - 2*BORDER_PIXELS_HEIGHT -20;
+               
+               if (w < 10)
+                       w = 10;
+               if (h < 10)
+                       h = 10;
+               
+               zh = ((double)w) / getFigureWidth();
+               zv = ((double)h) / getFigureHeight();
+                       
+               double s = Math.min(zh, zv)/dpi*0.0254 - 0.001;
+               
+               setScaling(s);
+       }
+
+       
+
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listeners.add(0,listener);
+       }
+
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listeners.remove(listener);
+       }
+       
+       private ChangeEvent changeEvent = null;
+       protected void fireChangeEvent() {
+               ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
+               for (ChangeListener l: list) {
+                       if (changeEvent == null)
+                               changeEvent = new ChangeEvent(this);
+                       l.stateChanged(changeEvent);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java b/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java
new file mode 100644 (file)
index 0000000..29e2112
--- /dev/null
@@ -0,0 +1,344 @@
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.unit.Tick;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+// TODO: MEDIUM:  the figure jumps and bugs when using automatic fitting
+
+public class FinPointFigure extends AbstractScaleFigure {
+
+       private static final int BOX_SIZE = 4;
+       
+       private final FreeformFinSet finset;
+       private int modID = -1;
+       
+       private double minX, maxX, maxY;
+       private double figureWidth = 0;
+       private double figureHeight = 0;
+       private double translateX = 0;
+       private double translateY = 0;
+       
+       private AffineTransform transform;
+       private Rectangle2D.Double[] handles = null;
+       
+       
+       public FinPointFigure(FreeformFinSet finset) {
+               this.finset = finset;
+       }
+       
+
+       @Override
+       public void paintComponent(Graphics g) {
+               super.paintComponent(g);
+               Graphics2D g2 = (Graphics2D) g;
+
+               if (modID != finset.getRocket().getAerodynamicModID()) {
+                       modID = finset.getRocket().getAerodynamicModID();
+                       calculateDimensions();
+               }
+
+               
+               double tx, ty;
+               // Calculate translation for figure centering
+               if (figureWidth*scale + 2*BORDER_PIXELS_WIDTH < getWidth()) {
+
+                       // Figure fits in the viewport
+                       tx = (getWidth()-figureWidth*scale)/2 - minX*scale;
+
+               } else {
+
+                       // Figure does not fit in viewport
+                       tx = BORDER_PIXELS_WIDTH - minX*scale;
+                       
+               }
+               
+
+               if (figureHeight*scale + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
+                       ty = getHeight() - BORDER_PIXELS_HEIGHT;
+               } else {
+                       ty = BORDER_PIXELS_HEIGHT + figureHeight*scale;
+               }
+               
+               if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+                       // Origin has changed, fire event
+                       translateX = tx;
+                       translateY = ty;
+                       fireChangeEvent();
+               }
+               
+               
+               if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+                       // Origin has changed, fire event
+                       translateX = tx;
+                       translateY = ty;
+                       fireChangeEvent();
+               }
+               
+
+               // Calculate and store the transformation used
+               transform = new AffineTransform();
+               transform.translate(translateX, translateY);
+               transform.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
+               
+               // TODO: HIGH:  border Y-scale upwards
+
+               g2.transform(transform);
+               
+               // Set rendering hints appropriately
+               g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
+                               RenderingHints.VALUE_STROKE_NORMALIZE);
+               g2.setRenderingHint(RenderingHints.KEY_RENDERING, 
+                               RenderingHints.VALUE_RENDER_QUALITY);
+               g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
+                               RenderingHints.VALUE_ANTIALIAS_ON);
+
+
+               
+               Rectangle visible = g2.getClipBounds();
+               double x0 = ((double)visible.x-3)/EXTRA_SCALE;
+               double x1 = ((double)visible.x+visible.width+4)/EXTRA_SCALE;
+               double y0 = ((double)visible.y-3)/EXTRA_SCALE;
+               double y1 = ((double)visible.y+visible.height+4)/EXTRA_SCALE;
+               
+               
+               // Background grid
+               
+               g2.setStroke(new BasicStroke((float)(1.0*EXTRA_SCALE/scale),
+                               BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+               g2.setColor(new Color(0,0,255,30));
+
+               Unit unit;
+               if (this.getParent() != null &&
+                               this.getParent().getParent() instanceof ScaleScrollPane) {
+                       unit = ((ScaleScrollPane)this.getParent().getParent()).getCurrentUnit();
+               } else {
+                       unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+               }
+               
+               // vertical
+        Tick[] ticks = unit.getTicks(x0, x1, 
+                       ScaleScrollPane.MINOR_TICKS/scale, 
+                       ScaleScrollPane.MAJOR_TICKS/scale);
+        Line2D.Double line = new Line2D.Double();
+        for (Tick t: ticks) {
+               if (t.major) {
+                       line.setLine(t.value*EXTRA_SCALE, y0*EXTRA_SCALE, 
+                                       t.value*EXTRA_SCALE, y1*EXTRA_SCALE);
+                       g2.draw(line);
+               }
+        }
+        
+        // horizontal
+        ticks = unit.getTicks(y0, y1, 
+                       ScaleScrollPane.MINOR_TICKS/scale, 
+                       ScaleScrollPane.MAJOR_TICKS/scale);
+        for (Tick t: ticks) {
+               if (t.major) {
+                       line.setLine(x0*EXTRA_SCALE, t.value*EXTRA_SCALE, 
+                                       x1*EXTRA_SCALE, t.value*EXTRA_SCALE);
+                       g2.draw(line);
+               }
+        }
+               
+        
+        
+        
+
+               // Base rocket line
+               g2.setStroke(new BasicStroke((float)(3.0*EXTRA_SCALE/scale),
+                               BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
+               g2.setColor(Color.GRAY);
+               
+               g2.drawLine((int)(x0*EXTRA_SCALE), 0, (int)(x1*EXTRA_SCALE), 0);
+               
+               
+               // Fin shape
+               Coordinate[] points = finset.getFinPoints();
+               Path2D.Double shape = new Path2D.Double();
+               shape.moveTo(0, 0);
+               for (int i=1; i < points.length; i++) {
+                       shape.lineTo(points[i].x*EXTRA_SCALE, points[i].y*EXTRA_SCALE);
+               }
+               
+               g2.setStroke(new BasicStroke((float)(1.0*EXTRA_SCALE/scale),
+                               BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
+               g2.setColor(Color.BLACK);
+               g2.draw(shape);
+
+               
+               // Fin point boxes
+               g2.setColor(new Color(150,0,0));
+               double s = BOX_SIZE*EXTRA_SCALE/scale;
+               handles = new Rectangle2D.Double[points.length];
+               for (int i=0; i < points.length; i++) {
+                       Coordinate c = points[i];
+                       handles[i] = new Rectangle2D.Double(c.x*EXTRA_SCALE-s, c.y*EXTRA_SCALE-s, 2*s, 2*s);
+                       g2.draw(handles[i]);
+               }
+               
+       }
+
+       
+       
+       public int getIndexByPoint(double x, double y) {
+               if (handles == null)
+                       return -1;
+               
+               // Calculate point in shapes' coordinates
+               Point2D.Double p = new Point2D.Double(x,y);
+               try {
+                       transform.inverseTransform(p,p);
+               } catch (NoninvertibleTransformException e) {
+                       return -1;
+               }
+
+               for (int i=0; i < handles.length; i++) {
+                       if (handles[i].contains(p))
+                               return i;
+               }
+               return -1;
+       }
+       
+       
+       public int getSegmentByPoint(double x, double y) {
+               if (handles == null)
+                       return -1;
+               
+               // Calculate point in shapes' coordinates
+               Point2D.Double p = new Point2D.Double(x,y);
+               try {
+                       transform.inverseTransform(p,p);
+               } catch (NoninvertibleTransformException e) {
+                       return -1;
+               }
+               
+               double x0 = p.x / EXTRA_SCALE;
+               double y0 = p.y / EXTRA_SCALE;
+               double delta = BOX_SIZE / scale;
+
+               System.out.println("Point: "+x0+","+y0);
+               System.out.println("delta: "+(BOX_SIZE/scale));
+               
+               Coordinate[] points = finset.getFinPoints();
+               for (int i=1; i < points.length; i++) {
+                       double x1 = points[i-1].x;
+                       double y1 = points[i-1].y;
+                       double x2 = points[i].x;
+                       double y2 = points[i].y;
+                       
+//                     System.out.println("point1:"+x1+","+y1+" point2:"+x2+","+y2);
+                       
+                       double u = Math.abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) / 
+                                               MathUtil.hypot(x2-x1, y2-y1);
+                       System.out.println("Distance of segment "+i+" is "+u);
+                       if (u < delta)
+                               return i;
+               }
+               
+               return -1;
+       }
+       
+       
+       public Point2D.Double convertPoint(double x, double y) {
+               Point2D.Double p = new Point2D.Double(x,y);
+               try {
+                       transform.inverseTransform(p,p);
+               } catch (NoninvertibleTransformException e) {
+                       assert(false): "Should not occur";
+                       return new Point2D.Double(0,0);
+               }
+       
+               p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE);
+               return p;
+       }
+       
+       
+
+       @Override
+       public Dimension getOrigin() {
+               if (modID != finset.getRocket().getAerodynamicModID()) {
+                       modID = finset.getRocket().getAerodynamicModID();
+                       calculateDimensions();
+               }
+               return new Dimension((int)translateX, (int)translateY);
+       }
+
+       @Override
+       public double getFigureWidth() {
+               if (modID != finset.getRocket().getAerodynamicModID()) {
+                       modID = finset.getRocket().getAerodynamicModID();
+                       calculateDimensions();
+               }
+               return figureWidth;
+       }
+
+       @Override
+       public double getFigureHeight() {
+               if (modID != finset.getRocket().getAerodynamicModID()) {
+                       modID = finset.getRocket().getAerodynamicModID();
+                       calculateDimensions();
+               }
+               return figureHeight;
+       }
+
+       
+       private void calculateDimensions() {
+               minX = 0;
+               maxX = 0;
+               maxY = 0;
+               
+               for (Coordinate c: finset.getFinPoints()) {
+                       if (c.x < minX)
+                               minX = c.x;
+                       if (c.x > maxX)
+                               maxX = c.x;
+                       if (c.y > maxY)
+                               maxY = c.y;
+               }
+               
+               if (maxX < 0.01)
+                       maxX = 0.01;
+               
+               figureWidth = maxX - minX;
+               figureHeight = maxY;
+               
+
+               Dimension d = new Dimension((int)(figureWidth*scale+2*BORDER_PIXELS_WIDTH),
+                               (int)(figureHeight*scale+2*BORDER_PIXELS_HEIGHT));
+               
+               if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
+                       setPreferredSize(d);
+                       setMinimumSize(d);
+                       revalidate();
+               }
+       }
+       
+       
+
+       @Override
+       public void updateFigure() {
+               repaint();
+       }
+       
+       
+
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java b/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java
new file mode 100644 (file)
index 0000000..d48010b
--- /dev/null
@@ -0,0 +1,542 @@
+package net.sf.openrocket.gui.scalefigure;
+
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+
+import net.sf.openrocket.gui.figureelements.FigureElement;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.Transformation;
+
+/**
+ * A <code>ScaleFigure</code> that draws a complete rocket.  Extra information can
+ * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
+ * {@link #clearRelativeExtra()}.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class RocketFigure extends AbstractScaleFigure {
+       private static final long serialVersionUID = 1L;
+       
+       private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
+       private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
+       
+       public static final int TYPE_SIDE = 1;
+       public static final int TYPE_BACK = 2;
+       
+       // Width for drawing normal and selected components
+       public static final double NORMAL_WIDTH = 1.0;
+       public static final double SELECTED_WIDTH = 2.0;
+
+       
+       private final Configuration configuration;
+       private RocketComponent[] selection = new RocketComponent[0];
+       
+       private int type = TYPE_SIDE;
+
+       private double rotation;
+       private Transformation transformation;
+       
+       private double translateX, translateY;
+       
+       
+       
+       /*
+        * figureComponents contains the corresponding RocketComponents of the figureShapes
+        */
+       private final ArrayList<Shape> figureShapes = new ArrayList<Shape>();
+       private final ArrayList<RocketComponent> figureComponents = 
+               new ArrayList<RocketComponent>();
+       
+       private double minX=0, maxX=0, maxR=0;
+       // Figure width and height in SI-units and pixels
+       private double figureWidth=0, figureHeight=0;
+       private int figureWidthPx=0, figureHeightPx=0;
+       
+       private AffineTransform g2transformation = null;
+       
+       private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
+       private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
+       
+       
+       /**
+        * Creates a new rocket figure.
+        */
+       public RocketFigure(Configuration configuration) {
+               super();
+               
+               this.configuration = configuration;
+               
+               this.rotation = 0.0;
+               this.transformation = Transformation.rotate_x(0.0);
+               
+               calculateSize();
+               updateFigure();
+       }
+       
+       
+       
+       public Dimension getOrigin() {
+               return new Dimension((int)translateX, (int)translateY);
+       }
+       
+       @Override
+       public double getFigureHeight() {
+               return figureHeight;
+       }
+
+       @Override
+       public double getFigureWidth() {
+               return figureWidth;
+       }
+
+       
+       public RocketComponent[] getSelection() {
+               return selection;
+       }
+       
+       public void setSelection(RocketComponent[] selection) {
+               if (selection == null) {
+                       selection = new RocketComponent[0];
+               } else {
+                       this.selection = selection;
+               }
+               updateFigure();
+       }
+       
+       
+       public double getRotation() {
+               return rotation;
+       }
+       
+       public Transformation getRotateTransformation() {
+               return transformation;
+       }
+       
+       public void setRotation(double rot) {
+               if (MathUtil.equals(rotation, rot))
+                       return;
+               this.rotation = rot;
+               this.transformation = Transformation.rotate_x(rotation);
+               updateFigure();
+       }
+       
+       
+       public int getType() {
+               return type;
+       }
+       
+       public void setType(int type) {
+               if (type != TYPE_BACK && type != TYPE_SIDE) {
+                       throw new IllegalArgumentException("Illegal type: "+type);
+               }
+               if (this.type == type)
+                       return;
+               this.type = type;
+               updateFigure();
+       }
+
+       
+       
+       
+
+
+       /**
+        * Updates the figure shapes and figure size.
+        */
+       @Override
+       public void updateFigure() {
+               figureShapes.clear();
+               figureComponents.clear();
+               
+               calculateSize();
+
+               // Get shapes for all active components
+               for (RocketComponent c: configuration) {
+                       Shape[] s = getShapes(c);
+                       for (int i=0; i < s.length; i++) {
+                               figureShapes.add(s[i]);
+                               figureComponents.add(c);
+                       }
+               }
+               
+               repaint();
+               fireChangeEvent();
+       }
+
+       
+       public void addRelativeExtra(FigureElement p) {
+               relativeExtra.add(p);
+       }
+       
+       public void removeRelativeExtra(FigureElement p) {
+               relativeExtra.remove(p);
+       }
+       
+       public void clearRelativeExtra() {
+               relativeExtra.clear();
+       }
+       
+       
+       public void addAbsoluteExtra(FigureElement p) {
+               absoluteExtra.add(p);
+       }
+       
+       public void removeAbsoluteExtra(FigureElement p) {
+               absoluteExtra.remove(p);
+       }
+       
+       public void clearAbsoluteExtra() {
+               absoluteExtra.clear();
+       }
+       
+
+       /**
+        * Paints the rocket on to the Graphics element.
+        * <p>
+        * Warning:  If paintComponent is used outside the normal Swing usage, some Swing
+        * dependent parameters may be left wrong (mainly transformation).  If it is used,
+        * the RocketFigure should be repainted immediately afterwards.
+        */
+       @Override
+       public void paintComponent(Graphics g) {
+               super.paintComponent(g);
+               Graphics2D g2 = (Graphics2D)g;
+               
+               
+               AffineTransform baseTransform = g2.getTransform();
+               
+               // Update figure shapes if necessary
+               if (figureShapes == null)
+                       updateFigure();
+
+
+               double tx, ty;
+               // Calculate translation for figure centering
+               if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
+
+                       // Figure fits in the viewport
+                       if (type == TYPE_BACK)
+                               tx = getWidth()/2;
+                       else 
+                               tx = (getWidth()-figureWidthPx)/2 - minX*scale;
+
+               } else {
+
+                       // Figure does not fit in viewport
+                       if (type == TYPE_BACK)
+                               tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
+                       else 
+                               tx = BORDER_PIXELS_WIDTH - minX*scale;
+                       
+               }
+               
+               if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
+                        ty = getHeight()/2;
+               } else {
+                        ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
+               }
+               
+               if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+                       // Origin has changed, fire event
+                       translateX = tx;
+                       translateY = ty;
+                       fireChangeEvent();
+               }
+               
+
+               // Calculate and store the transformation used
+               // (inverse is used in detecting clicks on objects)
+               g2transformation = new AffineTransform();
+               g2transformation.translate(translateX, translateY);
+               // Mirror position Y-axis upwards
+               g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
+
+               g2.transform(g2transformation);
+
+               // Set rendering hints appropriately
+               g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
+                               RenderingHints.VALUE_STROKE_NORMALIZE);
+               g2.setRenderingHint(RenderingHints.KEY_RENDERING, 
+                               RenderingHints.VALUE_RENDER_QUALITY);
+               g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
+                               RenderingHints.VALUE_ANTIALIAS_ON);
+
+
+               // Draw all shapes
+               
+               for (int i=0; i < figureShapes.size(); i++) {
+                       RocketComponent c = figureComponents.get(i);
+                       Shape s = figureShapes.get(i);
+                       boolean selected = false;
+                       
+                       // Check if component is in the selection
+                       for (int j=0; j < selection.length; j++) {
+                               if (c == selection[j]) {
+                                       selected = true;
+                                       break;
+                               }
+                       }
+                       
+                       // Set component color and line style
+                       Color color = c.getColor();
+                       if (color == null) {
+                               color = Prefs.getDefaultColor(c.getClass());
+                       }
+                       g2.setColor(color);
+
+                       LineStyle style = c.getLineStyle();
+                       if (style == null)
+                               style = Prefs.getDefaultLineStyle(c.getClass());
+                       
+                       float[] dashes = style.getDashes();
+                       for (int j=0; j<dashes.length; j++) {
+                               dashes[j] *= EXTRA_SCALE / scale;
+                       }
+                       
+                       if (selected) {
+                               g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
+                                               BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
+                               g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+                                               RenderingHints.VALUE_STROKE_PURE);
+                       } else {
+                               g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
+                                               BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
+                               g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
+                                               RenderingHints.VALUE_STROKE_NORMALIZE);
+                       }
+                       g2.draw(s);
+                       
+               }
+               
+               g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
+                               BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
+               g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 
+                               RenderingHints.VALUE_STROKE_NORMALIZE);
+
+               
+               // Draw motors
+               String motorID = configuration.getMotorConfigurationID();
+               Color fillColor = Prefs.getMotorFillColor();
+               Color borderColor = Prefs.getMotorBorderColor();
+               Iterator<MotorMount> iterator = configuration.motorIterator();
+               while (iterator.hasNext()) {
+                       MotorMount mount = iterator.next();
+                       Motor motor = mount.getMotor(motorID);
+                       double length = motor.getLength();
+                       double radius = motor.getDiameter() / 2;
+                       
+                       Coordinate[] position = ((RocketComponent)mount).toAbsolute(
+                                       new Coordinate(((RocketComponent)mount).getLength() + 
+                                                       mount.getMotorOverhang() - length));
+                       
+                       for (int i=0; i < position.length; i++) {
+                               position[i] = transformation.transform(position[i]);
+                       }
+                       
+                       for (Coordinate coord: position) {
+                               Shape s;
+                               if (type == TYPE_SIDE) {
+                                       s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
+                                                       EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length, 
+                                                       EXTRA_SCALE*2*radius);
+                               } else {
+                                       s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
+                                                       EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
+                                                       EXTRA_SCALE*2*radius);
+                               }
+                               g2.setColor(fillColor);
+                               g2.fill(s);
+                               g2.setColor(borderColor);
+                               g2.draw(s);
+                       }
+               }
+               
+               
+               
+               // Draw relative extras
+               for (FigureElement e: relativeExtra) {
+                       e.paint(g2, scale/EXTRA_SCALE);
+               }
+
+               // Draw absolute extras
+               g2.setTransform(baseTransform);
+               Rectangle rect = this.getVisibleRect();
+               
+               for (FigureElement e: absoluteExtra) {
+                       e.paint(g2, 1.0, rect);
+               }
+
+       }
+       
+       
+       public RocketComponent[] getComponentsByPoint(double x, double y) {
+               // Calculate point in shapes' coordinates
+               Point2D.Double p = new Point2D.Double(x,y);
+               try {
+                       g2transformation.inverseTransform(p,p);
+               } catch (NoninvertibleTransformException e) {
+                       return new RocketComponent[0];
+               }
+               
+               LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
+
+               for (int i=0; i<figureShapes.size(); i++) {
+                       if (figureShapes.get(i).contains(p))
+                               l.add(figureComponents.get(i));
+               }
+               return l.toArray(new RocketComponent[0]);
+       }
+       
+       
+       
+       /**
+        * Gets the shapes required to draw the component.
+        * 
+        * @param component
+        * @param params
+        * @return
+        */
+       private Shape[] getShapes(RocketComponent component) {
+               Reflection.Method m;
+
+               // Find the appropriate method
+               switch (type) {
+               case TYPE_SIDE:
+                       m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", 
+                                       RocketComponent.class, Transformation.class);
+                       break;
+                       
+               case TYPE_BACK:
+                       m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", 
+                                       RocketComponent.class, Transformation.class);
+                       break;
+                       
+               default:
+                       throw new RuntimeException("Unknown figure type = "+type);
+               }
+               
+               if (m == null) {
+                       System.err.println("ERROR: Rocket figure paint method not found for " + component);
+                       return new Shape[0];
+               }
+
+               return (Shape[])m.invokeStatic(component,transformation);
+       }
+       
+       
+       
+       /**
+        * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
+        * The bounds are stored in the variables minX, maxX and maxR.
+        */
+       private void calculateFigureBounds() {
+               Collection<Coordinate> bounds = configuration.getBounds();
+               
+               if (bounds.isEmpty()) {
+                       minX = 0;
+                       maxX = 0;
+                       maxR = 0;
+                       return;
+               }
+
+               minX = Double.MAX_VALUE;
+               maxX = Double.MIN_VALUE;
+               maxR = 0;
+               for (Coordinate c: bounds) {
+                       double x = c.x, r = MathUtil.hypot(c.y, c.z);
+                       if (x < minX)
+                               minX = x;
+                       if (x > maxX)
+                               maxX = x;
+                       if (r > maxR)
+                               maxR = r;
+               }
+       }
+       
+       
+       public double getBestZoom(Rectangle2D bounds) {
+               double zh=1, zv=1;
+               if (bounds.getWidth() > 0.0001)
+                       zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
+               if (bounds.getHeight() > 0.0001)
+                       zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
+               return Math.min(zh, zv);
+       }
+
+       
+       /**
+        * Calculates the necessary size of the figure and set the PreferredSize 
+        * property accordingly.
+        */
+       private void calculateSize() {
+               calculateFigureBounds();
+               
+               switch (type) {
+               case TYPE_SIDE:
+                       figureWidth = maxX-minX;
+                       figureHeight = 2*maxR;
+                       break;
+                       
+               case TYPE_BACK:
+                       figureWidth = 2*maxR;
+                       figureHeight = 2*maxR;
+                       break;
+                       
+               default:
+                       assert(false): "Should not occur, type="+type;
+                       figureWidth = 0;
+                       figureHeight = 0;
+               }
+               
+               figureWidthPx = (int)(figureWidth * scale);
+               figureHeightPx = (int)(figureHeight * scale);
+
+               Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
+                               figureHeightPx+2*BORDER_PIXELS_HEIGHT);
+               
+               if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
+                       setPreferredSize(d);
+                       setMinimumSize(d);
+                       revalidate();
+               }
+       }
+       
+       public Rectangle2D getDimensions() {
+               switch (type) {
+               case TYPE_SIDE:
+                       return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
+                       
+               case TYPE_BACK:
+                       return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
+                       
+               default:
+                       throw new RuntimeException("Illegal figure type = "+type);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
new file mode 100644 (file)
index 0000000..94bf101
--- /dev/null
@@ -0,0 +1,693 @@
+package net.sf.openrocket.gui.scalefigure;
+
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.JViewport;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.StageSelector;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.gui.figureelements.CGCaret;
+import net.sf.openrocket.gui.figureelements.CPCaret;
+import net.sf.openrocket.gui.figureelements.Caret;
+import net.sf.openrocket.gui.figureelements.RocketInfo;
+import net.sf.openrocket.gui.main.ComponentTreeModel;
+import net.sf.openrocket.gui.main.SimulationWorker;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.listeners.ApogeeEndListener;
+import net.sf.openrocket.simulation.listeners.InterruptListener;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
+       
+       private final RocketFigure figure;
+       private final ScaleScrollPane scrollPane;
+
+       private JLabel infoMessage;
+       
+       private TreeSelectionModel selectionModel = null;
+
+       
+       /* Calculation of CP and CG */
+       private AerodynamicCalculator calculator;
+       
+       
+       private final OpenRocketDocument document;
+       private final Configuration configuration;
+       
+       private Caret extraCP = null;
+       private Caret extraCG = null;
+       private RocketInfo extraText = null;
+       
+       
+       private double cpAOA = Double.NaN;
+       private double cpTheta = Double.NaN;
+       private double cpMach = Double.NaN;
+       private double cpRoll = Double.NaN;
+       
+       // The functional ID of the rocket that was simulated
+       private int flightDataFunctionalID = -1;
+       private String flightDataMotorID = null;
+       
+       
+       private SimulationWorker backgroundSimulationWorker = null;
+       
+
+       private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
+       
+       
+       /**
+        * The executor service used for running the background simulations.
+        * This uses a fixed-sized thread pool for all background simulations
+        * with all threads in daemon mode and with minimum priority.
+        */
+       private static final Executor backgroundSimulationExecutor;
+       static {
+               backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
+                               new ThreadFactory() {
+                       private ThreadFactory factory = Executors.defaultThreadFactory();
+                       @Override
+                       public Thread newThread(Runnable r) {
+                               Thread t = factory.newThread(r);
+                               t.setDaemon(true);
+                               t.setPriority(Thread.MIN_PRIORITY);
+                               return t;
+                       }
+               });
+       }
+       
+       
+       public RocketPanel(OpenRocketDocument document) {
+               
+               this.document = document;
+               configuration = document.getDefaultConfiguration();
+               
+               // TODO: FUTURE: calculator selection
+               calculator = new BarrowmanCalculator(configuration);
+               
+               // Create figure and custom scroll pane
+               figure = new RocketFigure(configuration);
+               
+               scrollPane = new ScaleScrollPane(figure) {
+                       @Override
+                       public void mouseClicked(MouseEvent event) {
+                               handleMouseClick(event);
+                       }
+               };
+               scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
+               scrollPane.setFitting(true);
+
+               createPanel();
+
+               configuration.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               System.out.println("Configuration changed, calling updateFigure");
+                               updateExtras();
+                               figure.updateFigure();
+                       }
+               });
+       }
+       
+       
+       /**
+        * Creates the layout and components of the panel.
+        */
+       private void createPanel() {
+               setLayout(new MigLayout("","[shrink][grow]","[shrink][shrink][grow][shrink]"));
+               
+               setPreferredSize(new Dimension(800,300));
+               
+
+               //// Create toolbar
+               
+               // Side/back buttons
+               FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
+               action.putValue(Action.NAME, "Side view");
+               action.putValue(Action.SHORT_DESCRIPTION, "Side view");
+               JToggleButton toggle = new JToggleButton(action);
+               add(toggle,"spanx, split");
+               
+               action = new FigureTypeAction(RocketFigure.TYPE_BACK);
+               action.putValue(Action.NAME, "Back view");
+               action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
+               toggle = new JToggleButton(action);
+               add(toggle,"gap rel");
+               
+
+               // Zoom level selector
+               ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
+               add(scaleSelector);
+               
+
+               
+               // Stage selector
+               StageSelector stageSelector = new StageSelector(configuration);
+               add(stageSelector,"");
+               
+               
+               
+               // Motor configuration selector
+               
+               JLabel label = new JLabel("Motor configuration:");
+               label.setHorizontalAlignment(JLabel.RIGHT);
+               add(label,"growx, right");
+               add(new JComboBox(new MotorConfigurationModel(configuration)),"wrap");
+               
+               
+               
+               
+               
+               // Create slider and scroll pane
+               
+               DoubleModel theta = new DoubleModel(figure,"Rotation",
+                               UnitGroup.UNITS_ANGLE,0,2*Math.PI);
+               UnitSelector us = new UnitSelector(theta,true);
+               us.setHorizontalAlignment(JLabel.CENTER);
+               add(us,"alignx 50%, growx");
+
+               // Add the rocket figure
+               add(scrollPane,"grow, spany 2, wmin 300lp, hmin 100lp, wrap");
+               
+               
+               // Add rotation slider
+               // Minimum size to fit "360deg"
+               JLabel l = new JLabel("360\u00b0");
+               Dimension d = l.getPreferredSize();
+               
+               add(new BasicSlider(theta.getSliderModel(0,2*Math.PI),JSlider.VERTICAL,true),
+                               "ax 50%, wrap, width "+(d.width+6)+"px:null:null, growy");
+
+
+               infoMessage = new JLabel("<html>" +
+                               "Click to select &nbsp;&nbsp; " +
+                               "Shift+click to select other &nbsp;&nbsp; " +
+                               "Double-click to edit &nbsp;&nbsp; " +
+                               "Click+drag to move");
+               infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
+               add(infoMessage,"skip, span, gapleft 25, wrap");
+               
+               addExtras();
+       }
+       
+       
+       
+       public RocketFigure getFigure() {
+               return figure;
+       }
+       
+       public AerodynamicCalculator getCalculator() {
+               return calculator;
+       }
+       
+       public Configuration getConfiguration() {
+               return configuration;
+       }
+       
+       public void setSelectionModel(TreeSelectionModel m) {
+               if (selectionModel != null) {
+                       selectionModel.removeTreeSelectionListener(this);
+               }
+               selectionModel = m;
+               selectionModel.addTreeSelectionListener(this);
+               valueChanged((TreeSelectionEvent)null);   // updates FigureParameters
+       }
+       
+       
+       
+       /**
+        * Return the angle of attack used in CP calculation.  NaN signifies the default value
+        * of zero.
+        * @return   the angle of attack used, or NaN.
+        */
+       public double getCPAOA() {
+               return cpAOA;
+       }
+       
+       /**
+        * Set the angle of attack to be used in CP calculation.  A value of NaN signifies that
+        * the default AOA (zero) should be used.
+        * @param aoa   the angle of attack to use, or NaN
+        */
+       public void setCPAOA(double aoa) {
+               if (MathUtil.equals(aoa, cpAOA) ||
+                               (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
+                       return;
+               cpAOA = aoa;
+               updateExtras();
+               figure.updateFigure();
+               fireChangeEvent();
+       }
+       
+       public double getCPTheta() {
+               return cpTheta;
+       }
+       
+       public void setCPTheta(double theta) {
+               if (MathUtil.equals(theta, cpTheta) ||
+                               (Double.isNaN(theta) && Double.isNaN(cpTheta)))
+                       return;
+               cpTheta = theta;
+               if (!Double.isNaN(theta))
+                       figure.setRotation(theta);
+               updateExtras();
+               figure.updateFigure();
+               fireChangeEvent();
+       }
+       
+       public double getCPMach() {
+               return cpMach;
+       }
+       
+       public void setCPMach(double mach) {
+               if (MathUtil.equals(mach, cpMach) ||
+                               (Double.isNaN(mach) && Double.isNaN(cpMach)))
+                       return;
+               cpMach = mach;
+               updateExtras();
+               figure.updateFigure();
+               fireChangeEvent();
+       }
+       
+       public double getCPRoll() {
+               return cpRoll;
+       }
+       
+       public void setCPRoll(double roll) {
+               if (MathUtil.equals(roll, cpRoll) ||
+                               (Double.isNaN(roll) && Double.isNaN(cpRoll)))
+                       return;
+               cpRoll = roll;
+               updateExtras();
+               figure.updateFigure();
+               fireChangeEvent();
+       }
+       
+       
+       
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listeners.add(0,listener);
+       }
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listeners.remove(listener);
+       }
+       
+       protected void fireChangeEvent() {
+               ChangeEvent e = new ChangeEvent(this);
+               ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
+               for (ChangeListener l: list) {
+                       l.stateChanged(e);
+               }
+       }
+
+       
+       
+       
+       /**
+        * Handle clicking on figure shapes.  The functioning is the following:
+        * 
+        * Get the components clicked.
+        * If no component is clicked, do nothing.
+        * If the primary currently selected component is in the set, keep it, 
+        * unless the selector specified is pressed.  If it is pressed, cycle to 
+        * the next component. Otherwise select the first component in the list. 
+        */
+       public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
+
+       private void handleMouseClick(MouseEvent event) {
+               if (event.getButton() != MouseEvent.BUTTON1)
+                       return;
+               Point p0 = event.getPoint();
+               Point p1 = scrollPane.getViewport().getViewPosition();
+               int x = p0.x + p1.x;
+               int y = p0.y + p1.y;
+               
+               RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
+
+               // If no component is clicked, do nothing
+               if (clicked.length == 0)
+                       return;
+               
+               // Check whether the currently selected component is in the clicked components.
+               TreePath path = selectionModel.getSelectionPath();
+               if (path != null) {
+                       RocketComponent current = (RocketComponent)path.getLastPathComponent();
+                       path = null;
+                       for (int i=0; i<clicked.length; i++) {
+                               if (clicked[i] == current) {
+                                       if (event.isShiftDown() && (event.getClickCount()==1)) {
+                                               path = ComponentTreeModel.makeTreePath(clicked[(i+1)%clicked.length]);
+                                       } else {
+                                               path = ComponentTreeModel.makeTreePath(clicked[i]);
+                                       }
+                                       break;
+                               }
+                       }
+               }
+
+               // Currently selected component not clicked
+               if (path == null) {
+                       path = ComponentTreeModel.makeTreePath(clicked[0]);
+               }
+               
+               // Set selection and check for double-click
+               selectionModel.setSelectionPath(path);
+               if (event.getClickCount() == 2) {
+                       RocketComponent component = (RocketComponent)path.getLastPathComponent();
+                       
+                       ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this), 
+                                       document, component);
+               }
+       }
+       
+
+       
+
+       /**
+        * Updates the extra data included in the figure.  Currently this includes
+        * the CP and CG carets.
+        */
+       private WarningSet warnings = new WarningSet();
+       private void updateExtras() {
+               Coordinate cp,cg;
+               double cpx, cgx;
+
+               // TODO: MEDIUM: User-definable conditions
+               FlightConditions conditions = new FlightConditions(configuration);
+               warnings.clear();
+
+               if (!Double.isNaN(cpMach)) {
+                       conditions.setMach(cpMach);
+                       extraText.setMach(cpMach);
+               } else {
+                       conditions.setMach(Prefs.getDefaultMach());
+                       extraText.setMach(Prefs.getDefaultMach());
+               }
+               
+               if (!Double.isNaN(cpAOA)) {
+                       conditions.setAOA(cpAOA);
+               } else {
+                       conditions.setAOA(0);
+               }
+               extraText.setAOA(cpAOA);
+               
+               if (!Double.isNaN(cpRoll)) {
+                       conditions.setRollRate(cpRoll);
+               } else {
+                       conditions.setRollRate(0);
+               }
+
+               if (!Double.isNaN(cpTheta)) {
+                       conditions.setTheta(cpTheta);
+                       cp = calculator.getCP(conditions, warnings);
+               } else {
+                       cp = calculator.getWorstCP(conditions, warnings);
+               }
+               extraText.setTheta(cpTheta);
+               
+
+               cg = calculator.getCG(0);
+//             System.out.println("CG computed as "+cg+ " CP as "+cp);
+               
+               if (cp.weight > 0.000001)
+                       cpx = cp.x;
+               else
+                       cpx = Double.NaN;
+               
+               if (cg.weight > 0.000001)
+                       cgx = cg.x;
+               else
+                       cgx = Double.NaN;
+               
+               // Length bound is assumed to be tight
+               double length = 0, diameter = 0;
+               Collection<Coordinate> bounds = configuration.getBounds();
+               if (!bounds.isEmpty()) {
+                       double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
+                       for (Coordinate c: bounds) {
+                               if (c.x < minX)
+                                       minX = c.x;
+                               if (c.x > maxX)
+                                       maxX = c.x;
+                       }
+                       length = maxX - minX;
+               }
+               
+               for (RocketComponent c: configuration) {
+                       if (c instanceof SymmetricComponent) {
+                               double d1 = ((SymmetricComponent)c).getForeRadius() * 2;
+                               double d2 = ((SymmetricComponent)c).getAftRadius() * 2;
+                               diameter = MathUtil.max(diameter, d1, d2);
+                       }
+               }
+
+               extraText.setCG(cgx);
+               extraText.setCP(cpx);
+               extraText.setLength(length);
+               extraText.setDiameter(diameter);
+               extraText.setMass(cg.weight);
+               extraText.setWarnings(warnings);
+                       
+               
+               if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
+
+                       // TODO: LOW: Y-coordinate and rotation
+                       extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
+                       extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
+
+               } else {
+                       
+                       extraCP.setPosition(Double.NaN, Double.NaN);
+                       extraCG.setPosition(Double.NaN, Double.NaN);
+                       
+               }
+               
+               
+               ////////  Flight simulation in background
+               
+               // Check whether to compute or not
+               if (!Prefs.computeFlightInBackground()) {
+                       extraText.setFlightData(null);
+                       extraText.setCalculatingData(false);
+                       stopBackgroundSimulation();
+                       return;
+               }
+               
+               // Check whether data is already up to date
+               if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
+                               flightDataMotorID == configuration.getMotorConfigurationID()) {
+                       return;
+               }
+
+               flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
+               flightDataMotorID = configuration.getMotorConfigurationID();
+               
+               // Stop previous computation (if any)
+               stopBackgroundSimulation();
+               
+               // Check that configuration has motors
+               if (!configuration.hasMotors()) {
+                       extraText.setFlightData(FlightData.NaN_DATA);
+                       extraText.setCalculatingData(false);
+                       return;
+               }
+
+               // Start calculation process
+               extraText.setCalculatingData(true);
+               
+               Rocket duplicate = configuration.getRocket().copy();
+               Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
+               simulation.getConditions().setMotorConfigurationID(
+                               configuration.getMotorConfigurationID());
+
+               backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
+               backgroundSimulationExecutor.execute(backgroundSimulationWorker);
+       }
+       
+       /**
+        * Cancels the current background simulation worker, if any.
+        */
+       private void stopBackgroundSimulation() {
+               if (backgroundSimulationWorker != null) {
+                       backgroundSimulationWorker.cancel(true);
+                       backgroundSimulationWorker = null;
+               }
+       }
+       
+
+       /**
+        * A SimulationWorker that simulates the rocket flight in the background and
+        * sets the results to the extra text when finished.  The worker can be cancelled
+        * if necessary.
+        */
+       private class BackgroundSimulationWorker extends SimulationWorker {
+
+               public BackgroundSimulationWorker(Simulation sim) {
+                       super(sim);
+               }
+               
+               @Override
+               protected FlightData doInBackground() {
+                       
+                       // Pause a little while to allow faster UI reaction
+                       try {
+                               Thread.sleep(300);
+                       } catch (InterruptedException ignore) { }
+                       if (isCancelled() || backgroundSimulationWorker != this)
+                               return null;
+                       
+                       return super.doInBackground();
+               }
+
+               @Override
+               protected void simulationDone() {
+                       // Do nothing if cancelled
+                       if (isCancelled() || backgroundSimulationWorker != this)  // Double-check
+                               return;
+                       
+                       backgroundSimulationWorker = null;
+                       extraText.setFlightData(simulation.getSimulatedData());
+                       extraText.setCalculatingData(false);
+                       figure.repaint();
+               }
+
+               @Override
+               protected SimulationListener[] getExtraListeners() {
+                       return new SimulationListener[] {
+                                       InterruptListener.INSTANCE,
+                                       ApogeeEndListener.INSTANCE
+                       };
+               }
+
+               @Override
+               protected void simulationInterrupted(Throwable t) {
+                       // Do nothing on cancel, set N/A data otherwise
+                       if (isCancelled() || backgroundSimulationWorker != this)  // Double-check
+                               return;
+                       
+                       backgroundSimulationWorker = null;
+                       extraText.setFlightData(FlightData.NaN_DATA);
+                       extraText.setCalculatingData(false);
+                       figure.repaint();
+               }
+       }
+       
+       
+       
+       /**
+        * Adds the extra data to the figure.  Currently this includes the CP and CG carets.
+        */
+       private void addExtras() {
+               figure.clearRelativeExtra();
+               extraCG = new CGCaret(0,0);
+               extraCP = new CPCaret(0,0);
+               extraText = new RocketInfo(configuration);
+               updateExtras();
+               figure.addRelativeExtra(extraCP);
+               figure.addRelativeExtra(extraCG);
+               figure.addAbsoluteExtra(extraText);
+       }
+
+       
+       /**
+        * Updates the selection in the FigureParameters and repaints the figure.  
+        * Ignores the event itself.
+        */
+       public void valueChanged(TreeSelectionEvent e) {
+               TreePath[] paths = selectionModel.getSelectionPaths();
+               if (paths==null) {
+                       figure.setSelection(null);
+                       return;
+               }
+               
+               RocketComponent[] components = new RocketComponent[paths.length];
+               for (int i=0; i<paths.length; i++)
+                       components[i] = (RocketComponent)paths[i].getLastPathComponent();
+               figure.setSelection(components);
+       }
+
+       
+       
+       /**
+        * An <code>Action</code> that shows whether the figure type is the type
+        * given in the constructor.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       private class FigureTypeAction extends AbstractAction implements ChangeListener {
+               private final int type;
+               
+               public FigureTypeAction(int type) {
+                       this.type = type;
+                       stateChanged(null);
+                       figure.addChangeListener(this);
+               }
+               
+               public void actionPerformed(ActionEvent e) {
+                       boolean state = (Boolean)getValue(Action.SELECTED_KEY);
+                       if (state == true) {
+                               // This view has been selected
+                               figure.setType(type);
+                               updateExtras();
+                       }
+                       stateChanged(null);
+               }
+
+               public void stateChanged(ChangeEvent e) {
+                       putValue(Action.SELECTED_KEY,figure.getType() == type);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java b/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java
new file mode 100644 (file)
index 0000000..161c835
--- /dev/null
@@ -0,0 +1,67 @@
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.Dimension;
+
+import net.sf.openrocket.util.ChangeSource;
+
+
+public interface ScaleFigure extends ChangeSource {
+       
+       /**
+        * Extra scaling applied to the figure.  The f***ing Java JRE doesn't know 
+        * how to draw shapes when using very large scaling factors, so this must 
+        * be manually applied to every single shape used.
+        * <p>
+        * The scaling factor used is divided by this value, and every coordinate used 
+        * in the figures must be multiplied by this factor.
+        */
+       public static final double EXTRA_SCALE = 1000;
+       
+       /**
+        * Shorthand for {@link #EXTRA_SCALE}.
+        */
+       public static final double S = EXTRA_SCALE;
+       
+       
+       /**
+        * Set the scale level of the figure.  A scale value of 1.0 indicates an original
+        * size when using the current DPI level.
+        * 
+        * @param scale   the scale level.
+        */
+       public void setScaling(double scale);
+       
+       
+       /**
+        * Set the scale level so that the figure fits into the given bounds.
+        * 
+        * @param bounds  the bounds of the figure.
+        */
+       public void setScaling(Dimension bounds);
+       
+       
+       /**
+        * Return the scale level of the figure.  A scale value of 1.0 indicates an original
+        * size when using the current DPI level.
+        * 
+        * @return   the current scale level.
+        */
+       public double getScaling();
+       
+
+       /**
+        * Return the scale of the figure on px/m.
+        * 
+        * @return   the current scale value.
+        */
+       public double getAbsoluteScale();
+       
+
+       /**
+        * Return the pixel coordinates of the figure origin.
+        * 
+        * @return      the pixel coordinates of the figure origin.
+        */
+       public Dimension getOrigin();
+       
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java b/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java
new file mode 100644 (file)
index 0000000..e1d08b6
--- /dev/null
@@ -0,0 +1,374 @@
+package net.sf.openrocket.gui.scalefigure;
+
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JViewport;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.unit.Tick;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+
+/**
+ * A scroll pane that holds a {@link ScaleFigure} and includes rulers that show
+ * natural units.  The figure can be moved by dragging on the figure.
+ * <p>
+ * This class implements both <code>MouseListener</code> and 
+ * <code>MouseMotionListener</code>.  If subclasses require extra functionality
+ * (e.g. checking for clicks) then these methods may be overridden, and only unhandled
+ * events passed to this class.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ScaleScrollPane extends JScrollPane 
+               implements MouseListener, MouseMotionListener {
+       
+       public static final int RULER_SIZE = 20;
+       public static final int MINOR_TICKS = 3;
+       public static final int MAJOR_TICKS = 30;
+
+       
+       private JComponent component;
+       private ScaleFigure figure;
+       private JViewport viewport;
+
+       private DoubleModel rulerUnit;
+       private Ruler horizontalRuler;
+       private Ruler verticalRuler;
+       
+       private final boolean allowFit;
+       
+       private boolean fit = false;
+       
+       
+       public ScaleScrollPane(JComponent component) {
+               this(component, true);
+       }
+       
+       public ScaleScrollPane(JComponent component, boolean allowFit) {
+               super(component);
+//             super(component, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, 
+//                             JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+               
+               if (!(component instanceof ScaleFigure)) {
+                       throw new IllegalArgumentException("component must implement ScaleFigure");
+               }
+               
+               this.component = component;
+               this.figure = (ScaleFigure)component;
+               this.allowFit = allowFit;
+               
+
+               rulerUnit = new DoubleModel(0.0,UnitGroup.UNITS_LENGTH);
+               rulerUnit.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               ScaleScrollPane.this.component.repaint();
+                       }
+               });
+               horizontalRuler = new Ruler(Ruler.HORIZONTAL);
+               verticalRuler = new Ruler(Ruler.VERTICAL);
+               this.setColumnHeaderView(horizontalRuler);
+               this.setRowHeaderView(verticalRuler);
+               
+               UnitSelector selector = new UnitSelector(rulerUnit);
+               selector.setFont(new Font("SansSerif", Font.PLAIN, 8));
+               this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector);
+               this.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new JPanel());
+               this.setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel());
+               this.setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel());
+
+               this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
+
+               
+               viewport = this.getViewport();
+               viewport.addMouseListener(this);
+               viewport.addMouseMotionListener(this);
+               
+               figure.addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               horizontalRuler.updateSize();
+                               verticalRuler.updateSize();
+                               if (fit) {
+                                       setFitting(true);
+                               }
+                       }
+               });
+               
+               viewport.addComponentListener(new ComponentAdapter() {
+                       @Override
+                       public void componentResized(ComponentEvent e) {
+                               if (fit) {
+                                       setFitting(true);
+                               }
+                       }
+               });
+               
+       }
+       
+       public ScaleFigure getFigure() {
+               return figure;
+       }
+       
+       
+       public boolean isFittingAllowed() {
+               return allowFit;
+       }
+       
+       public boolean isFitting() {
+               return fit;
+       }
+       
+       public void setFitting(boolean fit) {
+               if (fit && !allowFit) {
+                       throw new RuntimeException("Attempting to fit figure not allowing fit.");
+               }
+               this.fit = fit;
+               if (fit) {
+                       setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+                       setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
+                       validate();
+                       Dimension view = viewport.getExtentSize();
+                       figure.setScaling(view);
+               } else {
+                       setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+                       setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
+               }
+       }
+       
+       
+
+       public double getScaling() {
+               return figure.getScaling();
+       }
+       
+       public double getScale() {
+               return figure.getAbsoluteScale();
+       }
+
+       public void setScaling(double scale) {
+               if (fit) {
+                       setFitting(false);
+               }
+               figure.setScaling(scale);
+               horizontalRuler.repaint();
+               verticalRuler.repaint();
+       }
+       
+       
+       public Unit getCurrentUnit() {
+               return rulerUnit.getCurrentUnit();
+       }
+       
+       
+       ////////////////  Mouse handlers  ////////////////
+       
+
+       private int dragStartX=0;
+       private int dragStartY=0;
+       private Rectangle dragRectangle = null;
+
+       @Override
+       public void mousePressed(MouseEvent e) {
+               dragStartX = e.getX();
+               dragStartY = e.getY();
+               dragRectangle = viewport.getViewRect();
+       }
+
+       @Override
+       public void mouseReleased(MouseEvent e) {
+               dragRectangle = null;
+       }
+
+       @Override
+       public void mouseDragged(MouseEvent e) {
+               if (dragRectangle==null) {
+                       return;
+               }
+
+               dragRectangle.setLocation(dragStartX-e.getX(),dragStartY-e.getY());
+
+               dragStartX = e.getX();
+               dragStartY = e.getY();
+               
+               viewport.scrollRectToVisible(dragRectangle);
+       }
+
+       @Override
+       public void mouseClicked(MouseEvent e) {
+       }
+
+       @Override
+       public void mouseEntered(MouseEvent e) {
+       }
+
+       @Override
+       public void mouseExited(MouseEvent e) {
+       }
+
+       @Override
+       public void mouseMoved(MouseEvent e) {
+       }
+       
+
+       
+       ////////////////  The view port rulers  ////////////////
+       
+
+       private class Ruler extends JComponent {
+               public static final int HORIZONTAL = 0;
+               public static final int VERTICAL = 1;
+               
+               private final int orientation;
+               
+               public Ruler(int orientation) {
+                       this.orientation = orientation;
+                       updateSize();
+                       
+                       rulerUnit.addChangeListener(new ChangeListener() {
+                               @Override
+                               public void stateChanged(ChangeEvent e) {
+                                       Ruler.this.repaint();
+                               }
+                       });
+               }
+               
+               
+               public void updateSize() {
+                       Dimension d = component.getPreferredSize();
+                       if (orientation == HORIZONTAL) {
+                               setPreferredSize(new Dimension(d.width+10,RULER_SIZE));
+                       } else {
+                               setPreferredSize(new Dimension(RULER_SIZE,d.height+10));
+                       }
+                       revalidate();
+                       repaint();
+               }
+               
+               private double fromPx(int px) {
+                       Dimension origin = figure.getOrigin();
+                       if (orientation == HORIZONTAL) {
+                               px -= origin.width;
+                       } else {
+//                             px = -(px - origin.height);
+                               px -= origin.height;
+                       }
+                       return px/figure.getAbsoluteScale();
+               }
+               
+               private int toPx(double l) {
+                       Dimension origin = figure.getOrigin();
+                       int px = (int)(l * figure.getAbsoluteScale() + 0.5);
+                       if (orientation == HORIZONTAL) {
+                               px += origin.width;
+                       } else {
+                               px = px + origin.height;
+//                             px += origin.height;
+                       }
+                       return px;
+               }
+               
+               
+           @Override
+               protected void paintComponent(Graphics g) {
+               super.paintComponent(g);
+                       Graphics2D g2 = (Graphics2D)g;
+                       
+                       Rectangle area = g2.getClipBounds();
+
+               // Fill area with background color
+                       g2.setColor(getBackground());
+               g2.fillRect(area.x, area.y, area.width, area.height+100);
+
+
+               int startpx,endpx;
+               if (orientation == HORIZONTAL) {
+                       startpx = area.x;
+                       endpx = area.x+area.width;
+               } else {
+                       startpx = area.y;
+                       endpx = area.y+area.height;
+               }
+               
+               Unit unit = rulerUnit.getCurrentUnit();
+               double start,end,minor,major;
+               start = fromPx(startpx);
+               end = fromPx(endpx);
+               minor = MINOR_TICKS/figure.getAbsoluteScale();
+               major = MAJOR_TICKS/figure.getAbsoluteScale();
+
+               Tick[] ticks = unit.getTicks(start, end, minor, major);
+               
+               
+               // Set color & hints
+               g2.setColor(Color.BLACK);
+                       g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+                                       RenderingHints.VALUE_STROKE_NORMALIZE);
+                       g2.setRenderingHint(RenderingHints.KEY_RENDERING, 
+                                       RenderingHints.VALUE_RENDER_QUALITY);
+                       g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+                                       RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+                       for (Tick t: ticks) {
+                       int position = toPx(t.value);
+                       drawTick(g2,position,t);
+               }
+           }
+           
+           private void drawTick(Graphics g, int position, Tick t) {
+               int length;
+               String str = null;
+               if (t.major) {
+                       length = RULER_SIZE/2;
+               } else {
+                       if (t.notable)
+                               length = RULER_SIZE/3;
+                       else
+                               length = RULER_SIZE/6;
+               }
+               
+               // Set font
+               if (t.major) {
+                       str = rulerUnit.getCurrentUnit().toString(t.value);
+                       if (t.notable)
+                       g.setFont(new Font("SansSerif", Font.BOLD, 9));
+                       else 
+                               g.setFont(new Font("SansSerif", Font.PLAIN, 9));
+               }
+               
+               // Draw tick & text
+               if (orientation == HORIZONTAL) {
+                       g.drawLine(position, RULER_SIZE-length, position, RULER_SIZE);
+                       if (str != null)
+                               g.drawString(str, position, RULER_SIZE-length-1);
+               } else {
+                       g.drawLine(RULER_SIZE-length, position, RULER_SIZE, position);
+                       if (str != null)
+                               g.drawString(str, 1, position-1);
+               }
+           }
+       }
+}
diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java b/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java
new file mode 100644 (file)
index 0000000..d4452d6
--- /dev/null
@@ -0,0 +1,155 @@
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.util.Icons;
+
+public class ScaleSelector extends JPanel {
+
+       // Ready zoom settings
+       private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%");
+
+       private static final double[] ZOOM_LEVELS = { 0.15, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0 }; 
+       private static final String ZOOM_FIT = "Fit";
+       private static final String[] ZOOM_SETTINGS;
+       static {
+               ZOOM_SETTINGS = new String[ZOOM_LEVELS.length+1];
+               for (int i=0; i<ZOOM_LEVELS.length; i++)
+                       ZOOM_SETTINGS[i] = PERCENT_FORMAT.format(ZOOM_LEVELS[i]);
+               ZOOM_SETTINGS[ZOOM_SETTINGS.length-1] = ZOOM_FIT;
+       }
+       
+       
+       private final ScaleScrollPane scrollPane;
+       private JComboBox zoomSelector;
+       
+       
+       public ScaleSelector(ScaleScrollPane scroll) {
+               super(new MigLayout());
+
+               this.scrollPane = scroll;
+               
+               // Zoom out button
+               JButton button = new JButton(Icons.ZOOM_OUT);
+               button.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               double scale = scrollPane.getScaling();
+                               scale = getPreviousScale(scale);
+                               scrollPane.setScaling(scale);
+                       }
+               });
+               add(button, "gap");
+
+               // Zoom level selector
+               String[] settings = ZOOM_SETTINGS;
+               if (!scrollPane.isFittingAllowed()) {
+                       settings = Arrays.copyOf(settings, settings.length-1);
+               }
+               
+               zoomSelector = new JComboBox(settings);
+               zoomSelector.setEditable(true);
+               setZoomText();
+               zoomSelector.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               try {
+                                       String text = (String)zoomSelector.getSelectedItem();
+                                       text = text.replaceAll("%", "").trim();
+
+                                       if (text.toLowerCase().startsWith(ZOOM_FIT.toLowerCase()) &&
+                                                       scrollPane.isFittingAllowed()) {
+                                               scrollPane.setFitting(true);
+                                               setZoomText();
+                                               return;
+                                       }
+
+                                       double n = Double.parseDouble(text);
+                                       n /= 100;
+                                       if (n <= 0.005)
+                                               n = 0.005;
+
+                                       scrollPane.setScaling(n);
+                                       setZoomText();
+                               } catch (NumberFormatException ignore) {
+                               } finally {
+                                       setZoomText();
+                               }
+                       }
+               });
+               scrollPane.getFigure().addChangeListener(new ChangeListener() {
+                       @Override
+                       public void stateChanged(ChangeEvent e) {
+                               setZoomText();
+                       }
+               });
+               add(zoomSelector,"gap rel");
+
+
+               // Zoom in button
+               button = new JButton(Icons.ZOOM_IN);
+               button.addActionListener(new ActionListener() {
+                       public void actionPerformed(ActionEvent e) {
+                               double scale = scrollPane.getScaling();
+                               scale = getNextScale(scale);
+                               scrollPane.setScaling(scale);
+                       }
+               });
+               add(button,"gapleft rel");
+
+       }       
+
+       
+
+       private void setZoomText() {
+               String text;
+               double zoom = scrollPane.getScaling();
+               text = PERCENT_FORMAT.format(zoom);
+               if (scrollPane.isFitting()) {
+                       text = "Fit ("+text+")";
+               }
+               if (!text.equals(zoomSelector.getSelectedItem()))
+                       zoomSelector.setSelectedItem(text);
+       }
+       
+
+       
+       private double getPreviousScale(double scale) {
+               int i;
+               for (i=0; i<ZOOM_LEVELS.length-1; i++) {
+                       if (scale > ZOOM_LEVELS[i]+0.05 && scale < ZOOM_LEVELS[i+1]+0.05)
+                               return ZOOM_LEVELS[i];
+               }
+               if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) {
+                       // scale is large, drop to next lowest full 100%
+                       scale = Math.ceil(scale-1.05);
+                       return Math.max(scale, ZOOM_LEVELS[i]);
+               }
+               // scale is small
+               return scale/1.5;
+       }
+       
+       
+       private double getNextScale(double scale) {
+               int i;
+               for (i=0; i<ZOOM_LEVELS.length-1; i++) {
+                       if (scale > ZOOM_LEVELS[i]-0.05 && scale < ZOOM_LEVELS[i+1]-0.05)
+                               return ZOOM_LEVELS[i+1];
+               }
+               if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) {
+                       // scale is large, give next full 100%
+                       scale = Math.floor(scale+1.05);
+                       return scale;
+               }
+               return scale*1.5;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/material/Material.java b/src/net/sf/openrocket/material/Material.java
new file mode 100644 (file)
index 0000000..f59885d
--- /dev/null
@@ -0,0 +1,211 @@
+package net.sf.openrocket.material;
+
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * A class for different material types.  Each material has a name and density.
+ * The interpretation of the density depends on the material type.  For
+ * {@link Type#BULK} it is kg/m^3, for {@link Type#SURFACE} km/m^2.
+ * <p>
+ * Objects of this type are immutable.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class Material implements Comparable<Material> {
+
+       public enum Type {
+               LINE,
+               SURFACE,
+               BULK
+       }
+       
+       public static class Line extends Material {
+               public Line(String name, double density) {
+                       super(name, density);
+               }
+
+               @Override
+               public UnitGroup getUnitGroup() {
+                       return UnitGroup.UNITS_DENSITY_LINE;
+               }
+
+               @Override
+               public Type getType() {
+                       return Type.LINE;
+               }
+       }
+       
+       public static class Surface extends Material {
+               
+               public Surface(String name, double density) {
+                       super(name, density);
+               }
+               
+               @Override
+               public UnitGroup getUnitGroup() {
+                       return UnitGroup.UNITS_DENSITY_SURFACE;
+               }
+
+               @Override
+               public Type getType() {
+                       return Type.SURFACE;
+               }
+               
+               @Override
+               public String toStorableString() {
+                       return super.toStorableString();
+               }
+       }
+       
+       public static class Bulk extends Material {
+               public Bulk(String name, double density) {
+                       super(name, density);
+               }
+
+               @Override
+               public UnitGroup getUnitGroup() {
+                       return UnitGroup.UNITS_DENSITY_BULK;
+               }
+
+               @Override
+               public Type getType() {
+                       return Type.BULK;
+               }
+       }
+       
+       
+       
+       private final String name;
+       private final double density;
+       
+       
+       public Material(String name, double density) {
+               this.name = name;
+               this.density = density;
+       }
+       
+       
+       
+       public double getDensity() {
+               return density;
+       }
+       
+       public String getName() {
+               return name;
+       }
+       
+       public String getName(Unit u) {
+               return name + " (" + u.toStringUnit(density) + ")";
+       }
+       
+       public abstract UnitGroup getUnitGroup();
+       public abstract Type getType();
+       
+       @Override
+       public String toString() {
+               return getName(getUnitGroup().getDefaultUnit());
+       }
+       
+
+       /**
+        * Compares this object to another object.  Material objects are equal if and only if
+        * their types, names and densities are identical.
+        */
+       @Override
+       public boolean equals(Object o) {
+               if (o == null)
+                       return false;
+               if (this.getClass() != o.getClass())
+                       return false;
+               Material m = (Material)o;
+               return ((m.name.equals(this.name)) && 
+                               MathUtil.equals(m.density, this.density)); 
+       }
+
+
+       /**
+        * A hashCode() method giving a hash code compatible with the equals() method.
+        */
+       @Override
+       public int hashCode() {
+               return name.hashCode() + (int)(density*1000);
+       }
+
+       
+       /**
+        * Order the materials according to their name, secondarily according to density.
+        */
+       public int compareTo(Material o) {
+               int c = this.name.compareTo(o.name);
+               if (c != 0) {
+                       return c;
+               } else {
+                       return (int)((this.density - o.density)*1000);
+               }
+       }
+       
+       
+       
+       public static Material newMaterial(Type type, String name, double density) {
+               switch (type) {
+               case LINE:
+                       return new Material.Line(name, density);
+                       
+               case SURFACE:
+                       return new Material.Surface(name, density);
+                       
+               case BULK:
+                       return new Material.Bulk(name, density);
+                       
+               default:
+                       throw new IllegalArgumentException("Unknown material type: "+type);
+               }
+       }
+       
+       
+       public String toStorableString() {
+               return getType().name() + "|" + name.replace('|', ' ') + '|' + density;
+       }
+       
+       public static Material fromStorableString(String str) {
+               String[] split = str.split("\\|",3);
+               if (split.length < 3)
+                       throw new IllegalArgumentException("Illegal material string: "+str);
+
+               Type type = null;
+               String name;
+               double density;
+               
+               try {
+                       type = Type.valueOf(split[0]);
+               } catch (Exception e) {
+                       throw new IllegalArgumentException("Illegal material string: "+str, e);
+               }
+
+               name = split[1];
+               
+               try {
+                       density = Double.parseDouble(split[2]);
+               } catch (NumberFormatException e) {
+                       throw new IllegalArgumentException("Illegal material string: "+str, e);
+               }
+               
+               switch (type) {
+               case BULK:
+                       return new Material.Bulk(name, density);
+                       
+               case SURFACE:
+                       return new Material.Surface(name, density);
+                       
+               case LINE:
+                       return new Material.Line(name, density);
+                       
+               default:
+                       throw new IllegalArgumentException("Illegal material string: "+str);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/BodyComponent.java b/src/net/sf/openrocket/rocketcomponent/BodyComponent.java
new file mode 100644 (file)
index 0000000..f17bd3d
--- /dev/null
@@ -0,0 +1,79 @@
+package net.sf.openrocket.rocketcomponent;
+
+
+/**
+ * Class to represent a body object.  The object can be described as a function of
+ * the cylindrical coordinates x and angle theta as  r = f(x,theta).  The component 
+ * need not be symmetrical in any way (e.g. square tube, slanted cone etc).
+ * 
+ * It defines the methods getRadius(x,theta) and getInnerRadius(x,theta), as well
+ * as get/setLength().
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class BodyComponent extends ExternalComponent {
+
+       /**
+        * Default constructor.  Sets the relative position to POSITION_RELATIVE_AFTER,
+        * i.e. body components come after one another.
+        */
+       public BodyComponent() {
+               super(RocketComponent.Position.AFTER);
+       }
+       
+       
+       
+       /**
+        * Get the outer radius of the component at cylindrical coordinate (x,theta).
+        * 
+        * Note that the return value may be negative for a slanted object.
+        * 
+        * @param x  Distance in x direction
+        * @param theta  Angle about the x-axis
+        * @return  Distance to the outer edge of the object
+        */
+       public abstract double getRadius(double x, double theta);
+
+       
+       /**
+        * Get the inner radius of the component at cylindrical coordinate (x,theta).
+        * 
+        * Note that the return value may be negative for a slanted object.
+        * 
+        * @param x  Distance in x direction
+        * @param theta  Angle about the x-axis
+        * @return  Distance to the inner edge of the object
+        */
+       public abstract double getInnerRadius(double x, double theta);
+
+       
+
+       /**
+        * Sets the length of the body component.
+        */
+       public void setLength(double length) {
+               if (this.length == length)
+                       return;
+               this.length = Math.max(length,0);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       /**
+        * Check whether the given type can be added to this component.  BodyComponents allow any
+        * InternalComponents or ExternalComponents, excluding BodyComponents, to be added.
+        * 
+        * @param type  The RocketComponent class type to add.
+        * @return      Whether such a component can be added.
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               if (InternalComponent.class.isAssignableFrom(type))
+                       return true;
+               if (ExternalComponent.class.isAssignableFrom(type) &&
+                       !BodyComponent.class.isAssignableFrom(type))
+                       return true;
+               return false;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/BodyTube.java b/src/net/sf/openrocket/rocketcomponent/BodyTube.java
new file mode 100644 (file)
index 0000000..d22ab67
--- /dev/null
@@ -0,0 +1,388 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * Rocket body tube component.  Has only two parameters, a radius and length.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BodyTube extends SymmetricComponent implements MotorMount {
+
+       private double radius=0;
+       private boolean autoRadius = false;  // Radius chosen automatically based on parent component
+       
+       // When changing the inner radius, thickness is modified
+       
+       private boolean motorMount = false;
+       private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>();
+       private HashMap<String, Motor> motors = new HashMap<String, Motor>();
+       private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC;
+       private double ignitionDelay = 0;
+       private double overhang = 0;
+       
+
+       
+       public BodyTube() {
+               super();
+               this.length = 8*DEFAULT_RADIUS;
+               this.radius = DEFAULT_RADIUS;
+               this.autoRadius = true;
+       }
+       
+       public BodyTube(double length, double radius) {
+               super();
+               this.radius = Math.max(radius,0);
+               this.length = Math.max(length,0);
+       }
+       
+       
+       public BodyTube(double length, double radius, boolean filled) {
+               this(length,radius);
+               this.filled = filled;
+       }
+       
+       public BodyTube(double length, double radius, double thickness) {
+               this(length,radius);
+               this.filled = false;
+               this.thickness = thickness;
+       }
+       
+
+       /************  Get/set component parameter methods ************/
+
+       /**
+        * Return the outer radius of the body tube.
+        */
+       public double getRadius() {
+               if (autoRadius) {
+                       // Return auto radius from front or rear
+                       double r = -1;
+                       SymmetricComponent c = this.getPreviousSymmetricComponent();
+                       if (c != null) {
+                               r = c.getFrontAutoRadius();
+                       }
+                       if (r < 0) {
+                               c = this.getNextSymmetricComponent();
+                               if (c != null) {
+                                       r = c.getRearAutoRadius();
+                               }
+                       }
+                       if (r < 0)
+                               r = DEFAULT_RADIUS;
+                       return r;
+               }
+               return radius;
+       }
+       
+       
+       /**
+        * Set the outer radius of the body tube.  If the radius is less than the wall thickness,
+        * the wall thickness is decreased accordingly of the value of the radius.
+        * This method sets the automatic radius off.
+        */
+       public void setRadius(double radius) {
+               if ((this.radius == radius) && (autoRadius==false))
+                       return;
+               
+               this.autoRadius = false;
+               this.radius = Math.max(radius,0);
+
+               if (this.thickness > this.radius)
+                       this.thickness = this.radius;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+
+       /**
+        * Returns whether the radius is selected automatically or not.
+        * Returns false also in case automatic radius selection is not possible.
+        */
+       public boolean isRadiusAutomatic() {
+               return autoRadius;
+       }
+
+       /**
+        * Sets whether the radius is selected automatically or not.  
+        */
+       public void setRadiusAutomatic(boolean auto) {
+               if (autoRadius == auto)
+                       return;
+               
+               autoRadius = auto;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       @Override
+       public double getAftRadius() {  return getRadius(); }
+       @Override
+       public double getForeRadius() { return getRadius(); }
+       @Override
+       public boolean isAftRadiusAutomatic() { return isRadiusAutomatic();     }
+       @Override
+       public boolean isForeRadiusAutomatic() { return isRadiusAutomatic(); }
+       
+       
+       
+       @Override
+       protected double getFrontAutoRadius() {
+               if (isRadiusAutomatic()) {
+                       // Search for previous SymmetricComponent
+                       SymmetricComponent c = this.getPreviousSymmetricComponent();
+                       if (c != null) {
+                               return c.getFrontAutoRadius();
+                       } else {
+                               return -1;
+                       }
+               }
+               return getRadius();
+       }
+
+       @Override
+       protected double getRearAutoRadius() {
+               if (isRadiusAutomatic()) {
+                       // Search for next SymmetricComponent
+                       SymmetricComponent c = this.getNextSymmetricComponent();
+                       if (c != null) {
+                               return c.getRearAutoRadius();
+                       } else {
+                               return -1;
+                       }
+               }
+               return getRadius();
+       }
+
+
+       
+       
+       
+       
+       
+       public double getInnerRadius() {
+               if (filled)
+                       return 0;
+               return Math.max(getRadius()-thickness, 0);
+       }
+       
+       public void setInnerRadius(double r) {
+               setThickness(getRadius()-r);
+       }
+       
+       
+       
+       
+       /**
+        * Return the component name.
+        */
+       @Override
+       public String getComponentName() {
+               return "Body tube";
+       }
+
+
+       /************ Component calculations ***********/
+       
+       // From SymmetricComponent
+       /**
+        * Returns the outer radius at the position x.  This returns the same value as getRadius().
+        */
+       @Override
+       public double getRadius(double x) {
+               return getRadius();
+       }
+
+       /**
+        * Returns the inner radius at the position x.  If the tube is filled, returns always zero.
+        */
+       @Override
+       public double getInnerRadius(double x) {
+               if (filled)
+                       return 0.0;
+               else
+                       return Math.max(getRadius()-thickness,0);
+       }
+       
+
+       /**
+        * Returns the body tube's center of gravity.
+        */
+       @Override
+       public Coordinate getComponentCG() {
+               return new Coordinate(length/2,0,0,getComponentMass());
+       }
+       
+       /**
+        * Returns the body tube's volume.
+        */
+       @Override
+       public double getComponentVolume() {
+               double r = getRadius();
+               if (filled)
+                       return getFilledVolume(r,length);
+               else
+                       return getFilledVolume(r,length) - getFilledVolume(getInnerRadius(0),length);
+       }
+       
+       
+       @Override
+       public double getLongitudalUnitInertia() {
+               // 1/12 * (3 * (r1^2 + r2^2) + h^2)
+               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
+                               MathUtil.pow2(getLength())) / 12;
+       }
+
+       @Override
+       public double getRotationalUnitInertia() {
+               // 1/2 * (r1^2 + r2^2)
+               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+       }
+
+
+       
+
+       /**
+        * Helper function for cylinder volume.
+        */
+       private static double getFilledVolume(double r, double l) {
+               return Math.PI * r*r * l;
+       }
+
+       
+       /**
+        * Adds bounding coordinates to the given set.  The body tube will fit within the
+        * convex hull of the points.
+        * 
+        * Currently the points are simply a rectangular box around the body tube.
+        */
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               Collection<Coordinate> bounds = new ArrayList<Coordinate>(8);
+               double r = getRadius();
+               addBound(bounds,0,r);
+               addBound(bounds,length,r);
+               return bounds;
+       }
+
+
+       ////////////////  Motor mount  /////////////////
+       
+       @Override
+       public boolean isMotorMount() {
+               return motorMount;
+       }
+
+       @Override
+       public void setMotorMount(boolean mount) {
+               if (motorMount == mount)
+                       return;
+               motorMount = mount;
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+       
+       @Override
+       public Motor getMotor(String id) {
+               return motors.get(id);
+       }
+
+       @Override
+       public void setMotor(String id, Motor motor) {
+               Motor current = motors.get(id);
+               if ((motor == null && current == null) ||
+                               (motor != null && motor.equals(current)))
+                       return;
+               motors.put(id, motor);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+
+       @Override
+       public double getMotorDelay(String id) {
+               Double delay = ejectionDelays.get(id);
+               if (delay == null)
+                       return Motor.PLUGGED;
+               return delay;
+       }
+
+       @Override
+       public void setMotorDelay(String id, double delay) {
+               ejectionDelays.put(id, delay);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+       
+       @Override
+       public int getMotorCount() {
+               return 1;
+       }
+       
+       @Override
+       public double getMotorMountDiameter() {
+               return getInnerRadius()*2;
+       }
+
+       @Override
+       public IgnitionEvent getIgnitionEvent() {
+               return ignitionEvent;
+       }
+
+       @Override
+       public void setIgnitionEvent(IgnitionEvent event) {
+               if (ignitionEvent == event)
+                       return;
+               ignitionEvent = event;
+               fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+       }
+
+       
+       @Override
+       public double getIgnitionDelay() {
+               return ignitionDelay;
+       }
+
+       @Override
+       public void setIgnitionDelay(double delay) {
+               if (MathUtil.equals(delay, ignitionDelay))
+                       return;
+               ignitionDelay = delay;
+               fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+       }
+
+       
+       @Override
+       public double getMotorOverhang() {
+               return overhang;
+       }
+       
+       @Override
+       public void setMotorOverhang(double overhang) {
+               if (MathUtil.equals(this.overhang, overhang))
+                       return;
+               this.overhang = overhang;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+
+       
+       /*
+        * (non-Javadoc)
+        * Copy the motor and ejection delay HashMaps.
+        * 
+        * @see rocketcomponent.RocketComponent#copy()
+        */
+       @SuppressWarnings("unchecked")
+       @Override
+       public RocketComponent copy() {
+               RocketComponent c = super.copy();
+               ((BodyTube)c).motors = (HashMap<String,Motor>) motors.clone();
+               ((BodyTube)c).ejectionDelays = (HashMap<String,Double>) ejectionDelays.clone();
+               return c;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Bulkhead.java b/src/net/sf/openrocket/rocketcomponent/Bulkhead.java
new file mode 100644 (file)
index 0000000..c82aa97
--- /dev/null
@@ -0,0 +1,36 @@
+package net.sf.openrocket.rocketcomponent;
+
+
+public class Bulkhead extends RadiusRingComponent {
+
+       public Bulkhead() {
+               setOuterRadiusAutomatic(true);
+               setLength(0.002);
+       }
+
+       @Override
+       public double getInnerRadius() {
+               return 0;
+       }
+       
+       @Override
+       public void setInnerRadius(double r) {
+               // No-op
+       }
+       
+       @Override
+       public void setOuterRadiusAutomatic(boolean auto) {
+               super.setOuterRadiusAutomatic(auto);
+       }
+       
+       @Override
+       public String getComponentName() {
+               return "Bulkhead";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/CenteringRing.java b/src/net/sf/openrocket/rocketcomponent/CenteringRing.java
new file mode 100644 (file)
index 0000000..515667a
--- /dev/null
@@ -0,0 +1,58 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+
+
+public class CenteringRing extends RadiusRingComponent {
+
+       public CenteringRing() {
+               setOuterRadiusAutomatic(true);
+               setInnerRadiusAutomatic(true);
+               setLength(0.002);
+       }
+       
+       
+       @Override
+       public double getInnerRadius() {
+               // Implement sibling inner radius automation
+               if (isInnerRadiusAutomatic()) {
+                       innerRadius = 0;
+                       for (RocketComponent sibling: this.getParent().getChildren()) {
+                               if (!(sibling instanceof RadialParent))  // Excludes itself
+                                       continue;
+
+                               double pos1 = this.toRelative(Coordinate.NUL, sibling)[0].x;
+                               double pos2 = this.toRelative(new Coordinate(getLength()), sibling)[0].x;
+                               if (pos2 < 0 || pos1 > sibling.getLength())
+                                       continue;
+                               
+                               innerRadius = Math.max(innerRadius, ((InnerTube)sibling).getOuterRadius());
+                       }
+                       innerRadius = Math.min(innerRadius, getOuterRadius());
+               }
+               
+               return super.getInnerRadius();
+       }
+
+       
+       @Override
+       public void setOuterRadiusAutomatic(boolean auto) {
+               super.setOuterRadiusAutomatic(auto);
+       }
+       
+       @Override
+       public void setInnerRadiusAutomatic(boolean auto) {
+               super.setInnerRadiusAutomatic(auto);
+       }
+       
+       @Override
+       public String getComponentName() {
+               return "Centering ring";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java b/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java
new file mode 100644 (file)
index 0000000..5019fcc
--- /dev/null
@@ -0,0 +1,103 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Class that defines different cluster configurations available for the InnerTube.
+ * The class is immutable, and all the constructors are private.  Therefore the only
+ * available cluster configurations are those available in the static fields.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ClusterConfiguration {
+       // Helper vars
+       private static final double R5 = 1.0/(2*Math.sin(2*Math.PI/10));
+       private static final double SQRT2 = Math.sqrt(2);
+       private static final double SQRT3 = Math.sqrt(3);
+
+       /** A single motor */
+       public static final ClusterConfiguration SINGLE = new ClusterConfiguration("single", 0,0);
+
+       /** Definitions of cluster configurations.  Do not modify array. */
+       public static final ClusterConfiguration[] CONFIGURATIONS = {
+               // Single row
+               SINGLE,
+               new ClusterConfiguration("double", -0.5,0, 0.5,0),
+               new ClusterConfiguration("3-row", -1.0,0, 0.0,0, 1.0,0),
+               new ClusterConfiguration("4-row", -1.5,0, -0.5,0, 0.5,0, 1.5,0),
+               
+               // Ring of tubes
+               new ClusterConfiguration("3-ring", -0.5,-1.0/(2*SQRT3),
+                                                                 0.5,-1.0/(2*SQRT3),
+                                                                   0, 1.0/SQRT3),
+               new ClusterConfiguration("4-ring", -0.5,0.5, 0.5,0.5, 0.5,-0.5, -0.5,-0.5),
+               new ClusterConfiguration("5-ring", 0,R5,
+                                                                R5*Math.sin(2*Math.PI/5),R5*Math.cos(2*Math.PI/5),
+                                                                R5*Math.sin(2*Math.PI*2/5),R5*Math.cos(2*Math.PI*2/5),
+                                                                R5*Math.sin(2*Math.PI*3/5),R5*Math.cos(2*Math.PI*3/5),
+                                                                R5*Math.sin(2*Math.PI*4/5),R5*Math.cos(2*Math.PI*4/5)),
+               new ClusterConfiguration("6-ring", 0,1, SQRT3/2,0.5, SQRT3/2,-0.5,
+                                                                0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5),
+               
+               // Centered with ring
+               new ClusterConfiguration("3-star", 0,0, 0,1, SQRT3/2,-0.5, -SQRT3/2,-0.5),
+               new ClusterConfiguration("4-star", 0,0, -1/SQRT2,1/SQRT2, 1/SQRT2,1/SQRT2,
+                                                                1/SQRT2,-1/SQRT2, -1/SQRT2,-1/SQRT2),
+               new ClusterConfiguration("5-star", 0,0, 0,1, 
+                                                                Math.sin(2*Math.PI/5),Math.cos(2*Math.PI/5),
+                                                                Math.sin(2*Math.PI*2/5),Math.cos(2*Math.PI*2/5),
+                                                                Math.sin(2*Math.PI*3/5),Math.cos(2*Math.PI*3/5),
+                                                                Math.sin(2*Math.PI*4/5),Math.cos(2*Math.PI*4/5)),
+               new ClusterConfiguration("6-star", 0,0, 0,1, SQRT3/2,0.5, SQRT3/2,-0.5,
+                                                                0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5)
+       };
+       
+       
+       private final List<Double> points;
+       private final String xmlName;
+       
+       private ClusterConfiguration(String xmlName, double... points) {
+               this.xmlName = xmlName;
+               if (points.length == 0 || points.length%2 == 1) {
+                       throw new IllegalArgumentException("Illegal number of points specified: "+
+                                       points.length);
+               }
+               List<Double> l = new ArrayList<Double>(points.length);
+               for (double d: points)
+                       l.add(d);
+               
+               this.points = Collections.unmodifiableList(l);
+       }
+       
+       public String getXMLName() {
+               return xmlName;
+       }
+       
+       public int getClusterCount() {
+               return points.size()/2;
+       }
+       
+       public List<Double> getPoints() {
+               return points;  // Unmodifiable
+       }
+       
+       /**
+        * Return the points rotated by <code>rotation</code> radians.
+        * @param rotation  Rotation amount.
+        */
+       public List<Double> getPoints(double rotation) {
+               double cos = Math.cos(rotation);
+               double sin = Math.sin(rotation);
+               List<Double> ret = new ArrayList<Double>(points.size());
+               for (int i=0; i<points.size()/2; i++) {
+                       double x = points.get(2*i);
+                       double y = points.get(2*i+1);
+                       ret.add( x*cos + y*sin);
+                       ret.add(-x*sin + y*cos);
+               }
+               return ret;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Clusterable.java b/src/net/sf/openrocket/rocketcomponent/Clusterable.java
new file mode 100644 (file)
index 0000000..fba858b
--- /dev/null
@@ -0,0 +1,11 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.ChangeSource;
+
+public interface Clusterable extends ChangeSource {
+
+       public ClusterConfiguration getClusterConfiguration();
+       public void setClusterConfiguration(ClusterConfiguration cluster);
+       public double getClusterSeparation();
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentAssembly.java b/src/net/sf/openrocket/rocketcomponent/ComponentAssembly.java
new file mode 100644 (file)
index 0000000..1f60917
--- /dev/null
@@ -0,0 +1,100 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import net.sf.openrocket.util.Coordinate;
+
+
+
+/**
+ * A base of component assemblies.
+ * <p>
+ * Note that the mass and CG overrides of the <code>ComponentAssembly</code> class
+ * overrides all sibling mass/CG as well as its own.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class ComponentAssembly extends RocketComponent {
+
+       /**
+        * Sets the position of the components to POSITION_RELATIVE_AFTER.
+        * (Should have no effect.)
+        */
+       public ComponentAssembly() {
+               super(RocketComponent.Position.AFTER);
+       }
+       
+       /**
+        * Null method (ComponentAssembly has no bounds of itself).
+        */
+       @Override
+       public Collection<Coordinate> getComponentBounds() { 
+               return Collections.emptyList();
+       }
+
+       /**
+        * Null method (ComponentAssembly has no mass of itself).
+        */
+       @Override
+       public Coordinate getComponentCG() {
+               return Coordinate.NUL;
+       }
+
+       /**
+        * Null method (ComponentAssembly has no mass of itself).
+        */
+       @Override
+       public double getComponentMass() {
+               return 0;
+       }
+       
+       /**
+        * Null method (ComponentAssembly has no mass of itself).
+        */
+       @Override
+       public double getLongitudalUnitInertia() {
+               return 0;
+       }
+       
+       /**
+        * Null method (ComponentAssembly has no mass of itself).
+        */
+       @Override
+       public double getRotationalUnitInertia() {
+               return 0;
+       }
+       
+       /**
+        * Components have no aerodynamic effect, so return false.
+        */
+       @Override
+       public boolean isAerodynamic() {
+               return false;
+       }
+       
+       /**
+        * Component have no effect on mass, so return false (even though the override values
+        * may have an effect).
+        */
+       @Override
+       public boolean isMassive() {
+               return false;
+       }
+
+       @Override
+       public boolean getOverrideSubcomponents() {
+               return true;
+       }
+
+       @Override
+       public void setOverrideSubcomponents(boolean override) {
+               // No-op
+       }
+       
+       @Override
+       public boolean isOverrideSubcomponentsEnabled() {
+               return false;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java b/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java
new file mode 100644 (file)
index 0000000..5e7fea0
--- /dev/null
@@ -0,0 +1,94 @@
+package net.sf.openrocket.rocketcomponent;
+
+import javax.swing.event.ChangeEvent;
+
+public class ComponentChangeEvent extends ChangeEvent {
+       private static final long serialVersionUID = 1L;
+
+       
+       /** A change that does not affect simulation results in any way (name, color, etc.) */
+       public static final int NONFUNCTIONAL_CHANGE = 1;
+       /** A change that affects the mass properties of the rocket */
+       public static final int MASS_CHANGE = 2;
+       /** A change that affects the aerodynamic properties of the rocket */
+       public static final int AERODYNAMIC_CHANGE = 4;
+       /** A change that affects the mass and aerodynamic properties of the rocket */
+       public static final int BOTH_CHANGE = MASS_CHANGE|AERODYNAMIC_CHANGE; // Mass & Aerodynamic
+
+       /** A change that affects the rocket tree structure */
+       public static final int TREE_CHANGE = 8;
+       /** A change caused by undo/redo. */
+       public static final int UNDO_CHANGE = 16;
+       /** A change in the motor configurations or names */
+       public static final int MOTOR_CHANGE = 32;
+       /** A change in the events occurring during flight. */
+       public static final int EVENT_CHANGE = 64;
+       
+       /** A bit-field that contains all possible change types. */
+       public static final int ALL_CHANGE = 0xFFFFFFFF;
+       
+       private final int type;
+       
+
+       public ComponentChangeEvent(RocketComponent component, int type) {
+               super(component);
+               if (type == 0) {
+                       throw new IllegalArgumentException("no event type provided");
+               }
+               this.type = type;
+       }
+       
+       
+       public boolean isAerodynamicChange() {
+               return (type & AERODYNAMIC_CHANGE) != 0;
+       }
+       
+       public boolean isMassChange() {
+               return (type & MASS_CHANGE) != 0;
+       }
+       
+       public boolean isOtherChange() {
+               return (type & BOTH_CHANGE) == 0;
+       }
+       
+       public boolean isTreeChange() {
+               return (type & TREE_CHANGE) != 0;
+       }
+       
+       public boolean isUndoChange() {
+               return (type & UNDO_CHANGE) != 0;
+       }
+       
+       public boolean isMotorChange() {
+               return (type & MOTOR_CHANGE) != 0;
+       }
+
+       public int getType() {
+               return type;
+       }
+       
+       @Override
+       public String toString() {
+               String s = "";
+               
+               if ((type & NONFUNCTIONAL_CHANGE) != 0)
+                       s += ",nonfunc";
+               if (isMassChange())
+                       s += ",mass";
+               if (isAerodynamicChange())
+                       s += ",aero";
+               if (isTreeChange())
+                       s += ",tree";
+               if (isUndoChange())
+                       s += ",undo";
+               if (isMotorChange())
+                       s += ",motor";
+               if ((type & EVENT_CHANGE) != 0)
+                       s += ",event";
+               
+               if (s.length() > 0)
+                       s = s.substring(1);
+               
+               return "ComponentChangeEvent[" + s + "]";
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java b/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java
new file mode 100644 (file)
index 0000000..dba150a
--- /dev/null
@@ -0,0 +1,9 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.EventListener;
+
+public interface ComponentChangeListener extends EventListener {
+
+       public void componentChanged(ComponentChangeEvent e);
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Configuration.java b/src/net/sf/openrocket/rocketcomponent/Configuration.java
new file mode 100644 (file)
index 0000000..c133823
--- /dev/null
@@ -0,0 +1,509 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class Configuration implements Cloneable, ChangeSource, ComponentChangeListener, 
+               Iterable<RocketComponent> {
+       
+       public static final double DEFAULT_IGNITION_TIME = Double.MAX_VALUE;
+       
+
+       private Rocket rocket;
+       private BitSet stages = new BitSet();
+       
+       private String motorConfiguration = null;
+       
+       private EventListenerList listenerList = new EventListenerList();
+       
+       private final HashMap<MotorMount, Double> ignitionTimes =
+               new HashMap<MotorMount, Double>();
+       
+
+       /* Cached data */
+       private int boundsModID = -1;
+       private ArrayList<Coordinate> cachedBounds = new ArrayList<Coordinate>();
+       private double cachedLength = -1;
+
+       private int refLengthModID = -1;
+       private double cachedRefLength = -1;
+       
+       
+       
+       /**
+        * Create a new configuration with the specified <code>Rocket</code> with 
+        * <code>null</code> motor configuration.
+        * 
+        * @param rocket  the rocket
+        */
+       public Configuration(Rocket rocket) {
+               this.rocket = rocket;
+               setAllStages();
+               rocket.addComponentChangeListener(this);
+       }
+
+       
+       /**
+        * Create a new configuration with the specified <code>Rocket</code> and motor
+        * configuration.
+        * 
+        * @param rocket        the rocket.
+        * @param motorID       the motor configuration ID to use.
+        */
+       public Configuration(Rocket rocket, String motorID) {
+               this.rocket = rocket;
+               this.motorConfiguration = motorID;
+               setAllStages();
+               rocket.addComponentChangeListener(this);
+       }
+       
+       
+       
+       
+       public Rocket getRocket() {
+               return rocket;
+       }
+       
+       
+       public void setAllStages() {
+               stages.clear();
+               stages.set(0,rocket.getStageCount());
+               fireChangeEvent();
+       }
+       
+       
+       /**
+        * Set all stages up to and including the given stage number.  For example,
+        * <code>setToStage(0)</code> will set only the first stage active.
+        * 
+        * @param stage         the stage number.
+        */
+       public void setToStage(int stage) {
+               stages.clear();
+               stages.set(0, stage+1, true);
+//             stages.set(stage+1, rocket.getStageCount(), false);
+               fireChangeEvent();
+       }
+       
+       
+       /**
+        * Check whether the up-most stage of the rocket is in this configuration.
+        * 
+        * @return      <code>true</code> if the first stage is active in this configuration.
+        */
+       public boolean isHead() {
+               return isStageActive(0);
+       }
+       
+       public boolean isStageActive(RocketComponent stage) {
+               if (!(stage instanceof Stage)) {
+                       throw new IllegalArgumentException("called with component "+stage);
+               }
+               return stages.get(stage.getParent().getChildPosition(stage));
+       }
+       
+       
+       public boolean isStageActive(int stage) {
+               if (stage >= rocket.getStageCount())
+                       return false;
+               return stages.get(stage);
+       }
+       
+       public int getStageCount() {
+               return rocket.getStageCount();
+       }
+
+       public int getActiveStageCount() {
+               int count = 0;
+               int s = rocket.getStageCount();
+               
+               for (int i=0; i < s; i++) {
+                       if (stages.get(i))
+                               count++;
+               }
+               return count;
+       }
+
+       public int[] getActiveStages() {
+               int stageCount = rocket.getStageCount();
+               List<Integer> active = new ArrayList<Integer>();
+               int[] ret;
+
+               for (int i=0; i < stageCount; i++) {
+                       if (stages.get(i)) {
+                               active.add(i);
+                       }
+               }
+               
+               ret = new int[active.size()];
+               for (int i=0; i < ret.length; i++) {
+                       ret[i] = active.get(i);
+               }
+               
+               return ret;
+       }
+       
+       
+       /**
+        * Return the reference length associated with the current configuration.  The 
+        * reference length type is retrieved from the <code>Rocket</code>.
+        * 
+        * @return  the reference length for this configuration.
+        */
+       public double getReferenceLength() {
+               if (rocket.getModID() != refLengthModID) {
+                       refLengthModID = rocket.getModID();
+                       cachedRefLength = rocket.getReferenceType().getReferenceLength(this);
+               }
+               return cachedRefLength;
+       }
+       
+       
+       public double getReferenceArea() {
+               return Math.PI * MathUtil.pow2(getReferenceLength()/2);
+       }
+       
+       
+       public String getMotorConfigurationID() {
+               return motorConfiguration;
+       }
+       
+       public void setMotorConfigurationID(String id) {
+               if ((motorConfiguration == null  &&  id == null) ||
+                               (id != null && id.equals(motorConfiguration)))
+                       return;
+               
+               motorConfiguration = id;
+               fireChangeEvent();
+       }
+       
+       public String getMotorConfigurationDescription() {
+               return rocket.getMotorConfigurationDescription(motorConfiguration);
+       }
+       
+       
+       
+       /**
+        * Clear all motor ignition times.  All values are reset to their default of
+        * {@link #DEFAULT_IGNITION_TIME}.
+        */
+       public void resetIgnitionTimes() {
+               ignitionTimes.clear();
+       }
+       
+       /**
+        * Set the ignition time of the motor in the specified motor mount.  Negative or NaN
+        * time values will cause an <code>IllegalArgumentException</code>.
+        * 
+        * @param mount the motor mount to specify.
+        * @param time  the time at which to ignite the motors.
+        * @throws IllegalArgumentException   if <code>time</code> is negative of NaN.
+        */
+       public void setIgnitionTime(MotorMount mount, double time) {
+               if (time < 0) {
+                       throw new IllegalArgumentException("time is negative: "+time);
+               }
+               ignitionTimes.put(mount, time);
+               // TODO: MEDIUM: Should this fire events?
+       }
+       
+       /**
+        * Return the ignition time of the motor in the specified motor mount.  If no time
+        * has been specified, returns {@link #DEFAULT_IGNITION_TIME} as the default.
+        * 
+        * @param mount   the motor mount.
+        * @return                ignition time of the motors in the mount.
+        */
+       public double getIgnitionTime(MotorMount mount) {
+               Double d = ignitionTimes.get(mount);
+               if (d == null)
+                       return DEFAULT_IGNITION_TIME;
+               return d;
+       }
+       
+       
+
+       
+       /**
+        * Removes the listener connection to the rocket and listeners of this object.
+        * This configuration may not be used after a call to this method!
+        */
+       public void release() {
+               rocket.removeComponentChangeListener(this);
+               listenerList = null;
+               rocket = null;
+       }
+       
+       
+       ////////////////  Listeners  ////////////////
+       
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listenerList.add(ChangeListener.class, listener);
+       }
+
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listenerList.remove(ChangeListener.class, listener);
+       }
+       
+       protected void fireChangeEvent() {
+               Object[] listeners = listenerList.getListenerList();
+               ChangeEvent e = new ChangeEvent(this);
+               
+               boundsModID = -1;
+               refLengthModID = -1;
+               
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i] == ChangeListener.class) {
+                               ((ChangeListener) listeners[i+1]).stateChanged(e);
+                       }
+               }
+       }
+
+       
+       @Override
+       public void componentChanged(ComponentChangeEvent e) {
+               fireChangeEvent();
+       }
+       
+       
+       ///////////////  Helper methods  ///////////////
+       
+       /**
+        * Return whether this configuration has any motors defined to it.
+        * 
+        * @return  true if this configuration has active motor mounts with motors defined to them.
+        */
+       public boolean hasMotors() {
+               for (RocketComponent c: this) {
+                       if (c instanceof MotorMount) {
+                               MotorMount mount = (MotorMount) c;
+                               if (!mount.isMotorMount())
+                                       continue;
+                               if (mount.getMotor(this.motorConfiguration) != null) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       }
+       
+       
+       /**
+        * Return the bounds of the current configuration.  The bounds are cached.
+        * 
+        * @return      a <code>Collection</code> containing coordinates bouding the rocket.
+        */
+       @SuppressWarnings("unchecked")
+       public Collection<Coordinate> getBounds() {
+               if (rocket.getModID() != boundsModID) {
+                       boundsModID = rocket.getModID();
+                       cachedBounds.clear();
+                       
+                       double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
+                       for (RocketComponent component: this) {
+                               for (Coordinate c: component.getComponentBounds()) {
+                                       for (Coordinate coord: component.toAbsolute(c)) {
+                                               cachedBounds.add(coord);
+                                               if (coord.x < minX)
+                                                       minX = coord.x;
+                                               if (coord.x > maxX)
+                                                       maxX = coord.x;
+                                       }
+                               }
+                       }
+                       
+                       if (Double.isInfinite(minX) || Double.isInfinite(maxX)) {
+                               cachedLength = 0;
+                       } else {
+                               cachedLength = maxX - minX;
+                       }
+               }
+               return (ArrayList<Coordinate>) cachedBounds.clone();
+       }
+       
+       
+       /**
+        * Returns the length of the rocket configuration, from the foremost bound X-coordinate
+        * to the aft-most X-coordinate.  The value is cached.
+        * 
+        * @return      the length of the rocket in the X-direction.
+        */
+       public double getLength() {
+               if (rocket.getModID() != boundsModID)
+                       getBounds();  // Calculates the length
+               
+               return cachedLength;
+       }
+       
+       
+       
+
+       /**
+        * Return an iterator that iterates over the currently active components.
+        * The <code>Rocket</code> and <code>Stage</code> components are not returned,
+        * but instead all components that are within currently active stages.
+        */
+       @Override
+       public Iterator<RocketComponent> iterator() {
+               return new ConfigurationIterator();
+       }
+       
+       
+       /**
+        * Return an iterator that iterates over all <code>MotorMount</code>s within the
+        * current configuration that have an active motor.
+        * 
+        * @return  an iterator over active motor mounts.
+        */
+       public Iterator<MotorMount> motorIterator() {
+               return new MotorIterator();
+       }
+       
+       
+       /**
+        * Perform a deep-clone.  The object references are also cloned and no
+        * listeners are listening on the cloned object.  
+        */
+       @Override
+       public Configuration clone() {
+               try {
+                       Configuration config = (Configuration) super.clone();
+                       config.listenerList = new EventListenerList();
+                       config.stages = (BitSet) this.stages.clone();
+                       config.cachedBounds = new ArrayList<Coordinate>();
+                       config.boundsModID = -1;
+                       config.refLengthModID = -1;
+                       rocket.addComponentChangeListener(config);
+                       return config;
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("BUG: clone not supported!",e);
+               }
+       }
+
+       
+
+       /**
+        * A class that iterates over all currently active components.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       private class ConfigurationIterator implements Iterator<RocketComponent> {
+               Iterator<Iterator<RocketComponent>> iterators;
+               Iterator<RocketComponent> current = null;
+               
+               public ConfigurationIterator() {
+                       List<Iterator<RocketComponent>> list = new ArrayList<Iterator<RocketComponent>>();
+                       
+                       for (RocketComponent stage: rocket.getChildren()) {
+                               if (isStageActive(stage)) {
+                                       list.add(stage.deepIterator());
+                               }
+                       }
+                       
+                       // Get iterators and initialize current
+                       iterators = list.iterator();
+                       if (iterators.hasNext()) {
+                               current = iterators.next();
+                       } else {
+                               List<RocketComponent> l = Collections.emptyList();
+                               current = l.iterator();
+                       }
+               }
+               
+               
+               @Override
+               public boolean hasNext() {
+                       if (!current.hasNext())
+                               getNextIterator();
+                       
+                       return current.hasNext();
+               }
+
+               @Override
+               public RocketComponent next() {
+                       if (!current.hasNext())
+                               getNextIterator();
+                       
+                       return current.next();
+               }
+
+               /**
+                * Get the next iterator that has items.  If such an iterator does
+                * not exist, current is left to an empty iterator.
+                */
+               private void getNextIterator() {
+                       while ((!current.hasNext()) && iterators.hasNext()) {
+                               current = iterators.next();
+                       }
+               }
+               
+               @Override
+               public void remove() {
+                       throw new UnsupportedOperationException("remove unsupported");
+               }
+       }
+       
+       private class MotorIterator implements Iterator<MotorMount> {
+               private final Iterator<RocketComponent> iterator;
+               private MotorMount next = null;
+               
+               public MotorIterator() {
+                       this.iterator = iterator();
+               }
+
+               @Override
+               public boolean hasNext() {
+                       getNext();
+                       return (next != null);
+               }
+
+               @Override
+               public MotorMount next() {
+                       getNext();
+                       if (next == null) {
+                               throw new NoSuchElementException("iterator called for too long");
+                       }
+                       
+                       MotorMount ret = next;
+                       next = null;
+                       return ret;
+               }
+
+               @Override
+               public void remove() {
+                       throw new UnsupportedOperationException("remove unsupported");
+               }
+               
+               private void getNext() {
+                       if (next != null)
+                               return;
+                       while (iterator.hasNext()) {
+                               RocketComponent c = iterator.next();
+                               if (c instanceof MotorMount) {
+                                       MotorMount mount = (MotorMount) c;
+                                       if (mount.isMotorMount() && mount.getMotor(motorConfiguration) != null) {
+                                               next = mount;
+                                               return;
+                                       }
+                               }
+                       }
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java b/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java
new file mode 100644 (file)
index 0000000..d4ecac8
--- /dev/null
@@ -0,0 +1,70 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+public class EllipticalFinSet extends FinSet {
+       public static final int POINTS = 21;
+       
+       private static final double[] POINT_X = new double[POINTS];
+       private static final double[] POINT_Y = new double[POINTS];
+       static {
+               for (int i=0; i < POINTS; i++) {
+                       double a = Math.PI * (POINTS-1-i)/(POINTS-1);
+                       POINT_X[i] = (Math.cos(a)+1)/2;
+                       POINT_Y[i] = Math.sin(a);
+               }
+               POINT_X[0] = 0;
+               POINT_Y[0] = 0;
+               POINT_X[POINTS-1] = 1;
+               POINT_Y[POINTS-1] = 0;
+       }
+       
+       
+       private double height = 0.05;
+       
+       public EllipticalFinSet() {
+               this.length = 0.05;
+       }
+
+       
+       @Override
+       public Coordinate[] getFinPoints() {
+               Coordinate[] points = new Coordinate[POINTS];
+               for (int i=0; i < POINTS; i++) {
+                       points[i] = new Coordinate(POINT_X[i]*length, POINT_Y[i]*height);
+               }
+               return points;
+       }
+
+       @Override
+       public double getSpan() {
+               return height;
+       }
+
+       @Override
+       public String getComponentName() {
+               return "Elliptical fin set";
+       }
+       
+       
+       public double getHeight() {
+               return height;
+       }
+       
+       public void setHeight(double height) {
+               if (MathUtil.equals(this.height, height))
+                       return;
+               this.height = height;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       public void setLength(double length) {
+               if (MathUtil.equals(this.length, length))
+                       return;
+               this.length = length;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/EngineBlock.java b/src/net/sf/openrocket/rocketcomponent/EngineBlock.java
new file mode 100644 (file)
index 0000000..e6c06ae
--- /dev/null
@@ -0,0 +1,28 @@
+package net.sf.openrocket.rocketcomponent;
+
+
+public class EngineBlock extends ThicknessRingComponent {
+
+       public EngineBlock() {
+               super();
+               setOuterRadiusAutomatic(true);
+               setThickness(0.005);
+               setLength(0.005);
+       }
+
+       @Override
+       public void setOuterRadiusAutomatic(boolean auto) {
+               super.setOuterRadiusAutomatic(auto);
+       }
+       
+       @Override
+       public String getComponentName() {
+               return "Engine block";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java b/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java
new file mode 100644 (file)
index 0000000..0f6ece5
--- /dev/null
@@ -0,0 +1,129 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * Class of components with well-defined physical appearance and which have an effect on
+ * aerodynamic simulation.  They have material defined for them, and the mass of the component 
+ * is calculated using the component's volume.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class ExternalComponent extends RocketComponent {
+       
+       public enum Finish {
+               ROUGH("Rough", 500e-6),
+               UNFINISHED("Unfinished", 150e-6),
+               NORMAL("Regular paint", 60e-6),
+               SMOOTH("Smooth paint", 20e-6),
+               POLISHED("Polished", 2e-6);
+               
+               private final String name;
+               private final double roughnessSize;
+               
+               Finish(String name, double roughness) {
+                       this.name = name;
+                       this.roughnessSize = roughness;
+               }
+               
+               public double getRoughnessSize() {
+                       return roughnessSize;
+               }
+               
+               @Override
+               public String toString() {
+                       return name + " (" + UnitGroup.UNITS_ROUGHNESS.toStringUnit(roughnessSize) + ")";
+               }
+       }
+       
+
+       /**
+        * The material of the component.
+        */
+       protected Material material=null;
+       
+       protected Finish finish = Finish.NORMAL;
+       
+       
+       
+       /**
+        * Constructor that sets the relative position of the component.
+        */
+       public ExternalComponent(RocketComponent.Position relativePosition) {
+               super(relativePosition);
+               this.material = Prefs.getDefaultComponentMaterial(this.getClass(), Material.Type.BULK);
+       }
+
+       /**
+        * Returns the volume of the component.  This value is used in calculating the mass
+        * of the object.
+        */
+       public abstract double getComponentVolume();
+
+       /**
+        * Calculates the mass of the component as the product of the volume and interior density.
+        */
+       @Override
+       public double getComponentMass() {
+               return material.getDensity() * getComponentVolume();
+       }
+
+       /**
+        * ExternalComponent has aerodynamic effect, so return true.
+        */
+       @Override
+       public boolean isAerodynamic() {
+               return true;
+       }
+       
+       /**
+        * ExternalComponent has effect on the mass, so return true.
+        */
+       @Override
+       public boolean isMassive() {
+               return true;
+       }
+       
+       
+       public Material getMaterial() {
+               return material;
+       }
+       
+       public void setMaterial(Material mat) {
+               if (mat.getType() != Material.Type.BULK) {
+                       throw new IllegalArgumentException("ExternalComponent requires a bulk material" +
+                                       " type="+mat.getType());
+               }
+
+               if (material.equals(mat))
+                       return;
+               material = mat;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       public Finish getFinish() {
+               return finish;
+       }
+       
+       public void setFinish(Finish finish) {
+               if (this.finish == finish)
+                       return;
+               this.finish = finish;
+               fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+       }
+       
+       
+       
+       @Override
+       protected void copyFrom(RocketComponent c) {
+               super.copyFrom(c);
+               
+               ExternalComponent src = (ExternalComponent)c;
+               this.material = src.material;
+               this.finish = src.finish;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/FinSet.java b/src/net/sf/openrocket/rocketcomponent/FinSet.java
new file mode 100644 (file)
index 0000000..fc18062
--- /dev/null
@@ -0,0 +1,512 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Transformation;
+
+
+public abstract class FinSet extends ExternalComponent {
+       
+       /**
+        * Maximum allowed cant of fins.
+        */
+       public static final double MAX_CANT = (15.0 * Math.PI / 180);
+       
+       
+       public enum CrossSection {
+               SQUARE("Square",   1.00),
+               ROUNDED("Rounded", 0.99),
+               AIRFOIL("Airfoil", 0.85);
+               
+               private final String name;
+               private final double volume;
+               CrossSection(String name, double volume) {
+                       this.name = name;
+                       this.volume = volume;
+               }
+               
+               public double getRelativeVolume() {
+                       return volume;
+               }
+               @Override
+               public String toString() {
+                       return name;
+               }
+       }
+       
+       /**
+        * Number of fins.
+        */
+       protected int fins = 3;
+       
+       /**
+        * Rotation about the x-axis by 2*PI/fins.
+        */
+       protected Transformation finRotation = Transformation.rotate_x(2*Math.PI/fins);
+       
+       /**
+        * Rotation angle of the first fin.  Zero corresponds to the positive y-axis.
+        */
+       protected double rotation = 0;
+       
+       /**
+        * Rotation about the x-axis by angle this.rotation.
+        */
+       protected Transformation baseRotation = Transformation.rotate_x(rotation);
+       
+       
+       /**
+        * Cant angle of fins.
+        */
+       protected double cantAngle = 0;
+       
+       /* Cached value: */
+       private Transformation cantRotation = null;
+       
+
+       /**
+        * Thickness of the fins.
+        */
+       protected double thickness = 0;
+       
+       
+       /**
+        * The cross-section shape of the fins.
+        */
+       protected CrossSection crossSection = CrossSection.SQUARE;
+       
+       
+       // Cached fin area & CG.  Validity of both must be checked using finArea!
+       private double finArea = -1;
+       private double finCGx = -1;
+       private double finCGy = -1;
+       
+       
+       /**
+        * New FinSet with given number of fins and given base rotation angle.
+        * Sets the component relative position to POSITION_RELATIVE_BOTTOM,
+        * i.e. fins are positioned at the bottom of the parent component.
+        */
+       public FinSet() {
+               super(RocketComponent.Position.BOTTOM);
+       }
+
+       
+       
+       /**
+        * Return the number of fins in the set.
+        * @return The number of fins.
+        */
+       public int getFinCount() {
+               return fins;
+       }
+
+       /**
+        * Sets the number of fins in the set.
+        * @param n The number of fins, greater of equal to one.
+        */
+       public void setFinCount(int n) {
+               if (fins == n)
+                       return;
+               if (n < 1)
+                       n = 1;
+               if (n > 8)
+                       n = 8;
+               fins = n;
+               finRotation = Transformation.rotate_x(2*Math.PI/fins);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       public Transformation getFinRotationTransformation() {
+               return finRotation;
+       }
+
+       /**
+        * Gets the base rotation amount of the first fin.
+        * @return The base rotation amount.
+        */
+       public double getBaseRotation() {
+               return rotation;
+       }
+       
+       /**
+        * Sets the base rotation amount of the first fin.
+        * @param r The base rotation amount.
+        */
+       public void setBaseRotation(double r) {
+               r = MathUtil.reduce180(r);
+               if (MathUtil.equals(r, rotation))
+                       return;
+               rotation = r;
+               baseRotation = Transformation.rotate_x(rotation);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       public Transformation getBaseRotationTransformation() {
+               return baseRotation;
+       }
+       
+       
+       
+       public double getCantAngle() {
+               return cantAngle;
+       }
+       
+       public void setCantAngle(double cant) {
+               cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
+               if (MathUtil.equals(cant, cantAngle))
+                       return;
+               this.cantAngle = cant;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       public Transformation getCantRotation() {
+               if (cantRotation == null) {
+                       if (MathUtil.equals(cantAngle,0)) {
+                               cantRotation = Transformation.IDENTITY;
+                       } else {
+                               Transformation t = new Transformation(-length/2,0,0);
+                               t = Transformation.rotate_y(cantAngle).applyTransformation(t);
+                               t = new Transformation(length/2,0,0).applyTransformation(t);
+                               cantRotation = t;
+                       }
+               }
+               return cantRotation;
+       }
+       
+       
+
+       public double getThickness() {
+               return thickness;
+       }
+       
+       public void setThickness(double r) {
+               if (thickness == r)
+                       return;
+               thickness = Math.max(r,0);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       public CrossSection getCrossSection() {
+               return crossSection;
+       }
+       
+       public void setCrossSection(CrossSection cs) {
+               if (crossSection == cs)
+                       return;
+               crossSection = cs;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       
+       
+
+       @Override
+       public void setRelativePosition(RocketComponent.Position position) {
+               super.setRelativePosition(position);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+       @Override
+       public void setPositionValue(double value) {
+               super.setPositionValue(value);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+
+       
+       
+       
+       ///////////  Calculation methods  ///////////
+       
+       /**
+        * Return the area of one side of one fin.
+        * 
+        * @return   the area of one side of one fin.
+        */
+       public double getFinArea() {
+               if (finArea < 0)
+                       calculateAreaCG();
+               
+               return finArea;
+       }
+       
+       /**
+        * Return the unweighted CG of a single fin.  The X-coordinate is relative to
+        * the root chord trailing edge and the Y-coordinate to the fin root chord.
+        * 
+        * @return  the unweighted CG coordinate of a single fin. 
+        */
+       public Coordinate getFinCG() {
+               if (finArea < 0)
+                       calculateAreaCG();
+               
+               return new Coordinate(finCGx,finCGy,0);
+       }
+       
+       
+
+       @Override
+       public double getComponentVolume() {
+               return fins * getFinArea() * thickness * crossSection.getRelativeVolume();
+       }
+       
+
+       @Override
+       public Coordinate getComponentCG() {
+               if (finArea < 0)
+                       calculateAreaCG();
+               
+               double mass = getComponentMass();  // safe
+               
+               if (fins == 1) {
+                       return baseRotation.transform(
+                                       new Coordinate(finCGx,finCGy + getBodyRadius(), 0, mass));
+               } else {
+                       return new Coordinate(finCGx, 0, 0, mass);
+               }
+       }
+
+       
+       private void calculateAreaCG() {
+               Coordinate[] points = this.getFinPoints();
+               finArea = 0;
+               finCGx = 0;
+               finCGy = 0;
+               
+               for (int i=0; i < points.length-1; i++) {
+                       final double x0 = points[i].x;
+                       final double x1 = points[i+1].x;
+                       final double y0 = points[i].y;
+                       final double y1 = points[i+1].y;
+                       
+                       double da = (y0+y1)*(x1-x0) / 2;
+                       finArea += da;
+                       if (Math.abs(y0-y1) < 0.00001) {
+                               finCGx += (x0+x1)/2 * da;
+                               finCGy += y0/2 * da;
+                       } else {
+                               finCGx += (x0*(2*y0 + y1) + x1*(y0 + 2*y1)) / (3*(y0 + y1)) * da;
+                               finCGy += (y1 + y0*y0/(y0 + y1))/3 * da;
+                       }
+               }
+               
+               if (finArea < 0)
+                       finArea = 0;
+               
+               if (finArea > 0) {
+                       finCGx /= finArea;
+                       finCGy /= finArea;
+               } else {
+                       finCGx = (points[0].x + points[points.length-1].x)/2;
+                       finCGy = 0;
+               }
+       }
+       
+       
+       /*
+        * Return an approximation of the longitudal unitary inertia of the fin set.
+        * The process is the following:
+        * 
+        * 1. Approximate the fin with a rectangular fin
+        * 
+        * 2. The inertia of one fin is taken as the average of the moments of inertia
+        *    through its center perpendicular to the plane, and the inertia through
+        *    its center parallel to the plane
+        *    
+        * 3. If there are multiple fins, the inertia is shifted to the center of the fin
+        *    set and multiplied by the number of fins.
+        */
+       @Override
+       public double getLongitudalUnitInertia() {
+               double area = getFinArea();
+               if (MathUtil.equals(area, 0))
+                       return 0;
+               
+               // Approximate fin with a rectangular fin
+               // w2 and h2 are squares of the fin width and height
+               double w = getLength();
+               double h = getSpan();
+               double w2,h2;
+               
+               if (MathUtil.equals(w*h,0)) {
+                       w2 = area;
+                       h2 = area;
+               } else {
+                       w2 = w*area/h;
+                       h2 = h*area/w;
+               }
+               
+               double inertia = (h2 + 2*w2)/24;
+               
+               if (fins == 1)
+                       return inertia;
+               
+               double radius = getBodyRadius();
+
+               return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
+       }
+       
+       
+       /*
+        * Return an approximation of the rotational unitary inertia of the fin set.
+        * The process is the following:
+        * 
+        * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
+        *    rectangular approximate
+        *    
+        * 2. If there are multiple fins, shift the inertia center to the fin set center
+        *    and multiply with the number of fins.
+        */
+       @Override
+       public double getRotationalUnitInertia() {
+               double area = getFinArea();
+               if (MathUtil.equals(area, 0))
+                       return 0;
+               
+               // Approximate fin with a rectangular fin
+               double w = getLength();
+               double h = getSpan();
+               
+               if (MathUtil.equals(w*h,0)) {
+                       h = Math.sqrt(area);
+               } else {
+                       h = Math.sqrt(h*area/w);
+               }
+               
+               if (fins == 1)
+                       return h*h / 12;
+               
+               double radius = getBodyRadius();
+               
+               return fins * (h*h/12 + MathUtil.pow2(h/2 + radius));
+       }
+       
+       
+       /**
+        * Adds the fin set's bounds to the collection.
+        */
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               List<Coordinate> bounds = new ArrayList<Coordinate>();
+               double r = getBodyRadius();
+               
+               for (Coordinate point: getFinPoints()) {
+                       addFinBound(bounds, point.x, point.y + r);
+               }
+               
+               return bounds;
+       }
+
+       
+       /**
+        * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
+        * all fin rotations.
+        */
+       private void addFinBound(Collection<Coordinate> set, double x, double y) {
+               Coordinate c;
+               int i;
+               
+               c = new Coordinate(x,y,thickness/2);
+               c = baseRotation.transform(c);
+               set.add(c);
+               for (i=1; i<fins; i++) {
+                       c = finRotation.transform(c);
+                       set.add(c);
+               }
+               
+               c = new Coordinate(x,y,-thickness/2);
+               c = baseRotation.transform(c);
+               set.add(c);
+               for (i=1; i<fins; i++) {
+                       c = finRotation.transform(c);
+                       set.add(c);
+               }
+       }
+
+       
+       
+       @Override
+       public void componentChanged(ComponentChangeEvent e) {
+               if (e.isAerodynamicChange()) {
+                       finArea = -1;
+                       cantRotation = null;
+               }
+       }
+       
+       
+       /**
+        * Return the radius of the BodyComponent the fin set is situated on.  Currently
+        * only supports SymmetricComponents and returns the radius at the starting point of the
+        * root chord.
+        *  
+        * @return  radius of the underlying BodyComponent or 0 if none exists.
+        */
+       public double getBodyRadius() {
+               RocketComponent s;
+               
+               s = this.getParent();
+               while (s!=null) {
+                       if (s instanceof SymmetricComponent) {
+                               double x = this.toRelative(new Coordinate(0,0,0), s)[0].x;
+                               return ((SymmetricComponent)s).getRadius(x);
+                       }
+                       s = s.getParent();
+               }
+               return 0;
+       }
+       
+       /**
+        * Allows nothing to be attached to a FinSet.
+        * 
+        * @return <code>false</code>
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+       
+       
+       
+       
+       /**
+        * Return a list of coordinates defining the geometry of a single fin.  
+        * The coordinates are the XY-coordinates of points defining the shape of a single fin,
+        * where the origin is the leading root edge.  Therefore, the first point must be (0,0,0).
+        * All Z-coordinates must be zero, and the last coordinate must have Y=0.
+        * 
+        * @return  List of XY-coordinates.
+        */
+       public abstract Coordinate[] getFinPoints();
+       
+       /**
+        * Get the span of a single fin.  That is, the length from the root to the tip of the fin.
+        * @return  Span of a single fin.
+        */
+       public abstract double getSpan();
+       
+       
+       @Override
+       protected void copyFrom(RocketComponent c) {
+               super.copyFrom(c);
+               
+               FinSet src = (FinSet)c;
+               this.fins = src.fins;
+               this.finRotation = src.finRotation;
+               this.rotation = src.rotation;
+               this.baseRotation = src.baseRotation;
+               this.cantAngle = src.cantAngle;
+               this.cantRotation = src.cantRotation;
+               this.thickness = src.thickness;
+               this.crossSection = src.crossSection;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java b/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java
new file mode 100644 (file)
index 0000000..1ea92ca
--- /dev/null
@@ -0,0 +1,229 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+
+
+public class FreeformFinSet extends FinSet {
+
+       private final List<Coordinate> points = new ArrayList<Coordinate>();
+       
+       public FreeformFinSet() {
+               points.add(Coordinate.NUL);
+               points.add(new Coordinate(0.025,0.05));
+               points.add(new Coordinate(0.075,0.05));
+               points.add(new Coordinate(0.05,0));
+               
+               this.length = 0.05;
+       }
+       
+       
+       public FreeformFinSet(FinSet finset) {
+               Coordinate[] finpoints = finset.getFinPoints();
+               this.copyFrom(finset);
+               
+               points.clear();
+               for (Coordinate c: finpoints) {
+                       points.add(c);
+               }
+               this.length = points.get(points.size()-1).x - points.get(0).x;
+       }
+       
+       
+       
+       /**
+        * Add a fin point between indices <code>index-1</code> and <code>index</code>.
+        * The point is placed at the midpoint of the current segment.
+        * 
+        * @param index   the fin point before which to add the new point.
+        */
+       public void addPoint(int index) {
+               double x0, y0, x1, y1;
+               
+               x0 = points.get(index-1).x;
+               y0 = points.get(index-1).y;
+               x1 = points.get(index).x;
+               y1 = points.get(index).y;
+               
+               points.add(index, new Coordinate((x0+x1)/2, (y0+y1)/2));
+               // adding a point within the segment affects neither mass nor aerodynamics
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+       /**
+        * Remove the fin point with the given index.  The first and last fin points
+        * cannot be removed, and will cause an <code>IllegalArgumentException</code>
+        * if attempted.
+        * 
+        * @param index   the fin point index to remove
+        */
+       public void removePoint(int index) {
+               if (index == 0  ||  index == points.size()-1) {
+                       throw new IllegalArgumentException("cannot remove first or last point");
+               }
+               points.remove(index);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       public int getPointCount() {
+               return points.size();
+       }
+       
+       public void setPoints(Coordinate[] p) {
+               if (p[0].x != 0 || p[0].y != 0 || p[p.length-1].y != 0) {
+                       throw new IllegalArgumentException("Start or end point illegal.");
+               }
+               for (int i=0; i < p.length-1; i++) {
+                       for (int j=i+2; j < p.length-1; j++) {
+                               if (intersects(p[i].x, p[i].y, p[i+1].x, p[i+1].y,
+                                                      p[j].x, p[j].y, p[j+1].x, p[j+1].y)) {
+                                       throw new IllegalArgumentException("segments intersect");
+                               }
+                       }
+                       if (p[i].z != 0) {
+                               throw new IllegalArgumentException("z-coordinate not zero");
+                       }
+               }
+               
+               points.clear();
+               for (Coordinate c: p) {
+                       points.add(c);
+               }
+               this.length = p[p.length-1].x;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+
+       /**
+        * Set the point at position <code>i</code> to coordinates (x,y).
+        * <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.
+        * <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.
+        * 
+        * @param index the point index to modify.
+        * @param x             the x-coordinate.
+        * @param y             the y-coordinate.
+        */
+       public void setPoint(int index, double x, double y) {
+               if (y < 0)
+                       y = 0;
+               
+               double x0,y0,x1,y1;
+               
+               if (index == 0) {
+                       
+                       // Restrict point
+                       x = Math.min(x, points.get(points.size()-1).x);
+                       y = 0;
+                       x0 = Double.NaN;
+                       y0 = Double.NaN;
+                       x1 = points.get(1).x;
+                       y1 = points.get(1).y;
+                       
+               } else if (index == points.size()-1) {
+                       
+                       // Restrict point
+                       x = Math.max(x, 0);
+                       y = 0;
+                       x0 = points.get(index-1).x;
+                       y0 = points.get(index-1).y;
+                       x1 = Double.NaN;
+                       y1 = Double.NaN;
+                       
+               } else {
+                       
+                       x0 = points.get(index-1).x;
+                       y0 = points.get(index-1).y;
+                       x1 = points.get(index+1).x;
+                       y1 = points.get(index+1).y;
+                       
+               }
+               
+               
+               
+               // Check for intersecting
+               double px0, py0, px1, py1;
+               px0 = 0;
+               py0 = 0;
+               for (int i=1; i < points.size(); i++) {
+                       px1 = points.get(i).x;
+                       py1 = points.get(i).y;
+                       
+                       if (i != index-1 && i != index && i != index+1) {
+                               if (intersects(x0,y0,x,y,px0,py0,px1,py1)) {
+                                       throw new IllegalArgumentException("segments intersect");
+                               }
+                       }
+                       if (i != index && i != index+1 && i != index+2) {
+                               if (intersects(x,y,x1,y1,px0,py0,px1,py1)) {
+                                       throw new IllegalArgumentException("segments intersect");
+                               }
+                       }
+                       
+                       px0 = px1;
+                       py0 = py1;
+               }
+               
+               if (index == 0) {
+                       
+                       System.out.println("Set point zero to x:"+x);
+                       for (int i=1; i < points.size(); i++) {
+                               Coordinate c = points.get(i);
+                               points.set(i, c.setX(c.x - x));
+                       }
+                       
+               } else {
+                       
+                       points.set(index,new Coordinate(x,y));
+                       
+               }
+               if (index == 0 || index == points.size()-1) {
+                       this.length = points.get(points.size()-1).x;
+               }
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       
+       private boolean intersects(double ax0, double ay0, double ax1, double ay1,
+                       double bx0, double by0, double bx1, double by1) {
+               
+               double d = ((by1-by0)*(ax1-ax0) - (bx1-bx0)*(ay1-ay0));
+               
+               double ua = ((bx1-bx0)*(ay0-by0) - (by1-by0)*(ax0-bx0)) / d;
+               double ub = ((ax1-ax0)*(ay0-by0) - (ay1-ay0)*(ax0-bx0)) / d;
+               
+               return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1);
+       }
+       
+
+       @Override
+       public Coordinate[] getFinPoints() {
+               return points.toArray(new Coordinate[0]);
+       }
+
+       @Override
+       public double getSpan() {
+               double max = 0;
+               for (Coordinate c: points) {
+                       if (c.y > max)
+                               max = c.y;
+               }
+               return max;
+       }
+
+       @Override
+       public String getComponentName() {
+               return "Freeform fin set";
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/src/net/sf/openrocket/rocketcomponent/InnerTube.java
new file mode 100644 (file)
index 0000000..fe485e2
--- /dev/null
@@ -0,0 +1,276 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * This class defines an inner tube that can be used as a motor mount.  The component
+ * may also be clustered.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class InnerTube extends ThicknessRingComponent 
+implements Clusterable, RadialParent, MotorMount {
+
+       private ClusterConfiguration cluster = ClusterConfiguration.SINGLE;
+       private double clusterScale = 1.0;
+       private double clusterRotation = 0.0;
+       
+       
+       private boolean motorMount = false;
+       private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>();
+       private HashMap<String, Motor> motors = new HashMap<String, Motor>();
+       private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC;
+       private double ignitionDelay = 0;
+       private double overhang = 0;
+
+       
+       /**
+        * Main constructor.
+        */
+       public InnerTube() {
+               // A-C motor size:
+               this.setOuterRadius(0.019/2);
+               this.setInnerRadius(0.018/2);
+               this.setLength(0.070);
+       }
+       
+       
+       @Override
+       public double getInnerRadius(double x) {
+               return getInnerRadius();
+       }
+
+
+       @Override
+       public double getOuterRadius(double x) {
+               return getOuterRadius();
+       }
+       
+
+       @Override
+       public String getComponentName() {
+               return "Inner Tube";
+       }
+
+       /**
+        * Allow all InternalComponents to be added to this component.
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return InternalComponent.class.isAssignableFrom(type);
+       }
+
+       
+       
+       /////////////  Cluster methods  //////////////
+       
+       /**
+        * Get the current cluster configuration.
+        * @return  The current cluster configuration.
+        */
+       public ClusterConfiguration getClusterConfiguration() {
+               return cluster;
+       }
+       
+       /**
+        * Set the current cluster configuration.
+        * @param cluster  The cluster configuration.
+        */
+       public void setClusterConfiguration(ClusterConfiguration cluster) {
+               this.cluster = cluster;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       /**
+        * Return the number of tubes in the cluster.
+        * @return Number of tubes in the current cluster.
+        */
+       @Override
+       public int getClusterCount() {
+               return cluster.getClusterCount();
+       }
+       
+       /**
+        * Get the cluster scaling.  A value of 1.0 indicates that the tubes are packed
+        * touching each other, larger values separate the tubes and smaller values
+        * pack inside each other.
+        */
+       public double getClusterScale() {
+               return clusterScale;
+       }
+
+       /**
+        * Set the cluster scaling.
+        * @see #getClusterScale()
+        */
+       public void setClusterScale(double scale) {
+               scale = Math.max(scale,0);
+               if (MathUtil.equals(clusterScale, scale))
+                       return;
+               clusterScale = scale;
+               fireComponentChangeEvent(new ComponentChangeEvent(this,ComponentChangeEvent.MASS_CHANGE));
+       }
+       
+       
+
+       /**
+        * @return the clusterRotation
+        */
+       public double getClusterRotation() {
+               return clusterRotation;
+       }
+
+
+       /**
+        * @param rotation the clusterRotation to set
+        */
+       public void setClusterRotation(double rotation) {
+               rotation = MathUtil.reduce180(rotation);
+               if (clusterRotation == rotation)
+                       return;
+               this.clusterRotation = rotation;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+
+       @Override
+       public double getClusterSeparation() {
+               return 2*getOuterRadius()*clusterScale;
+       }
+       
+       
+       public List<Coordinate> getClusterPoints() {
+               List<Coordinate> list = new ArrayList<Coordinate>(getClusterCount());
+               List<Double> points = cluster.getPoints(clusterRotation - getRadialDirection());
+               double separation = getClusterSeparation();
+               for (int i=0; i < points.size()/2; i++) {
+                       list.add(new Coordinate(0,points.get(2*i)*separation,points.get(2*i+1)*separation));
+               }
+               return list;
+       }
+       
+       
+       @Override
+       public Coordinate[] shiftCoordinates(Coordinate[] array) {
+               array = super.shiftCoordinates(array);
+               
+               int count = getClusterCount();
+               if (count == 1)
+                       return array;
+
+               List<Coordinate> points = getClusterPoints();
+               assert(points.size() == count);
+               Coordinate[] newArray = new Coordinate[array.length * count];
+               for (int i=0; i < array.length; i++) {
+                       for (int j=0; j < count; j++) {
+                               newArray[i*count + j] = array[i].add(points.get(j));
+                       }
+               }
+               
+               return newArray;
+       }
+       
+
+
+
+       ////////////////  Motor mount  /////////////////
+       
+       @Override
+       public boolean isMotorMount() {
+               return motorMount;
+       }
+
+       @Override
+       public void setMotorMount(boolean mount) {
+               if (motorMount == mount)
+                       return;
+               motorMount = mount;
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+       
+       @Override
+       public Motor getMotor(String id) {
+               return motors.get(id);
+       }
+
+       @Override
+       public void setMotor(String id, Motor motor) {
+               Motor current = motors.get(id);
+               if ((motor == null && current == null) ||
+                               (motor != null && motor.equals(current)))
+                       return;
+               motors.put(id, motor);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+
+       @Override
+       public double getMotorDelay(String id) {
+               Double delay = ejectionDelays.get(id);
+               if (delay == null)
+                       return Motor.PLUGGED;
+               return delay;
+       }
+
+       @Override
+       public void setMotorDelay(String id, double delay) {
+               ejectionDelays.put(id, delay);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+       
+       @Override
+       public int getMotorCount() {
+               return getClusterCount();
+       }
+       
+       @Override
+       public double getMotorMountDiameter() {
+               return getInnerRadius()*2;
+       }
+
+       @Override
+       public IgnitionEvent getIgnitionEvent() {
+               return ignitionEvent;
+       }
+
+       @Override
+       public void setIgnitionEvent(IgnitionEvent event) {
+               if (ignitionEvent == event)
+                       return;
+               ignitionEvent = event;
+               fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+       }
+
+       
+       @Override
+       public double getIgnitionDelay() {
+               return ignitionDelay;
+       }
+
+       @Override
+       public void setIgnitionDelay(double delay) {
+               if (MathUtil.equals(delay, ignitionDelay))
+                       return;
+               ignitionDelay = delay;
+               fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+       }
+
+       
+       @Override
+       public double getMotorOverhang() {
+               return overhang;
+       }
+       
+       @Override
+       public void setMotorOverhang(double overhang) {
+               if (MathUtil.equals(this.overhang, overhang))
+                       return;
+               this.overhang = overhang;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/rocketcomponent/InternalComponent.java b/src/net/sf/openrocket/rocketcomponent/InternalComponent.java
new file mode 100644 (file)
index 0000000..1f2dba4
--- /dev/null
@@ -0,0 +1,51 @@
+package net.sf.openrocket.rocketcomponent;
+
+
+/**
+ * A component internal to the rocket.  Internal components have no effect on the
+ * the aerodynamics of a rocket, only its mass properties (though the location of the
+ * components is not enforced to be within external components).  Internal components 
+ * are always attached relative to the parent component, which can be internal or
+ * external, or absolutely.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class InternalComponent extends RocketComponent {
+
+       public InternalComponent() {
+               super(RocketComponent.Position.BOTTOM);
+       }
+       
+       
+       @Override
+       public final void setRelativePosition(RocketComponent.Position position) {
+               super.setRelativePosition(position);
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+       @Override
+       public final void setPositionValue(double value) {
+               super.setPositionValue(value);
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+
+       /**
+        * Non-aerodynamic components.
+        * @return <code>false</code>
+        */
+       @Override
+       public final boolean isAerodynamic() {
+               return false;
+       }
+
+       /**
+        * Is massive.
+        * @return <code>true</code>
+        */
+       @Override
+       public final boolean isMassive() {
+               return true;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/LaunchLug.java b/src/net/sf/openrocket/rocketcomponent/LaunchLug.java
new file mode 100644 (file)
index 0000000..9fbfcae
--- /dev/null
@@ -0,0 +1,195 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class LaunchLug extends ExternalComponent {
+
+       private double radius;
+       private double thickness;
+       
+       private double radialDirection = 0;
+       
+       /* These are calculated when the component is first attached to any Rocket */
+       private double shiftY, shiftZ;
+       
+       
+       
+       public LaunchLug() {
+               super(Position.MIDDLE);
+               radius = 0.01/2;
+               thickness = 0.001;
+               length = 0.03;
+       }
+       
+       
+       public double getRadius() {
+               return radius;
+       }
+
+       public void setRadius(double radius) {
+               if (MathUtil.equals(this.radius, radius))
+                       return;
+               this.radius = radius;
+               this.thickness = Math.min(this.thickness, this.radius);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       public double getInnerRadius() {
+               return radius-thickness;
+       }
+
+       public void setInnerRadius(double innerRadius) {
+               setRadius(innerRadius + thickness);
+       }
+
+       public double getThickness() {
+               return thickness;
+       }
+       
+       public void setThickness(double thickness) {
+               if (MathUtil.equals(this.thickness, thickness))
+                       return;
+               this.thickness = MathUtil.clamp(thickness, 0, radius);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+       public double getRadialDirection() {
+               return radialDirection;
+       }
+
+       public void setRadialDirection(double direction) {
+               direction = MathUtil.reduce180(direction);
+               if (MathUtil.equals(this.radialDirection, direction))
+                       return;
+               this.radialDirection = direction;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+
+       
+       public void setLength(double length) {
+               if (MathUtil.equals(this.length, length))
+                       return;
+               this.length = length;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       
+
+
+
+       @Override
+       public void setRelativePosition(RocketComponent.Position position) {
+               super.setRelativePosition(position);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+       @Override
+       public void setPositionValue(double value) {
+               super.setPositionValue(value);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+       
+       @Override
+       public Coordinate[] shiftCoordinates(Coordinate[] array) {
+               array = super.shiftCoordinates(array);
+               
+               for (int i=0; i < array.length; i++) {
+                       array[i] = array[i].add(0, shiftY, shiftZ);
+               }
+               
+               return array;
+       }
+       
+       
+       @Override
+       public void componentChanged(ComponentChangeEvent e) {
+               super.componentChanged(e);
+               
+               /* 
+                * shiftY and shiftZ must be computed here since calculating them
+                * in shiftCoordinates() would cause an infinite loop due to .toRelative
+                */
+               RocketComponent body;
+               double parentRadius;
+
+               for (body = this.getParent(); body != null; body = body.getParent()) {
+                       if (body instanceof SymmetricComponent)
+                               break;
+               }
+
+               if (body == null) {
+                       parentRadius = 0;
+               } else {
+                       SymmetricComponent s = (SymmetricComponent)body;
+                       double x1, x2;
+                       x1 = this.toRelative(Coordinate.NUL, body)[0].x;
+                       x2 = this.toRelative(new Coordinate(length,0,0), body)[0].x;
+                       x1 = MathUtil.clamp(x1, 0, body.getLength());
+                       x2 = MathUtil.clamp(x2, 0, body.getLength());
+                       parentRadius = Math.max(s.getRadius(x1), s.getRadius(x2));
+               }
+
+               shiftY = Math.cos(radialDirection) * (parentRadius + radius);
+               shiftZ = Math.sin(radialDirection) * (parentRadius + radius);
+
+//             System.out.println("Computed shift: y="+shiftY+" z="+shiftZ);
+}
+
+       
+       
+
+       @Override
+       public double getComponentVolume() {
+               return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius-thickness));
+       }
+
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               ArrayList<Coordinate> set = new ArrayList<Coordinate>();
+               addBound(set, 0, radius);
+               addBound(set, length, radius);
+               return set;
+       }
+
+       @Override
+       public Coordinate getComponentCG() {
+               return new Coordinate(length/2, 0, 0, getComponentMass());
+       }
+
+       @Override
+       public String getComponentName() {
+               return "Launch lug";
+       }
+
+       @Override
+       public double getLongitudalUnitInertia() {
+               // 1/12 * (3 * (r1^2 + r2^2) + h^2)
+               return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
+                               MathUtil.pow2(getLength())) / 12;
+       }
+
+       @Override
+       public double getRotationalUnitInertia() {
+               // 1/2 * (r1^2 + r2^2)
+               return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+       }
+
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               // Allow nothing to be attached to a LaunchLug
+               return false;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/MassComponent.java b/src/net/sf/openrocket/rocketcomponent/MassComponent.java
new file mode 100644 (file)
index 0000000..951d4dc
--- /dev/null
@@ -0,0 +1,44 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.MathUtil;
+
+public class MassComponent extends MassObject {
+       private double mass = 0;
+
+       
+       public MassComponent() {
+               super();
+       }
+       
+       public MassComponent(double length, double radius, double mass) {
+               super(length, radius);
+               this.mass = mass;
+       }
+
+
+       @Override
+       public double getComponentMass() {
+               return mass;
+       }
+
+       public void setComponentMass(double mass) {
+               mass = Math.max(mass, 0);
+               if (MathUtil.equals(this.mass, mass))
+                       return;
+               this.mass = mass;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       @Override
+       public String getComponentName() {
+               return "Mass component";
+       }
+
+       
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               // Allow no components to be attached to a MassComponent
+               return false;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/MassObject.java b/src/net/sf/openrocket/rocketcomponent/MassObject.java
new file mode 100644 (file)
index 0000000..cadb3b0
--- /dev/null
@@ -0,0 +1,141 @@
+package net.sf.openrocket.rocketcomponent;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+
+/**
+ * A MassObject is an internal component that can a specific weight, but not
+ * necessarily a strictly bound shape.  It is represented as a homogeneous
+ * cylinder and drawn in the rocket figure with rounded corners.
+ * <p>
+ * Subclasses of this class need only implement the {@link #getComponentMass()},
+ * {@link #getComponentName()} and {@link #isCompatible(RocketComponent)}
+ * methods. 
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class MassObject extends InternalComponent {
+
+       private double radius;
+       
+       private double radialPosition;
+       private double radialDirection;
+       
+       private double shiftY = 0;
+       private double shiftZ = 0;
+       
+       
+       public MassObject() {
+               this(0.03, 0.015);
+       }
+       
+       public MassObject(double length, double radius) {
+               super();
+
+               this.length = length;
+               this.radius = radius;
+               
+               this.setRelativePosition(Position.TOP);
+               this.setPositionValue(0.0);
+       }
+
+       
+
+
+       public void setLength(double length) {
+               length = Math.max(length, 0);
+               if (MathUtil.equals(this.length, length))
+                       return;
+               this.length = length;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+
+       public final double getRadius() {
+               return radius;
+       }
+
+
+       public final void setRadius(double radius) {
+               radius = Math.max(radius, 0);
+               if (MathUtil.equals(this.radius, radius))
+                       return;
+               this.radius = radius;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+       
+       public final double getRadialPosition() {
+               return radialPosition;
+       }
+
+       public final void setRadialPosition(double radialPosition) {
+               radialPosition = Math.max(radialPosition, 0);
+               if (MathUtil.equals(this.radialPosition, radialPosition))
+                       return;
+               this.radialPosition = radialPosition;
+               shiftY = radialPosition * Math.cos(radialDirection);
+               shiftZ = radialPosition * Math.sin(radialDirection);
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       public final double getRadialDirection() {
+               return radialDirection;
+       }
+
+       public final void setRadialDirection(double radialDirection) {
+               radialDirection = MathUtil.reduce180(radialDirection);
+               if (MathUtil.equals(this.radialDirection, radialDirection))
+                       return;
+               this.radialDirection = radialDirection;
+               shiftY = radialPosition * Math.cos(radialDirection);
+               shiftZ = radialPosition * Math.sin(radialDirection);
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+
+       
+       /**
+        * Shift the coordinates according to the radial position and direction.
+        */
+       @Override
+       public final Coordinate[] shiftCoordinates(Coordinate[] array) {
+               for (int i=0; i < array.length; i++) {
+                       array[i] = array[i].add(0, shiftY, shiftZ);
+               }
+               return array;
+       }
+       
+       @Override
+       public final Coordinate getComponentCG() {
+               return new Coordinate(length/2, shiftY, shiftZ, getComponentMass());
+       }
+       
+       @Override
+       public final double getLongitudalUnitInertia() {
+               return (3*pow2(radius) + pow2(length)) / 12;
+       }
+
+       @Override
+       public final double getRotationalUnitInertia() {
+               return pow2(radius) / 2;
+       }
+       
+       @Override
+       public final Collection<Coordinate> getComponentBounds() {
+               Collection<Coordinate> c = new ArrayList<Coordinate>();
+               addBound(c, 0, radius);
+               addBound(c, length, radius);
+               return c;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Motor.java b/src/net/sf/openrocket/rocketcomponent/Motor.java
new file mode 100644 (file)
index 0000000..9f84518
--- /dev/null
@@ -0,0 +1,623 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+
+/**
+ * Abstract base class for motors.  The methods that must be implemented are
+ * {@link #getTotalTime()}, {@link #getThrust(double)} and {@link #getCG(double)}.
+ * Additionally the method {@link #getMaxThrust()} may be overridden for efficiency.
+ * <p>
+ * 
+ * NOTE:  The current implementation of {@link #getAverageTime()} and 
+ * {@link #getAverageThrust()} assume that the class is immutable!
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class Motor implements Comparable<Motor> {
+       
+       /**
+        * Enum of rocket motor types.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       public enum Type {
+               SINGLE("Single-use", "Single-use solid propellant motor"), 
+               RELOAD("Reloadable", "Reloadable solid propellant motor"), 
+               HYBRID("Hybrid", "Hybrid rocket motor engine"), 
+               UNKNOWN("Unknown", "Unknown motor type");
+               
+               private final String name;
+               private final String description;
+               
+               Type(String name, String description) {
+                       this.name = name;
+                       this.description = description;
+               }
+
+               /**
+                * Return a short name of this motor type.
+                * @return  a short name of the motor type.
+                */
+               public String getName() {
+                       return name;
+               }
+               
+               /**
+                * Return a long description of this motor type.
+                * @return  a description of the motor type.
+                */
+               public String getDescription() {
+                       return description;
+               }
+               
+               @Override
+               public String toString() {
+                       return name;
+               }
+       }
+       
+       
+       /**
+        * Ejection charge delay value signifying a "plugged" motor with no ejection charge.
+        * The value is that of <code>Double.POSITIVE_INFINITY</code>.
+        */
+       public static final double PLUGGED = Double.POSITIVE_INFINITY;
+       
+       
+       /**
+        * Below what portion of maximum thrust is the motor chosen to be off when
+        * calculating average thrust and burn time.double
+        */
+       public static final double AVERAGE_MARGINAL = 0.05;
+       
+       /* All data is cached, so divisions can be very tight. */
+       private static final int DIVISIONS = 1000;
+
+       
+       //  Comparators:
+       private static final Collator COLLATOR = Collator.getInstance(Locale.US);
+       static {
+               COLLATOR.setStrength(Collator.PRIMARY);
+       }
+       private static DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
+       
+       
+       
+       
+       private final String manufacturer;
+       private final String designation;
+       private final String description;
+       private final Type motorType;
+       
+       private final double[] delays;
+       
+       private final double diameter;
+       private final double length;
+       
+       /* Cached data */
+       private double maxThrust = -1;
+       private double avgTime = -1;
+       private double avgThrust = -1;
+       private double totalImpulse = -1;
+       
+       
+       
+       /**
+        * Sole constructor.  None of the parameters may be <code>null</code>.
+        * 
+        * @param manufacturer  the manufacturer of the motor.
+        * @param designation   the motor designation.
+        * @param description   further description, including any comments on the origin
+        *                                              of the thrust curve.
+        * @param delays                an array of the standard ejection charge delays.  A plugged
+        *                                              motor (no ejection charge) is specified by a delay of
+        *                                              {@link #PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
+        * @param diameter              maximum diameter of the motor
+        * @param length                length of the motor
+        */
+       protected Motor(String manufacturer, String designation, String description, 
+                       Type type, double[] delays, double diameter, double length) {
+
+               if (manufacturer == null || designation == null || description == null ||
+                               type == null || delays == null) {
+                       throw new IllegalArgumentException("Parameters cannot be null.");
+               }
+               
+               this.manufacturer = manufacturer;
+               this.designation = designation;
+               this.description = description.trim();
+               this.motorType = type;
+               this.delays = delays.clone();
+               this.diameter = diameter;
+               this.length = length;
+       }
+
+
+       
+       /**
+        * Return the total burn time of the motor.  The method {@link #getThrust(double)}
+        * must return zero for time values greater than the return value.
+        * 
+        * @return  the total burn time of the motor.
+        */
+       public abstract double getTotalTime();
+
+
+       /**
+        * Return the thrust of the motor at the specified time.
+        * 
+        * @param time  time since the ignition of the motor.
+        * @return      the thrust at the specified time.
+        */
+       public abstract double getThrust(double time);
+       
+       
+       /**
+        * Return the average thrust of the motor between times t1 and t2.
+        * 
+        * @param t1    starting time since the ignition of the motor.
+        * @param t2    end time since the ignition of the motor.
+        * @return              the average thrust during the time period.
+        */
+       /* TODO: MEDIUM: Implement better method in subclass */
+       public double getThrust(double t1, double t2) {
+               double f = 0;
+               f += getThrust(t1);
+               f += getThrust(0.8*t1 + 0.2*t2);
+               f += getThrust(0.6*t1 + 0.4*t2);
+               f += getThrust(0.4*t1 + 0.6*t2);
+               f += getThrust(0.2*t1 + 0.8*t2);
+               f += getThrust(t2);
+               return f/6;
+       }
+
+       
+       /**
+        * Return the mass and CG of the motor at the specified time.
+        * 
+        * @param time  time since the ignition of the motor.
+        * @return      the mass and CG of the motor.
+        */
+       public abstract Coordinate getCG(double time);
+       
+       
+       
+       /**
+        * Return the mass of the motor at the specified time.  The original mass
+        * of the motor can be queried by <code>getMass(0)</code> and the burnt mass
+        * by <code>getMass(Double.MAX_VALUE)</code>.
+        * 
+        * @param time  time since the ignition of the motor.
+        * @return      the mass of the motor.
+        */
+       public double getMass(double time) {
+               return getCG(time).weight;
+       }
+       
+       
+       /**
+        * Return the longitudal moment of inertia of the motor at the specified time.
+        * This default method assumes that the mass of the motor is evenly distributed
+        * in a cylinder with the diameter and length of the motor.
+        * 
+        * @param time  time since the ignition of the motor.
+        * @return              the longitudal moment of inertia of the motor.
+        */
+       public double getLongitudalInertia(double time) {
+               return getMass(time) * (3.0*MathUtil.pow2(diameter/2) + MathUtil.pow2(length))/12;
+       }
+       
+       
+       
+       /**
+        * Return the rotational moment of inertia of the motor at the specified time.
+        * This default method assumes that the mass of the motor is evenly distributed
+        * in a cylinder with the diameter and length of the motor.
+        * 
+        * @param time  time since the ignition of the motor.
+        * @return              the rotational moment of inertia of the motor.
+        */
+       public double getRotationalInertia(double time) {
+               return getMass(time) * MathUtil.pow2(diameter) / 8;
+       }
+       
+       
+       
+       
+       /**
+        * Return the maximum thrust.  This implementation slices through the thrust curve
+        * searching for the maximum thrust.  Subclasses may wish to override this with a
+        * more efficient method.
+        * 
+        * @return  the maximum thrust of the motor
+        */
+       public double getMaxThrust() {
+               if (maxThrust < 0) {
+                       double time = getTotalTime();
+                       maxThrust = 0;
+                       
+                       for (int i=0; i < DIVISIONS; i++) {
+                               double t = time * i / DIVISIONS;
+                               double thrust = getThrust(t);
+                               
+                               if (thrust > maxThrust)
+                                       maxThrust = thrust;
+                       }
+               }
+               return maxThrust;
+       }
+       
+       
+       /**
+        * Return the time used in calculating the average thrust.  The time is the
+        * length of time from motor ignition until the thrust has dropped below
+        * {@link #AVERAGE_MARGINAL} times the maximum thrust.
+        * 
+        * @return  the nominal burn time.
+        */
+       public double getAverageTime() {
+               // Compute average time lazily
+               if (avgTime < 0) {
+                       double max = getMaxThrust();
+                       double time = getTotalTime();
+                       
+                       for (int i=DIVISIONS; i >= 0; i--) {
+                               avgTime = time * i / DIVISIONS;
+                               if (getThrust(avgTime) > max*AVERAGE_MARGINAL)
+                                       break;
+                       }
+               }
+               return avgTime;
+       }
+       
+       
+       /**
+        * Return the calculated average thrust during time from ignition to
+        * {@link #getAverageTime()}.
+        * 
+        * @return  the nominal average thrust.
+        */
+       public double getAverageThrust() {
+               // Compute average thrust lazily
+               if (avgThrust < 0) {
+                       double time = getAverageTime();
+                       
+                       avgThrust = 0;
+                       for (int i=0; i < DIVISIONS; i++) {
+                               double t = time * i / DIVISIONS;
+                               avgThrust += getThrust(t);
+                       }
+                       avgThrust /= DIVISIONS;
+               }
+               return avgThrust;
+       }
+       
+       
+       /**
+        * Return the total impulse of the motor.  This is calculated from the entire
+        * burn time, and therefore may differ from the value of {@link #getAverageTime()}
+        * and {@link #getAverageThrust()} multiplied together.
+        * 
+        * @return  the total impulse of the motor.
+        */
+       public double getTotalImpulse() {
+               // Compute total impulse lazily
+               if (totalImpulse < 0) {
+                       double time = getTotalTime();
+                       double f0, t0;
+                       
+                       totalImpulse = 0;
+                       t0 = 0;
+                       f0 = getThrust(0);
+                       for (int i=1; i < DIVISIONS; i++) {
+                               double t1 = time * i / DIVISIONS;
+                               double f1 = getThrust(t1); 
+                               totalImpulse += 0.5*(f0+f1)*(t1-t0);
+                               t0 = t1;
+                               f0 = f1;
+                       }
+               }
+               return totalImpulse;
+       }
+       
+
+       /**
+        * Return the manufacturer of the motor.
+        * 
+        * @return the manufacturer
+        */
+       public String getManufacturer() {
+               return manufacturer;
+       }
+       
+       /**
+        * Return the designation of the motor.
+        * 
+        * @return the designation
+        */
+       public String getDesignation() {
+               return designation;
+       }
+       
+       /**
+        * Return the designation of the motor, including a delay.
+        * 
+        * @param delay  the delay of the motor.
+        * @return               designation with delay.
+        */
+       public String getDesignation(double delay) {
+               return getDesignation() + "-" + getDelayString(delay);
+       }
+
+       
+       /**
+        * Return extra description for the motor.  This may include for example 
+        * comments on the source of the thrust curve.  The returned <code>String</code>
+        * may include new-lines.
+        * 
+        * @return the description
+        */
+       public String getDescription() {
+               return description;
+       }
+       
+       
+       /**
+        * Return the motor type.
+        * 
+        * @return  the motorType
+        */
+       public Type getMotorType() {
+               return motorType;
+       }
+
+
+
+       /**
+        * Return the standard ejection charge delays for the motor.  "Plugged" motors
+        * with no ejection charge are signified by the value {@link #PLUGGED}
+        * (<code>Double.POSITIVE_INFINITY</code>).
+        * 
+        * @return  the list of standard ejection charge delays, which may be empty.
+        */
+       public double[] getStandardDelays() {
+               return delays.clone();
+       }
+
+       /**
+        * Return the maximum diameter of the motor.
+        * 
+        * @return the diameter
+        */
+       public double getDiameter() {
+               return diameter;
+       }
+
+       /**
+        * Return the length of the motor.  This should be a "characteristic" length,
+        * and the exact definition may depend on the motor type.  Typically this should
+        * be the length from the bottom of the motor to the end of the maximum diameter
+        * portion, ignoring any smaller ejection charge compartments.
+        * 
+        * @return the length
+        */
+       public double getLength() {
+               return length;
+       }
+       
+       
+       /**
+        * Compares two <code>Motor</code> objects.  The motors are considered equal
+        * if they have identical manufacturers, designations and types, near-identical
+        * dimensions, burn times and delays and near-identical thrust curves
+        * (sampled at 10 equidistant points).
+        * <p>
+        * The comment field is ignored when comparing equality.
+        */
+       @Override
+       public boolean equals(Object o) {
+               if (!(o instanceof Motor))
+                       return false;
+               
+               Motor other = (Motor) o;
+               
+               // Tests manufacturer, designation, diameter and length
+               if (this.compareTo(other) != 0)
+                       return false;
+               
+               if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5 ||
+                               this.motorType != other.motorType ||
+                               this.delays.length != other.delays.length) {
+                       
+                       return false;
+               }
+               
+               for (int i=0; i < delays.length; i++) {
+                       // INF - INF == NaN, which produces false when compared
+                       if (Math.abs(this.delays[i] - other.delays[i]) > 0.5) {
+                               return false;
+                       }
+               }
+               
+               double time = getTotalTime();
+               for (int i=0; i < 10; i++) {
+                       double t = time * i/10;
+                       if (Math.abs(this.getThrust(t) - other.getThrust(t)) > 1) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+       
+       /**
+        * A <code>hashCode</code> method compatible with the <code>equals</code>
+        * method.
+        */
+       @Override
+       public int hashCode() {
+               return (manufacturer.hashCode() + designation.hashCode() + 
+                               ((int)(length*1000)) + ((int)(diameter*1000)));
+       }
+       
+       
+       
+       @Override
+       public String toString() {
+               return manufacturer + " " + designation;
+       }
+       
+       
+       //////////  Static methods
+
+       
+       /**
+        * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
+        * returns "P".
+        *  
+        * @param delay         the delay time.
+        * @return                      the <code>String</code> representation.
+        */
+       public static String getDelayString(double delay) {
+               return getDelayString(delay,"P");
+       }
+       
+       /**
+        * Return a String representation of a delay time.  If the delay is {@link #PLUGGED},
+        * <code>plugged</code> is returned.
+        *   
+        * @param delay         the delay time.
+        * @param plugged       the return value if there is no ejection charge.
+        * @return                      the String representation.
+        */
+       public static String getDelayString(double delay, String plugged) {
+               if (delay == PLUGGED)
+                       return plugged;
+               delay = Math.rint(delay*10)/10;
+               if (MathUtil.equals(delay, Math.rint(delay)))
+                       return "" + ((int)delay);
+               return "" + delay;
+       }
+       
+
+       
+       
+       ////////////  Comparation
+       
+       
+
+       @Override
+       public int compareTo(Motor other) {
+               int value;
+               
+               if (COLLATOR == null) {
+               }
+               
+               // 1. Manufacturer
+               value = COLLATOR.compare(this.manufacturer, other.manufacturer);
+               if (value != 0)
+                       return value;
+               
+               // 2. Designation
+               value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation);
+               if (value != 0)
+                       return value;
+               
+               // 3. Diameter
+               value = (int)((this.diameter - other.diameter)*1000000);
+               if (value != 0)
+                       return value;
+                               
+               // 4. Length
+               value = (int)((this.length - other.length)*1000000);
+               if (value != 0)
+                       return value;
+               
+               // 5. Total impulse
+               value = (int)((this.getTotalImpulse() - other.getTotalImpulse())*1000);
+               return value;
+       }
+       
+       
+       
+       public static Comparator<String> getDesignationComparator() {
+               return DESIGNATION_COMPARATOR;
+       }
+       
+       
+       /**
+        * Compares two motors by their designations.  The motors are ordered first
+        * by their motor class, second by their average thrust and lastly by any
+        * extra modifiers at the end of the designation.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       private static class DesignationComparator implements Comparator<String> {
+               private Pattern pattern = 
+                       Pattern.compile("^([0-9][0-9]+|1/([1-8]))?([a-zA-Z])([0-9]+)(.*?)$");
+               
+               @Override
+               public int compare(String o1, String o2) {
+                       int value;
+                       Matcher m1, m2;
+                       
+                       m1 = pattern.matcher(o1);
+                       m2 = pattern.matcher(o2);
+                       
+                       if (m1.find() && m2.find()) {
+
+                               String o1Class = m1.group(3);
+                               int o1Thrust = Integer.parseInt(m1.group(4));
+                               String o1Extra = m1.group(5);
+                               
+                               String o2Class = m2.group(3);
+                               int o2Thrust = Integer.parseInt(m2.group(4));
+                               String o2Extra = m2.group(5);
+                               
+                               // 1. Motor class
+                               if (o1Class.equalsIgnoreCase("A") && o2Class.equalsIgnoreCase("A")) {
+                                       //  1/2A and 1/4A comparison
+                                       String sub1 = m1.group(2);
+                                       String sub2 = m2.group(2);
+
+                                       if (sub1 != null || sub2 != null) {
+                                               if (sub1 == null)
+                                                       sub1 = "1";
+                                               if (sub2 == null)
+                                                       sub2 = "1";
+                                               value = -COLLATOR.compare(sub1,sub2);
+                                               if (value != 0)
+                                                       return value;
+                                       }
+                               }
+                               value = COLLATOR.compare(o1Class,o2Class);
+                               if (value != 0)
+                                       return value;
+                               
+                               // 2. Average thrust
+                               if (o1Thrust != o2Thrust)
+                                       return o1Thrust - o2Thrust;
+                               
+                               // 3. Extra modifier
+                               return COLLATOR.compare(o1Extra, o2Extra);
+                               
+                       } else {
+                               
+                               System.out.println("Falling back");
+                               System.out.println("o1:"+o1 + " o2:"+o2);
+                               
+                               // Not understandable designation, simply compare strings
+                               return COLLATOR.compare(o1, o2);
+                       }
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/MotorMount.java b/src/net/sf/openrocket/rocketcomponent/MotorMount.java
new file mode 100644 (file)
index 0000000..b6dbb5f
--- /dev/null
@@ -0,0 +1,189 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.util.ChangeSource;
+
+public interface MotorMount extends ChangeSource {
+       
+       public static enum IgnitionEvent {
+               AUTOMATIC("Automatic (launch or ejection charge)") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               int count = source.getRocket().getStageCount();
+                               int stage = source.getStageNumber();
+                               
+                               if (stage == count-1) {
+                                       return LAUNCH.isActivationEvent(e, source);
+                               } else {
+                                       return EJECTION_CHARGE.isActivationEvent(e, source);
+                               }
+                       }
+               },
+               LAUNCH("Launch") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               return (e.getType() == FlightEvent.Type.LAUNCH);
+                       }
+               },
+               EJECTION_CHARGE("First ejection charge of previous stage") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               if (e.getType() != FlightEvent.Type.EJECTION_CHARGE)
+                                       return false;
+
+                               int charge = e.getSource().getStageNumber();
+                               int mount = source.getStageNumber();
+                               return (mount+1 == charge);
+                       }
+               },
+               BURNOUT("First burnout of previous stage") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               if (e.getType() != FlightEvent.Type.BURNOUT)
+                                       return false;
+
+                               int charge = e.getSource().getStageNumber();
+                               int mount = source.getStageNumber();
+                               return (mount+1 == charge);
+                       }
+               },
+               NEVER("Never") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               return false;
+                       }
+               },
+               ;
+               
+               
+               private final String description;
+               
+               IgnitionEvent(String description) {
+                       this.description = description;
+               }
+               
+               public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source);
+               
+               @Override
+               public String toString() {
+                       return description;
+               }
+       };
+       
+       
+       /**
+        * Is the component currently a motor mount.
+        * 
+        * @return  whether the component holds a motor.
+        */
+       public boolean isMotorMount();
+       
+       /**
+        * Set whether the component is currently a motor mount.
+        */
+       public void setMotorMount(boolean mount);
+       
+       
+       /**
+        * 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>.
+        * 
+        * @param id    the motor configuration ID
+        * @return      the motor, or <code>null</code> if not set.
+        */
+       public Motor getMotor(String id);
+       
+       /**
+        * Set the motor for the motor configuration.  May be set to <code>null</code>
+        * to remove the motor.
+        * 
+        * @param id     the motor configuration ID
+        * @param motor  the motor, or <code>null</code>.
+        */
+       public void setMotor(String id, Motor motor);
+
+       /**
+        * Get the number of similar motors clustered.
+        * 
+        * @return  the number of motors.
+        */
+       public int getMotorCount();
+       
+       
+       
+       /**
+        * Return the ejection charge delay of given motor configuration.
+        * A "plugged" motor without an ejection charge is given by
+        * {@link Motor#PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
+        * 
+        * @param id    the motor configuration ID
+        * @return      the ejection charge delay.
+        */
+       public double getMotorDelay(String id);
+       
+       /**
+        * Set the ejection change delay of the given motor configuration.  
+        * The ejection charge is disable (a "plugged" motor) is set by
+        * {@link Motor#PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
+        * 
+        * @param id     the motor configuration ID
+        * @param delay  the ejection charge delay.
+        */
+       public void setMotorDelay(String id, double delay);
+       
+       
+       /**
+        * Return the event that ignites this motor.
+        * 
+        * @return   the {@link IgnitionEvent} that ignites this motor.
+        */
+       public IgnitionEvent getIgnitionEvent();
+       
+       /**
+        * Sets the event that ignites this motor.
+        * 
+        * @param event   the {@link IgnitionEvent} that ignites this motor.
+        */
+       public void setIgnitionEvent(IgnitionEvent event);
+       
+       
+       /**
+        * Returns the ignition delay of this motor.
+        * 
+        * @return  the ignition delay
+        */
+       public double getIgnitionDelay();
+       
+       /**
+        * Sets the ignition delay of this motor.
+        * 
+        * @param delay   the ignition delay.
+        */
+       public void setIgnitionDelay(double delay);
+       
+       
+       /**
+        * Return the distance that the motors hang outside this motor mount.
+        * 
+        * @return  the overhang length.
+        */
+       public double getMotorOverhang();
+       
+       /**
+        * Sets the distance that the motors hang outside this motor mount.
+        * 
+        * @param overhang   the overhang length.
+        */
+       public void setMotorOverhang(double overhang);
+       
+       
+       
+       /**
+        * Return the inner diameter of the motor mount.
+        * 
+        * @return  the inner diameter of the motor mount.
+        */
+       public double getMotorMountDiameter();
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/NoseCone.java b/src/net/sf/openrocket/rocketcomponent/NoseCone.java
new file mode 100644 (file)
index 0000000..04eaa77
--- /dev/null
@@ -0,0 +1,117 @@
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * Rocket nose cones of various types.  Implemented as a transition with the
+ * fore radius == 0.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class NoseCone extends Transition {
+       
+       
+       /********* Constructors **********/
+       public NoseCone() {
+               this(Transition.Shape.OGIVE, 6*DEFAULT_RADIUS, DEFAULT_RADIUS);
+       }
+       
+       public NoseCone(Transition.Shape type, double length, double radius) {
+               super();
+               super.setType(type);
+               super.setForeRadiusAutomatic(false);
+               super.setForeRadius(0);
+               super.setForeShoulderLength(0);
+               super.setForeShoulderRadius(0.9*radius);
+               super.setForeShoulderThickness(0);
+               super.setForeShoulderCapped(filled);
+               super.setThickness(0.002);
+               super.setLength(length);
+               super.setClipped(false);
+               
+       }
+       
+
+       /********** Get/set methods for component parameters **********/
+
+       @Override
+       public double getForeRadius() {
+               return 0;
+       }
+       
+       @Override
+       public void setForeRadius(double r) {
+               // No-op
+       }
+
+       @Override
+       public boolean isForeRadiusAutomatic() {
+               return false;
+       }
+       
+       @Override
+       public void setForeRadiusAutomatic(boolean b) {
+               // No-op
+       }
+
+       @Override
+       public double getForeShoulderLength() {
+               return 0;
+       }
+
+       @Override
+       public double getForeShoulderRadius() {
+               return 0;
+       }
+
+       @Override
+       public double getForeShoulderThickness() {
+               return 0;
+       }
+
+       @Override
+       public boolean isForeShoulderCapped() {
+               return false;
+       }
+
+       @Override
+       public void setForeShoulderCapped(boolean capped) {
+               // No-op
+       }
+
+       @Override
+       public void setForeShoulderLength(double foreShoulderLength) {
+               // No-op
+       }
+
+       @Override
+       public void setForeShoulderRadius(double foreShoulderRadius) {
+               // No-op
+       }
+
+       @Override
+       public void setForeShoulderThickness(double foreShoulderThickness) {
+               // No-op
+       }
+
+       @Override
+       public boolean isClipped() {
+               return false;
+       }
+       
+       @Override
+       public void setClipped(boolean b) {
+               // No-op
+       }
+       
+
+       
+       /********** RocketComponent methods **********/
+
+       /**
+        * Return component name.
+        */
+       @Override
+       public String getComponentName() {
+               return "Nose cone";
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Parachute.java b/src/net/sf/openrocket/rocketcomponent/Parachute.java
new file mode 100644 (file)
index 0000000..38ff5b3
--- /dev/null
@@ -0,0 +1,114 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class Parachute extends RecoveryDevice {
+
+       public static final double DEFAULT_CD = 0.8;
+       
+       private double diameter;
+       
+       private Material lineMaterial;
+       private int lineCount = 6;
+       private double lineLength = 0.3;
+
+       
+       public Parachute() {
+               this.diameter = 0.3;
+               this.lineMaterial = Prefs.getDefaultComponentMaterial(Parachute.class, Material.Type.LINE);
+               this.lineLength = 0.3;
+       }
+
+
+       public double getDiameter() {
+               return diameter;
+       }
+       
+       public void setDiameter(double d) {
+               if (MathUtil.equals(this.diameter, d))
+                       return;
+               this.diameter = d;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       public final Material getLineMaterial() {
+               return lineMaterial;
+       }
+       
+       public final void setLineMaterial(Material mat) {
+               if (mat.getType() != Material.Type.LINE) {
+                       throw new IllegalArgumentException("Attempted to set non-line material "+mat);
+               }
+               if (mat.equals(lineMaterial))
+                       return;
+               this.lineMaterial = mat;
+               if (getLineCount() != 0)
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               else
+                       fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+               
+       public final int getLineCount() {
+               return lineCount;
+       }
+       
+       public final void setLineCount(int n) {
+               if (this.lineCount == n)
+                       return;
+               this.lineCount = n;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       public final double getLineLength() {
+               return lineLength;
+       }
+       
+       public final void setLineLength(double length) {
+               if (MathUtil.equals(this.lineLength, length))
+                       return;
+               this.lineLength = length;
+               if (getLineCount() != 0)
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               else
+                       fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+
+
+       @Override
+       public double getComponentCD(double mach) {
+               return DEFAULT_CD;  // TODO: HIGH:  Better parachute CD estimate?
+       }
+       
+       @Override
+       public double getArea() {
+               return Math.PI * MathUtil.pow2(diameter/2);
+       }
+       
+       public void setArea(double area) {
+               if (MathUtil.equals(getArea(), area))
+                       return;
+               diameter = Math.sqrt(area / Math.PI) * 2;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       @Override
+       public double getComponentMass() {
+               return super.getComponentMass() + 
+                       getLineCount() * getLineLength() * getLineMaterial().getDensity();
+       }
+
+       @Override
+       public String getComponentName() {
+               return "Parachute";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/RadialParent.java b/src/net/sf/openrocket/rocketcomponent/RadialParent.java
new file mode 100644 (file)
index 0000000..41c731a
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.rocketcomponent;
+
+public interface RadialParent {
+
+       /**
+        * Return the outer radius of the component at local coordinate <code>x</code>.
+        * Values for <code>x < 0</code> and <code>x > getLength()</code> are undefined.
+        * 
+        * @param x             the lengthwise position in the coordinates of this component.
+        * @return              the outer radius of the component at that position.
+        */
+       public double getOuterRadius(double x);
+
+       /**
+        * Return the inner radius of the component at local coordinate <code>x</code>.
+        * Values for <code>x < 0</code> and <code>x > getLength()</code> are undefined.
+        * 
+        * @param x             the lengthwise position in the coordinates of this component.
+        * @return              the inner radius of the component at that position.
+        */
+       public double getInnerRadius(double x);
+       
+       
+       /**
+        * Return the length of this component.
+        * 
+        * @return              the length of this component.
+        */
+       public double getLength();
+               
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java b/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java
new file mode 100644 (file)
index 0000000..f7b7be8
--- /dev/null
@@ -0,0 +1,83 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * An inner component that consists of a hollow cylindrical component.  This can be
+ * an inner tube, tube coupler, centering ring, bulkhead etc.
+ * 
+ * The properties include the inner and outer radii, length and radial position.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class RadiusRingComponent extends RingComponent {
+
+       protected double outerRadius = 0;
+       protected double innerRadius = 0;
+       
+       @Override
+       public double getOuterRadius() {
+               if (outerRadiusAutomatic && getParent() instanceof RadialParent) {
+                       RocketComponent parent = getParent();
+                       double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x;
+                       double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
+                       pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
+                       pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
+                       outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1),
+                                       ((RadialParent)parent).getInnerRadius(pos2));
+               }
+                               
+               return outerRadius;
+       }
+
+       @Override
+       public void setOuterRadius(double r) {
+               r = Math.max(r,0);
+               if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic())
+                       return;
+               
+               outerRadius = r;
+               outerRadiusAutomatic = false;
+               if (getInnerRadius() > r) {
+                       innerRadius = r;
+                       innerRadiusAutomatic = false;
+               }
+
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       @Override
+       public double getInnerRadius() {
+               return innerRadius;
+       }
+       @Override
+       public void setInnerRadius(double r) {
+               r = Math.max(r,0);
+               if (MathUtil.equals(innerRadius, r))
+                       return;
+               
+               innerRadius = r;
+               innerRadiusAutomatic = false;
+               if (getOuterRadius() < r) {
+                       outerRadius = r;
+                       outerRadiusAutomatic = false;
+               }
+               
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       @Override
+       public double getThickness() {
+               return Math.max(getOuterRadius() - getInnerRadius(), 0);
+       }
+       @Override
+       public void setThickness(double thickness) {
+               double outer = getOuterRadius();
+               
+               thickness = MathUtil.clamp(thickness, 0, outer);
+               setInnerRadius(outer - thickness);
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java b/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java
new file mode 100644 (file)
index 0000000..e859651
--- /dev/null
@@ -0,0 +1,212 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+import net.sf.openrocket.util.Prefs;
+
+
+/**
+ * RecoveryDevice is a class representing devices that slow down descent.
+ * Recovery devices report that they have no aerodynamic effect, since they
+ * are within the rocket during ascent.
+ * <p>
+ * A recovery device includes a surface material of which it is made of.
+ * The mass of the component is calculated based on the material and the
+ * area of the device from {@link #getArea()}.  {@link #getComponentMass()}
+ * may be overridden if additional mass needs to be included.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class RecoveryDevice extends MassObject {
+
+       public static enum DeployEvent {
+               LAUNCH("Launch (plus NN seconds)") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               return e.getType() == FlightEvent.Type.LAUNCH;
+                       }
+               },
+               EJECTION("First ejection charge of this stage") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               if (e.getType() != FlightEvent.Type.EJECTION_CHARGE)
+                                       return false;
+                               RocketComponent charge = e.getSource();
+                               return charge.getStageNumber() == source.getStageNumber();
+                       }
+               },
+               APOGEE("Apogee") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               return e.getType() == FlightEvent.Type.APOGEE;
+                       }
+               },
+               ALTITUDE("Specific altitude during descent") {
+                       @SuppressWarnings("unchecked")
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               if (e.getType() != FlightEvent.Type.ALTITUDE)
+                                       return false;
+
+                               double alt = ((RecoveryDevice)source).getDeployAltitude();
+                               Pair<Double,Double> altitude = (Pair<Double,Double>)e.getData();
+                               
+                               return (altitude.getU() >= alt) && (altitude.getV() <= alt);
+                       }
+               },
+               NEVER("Never") {
+                       @Override
+                       public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+                               return false;
+                       }
+               }
+               ;
+               
+               private final String description;
+               
+               DeployEvent(String description) {
+                       this.description = description;
+               }
+               
+               public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source);
+               
+               @Override
+               public String toString() {
+                       return description;
+               }
+
+       }
+       
+       
+       private DeployEvent deployEvent = DeployEvent.EJECTION;
+       private double deployAltitude = 200;
+       private double deployDelay = 0;
+       
+       private double cd = Parachute.DEFAULT_CD;
+       private boolean cdAutomatic = true;
+       
+       
+       private Material.Surface material;
+
+       
+       public RecoveryDevice() {
+               this(Prefs.getDefaultComponentMaterial(RecoveryDevice.class, Material.Type.SURFACE));
+       }
+       
+       public RecoveryDevice(Material material) {
+               super();
+               setMaterial(material);
+       }
+
+       public RecoveryDevice(double length, double radius, Material material) {
+               super(length, radius);
+               setMaterial(material);
+       }
+       
+       
+
+       
+       public abstract double getArea();
+       
+       public abstract double getComponentCD(double mach);
+
+       
+       
+       public double getCD() {
+               return getCD(0);
+       }
+       
+       public double getCD(double mach) {
+               if (cdAutomatic)
+                       cd = getComponentCD(mach);
+               return cd;
+       }
+
+       public void setCD(double cd) {
+               if (MathUtil.equals(this.cd, cd) && !isCDAutomatic())
+                       return;
+               this.cd = cd;
+               this.cdAutomatic = false;
+               fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+       }
+
+       
+       public boolean isCDAutomatic() {
+               return cdAutomatic;
+       }
+       
+       public void setCDAutomatic(boolean auto) {
+               if (cdAutomatic == auto)
+                       return;
+               this.cdAutomatic = auto;
+               fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+       }
+       
+       
+       
+       public final Material getMaterial() {
+               return material;
+       }
+       
+       public final void setMaterial(Material mat) {
+               if (!(mat instanceof Material.Surface)) {
+                       throw new IllegalArgumentException("Attempted to set non-surface material "+mat);
+               }
+               if (mat.equals(material))
+                       return;
+               this.material = (Material.Surface)mat;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       
+       
+       public DeployEvent getDeployEvent() {
+               return deployEvent;
+       }
+
+       public void setDeployEvent(DeployEvent deployEvent) {
+               if (this.deployEvent == deployEvent)
+                       return;
+               this.deployEvent = deployEvent;
+               fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+       }
+       
+
+       public double getDeployAltitude() {
+               return deployAltitude;
+       }
+
+       public void setDeployAltitude(double deployAltitude) {
+               if (MathUtil.equals(this.deployAltitude, deployAltitude))
+                       return;
+               this.deployAltitude = deployAltitude;
+               if (getDeployEvent() == DeployEvent.ALTITUDE)
+                       fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+               else
+                       fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+       public double getDeployDelay() {
+               return deployDelay;
+       }
+       
+       public void setDeployDelay(double delay) {
+               delay = MathUtil.max(delay, 0);
+               if (MathUtil.equals(this.deployDelay, delay))
+                       return;
+               this.deployDelay = delay;
+               fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+       }
+       
+       
+
+       @Override
+       public double getComponentMass() {
+               return getArea() * getMaterial().getDensity();
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ReferenceType.java b/src/net/sf/openrocket/rocketcomponent/ReferenceType.java
new file mode 100644 (file)
index 0000000..02264eb
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * 
+ */
+package net.sf.openrocket.rocketcomponent;
+
+public enum ReferenceType {
+       
+       NOSECONE {
+               @Override
+               public double getReferenceLength(Configuration config) {
+                       for (RocketComponent c: config) {
+                               if (c instanceof SymmetricComponent) {
+                                       SymmetricComponent s = (SymmetricComponent)c;
+                                       if (s.getForeRadius() >= 0.0005)
+                                               return s.getForeRadius() * 2;
+                                       if (s.getAftRadius() >= 0.0005)
+                                               return s.getAftRadius() * 2;
+                               }
+                       }
+                       return Rocket.DEFAULT_REFERENCE_LENGTH;
+               }
+       },
+       
+       MAXIMUM {
+               @Override
+               public double getReferenceLength(Configuration config) {
+                       double r = 0;
+                       for (RocketComponent c: config) {
+                               if (c instanceof SymmetricComponent) {
+                                       SymmetricComponent s = (SymmetricComponent)c;
+                                       r = Math.max(r, s.getForeRadius());
+                                       r = Math.max(r, s.getAftRadius());
+                               }
+                       }
+                       r *= 2;
+                       if (r < 0.001)
+                               r = Rocket.DEFAULT_REFERENCE_LENGTH;
+                       return r;
+               }
+       }, 
+
+       CUSTOM {
+               @Override
+               public double getReferenceLength(Configuration config) {
+                       return config.getRocket().getCustomReferenceLength();
+               }
+       };
+       
+       public abstract double getReferenceLength(Configuration rocket);
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/RingComponent.java b/src/net/sf/openrocket/rocketcomponent/RingComponent.java
new file mode 100644 (file)
index 0000000..7c8523b
--- /dev/null
@@ -0,0 +1,191 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * An inner component that consists of a hollow cylindrical component.  This can be
+ * an inner tube, tube coupler, centering ring, bulkhead etc.
+ * 
+ * The properties include the inner and outer radii, length and radial position.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class RingComponent extends StructuralComponent {
+
+       protected boolean outerRadiusAutomatic = false;
+       protected boolean innerRadiusAutomatic = false;
+       
+       
+       private double radialDirection = 0;
+       private double radialPosition = 0;
+       
+       private double shiftY = 0;
+       private double shiftZ = 0;
+       
+
+       
+
+       public abstract double getOuterRadius();
+       public abstract void setOuterRadius(double r);
+       
+       public abstract double getInnerRadius();        
+       public abstract void setInnerRadius(double r);
+       
+       public abstract double getThickness();
+       public abstract void setThickness(double thickness);
+       
+       
+       public final boolean isOuterRadiusAutomatic() {
+               return outerRadiusAutomatic;
+       }
+       
+       protected void setOuterRadiusAutomatic(boolean auto) {
+               if (auto == outerRadiusAutomatic)
+                       return;
+               outerRadiusAutomatic = auto;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       public final boolean isInnerRadiusAutomatic() {
+               return innerRadiusAutomatic;
+       }
+       
+       protected void setInnerRadiusAutomatic(boolean auto) {
+               if (auto == innerRadiusAutomatic)
+                       return;
+               innerRadiusAutomatic = auto;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       
+       
+       public final void setLength(double length) {
+               double l = Math.max(length,0);
+               if (this.length == l)
+                       return;
+               
+               this.length = l;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+       /**
+        * Return the radial direction of displacement of the component.  Direction 0
+        * is equivalent to the Y-direction.
+        * 
+        * @return  the radial direction.
+        */
+       public double getRadialDirection() {
+               return radialDirection;
+       }
+       
+       /**
+        * Set the radial direction of displacement of the component.  Direction 0
+        * is equivalent to the Y-direction.
+        * 
+        * @param dir  the radial direction.
+        */
+       public void setRadialDirection(double dir) {
+               dir = MathUtil.reduce180(dir);
+               if (radialDirection == dir)
+                       return;
+               radialDirection = dir;
+               shiftY = radialPosition * Math.cos(radialDirection);
+               shiftZ = radialPosition * Math.sin(radialDirection);
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+       
+       
+       /**
+        * Return the radial position of the component.  The position is the distance
+        * of the center of the component from the center of the parent component.
+        * 
+        * @return  the radial position.
+        */
+       public double getRadialPosition() {
+               return radialPosition;
+       }
+       
+       /**
+        * Set the radial position of the component.  The position is the distance
+        * of the center of the component from the center of the parent component.
+        * 
+        * @param pos  the radial position.
+        */
+       public void setRadialPosition(double pos) {
+               pos = Math.max(pos, 0);
+               if (radialPosition == pos)
+                       return;
+               radialPosition = pos;
+               shiftY = radialPosition * Math.cos(radialDirection);
+               shiftZ = radialPosition * Math.sin(radialDirection);
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+
+
+       /**
+        * Return the number of times the component is multiplied.
+        */
+       public int getClusterCount() {
+               if (this instanceof Clusterable)
+                       return ((Clusterable)this).getClusterConfiguration().getClusterCount();
+               return 1;
+       }
+       
+       
+       /**
+        * Shift the coordinates according to the radial position and direction.
+        */
+       @Override
+       public Coordinate[] shiftCoordinates(Coordinate[] array) {
+               for (int i=0; i < array.length; i++) {
+                       array[i] = array[i].add(0, shiftY, shiftZ);
+               }
+               return array;
+       }
+       
+       
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               List<Coordinate> bounds = new ArrayList<Coordinate>();
+               addBound(bounds,0,getOuterRadius());
+               addBound(bounds,length,getOuterRadius());
+               return bounds;
+       }
+       
+
+       
+       @Override
+       public Coordinate getComponentCG() {
+               return new Coordinate(length/2, 0, 0, getComponentMass());
+       }
+
+       @Override
+       public double getComponentMass() {
+               return ringMass(getOuterRadius(), getInnerRadius(), getLength(),
+                               getMaterial().getDensity()) * getClusterCount();
+       }
+       
+
+       @Override
+       public double getLongitudalUnitInertia() {
+               return ringLongitudalUnitInertia(getOuterRadius(), getInnerRadius(), getLength());
+       }
+
+       @Override
+       public double getRotationalUnitInertia() {
+               return ringRotationalUnitInertia(getOuterRadius(), getInnerRadius());
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Rocket.java b/src/net/sf/openrocket/rocketcomponent/Rocket.java
new file mode 100644 (file)
index 0000000..709c7a5
--- /dev/null
@@ -0,0 +1,740 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * Base for all rocket components.  This is the "starting point" for all rocket trees.
+ * It provides the actual implementations of several methods defined in RocketComponent
+ * (eg. the rocket listener lists) and the methods defined in RocketComponent call these.
+ * It also defines some other methods that concern the whole rocket, and helper methods
+ * that keep information about the program state.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class Rocket extends RocketComponent {
+       public static final double DEFAULT_REFERENCE_LENGTH = 0.01;
+       
+       private static final boolean DEBUG_LISTENERS = false;
+
+       
+       /**
+        * The next modification ID to use.  This variable may only be accessed via
+        * the synchronized {@link #getNextModID()} method!
+        */
+       private static int nextModID = 1;
+
+
+       /**
+        * List of component change listeners.
+        */
+       private EventListenerList listenerList = new EventListenerList();
+       
+       /**
+        * When freezeList != null, events are not dispatched but stored in the list.
+        * When the structure is thawed, a single combined event will be fired.
+        */
+       private List<ComponentChangeEvent> freezeList = null;
+       
+       
+       private int modID;
+       private int massModID;
+       private int aeroModID;
+       private int treeModID;
+       private int functionalModID;
+       
+       
+       private ReferenceType refType = ReferenceType.MAXIMUM;  // Set in constructor
+       private double customReferenceLength = DEFAULT_REFERENCE_LENGTH;
+       
+       
+       // The default configuration used in dialogs
+       private final Configuration defaultConfiguration;
+       
+       
+       private String designer = "";
+       private String revision = "";
+       
+       
+       // Motor configuration list
+       private List<String> motorConfigurationIDs = new ArrayList<String>();
+       private Map<String, String> motorConfigurationNames = new HashMap<String, String>();
+       {
+               motorConfigurationIDs.add(null);
+       }
+       
+       
+       // Does the rocket have a perfect finish (a notable amount of laminar flow)
+       private boolean perfectFinish = false;
+       
+       
+       
+       /////////////  Constructor  /////////////
+       
+       public Rocket() {
+               super(RocketComponent.Position.AFTER);
+               modID = getNextModID();
+               massModID = modID;
+               aeroModID = modID;
+               treeModID = modID;
+               functionalModID = modID;
+               defaultConfiguration = new Configuration(this);
+       }
+       
+       
+       
+       public String getDesigner() {
+               return designer;
+       }
+       
+       public void setDesigner(String s) {
+               if (s == null)
+                       s = "";
+               designer = s;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+
+       public String getRevision() {
+               return revision;
+       }
+       
+       public void setRevision(String s) {
+               if (s == null)
+                       s = "";
+               revision = s;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+       
+
+       /**
+        * Return the number of stages in this rocket.
+        * 
+        * @return   the number of stages in this rocket.
+        */
+       public int getStageCount() {
+               return this.getChildCount();
+       }
+       
+       
+       
+       /**
+        * Return the non-negative modification ID of this rocket.  The ID is changed
+        * every time any change occurs in the rocket.  This can be used to check 
+        * whether it is necessary to void cached data in cases where listeners can not
+        * or should not be used.
+        * <p>
+        * Three other modification IDs are also available, {@link #getMassModID()},
+        * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time 
+        * a mass change, aerodynamic change, or tree change occur.  Even though the values 
+        * of the different modification ID's may be equal, they should be treated totally 
+        * separate.
+        * <p>
+        * Note that undo events restore the modification IDs that were in use at the
+        * corresponding undo level.  Subsequent modifications, however, produce modIDs
+        * distinct from those already used.
+        * 
+        * @return   a unique ID number for this modification state.
+        */
+       public int getModID() {
+               return modID;
+       }
+       
+       /**
+        * Return the non-negative mass modification ID of this rocket.  See
+        * {@link #getModID()} for details.
+        * 
+        * @return   a unique ID number for this mass-modification state.
+        */
+       public int getMassModID() {
+               return massModID;
+       }
+       
+       /**
+        * Return the non-negative aerodynamic modification ID of this rocket.  See
+        * {@link #getModID()} for details.
+        * 
+        * @return   a unique ID number for this aerodynamic-modification state.
+        */
+       public int getAerodynamicModID() {
+               return aeroModID;
+       }
+       
+       /**
+        * Return the non-negative tree modification ID of this rocket.  See
+        * {@link #getModID()} for details.
+        * 
+        * @return   a unique ID number for this tree-modification state.
+        */
+       public int getTreeModID() {
+               return treeModID;
+       }
+       
+       /**
+        * Return the non-negative functional modificationID of this rocket.
+        * This changes every time a functional change occurs.
+        * 
+        * @return      a unique ID number for this functional modification state.
+        */
+       public int getFunctionalModID() {
+               return functionalModID;
+       }
+       
+       
+       
+       
+       public ReferenceType getReferenceType() {
+               return refType;
+       }
+       
+       public void setReferenceType(ReferenceType type) {
+               if (refType == type)
+                       return;
+               refType = type;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+       public double getCustomReferenceLength() {
+               return customReferenceLength;
+       }
+       
+       public void setCustomReferenceLength(double length) {
+               if (MathUtil.equals(customReferenceLength, length))
+                       return;
+               
+               this.customReferenceLength = Math.max(length,0.001);
+               
+               if (refType == ReferenceType.CUSTOM) {
+                       fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+               }
+       }
+       
+       
+       
+       
+       
+       /**
+        * Set whether the rocket has a perfect finish.  This will affect whether the
+        * boundary layer is assumed to be fully turbulent or not.
+        * 
+        * @param perfectFinish         whether the finish is perfect.
+        */
+       public void setPerfectFinish(boolean perfectFinish) {
+               if (this.perfectFinish == perfectFinish)
+                       return;
+               this.perfectFinish = perfectFinish;
+               fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+       }
+
+
+
+       /**
+        * Get whether the rocket has a perfect finish.
+        * 
+        * @return the perfectFinish
+        */
+       public boolean isPerfectFinish() {
+               return perfectFinish;
+       }
+
+
+
+       /**
+        * Return a new unique modification ID.  This method is thread-safe.
+        * 
+        * @return  a new modification ID unique to this session.
+        */
+       private synchronized int getNextModID() {
+               return nextModID++;
+       }
+       
+
+       /**
+        * Make a deep copy of the Rocket structure.  This is a helper method which simply 
+        * casts the result of the superclass method to a Rocket.
+        */
+       @Override
+       public Rocket copy() {
+               Rocket copy = (Rocket)super.copy();
+               copy.resetListeners();
+               return copy;
+       }
+       
+       
+       
+
+       
+       
+       /**
+        * Load the rocket structure from the source.  The method loads the fields of this
+        * Rocket object and copies the references to siblings from the <code>source</code>.
+        * The object <code>source</code> should not be used after this call, as it is in
+        * an illegal state!
+        * <p>
+        * This method is meant to be used in conjunction with undo/redo functionality,
+        * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree
+        * changes.
+        */
+       public void loadFrom(Rocket r) {
+               super.copyFrom(r);
+               
+               int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE;
+               if (this.massModID != r.massModID)
+                       type |= ComponentChangeEvent.MASS_CHANGE;
+               if (this.aeroModID != r.aeroModID)
+                       type |= ComponentChangeEvent.AERODYNAMIC_CHANGE;
+               if (this.treeModID != r.treeModID)
+                       type |= ComponentChangeEvent.TREE_CHANGE;
+               
+               this.modID = r.modID;
+               this.massModID = r.massModID;
+               this.aeroModID = r.aeroModID;
+               this.treeModID = r.treeModID;
+               this.functionalModID = r.functionalModID;
+               this.refType = r.refType;
+               this.customReferenceLength = r.customReferenceLength;
+               
+               this.motorConfigurationIDs = r.motorConfigurationIDs;
+               this.motorConfigurationNames = r.motorConfigurationNames;
+               this.perfectFinish = r.perfectFinish;
+               
+               fireComponentChangeEvent(type);
+       }
+
+       
+       
+       
+       ///////  Implement the ComponentChangeListener lists
+       
+       /**
+        * Creates a new EventListenerList for this component.  This is necessary when cloning
+        * the structure.
+        */
+       public void resetListeners() {
+//             System.out.println("RESETTING LISTENER LIST of Rocket "+this);
+               listenerList = new EventListenerList();
+       }
+       
+       
+       public void printListeners() {
+               System.out.println(""+this+" has "+listenerList.getListenerCount()+" listeners:");
+               Object[] list = listenerList.getListenerList();
+               for (int i=1; i<list.length; i+=2)
+                       System.out.println("  "+((i+1)/2)+": "+list[i]);
+       }
+       
+       @Override
+       public void addComponentChangeListener(ComponentChangeListener l) {
+               listenerList.add(ComponentChangeListener.class,l);
+               if (DEBUG_LISTENERS)
+                       System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
+                                       " listeners): "+l);
+       }
+       @Override
+       public void removeComponentChangeListener(ComponentChangeListener l) {
+               listenerList.remove(ComponentChangeListener.class, l);
+               if (DEBUG_LISTENERS)
+                       System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
+                                       " listeners): "+l);
+       }
+       
+
+       @Override
+       public void addChangeListener(ChangeListener l) {
+               listenerList.add(ChangeListener.class,l);
+               if (DEBUG_LISTENERS)
+                       System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
+                                       " listeners): "+l);
+       }
+       @Override
+       public void removeChangeListener(ChangeListener l) {
+               listenerList.remove(ChangeListener.class, l);
+               if (DEBUG_LISTENERS)
+                       System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
+                                       " listeners): "+l);
+       }
+
+       
+       @Override
+       protected void fireComponentChangeEvent(ComponentChangeEvent e) {
+
+               // Update modification ID's only for normal (not undo/redo) events
+               if (!e.isUndoChange()) {
+                       modID = getNextModID();
+                       if (e.isMassChange())
+                               massModID = modID;
+                       if (e.isAerodynamicChange())
+                               aeroModID = modID;
+                       if (e.isTreeChange())
+                               treeModID = modID;
+                       if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE)
+                               functionalModID = modID;
+               }
+               
+               if (DEBUG_LISTENERS)
+                       System.out.println("FIRING "+e);
+               
+               // Check whether frozen
+               if (freezeList != null) {
+                       freezeList.add(e);
+                       return;
+               }
+               
+               // Notify all components first
+               Iterator<RocketComponent> iterator = this.deepIterator(true);
+               while (iterator.hasNext()) {
+                       iterator.next().componentChanged(e);
+               }
+
+               // Notify all listeners
+               Object[] listeners = listenerList.getListenerList();
+               for (int i = listeners.length-2; i>=0; i-=2) {
+                       if (listeners[i] == ComponentChangeListener.class) {
+                               ((ComponentChangeListener) listeners[i+1]).componentChanged(e);
+                       } else if (listeners[i] == ChangeListener.class) {
+                               ((ChangeListener) listeners[i+1]).stateChanged(e);
+                       }
+               }
+       }
+       
+               
+       /**
+        * Freezes the rocket structure from firing any events.  This may be performed to
+        * combine several actions on the structure into a single large action.
+        * <code>thaw()</code> must always be called afterwards.
+        * 
+        * NOTE:  Always use a try/finally to ensure <code>thaw()</code> is called:
+        * <pre>
+        *     Rocket r = c.getRocket();
+        *     try {
+        *         r.freeze();
+        *         // do stuff
+        *     } finally {
+        *         r.thaw();
+        *     }
+        * </pre>
+        * 
+        * @see #thaw()
+        */
+       public void freeze() {
+               if (freezeList == null)
+                       freezeList = new LinkedList<ComponentChangeEvent>();
+       }
+       
+       /**
+        * Thaws a frozen rocket structure and fires a combination of the events fired during
+        * the freeze.  The event type is a combination of those fired and the source is the
+        * last component to have been an event source.
+        *
+        * @see #freeze()
+        */
+       public void thaw() {
+               if (freezeList == null)
+                       return;
+               if (freezeList.size()==0) {
+                       freezeList = null;
+                       return;
+               }
+               
+               int type = 0;
+               Object c = null;
+               for (ComponentChangeEvent e: freezeList) {
+                       type = type | e.getType();
+                       c = e.getSource();
+               }
+               freezeList = null;
+               
+               fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent)c,type));
+       }
+       
+       
+
+       
+       ////////  Motor configurations  ////////
+       
+       
+       /**
+        * Return the default configuration.  This should be used in the user interface
+        * to ensure a consistent rocket configuration between dialogs.  It should NOT
+        * be used in simulations not relating to the UI.
+        * 
+        * @return   the default {@link Configuration}.
+        */
+       public Configuration getDefaultConfiguration() {
+               return defaultConfiguration;
+       }
+       
+       
+       /**
+        * Return an array of the motor configuration IDs.  This array is guaranteed
+        * to contain the <code>null</code> ID as the first element.
+        * 
+        * @return  an array of the motor configuration IDs.
+        */
+       public String[] getMotorConfigurationIDs() {
+               return motorConfigurationIDs.toArray(new String[0]);
+       }
+       
+       /**
+        * Add a new motor configuration ID to the motor configurations.  The new ID
+        * is returned.
+        * 
+        * @return  the new motor configuration ID.
+        */
+       public String newMotorConfigurationID() {
+               String id = UUID.randomUUID().toString();
+               motorConfigurationIDs.add(id);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+               return id;
+       }
+       
+       /**
+        * Add a specified motor configuration ID to the motor configurations.
+        * 
+        * @param id    the motor configuration ID.
+        * @return              true if successful, false if the ID was already used.
+        */
+       public boolean addMotorConfigurationID(String id) {
+               if (id == null || motorConfigurationIDs.contains(id))
+                       return false;
+               motorConfigurationIDs.add(id);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+               return true;
+       }
+
+       /**
+        * Remove a motor configuration ID from the configuration IDs.  The <code>null</code>
+        * ID cannot be removed, and an attempt to remove it will be silently ignored.
+        * 
+        * @param id   the motor configuration ID to remove
+        */
+       public void removeMotorConfigurationID(String id) {
+               if (id == null)
+                       return;
+               motorConfigurationIDs.remove(id);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+
+       
+       /**
+        * Return the user-set name of the motor configuration.  If no name has been set,
+        * returns an empty string (not null).
+        *  
+        * @param id   the motor configuration id
+        * @return         the configuration name
+        */
+       public String getMotorConfigurationName(String id) {
+               String s = motorConfigurationNames.get(id);
+               if (s == null)
+                       return "";
+               return s;
+       }
+       
+       
+       /**
+        * Set the name of the motor configuration.  A name can be unset by passing
+        * <code>null</code> or an empty string.
+        * 
+        * @param id    the motor configuration id
+        * @param name  the name for the motor configuration
+        */
+       public void setMotorConfigurationName(String id, String name) {
+               motorConfigurationNames.put(id,name);
+               fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+       }
+       
+               
+       /**
+        * Return a description for the motor configuration.  This is either the 
+        * name previously set by {@link #setMotorConfigurationName(String, String)} or
+        * a string generated from the motor designations of the components.
+        * 
+        * @param id  the motor configuration ID.
+        * @return    a textual representation of the configuration
+        */
+       @SuppressWarnings("null")
+       public String getMotorConfigurationDescription(String id) {
+               String name;
+               int motorCount = 0;
+               
+               if (!motorConfigurationIDs.contains(id)) {
+                       throw new IllegalArgumentException("Motor configuration ID does not exist: "+id);
+               }
+               
+               name = motorConfigurationNames.get(id);
+               if (name != null  &&  !name.equals(""))
+                       return name;
+               
+               // Generate the description
+               
+               // First iterate over each stage and store the designations of each motor
+               List<List<String>> list = new ArrayList<List<String>>();
+               List<String> currentList = null;
+               
+               Iterator<RocketComponent> iterator = this.deepIterator();
+               while (iterator.hasNext()) {
+                       RocketComponent c = iterator.next();
+                       
+                       if (c instanceof Stage) {
+                               
+                               currentList = new ArrayList<String>();
+                               list.add(currentList);
+                               
+                       } else if (c instanceof MotorMount) {
+                               
+                               MotorMount mount = (MotorMount) c;
+                               Motor motor = mount.getMotor(id);
+                               
+                               if (mount.isMotorMount() && motor != null) {
+                                       String designation = motor.getDesignation(mount.getMotorDelay(id));
+                                       
+                                       for (int i=0; i < mount.getMotorCount(); i++) {
+                                               currentList.add(designation);
+                                               motorCount++;
+                                       }
+                               }
+                               
+                       }
+               }
+               
+               if (motorCount == 0) {
+                       return "[No motors]";
+               }
+               
+               // Change multiple occurrences of a motor to n x motor 
+               List<String> stages = new ArrayList<String>();
+               
+               for (List<String> stage: list) {
+                       String stageName = "";
+                       String previous = null;
+                       int count = 0;
+                       
+                       Collections.sort(stage);
+                       for (String current: stage) {
+                               if (current.equals(previous)) {
+                                       
+                                       count++;
+                                       
+                               } else {
+                                       
+                                       if (previous != null) {
+                                               String s = "";
+                                               if (count > 1) {
+                                                       s = "" + count + "\u00d7" + previous;
+                                               } else {
+                                                       s = previous;
+                                               }
+                                               
+                                               if (stageName.equals(""))
+                                                       stageName = s;
+                                               else
+                                                       stageName = stageName + "," + s;
+                                       }
+                                       
+                                       previous = current;
+                                       count = 1;
+                                       
+                               }
+                       }
+                       if (previous != null) {
+                               String s = "";
+                               if (count > 1) {
+                                       s = "" + count + "\u00d7" + previous;
+                               } else {
+                                       s = previous;
+                               }
+                               
+                               if (stageName.equals(""))
+                                       stageName = s;
+                               else
+                                       stageName = stageName + "," + s;
+                       }
+                       
+                       stages.add(stageName);
+               }
+               
+               name = "[";
+               for (int i=0; i < stages.size(); i++) {
+                       String s = stages.get(i);
+                       if (s.equals(""))
+                               s = "None";
+                       if (i==0)
+                               name = name + s;
+                       else
+                               name = name + "; " + s;
+               }
+               name += "]";
+               return name;
+       }
+       
+
+
+       ////////  Obligatory component information
+       
+       
+       @Override
+       public String getComponentName() {
+               return "Rocket";
+       }
+
+       @Override
+       public Coordinate getComponentCG() {
+               return new Coordinate(0,0,0,0);
+       }
+
+       @Override
+       public double getComponentMass() {
+               return 0;
+       }
+
+       @Override
+       public double getLongitudalUnitInertia() {
+               return 0;
+       }
+
+       @Override
+       public double getRotationalUnitInertia() {
+               return 0;
+       }
+       
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               return Collections.emptyList();
+       }
+
+       @Override
+       public boolean isAerodynamic() {
+               return false;
+       }
+
+       @Override
+       public boolean isMassive() {
+               return false;
+       }
+
+       /**
+        * Allows only <code>Stage</code> components to be added to the type Rocket.
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return (Stage.class.isAssignableFrom(type));
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/src/net/sf/openrocket/rocketcomponent/RocketComponent.java
new file mode 100644 (file)
index 0000000..335a223
--- /dev/null
@@ -0,0 +1,1438 @@
+package net.sf.openrocket.rocketcomponent;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EmptyStackException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Stack;
+import java.util.UUID;
+
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.MathUtil;
+
+
+public abstract class RocketComponent implements ChangeSource, Cloneable, 
+               Iterable<RocketComponent> {
+
+       /*
+        * Text is suitable to the form
+        *    Position relative to:  <title>
+        */
+       public enum Position {
+               /** Position relative to the top of the parent component. */
+               TOP("Top of the parent component"),
+               /** Position relative to the middle of the parent component. */
+               MIDDLE("Middle of the parent component"),
+               /** Position relative to the bottom of the parent component. */
+               BOTTOM("Bottom of the parent component"),
+               /** Position after the parent component (for body components). */
+               AFTER("After the parent component"),
+               /** Specify an absolute X-coordinate position. */
+               ABSOLUTE("Tip of the nose cone");
+               
+               private String title;
+               Position(String title) {
+                       this.title = title;
+               }
+               
+               @Override
+               public String toString() {
+                       return title;
+               }
+       }
+       
+       ////////  Parent/child trees
+       /**
+        * Parent component of the current component, or null if none exists.
+        */
+       private RocketComponent parent = null;
+       
+       /**
+        * List of child components of this component.
+        */
+       private List<RocketComponent> children = new ArrayList<RocketComponent>();
+
+       
+       ////////  Parameters common to all components:
+       
+       /**
+        * Characteristic length of the component.  This is used in calculating the coordinate
+        * transformations and positions of other components in reference to this component.
+        * This may and should be used as the "true" length of the component, where applicable.
+        * By default it is zero, i.e. no translation.
+        */
+       protected double length = 0;
+
+       /**
+        * Positioning of this component relative to the parent component.
+        */
+       protected Position relativePosition;
+       
+       /**
+        * Offset of the position of this component relative to the normal position given by
+        * relativePosition.  By default zero, i.e. no position change.
+        */
+       protected double position = 0;
+       
+       
+       // Color of the component, null means to use the default color
+       private Color color = null;
+       private LineStyle lineStyle = null;
+       
+       
+       // Override mass/CG
+       private double overrideMass = 0;
+       private boolean massOverriden = false;
+       private double overrideCGX = 0;
+       private boolean cgOverriden = false;
+       
+       private boolean overrideSubcomponents = false;
+       
+
+       // User-given name of the component
+       private String name = null;
+       
+       // User-specified comment
+       private String comment = "";
+
+       // Unique ID of the component
+       private String id = null;
+       
+       ////  NOTE !!!  All fields must be copied in the method copyFrom()!  ////
+       
+       
+       
+       /**
+        * Default constructor.  Sets the name of the component to the component's static name
+        * and the relative position of the component.
+        */
+       public RocketComponent(Position relativePosition) {
+               // These must not fire any events, due to Rocket undo system initialization
+               this.name = getComponentName();
+               this.relativePosition = relativePosition;
+               this.id = UUID.randomUUID().toString();
+       }
+       
+       
+       
+       
+       
+       ////////////  Methods that must be implemented  ////////////
+
+
+       /**
+        * Static component name.  The name may not vary of the parameters, it must be static.
+        */
+       public abstract String getComponentName();  // Static component type name
+
+       /**
+        * Return the component mass (regardless of mass overriding).
+        */
+       public abstract double getComponentMass();  // Mass of non-overridden component
+
+       /**
+        * Return the component CG and mass (regardless of CG or mass overriding).
+        */
+       public abstract Coordinate getComponentCG();    // CG of non-overridden component
+       
+       
+       /**
+        * Return the longitudal (around the y- or z-axis) unitary moment of inertia.  
+        * The unitary moment of inertia is the moment of inertia with the assumption that
+        * the mass of the component is one kilogram.  The inertia is measured in
+        * respect to the non-overridden CG.
+        * 
+        * @return   the longitudal unitary moment of inertia of this component.
+        */
+       public abstract double getLongitudalUnitInertia();
+       
+       
+       /**
+        * Return the rotational (around the x-axis) unitary moment of inertia.  
+        * The unitary moment of inertia is the moment of inertia with the assumption that
+        * the mass of the component is one kilogram.  The inertia is measured in
+        * respect to the non-overridden CG.
+        * 
+        * @return   the rotational unitary moment of inertia of this component.
+        */
+       public abstract double getRotationalUnitInertia();
+       
+       
+       
+       
+       /**
+        * Test whether the given component type can be added to this component.  This type safety
+        * is enforced by the <code>addChild()</code> methods.  The return value of this method
+        * may change to reflect the current state of this component (e.g. two components of some
+        * type cannot be placed as children).
+        * 
+        * @param type  The RocketComponent class type to add.
+        * @return      Whether such a component can be added.
+        */
+       public abstract boolean isCompatible(Class<? extends RocketComponent> type);
+       
+       
+       /* Non-abstract helper method */
+       /**
+        * Test whether the given component can be added to this component.  This is equivalent
+        * to calling <code>isCompatible(c.getClass())</code>.
+        * 
+        * @param c  Component to test.
+        * @return   Whether the component can be added.
+        * @see #isCompatible(Class)
+        */
+       public final boolean isCompatible(RocketComponent c) {
+               return isCompatible(c.getClass());
+       }
+       
+       
+       
+       /**
+        * Return a collection of bounding coordinates.  The coordinates must be such that
+        * the component is fully enclosed in their convex hull.
+        * 
+        * @return      a collection of coordinates that bound the component.
+        */
+       public abstract Collection<Coordinate> getComponentBounds();
+
+       /**
+        * Return true if the component may have an aerodynamic effect on the rocket.
+        */
+       public abstract boolean isAerodynamic();
+
+       /**
+        * Return true if the component may have an effect on the rocket's mass.
+        */
+       public abstract boolean isMassive();
+       
+       
+       
+       
+
+       ////////////  Methods that may be overridden  ////////////
+
+       
+       /**
+        * Shift the coordinates in the array corresponding to radial movement.  A component
+        * that has a radial position must shift the coordinates in this array suitably.
+        * If the component is clustered, then a new array must be returned with a
+        * coordinate for each cluster.
+        * <p>
+        * The default implementation simply returns the array, and thus produces no shift.
+        * 
+        * @param c   an array of coordinates to shift.
+        * @return    an array of shifted coordinates.  The method may modify the contents
+        *                        of the passed array and return the array itself.
+        */
+       public Coordinate[] shiftCoordinates(Coordinate[] c) {
+               return c;
+       }
+       
+       
+       /**
+        * Called when any component in the tree fires a ComponentChangeEvent.  This is by 
+        * default a no-op, but subclasses may override this method to e.g. invalidate 
+        * cached data.  The overriding method *must* call 
+        * <code>super.componentChanged(e)</code> at some point.
+        * 
+        * @param e  The event fired
+        */
+       protected void componentChanged(ComponentChangeEvent e) {
+               // No-op
+       }
+       
+
+       
+       
+       /**
+        * Return a descriptive name of the component.
+        * 
+        * The description may include extra information about the type of component,
+        * e.g. "Conical nose cone".
+        * 
+        * @return A string describing the component.
+        */
+       @Override
+       public final String toString() {
+               if (name.equals(""))
+                       return getComponentName();
+               else
+                       return name;
+       }
+
+       
+       public final void printStructure() {
+               System.out.println("Rocket structure from '"+this.toString()+"':");
+               printStructure(0);
+       }
+       
+       private void printStructure(int level) {
+               String s = "";
+               
+               for (int i=0; i < level; i++) {
+                       s += "  ";
+               }
+               s += this.toString() + " (" + this.getComponentName()+")";
+               System.out.println(s);
+               
+               for (RocketComponent c: children) {
+                       c.printStructure(level+1);
+               }
+       }
+       
+       
+       /**
+        * Make a deep copy of the rocket component tree structure from this component
+        * downwards.  This method does not fire any events.
+        * <p>
+        * This method must be overridden by any component that refers to mutable objects, 
+        * or if some fields should not be copied.  This should be performed by
+        * <code>RocketComponent c = super.copy();</code> and then cloning/modifying the
+        * appropriate fields.
+        * <p>
+        * This is not performed as serializing/deserializing for performance reasons.
+        * 
+        * @return A deep copy of the structure.
+        */
+       public RocketComponent copy() {
+               RocketComponent clone;
+               try {
+                       clone = (RocketComponent)this.clone();
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("CloneNotSupportedException encountered, " +
+                                       "report a bug!",e);
+               }
+
+               // Reset all parent/child information
+               clone.parent = null;
+               clone.children = new ArrayList<RocketComponent>();
+
+               // Add copied children to the structure without firing events.
+               for (RocketComponent c: this.children) {
+                       RocketComponent copy = c.copy();
+                       clone.children.add(copy);
+                       copy.parent = clone;
+               }
+
+               return clone;
+       }
+
+
+       //////////////  Methods that may not be overridden  ////////////
+       
+       
+
+       ////////// Common parameter setting/getting //////////
+       
+       /**
+        * Return the color of the object to use in 2D figures, or <code>null</code>
+        * to use the default color.
+        */
+       public final Color getColor() {
+               return color;
+       }
+       
+       /**
+        * Set the color of the object to use in 2D figures.  
+        */
+       public final void setColor(Color c) {
+               if ((color == null && c == null) ||
+                               (color != null && color.equals(c)))
+                       return;
+               
+               this.color = c;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+       public final LineStyle getLineStyle() {
+               return lineStyle;
+       }
+       
+       public final void setLineStyle(LineStyle style) {
+               if (this.lineStyle == style)
+                       return;
+               this.lineStyle = style;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+
+       
+       
+       
+       /**
+        * Get the current override mass.  The mass is not necessarily in use
+        * at the moment.
+        * 
+        * @return  the override mass
+        */
+       public final double getOverrideMass() {
+               return overrideMass;
+       }
+       
+       /**
+        * Set the current override mass.  The mass is not set to use by this
+        * method.
+        * 
+        * @param m  the override mass
+        */
+       public final void setOverrideMass(double m) {
+               overrideMass = Math.max(m,0);
+               if (massOverriden)
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       /**
+        * Return whether mass override is active for this component.  This does NOT
+        * take into account whether a parent component is overriding the mass.
+        * 
+        * @return  whether the mass is overridden
+        */
+       public final boolean isMassOverridden() {
+               return massOverriden;
+       }
+       
+       /**
+        * Set whether the mass is currently overridden.
+        * 
+        * @param o  whether the mass is overridden
+        */
+       public final void setMassOverridden(boolean o) {
+               if (massOverriden != o) {
+                       massOverriden = o;
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               }
+       }
+
+       
+       
+       
+       
+       /**
+        * Return the current override CG.  The CG is not necessarily overridden.
+        * 
+        * @return  the override CG
+        */
+       public final Coordinate getOverrideCG() {
+               return getComponentCG().setX(overrideCGX);
+       }
+
+       /**
+        * Return the x-coordinate of the current override CG.
+        * 
+        * @return      the x-coordinate of the override CG.
+        */
+       public final double getOverrideCGX() {
+               return overrideCGX;
+       }
+       
+       /**
+        * Set the current override CG to (x,0,0).
+        * 
+        * @param x  the x-coordinate of the override CG to set.
+        */
+       public final void setOverrideCGX(double x) {
+               if (MathUtil.equals(overrideCGX, x))
+                       return;
+               this.overrideCGX = x;
+               if (isCGOverridden())
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               else
+                       fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       /**
+        * Return whether the CG is currently overridden.
+        * 
+        * @return  whether the CG is overridden
+        */
+       public final boolean isCGOverridden() {
+               return cgOverriden;
+       }
+       
+       /**
+        * Set whether the CG is currently overridden.
+        * 
+        * @param o  whether the CG is overridden
+        */
+       public final void setCGOverridden(boolean o) {
+               if (cgOverriden != o) {
+                       cgOverriden = o;
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               }
+       }
+
+       
+       
+       /**
+        * Return whether the mass and/or CG override overrides all subcomponent values
+        * as well.  The default implementation is a normal getter/setter implementation,
+        * however, subclasses are allowed to override this behavior if some subclass
+        * always or never overrides subcomponents.  In this case the subclass should
+        * also override {@link #isOverrideSubcomponentsEnabled()} to return
+        * <code>false</code>.
+        * 
+        * @return      whether the current mass and/or CG override overrides subcomponents as well.
+        */
+       public boolean getOverrideSubcomponents() {
+               return overrideSubcomponents;
+       }
+       
+       
+       /**
+        * Set whether the mass and/or CG override overrides all subcomponent values
+        * as well.  See {@link #getOverrideSubcomponents()} for details.
+        * 
+        * @param override      whether the mass and/or CG override overrides all subcomponent.
+        */
+       public void setOverrideSubcomponents(boolean override) {
+               if (overrideSubcomponents != override) {
+                       overrideSubcomponents = override;
+                       fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+               }
+       }
+       
+       /**
+        * Return whether the option to override all subcomponents is enabled or not.
+        * The default implementation returns <code>false</code> if neither mass nor
+        * CG is overridden, <code>true</code> otherwise.
+        * <p>
+        * This method may be overridden if the setting of overriding subcomponents
+        * cannot be set.
+        * 
+        * @return      whether the option to override subcomponents is currently enabled.
+        */
+       public boolean isOverrideSubcomponentsEnabled() {
+               return isCGOverridden() || isMassOverridden();
+       }
+       
+       
+       
+       
+       /**
+        * Get the user-defined name of the component.
+        */
+       public final String getName() {
+               return name;
+       }
+       
+       /**
+        * Set the user-defined name of the component.  If name==null, sets the name to
+        * the default name, currently the component name.
+        */
+       public final void setName(String name) {
+//             System.out.println("Set name called:"+name+" orig:"+this.name);
+               if (name==null || name.matches("^\\s*$"))
+                       this.name = getComponentName();
+               else
+                       this.name = name;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+       
+       /**
+        * Return the comment of the component.  The component may contain multiple lines
+        * using \n as a newline separator.
+        * 
+        * @return  the comment of the component.
+        */
+       public final String getComment() {
+               return comment;
+       }
+       
+       /**
+        * Set the comment of the component.
+        * 
+        * @param comment  the comment of the component.
+        */
+       public final void setComment(String comment) {
+               if (this.comment.equals(comment))
+                       return;
+               if (comment == null)
+                       this.comment = "";
+               else
+                       this.comment = comment;
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+       }
+       
+
+       
+       /**
+        * Returns the unique ID of the component.
+        * 
+        * @return      the ID of the component.
+        */
+       public final String getID() {
+               return id;
+       }
+       
+       
+       /**
+        * Set the unique ID of the component.  If <code>id</code> in <code>null</code> then
+        * this method generates a new unique ID for the component.
+        * <p>
+        * This method should be used only in special cases, such as when creating database
+        * entries with empty IDs.
+        * 
+        * @param id    the ID to set.
+        */
+       public final void setID(String id) {
+               if (id == null) {
+                       this.id = UUID.randomUUID().toString();
+               } else {
+                       this.id = id;
+               }
+               fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);     
+       }
+       
+       
+       
+       
+       /**
+        * Get the characteristic length of the component, for example the length of a body tube
+        * of the length of the root chord of a fin.  This is used in positioning the component
+        * relative to its parent.
+        * 
+        * If the length of a component is settable, the class must define the setter method
+        * itself.
+        */
+       public final double getLength() {
+               return length;
+       }
+
+       /**
+        * Get the positioning of the component relative to its parent component.
+        * This is one of the enums of {@link Position}.  A setter method is not provided,
+        * but can be provided by a subclass.
+        */
+       public final Position getRelativePosition() {
+               return relativePosition;
+       }
+       
+       
+       /**
+        * Set the positioning of the component relative to its parent component.
+        * The actual position of the component is maintained to the best ability.
+        * <p>
+        * The default implementation is of protected visibility, since many components
+        * do not support setting the relative position.  A component that does support
+        * it should override this with a public method that simply calls this
+        * supermethod AND fire a suitable ComponentChangeEvent.
+        * 
+        * @param position      the relative positioning.
+        */
+       protected void setRelativePosition(RocketComponent.Position position) {
+               if (this.relativePosition == position)
+                       return;
+               
+               // Update position so as not to move the component
+               if (this.parent != null) {
+                       double thisPos = this.toRelative(Coordinate.NUL,this.parent)[0].x;
+
+                       switch (position) {
+                       case ABSOLUTE:
+                               this.position = this.toAbsolute(Coordinate.NUL)[0].x;
+                               break;
+                               
+                       case TOP:
+                               this.position = thisPos;
+                               break;
+                               
+                       case MIDDLE:
+                               this.position = thisPos - (this.parent.length - this.length)/2;
+                               break;
+                               
+                       case BOTTOM:
+                               this.position = thisPos - (this.parent.length - this.length);
+                               break;
+                               
+                       default:
+                               assert(false): "Should not occur";
+                       }
+               }
+               
+               this.relativePosition = position;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+
+       
+       
+       /**
+        * Get the position value of the component.  The exact meaning of the value is
+        * dependent on the current relative positioning.
+        * 
+        * @return  the positional value.
+        */
+       public final double getPositionValue() {
+               return position;
+       }
+       
+
+       /**
+        * Set the position value of the component.  The exact meaning of the value
+        * depends on the current relative positioning.
+        * <p>
+        * The default implementation is of protected visibility, since many components
+        * do not support setting the relative position.  A component that does support
+        * it should override this with a public method that simply calls this
+        * supermethod AND fire a suitable ComponentChangeEvent.
+        * 
+        * @param value         the position value of the component.
+        */
+       public void setPositionValue(double value) {
+               if (MathUtil.equals(this.position, value))
+                       return;
+               this.position = value;
+       }
+
+       
+       
+       ///////////  Coordinate changes  ///////////
+
+       /**
+        * Returns coordinate c in absolute coordinates.  Equivalent to toComponent(c,null).
+        */
+       public Coordinate[] toAbsolute(Coordinate c) {
+               return toRelative(c,null);
+       }
+       
+
+       /**
+        * Return coordinate <code>c</code> described in the coordinate system of 
+        * <code>dest</code>.  If <code>dest</code> is <code>null</code> returns
+        * absolute coordinates.
+        * <p>
+        * This method returns an array of coordinates, each of which represents a
+        * position of the coordinate in clustered cases.  The array is guaranteed
+        * to contain at least one element.  
+        * <p>
+        * The current implementation does not support rotating components.
+        * 
+        * @param c    Coordinate in the component's coordinate system.
+        * @param dest Destination component coordinate system.
+        * @return     an array of coordinates describing <code>c</code> in coordinates
+        *                         relative to <code>dest</code>.
+        */
+       public final Coordinate[] toRelative(Coordinate c, RocketComponent dest) {
+               double absoluteX = Double.NaN;
+               RocketComponent search = dest;
+               Coordinate[] array = new Coordinate[1];
+               array[0] = c;
+               
+               RocketComponent component = this;
+               while ((component != search) && (component.parent != null)) {
+
+                       array = component.shiftCoordinates(array);
+                       
+                       switch (component.relativePosition) {
+                       case TOP:
+                               for (int i=0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position,0,0);
+                               }
+                               break;
+                               
+                       case MIDDLE:
+                               for (int i=0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position + 
+                                                       (component.parent.length-component.length)/2,0,0);
+                               }
+                               break;
+                               
+                       case BOTTOM:
+                               for (int i=0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position + 
+                                                       (component.parent.length-component.length),0,0);
+                               }
+                               break;
+                               
+                       case AFTER:
+                               // Add length of all previous brother-components with POSITION_RELATIVE_AFTER
+                               int index = component.parent.children.indexOf(component);
+                               assert(index >= 0);
+                               for (index--; index >= 0; index--) {
+                                       RocketComponent comp = component.parent.children.get(index);
+                                       double length = comp.getTotalLength();
+                                       for (int i=0; i < array.length; i++) {
+                                               array[i] = array[i].add(length,0,0);
+                                       }
+                               }
+                               for (int i=0; i < array.length; i++) {
+                                       array[i] = array[i].add(component.position + component.parent.length,0,0);
+                               }
+                               break;
+                               
+                       case ABSOLUTE:
+                               search = null;  // Requires back-search if dest!=null
+                               if (Double.isNaN(absoluteX)) {
+                                       absoluteX = component.position;
+                               }
+                               break;
+                               
+                       default:
+                               throw new RuntimeException("Unknown relative positioning type of component"+
+                                               component+": "+component.relativePosition);
+                       }
+
+                       component = component.parent;  // parent != null
+               }
+
+               if (!Double.isNaN(absoluteX)) {
+                       for (int i=0; i < array.length; i++) {
+                               array[i] = array[i].setX(absoluteX + c.x);
+                       }
+               }
+
+               // Check whether destination has been found or whether to backtrack
+               // TODO: LOW: Backtracking into clustered components uses only one component 
+               if ((dest != null) && (component != dest)) {
+                       Coordinate[] origin = dest.toAbsolute(Coordinate.NUL);
+                       for (int i=0; i < array.length; i++) {
+                               array[i] = array[i].sub(origin[0]);
+                       }
+               }
+               
+               return array;
+       }
+       
+       
+       /**
+        * Recursively sum the lengths of all subcomponents that have position 
+        * Position.AFTER.
+        * 
+        * @return  Sum of the lengths.
+        */
+       private final double getTotalLength() {
+               double l=0;
+               if (relativePosition == Position.AFTER)
+                       l = length;
+               for (int i=0; i<children.size(); i++)
+                       l += children.get(i).getTotalLength();
+               return l;
+       }
+       
+
+       
+       /////////// Total mass and CG calculation ////////////
+
+       /**
+        * Return the (possibly overridden) mass of component.
+        * 
+        * @return The mass of the component or the given override mass.
+        */
+       public final double getMass() {
+               if (massOverriden)
+                       return overrideMass;
+               return getComponentMass();
+       }
+       
+       /**
+        * Return the (possibly overridden) center of gravity and mass.
+        * 
+        * Returns the CG with the weight of the coordinate set to the weight of the component.
+        * Both CG and mass may be separately overridden.
+        * 
+        * @return The CG of the component or the given override CG.
+        */
+       public final Coordinate getCG() {
+               if (cgOverriden)
+                       return getOverrideCG().setWeight(getMass());
+
+               if (massOverriden)
+                       return getComponentCG().setWeight(getMass());
+               
+               return getComponentCG();
+       }
+       
+
+       /**
+        * Return the longitudal (around the y- or z-axis) moment of inertia of this component.
+        * The moment of inertia is scaled in reference to the (possibly overridden) mass
+        * and is relative to the non-overridden CG.
+        * 
+        * @return    the longitudal moment of inertia of this component.
+        */
+       public final double getLongitudalInertia() {
+               return getLongitudalUnitInertia() * getMass();
+       }
+       
+       /**
+        * Return the rotational (around the y- or z-axis) moment of inertia of this component.
+        * The moment of inertia is scaled in reference to the (possibly overridden) mass
+        * and is relative to the non-overridden CG.
+        * 
+        * @return    the rotational moment of inertia of this component.
+        */
+       public final double getRotationalInertia() {
+               return getRotationalUnitInertia() * getMass();
+       }
+       
+       
+       
+       ///////////  Children handling  ///////////
+       
+
+       /**
+        * Adds a child to the rocket component tree.  The component is added to the end
+        * of the component's child list.  This is a helper method that calls 
+        * {@link #addChild(RocketComponent,int)}.
+        * 
+        * @param component  The component to add.
+        * @throws IllegalArgumentException  if the component is already part of some 
+        *                                                                       component tree.
+        * @see #addChild(RocketComponent,int)
+        */
+       public final void addChild(RocketComponent component) {
+               addChild(component,children.size());
+       }
+
+       
+       /**
+        * Adds a child to the rocket component tree.  The component is added to 
+        * the given position of the component's child list.
+        * <p>
+        * This method may be overridden to enforce more strict component addition rules.  
+        * The tests should be performed first and then this method called.
+        * 
+        * @param component  The component to add.
+        * @param position   Position to add component to.
+        * @throws IllegalArgumentException  If the component is already part of 
+        *                                                                       some component tree.
+        */
+       public void addChild(RocketComponent component, int position) {
+               if (component.parent != null) {
+                       throw new IllegalArgumentException("component "+component.getComponentName()+
+                                       " is already in a tree");
+               }
+               if (!isCompatible(component)) {
+                       throw new IllegalStateException("Component "+component.getComponentName()+
+                                       " not currently compatible with component "+getComponentName());
+               }
+               
+               children.add(position,component);
+               component.parent = this;
+               
+               fireAddRemoveEvent(component);
+       }
+       
+       
+       /**
+        * Removes a child from the rocket component tree.
+        * 
+        * @param n  remove the n'th child.
+        * @throws IndexOutOfBoundsException  if n is out of bounds
+        */
+       public final void removeChild(int n) {
+               RocketComponent component = children.remove(n);
+               component.parent = null;
+               fireAddRemoveEvent(component);
+       }
+       
+       /**
+        * Removes a child from the rocket component tree.  Does nothing if the component
+        * is not present as a child.
+        * 
+        * @param component  the component to remove
+        */
+       public final void removeChild(RocketComponent component) {
+               if (children.remove(component)) {
+                       component.parent = null;
+                       
+                       fireAddRemoveEvent(component);
+               }
+       }
+
+       
+
+       
+       /**
+        * Move a child to another position.
+        * 
+        * @param component     the component to move
+        * @param position      the component's new position
+        * @throws IllegalArgumentException If an illegal placement was attempted.
+        */
+       public final void moveChild(RocketComponent component, int position) {
+               if (children.remove(component)) {
+                       children.add(position, component);
+                       fireAddRemoveEvent(component);
+               }
+       }
+       
+       
+       /**
+        * Fires an AERODYNAMIC_CHANGE, MASS_CHANGE or OTHER_CHANGE event depending on the
+        * type of component removed.
+        */
+       private void fireAddRemoveEvent(RocketComponent component) {
+               Iterator<RocketComponent> iter = component.deepIterator(true);
+               int type = ComponentChangeEvent.TREE_CHANGE;
+               while (iter.hasNext()) {
+                       RocketComponent c = iter.next();
+                       if (c.isAerodynamic())
+                               type |= ComponentChangeEvent.AERODYNAMIC_CHANGE;
+                       if (c.isMassive())
+                               type |= ComponentChangeEvent.MASS_CHANGE;
+               }
+               
+               fireComponentChangeEvent(type);
+       }
+       
+       
+       public final int getChildCount() {
+               return children.size();
+       }
+       
+       public final RocketComponent getChild(int n) {
+               return children.get(n);
+       }
+       
+       public final RocketComponent[] getChildren() {
+               return children.toArray(new RocketComponent[0]);
+       }
+       
+       
+       /**
+        * Returns the position of the child in this components child list, or -1 if the
+        * component is not a child of this component.
+        * 
+        * @param child  The child to search for.
+        * @return  Position in the list or -1 if not found.
+        */
+       public final int getChildPosition(RocketComponent child) {
+               return children.indexOf(child);
+       }
+       
+       /**
+        * Get the parent component of this component.  Returns <code>null</code> if the component
+        * has no parent.
+        * 
+        * @return  The parent of this component or <code>null</code>.
+        */
+       public final RocketComponent getParent() {
+               return parent;
+       }
+       
+       /**
+        * Get the root component of the component tree.
+        * 
+        * @return  The root component of the component tree.
+        */
+       public final RocketComponent getRoot() {
+               RocketComponent gp = this;
+               while (gp.parent != null)
+                       gp = gp.parent;
+               return gp;
+       }
+       
+       /**
+        * Returns the root Rocket component of this component tree.  Throws an 
+        * IllegalStateException if the root component is not a Rocket.
+        * 
+        * @return  The root Rocket component of the component tree.
+        * @throws  IllegalStateException  If the root component is not a Rocket.
+        */
+       public final Rocket getRocket() {
+               RocketComponent r = getRoot();
+               if (r instanceof Rocket)
+                       return (Rocket)r;
+               throw new IllegalStateException("getRocket() called with root component "
+                               +r.getComponentName());
+       }
+       
+       
+       /**
+        * Return the Stage component that this component belongs to.  Throws an
+        * IllegalStateException if a Stage is not in the parentage of this component.
+        * 
+        * @return      The Stage component this component belongs to.
+        * @throws      IllegalStateException   if a Stage component is not in the parentage.
+        */
+       public final Stage getStage() {
+               RocketComponent c = this;
+               while (c != null) {
+                       if (c instanceof Stage)
+                               return (Stage)c;
+                       c = c.getParent();
+               }
+               throw new IllegalStateException("getStage() called without Stage as a parent.");
+       }
+       
+       /**
+        * Return the stage number of the stage this component belongs to.  The stages
+        * are numbered from zero upwards.
+        * 
+        * @return   the stage number this component belongs to.
+        */
+       public final int getStageNumber() {
+               if (parent == null) {
+                       throw new IllegalArgumentException("getStageNumber() called for root component");
+               }
+               
+               RocketComponent stage = this;
+               while (!(stage instanceof Stage)) {
+                       stage = stage.parent;
+               }
+               return stage.parent.getChildPosition(stage);
+       }
+       
+       
+       /**
+        * Find a component with the given ID.  The component tree is searched from this component
+        * down (including this component) for the ID and the corresponding component is returned,
+        * or null if not found.
+        * 
+        * @param id  ID to search for.
+        * @return    The component with the ID, or null if not found.
+        */
+       public final RocketComponent findComponent(String id) {
+               Iterator<RocketComponent> iter = this.deepIterator(true);
+               while (iter.hasNext()) {
+                       RocketComponent c = iter.next();
+                       if (c.id.equals(id))
+                               return c;
+               }
+               return null;
+       }
+
+       
+       public final RocketComponent getPreviousComponent() {
+               if (parent == null)
+                       return null;
+               int pos = parent.getChildPosition(this);
+               assert(pos >= 0);
+               if (pos == 0)
+                       return parent;
+               RocketComponent c = parent.getChild(pos-1);
+               while (c.getChildCount() > 0)
+                       c = c.getChild(c.getChildCount()-1);
+               return c;
+       }
+       
+       public final RocketComponent getNextComponent() {
+               if (getChildCount() > 0)
+                       return getChild(0);
+               
+               RocketComponent current = this;
+               RocketComponent parent = this.parent;
+               
+               while (parent != null) {
+                       int pos = parent.getChildPosition(current);
+                       if (pos < parent.getChildCount()-1)
+                               return parent.getChild(pos+1);
+                               
+                       current = parent;
+                       parent = current.parent;
+               }
+               return null;
+       }
+       
+       
+       ///////////  Event handling  //////////
+       //
+       // Listener lists are provided by the root Rocket component,
+       // a single listener list for the whole rocket.
+       //
+       
+       /**
+        * Adds a ComponentChangeListener to the rocket tree.  The listener is added to the root
+        * component, which must be of type Rocket (which overrides this method).  Events of all
+        * subcomponents are sent to all listeners.
+        * 
+        * @throws IllegalStateException - if the root component is not a Rocket
+        */
+       public void addComponentChangeListener(ComponentChangeListener l) {
+               getRocket().addComponentChangeListener(l);
+       }
+       
+       /**
+        * Removes a ComponentChangeListener from the rocket tree.  The listener is removed from
+        * the root component, which must be of type Rocket (which overrides this method).
+        * 
+        * @param l  Listener to remove
+        * @throws IllegalStateException - if the root component is not a Rocket
+        */
+       public void removeComponentChangeListener(ComponentChangeListener l) {
+               getRocket().removeComponentChangeListener(l);
+       }
+       
+
+       /**
+        * Adds a <code>ChangeListener</code> to the rocket tree.  This is identical to 
+        * <code>addComponentChangeListener()</code> except that it uses a 
+        * <code>ChangeListener</code>.  The same events are dispatched to the
+        * <code>ChangeListener</code>, as <code>ComponentChangeEvent</code> is a subclass 
+        * of <code>ChangeEvent</code>.
+        * 
+        * @throws IllegalStateException - if the root component is not a <code>Rocket</code>
+        */
+       public void addChangeListener(ChangeListener l) {
+               getRocket().addChangeListener(l);
+       }
+       
+       /**
+        * Removes a ChangeListener from the rocket tree.  This is identical to
+        * removeComponentChangeListener() except it uses a ChangeListener.
+        * 
+        * @param l  Listener to remove
+        * @throws IllegalStateException - if the root component is not a Rocket
+        */
+       public void removeChangeListener(ChangeListener l) {
+               getRocket().removeChangeListener(l);
+       }
+       
+       
+       /**
+        * Fires a ComponentChangeEvent on the rocket structure.  The call is passed to the 
+        * root component, which must be of type Rocket (which overrides this method).
+        * Events of all subcomponents are sent to all listeners.
+        * 
+        * If the component tree root is not a Rocket, the event is ignored.  This is the 
+        * case when constructing components not in any Rocket tree.  In this case it 
+        * would be impossible for the component to have listeners in any case.
+        *  
+        * @param e  Event to send
+        */
+       protected void fireComponentChangeEvent(ComponentChangeEvent e) {
+               if (parent==null) {
+                       /* Ignore if root invalid. */
+                       return;
+               }
+               getRoot().fireComponentChangeEvent(e);
+       }
+
+       
+       /**
+        * Fires a ComponentChangeEvent of the given type.  The source of the event is set to
+        * this component.
+        * 
+        * @param type  Type of event
+        * @see #fireComponentChangeEvent(ComponentChangeEvent)
+        */
+       protected void fireComponentChangeEvent(int type) {
+               fireComponentChangeEvent(new ComponentChangeEvent(this,type));
+       }
+       
+
+       
+       ///////////  Iterator implementation  //////////
+       
+       /**
+        * Private inner class to implement the Iterator.
+        * 
+        * This iterator is fail-fast if the root of the structure is a Rocket.
+        */
+       private class RocketComponentIterator implements Iterator<RocketComponent> {
+               // Stack holds iterators which still have some components left.
+               private final Stack<Iterator<RocketComponent>> iteratorstack =
+                                       new Stack<Iterator<RocketComponent>>();
+               
+               private final Rocket root;
+               private final int treeModID;
+
+               private final RocketComponent original;
+               private boolean returnSelf=false;
+               
+               // Construct iterator with component's child's iterator, if it has elements
+               public RocketComponentIterator(RocketComponent c, boolean returnSelf) {
+                       
+                       RocketComponent gp = c.getRoot();
+                       if (gp instanceof Rocket) {
+                               root = (Rocket)gp;
+                               treeModID = root.getTreeModID();
+                       } else {
+                               root = null;
+                               treeModID = -1;
+                       }
+                       
+                       Iterator<RocketComponent> i = c.children.iterator();
+                       if (i.hasNext())
+                               iteratorstack.push(i);
+                       
+                       this.original = c;
+                       this.returnSelf = returnSelf;
+               }
+               
+               public boolean hasNext() {
+                       checkID();
+                       if (returnSelf)
+                               return true;
+                       return !iteratorstack.empty();  // Elements remain if stack is not empty
+               }
+
+               public RocketComponent next() {
+                       Iterator<RocketComponent> i;
+
+                       checkID();
+                       
+                       // Return original component first
+                       if (returnSelf) {
+                               returnSelf=false;
+                               return original;
+                       }
+                       
+                       // Peek first iterator from stack, throw exception if empty
+                       try {
+                               i = iteratorstack.peek();
+                       } catch (EmptyStackException e) {
+                               throw new NoSuchElementException("No further elements in " +
+                                               "RocketComponent iterator");
+                       }
+                       
+                       // Retrieve next component of the iterator, remove iterator from stack if empty
+                       RocketComponent c = i.next();
+                       if (!i.hasNext())
+                               iteratorstack.pop();
+                       
+                       // Add iterator of component children to stack if it has children
+                       i = c.children.iterator();
+                       if (i.hasNext())
+                               iteratorstack.push(i);
+                       
+                       return c;
+               }
+
+               private void checkID() {
+                       if (root != null) {
+                               if (root.getTreeModID() != treeModID) {
+                                       throw new IllegalStateException("Rocket modified while being iterated");
+                               }
+                       }
+               }
+               
+               public void remove() {
+                       throw new UnsupportedOperationException("remove() not supported by " +
+                                       "RocketComponent iterator");
+               }
+       }
+
+       /**
+        * Returns an iterator that iterates over all children and sub-children.
+        * 
+        * The iterator iterates through all children below this object, including itself if
+        * returnSelf is true.  The order of the iteration is not specified
+        * (it may be specified in the future).
+        * 
+        * If an iterator iterating over only the direct children of the component is required,
+        * use  component.getChildren().iterator()
+        * 
+        * @param returnSelf boolean value specifying whether the component itself should be 
+        *                                       returned
+        * @return An iterator for the children and sub-children.
+        */
+       public final Iterator<RocketComponent> deepIterator(boolean returnSelf) {
+               return new RocketComponentIterator(this,returnSelf);
+       }
+       
+       /**
+        * Returns an iterator that iterates over all children and sub-children.
+        * 
+        * The iterator does NOT return the component itself.  It is thus equivalent to
+        * deepIterator(false).
+        * 
+        * @see #iterator()
+        * @return An iterator for the children and sub-children.
+        */
+       public final Iterator<RocketComponent> deepIterator() {
+               return new RocketComponentIterator(this,false);
+       }
+       
+       
+       /**
+        * Return an iterator that iterates of the children of the component.  The iterator
+        * does NOT recurse to sub-children nor return itself.
+        * 
+        * @return An iterator for the children.
+        */
+       public final Iterator<RocketComponent> iterator() {
+               return Collections.unmodifiableList(children).iterator();
+       }
+       
+       ////////////  Helper methods for subclasses
+       
+       /**
+        * Helper method to add rotationally symmetric bounds at the specified coordinates.
+        * The X-axis value is <code>x</code> and the radius at the specified position is
+        * <code>r</code>. 
+        */
+       protected static final void addBound(Collection<Coordinate> bounds, double x, double r) {
+               bounds.add(new Coordinate(x,-r,-r));
+               bounds.add(new Coordinate(x, r,-r));
+               bounds.add(new Coordinate(x, r, r));
+               bounds.add(new Coordinate(x,-r, r));
+       }
+       
+       
+       protected static final Coordinate ringCG(double outerRadius, double innerRadius, 
+                       double x1, double x2, double density) {
+               return new Coordinate((x1+x2)/2, 0, 0, 
+                               ringMass(outerRadius, innerRadius, x2-x1, density));
+       }
+       
+       protected static final double ringMass(double outerRadius, double innerRadius,
+                       double length, double density) {
+               return Math.PI*(MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) *
+                                       length * density;
+       }
+
+       protected static final double ringLongitudalUnitInertia(double outerRadius, 
+                       double innerRadius, double length) {
+               // 1/12 * (3 * (r1^2 + r2^2) + h^2)
+               return (3 * (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) +
+                               MathUtil.pow2(length)) / 12;
+       }
+
+       protected static final double ringRotationalUnitInertia(double outerRadius, 
+                       double innerRadius) {
+               // 1/2 * (r1^2 + r2^2)
+               return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius))/2;
+       }
+
+
+       
+       ////////////  OTHER
+
+       
+       /**
+        * Loads the RocketComponent fields from the given component.  This method is meant
+        * for use with the undo/redo mechanism.
+        *
+        * The fields are copied by reference, and the supplied component must not be used
+        * after the call, as it is in an undefined state.
+        * 
+        * TODO: MEDIUM: Make general to copy all private/protected fields...
+        */
+       protected void copyFrom(RocketComponent src) {
+               // Set parents and children
+               this.parent = null;
+               this.children = src.children;
+               src.children = new ArrayList<RocketComponent>();
+               
+               for (RocketComponent c: this.children) {
+                       c.parent = this;
+               }
+
+               // Set all parameters
+               this.length = src.length;
+               this.relativePosition = src.relativePosition;
+               this.position = src.position;
+               this.color = src.color;
+               this.lineStyle = src.lineStyle;
+               this.overrideMass = src.overrideMass;
+               this.massOverriden = src.massOverriden;
+               this.overrideCGX = src.overrideCGX;
+               this.cgOverriden = src.cgOverriden;
+               this.overrideSubcomponents = src.overrideSubcomponents;
+               this.name = src.name;
+               this.comment = src.comment;
+               this.id = src.id;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ShockCord.java b/src/net/sf/openrocket/rocketcomponent/ShockCord.java
new file mode 100644 (file)
index 0000000..2e7e4cf
--- /dev/null
@@ -0,0 +1,62 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class ShockCord extends MassObject {
+
+       private Material material;
+       private double cordLength;
+
+       public ShockCord() {
+               material = Prefs.getDefaultComponentMaterial(ShockCord.class, Material.Type.LINE);
+               cordLength = 0.4;
+       }
+       
+       
+       
+       public Material getMaterial() {
+               return material;
+       }
+       
+       public void setMaterial(Material m) {
+               if (m.getType() != Material.Type.LINE)
+                       throw new RuntimeException("Attempting to set non-linear material.");
+               if (material.equals(m))
+                       return;
+               this.material = m;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       public double getCordLength() {
+               return cordLength;
+       }
+       
+       public void setCordLength(double length) {
+               length = MathUtil.max(length, 0);
+               if (MathUtil.equals(length, this.length))
+                       return;
+               this.cordLength = length;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       
+       @Override
+       public double getComponentMass() {
+               return material.getDensity() * cordLength;
+       }
+
+       @Override
+       public String getComponentName() {
+               return "Shock cord";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Sleeve.java b/src/net/sf/openrocket/rocketcomponent/Sleeve.java
new file mode 100644 (file)
index 0000000..fd0db53
--- /dev/null
@@ -0,0 +1,101 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A RingComponent that comes on top of another tube.  It's defined by the inner
+ * radius and thickness.  The inner radius can be automatic, in which case it
+ * takes the radius of the parent component.
+ *  
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class Sleeve extends RingComponent {
+
+       protected double innerRadius = 0;
+       protected double thickness = 0;
+
+       
+       public Sleeve() {
+               super();
+               setInnerRadiusAutomatic(true);
+               setThickness(0.001);
+               setLength(0.05);
+       }
+       
+       
+       @Override
+       public double getOuterRadius() {
+               return getInnerRadius() + thickness;
+       }
+       
+       @Override
+       public void setOuterRadius(double r) {
+               if (MathUtil.equals(getOuterRadius(), r))
+                       return;
+               
+               innerRadius = Math.max(r - thickness, 0);
+               if (thickness > r)
+                       thickness = r;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+       @Override
+       public double getInnerRadius() {
+               // Implement parent inner radius automation
+               if (isInnerRadiusAutomatic() && getParent() instanceof RadialParent) {
+                       RocketComponent parent = getParent();
+                       double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x;
+                       double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
+                       pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
+                       pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
+                       innerRadius = Math.max(((RadialParent)parent).getOuterRadius(pos1),
+                                       ((RadialParent)parent).getOuterRadius(pos2));
+               }
+               
+               return innerRadius;
+       }
+       
+       @Override
+       public void setInnerRadius(double r) {
+               r = Math.max(r,0);
+               if (MathUtil.equals(innerRadius, r))
+                       return;
+               innerRadius = r;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       @Override
+       public double getThickness() {
+               return thickness;
+       }
+       
+       @Override
+       public void setThickness(double t) {
+               t = Math.max(t, 0);
+               if (MathUtil.equals(thickness, t))
+                       return;
+               thickness = t;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+
+       
+       @Override
+       public void setInnerRadiusAutomatic(boolean auto) {
+               super.setOuterRadiusAutomatic(auto);
+       }
+       
+       @Override
+       public String getComponentName() {
+               return "Sleeve";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Stage.java b/src/net/sf/openrocket/rocketcomponent/Stage.java
new file mode 100644 (file)
index 0000000..74a36a5
--- /dev/null
@@ -0,0 +1,23 @@
+package net.sf.openrocket.rocketcomponent;
+
+public class Stage extends ComponentAssembly {
+
+       @Override
+       public String getComponentName() {
+               return "Stage";
+       }
+
+       
+       /**
+        * Check whether the given type can be added to this component.  A Stage allows 
+        * only BodyComponents to be added.
+        * 
+        * @param type  The RocketComponent class type to add.
+        * @return      Whether such a component can be added.
+        */
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return BodyComponent.class.isAssignableFrom(type);
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Streamer.java b/src/net/sf/openrocket/rocketcomponent/Streamer.java
new file mode 100644 (file)
index 0000000..134f3ee
--- /dev/null
@@ -0,0 +1,105 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.MathUtil;
+
+public class Streamer extends RecoveryDevice {
+
+       public static final double DEFAULT_CD = 0.6;
+       
+       public static final double MAX_COMPUTED_CD = 0.4;
+       
+       
+       private double stripLength;
+       private double stripWidth;
+       
+       
+       public Streamer() {
+               this.stripLength = 0.5;
+               this.stripWidth = 0.05;
+       }
+       
+       
+       public double getStripLength() {
+               return stripLength;
+       }
+
+       public void setStripLength(double stripLength) {
+               if (MathUtil.equals(this.stripLength, stripLength))
+                       return;
+               this.stripLength = stripLength;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       public double getStripWidth() {
+               return stripWidth;
+       }
+
+       public void setStripWidth(double stripWidth) {
+               if (MathUtil.equals(this.stripWidth, stripWidth))
+                       return;
+               this.stripWidth = stripWidth;
+               this.length = stripWidth;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       @Override
+       public void setLength(double length) {
+               setStripWidth(length);
+       }
+
+       
+       public double getAspectRatio() {
+               if (stripWidth > 0.0001)
+                       return stripLength/stripWidth;
+               return 1000;
+       }
+       
+       public void setAspectRatio(double ratio) {
+               if (MathUtil.equals(getAspectRatio(), ratio))
+                       return;
+                               
+               ratio = Math.max(ratio, 0.01);
+               double area = getArea();
+               stripWidth = Math.sqrt(area / ratio);
+               stripLength = ratio * stripWidth;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+
+       @Override
+       public double getArea() {
+               return stripWidth * stripLength;
+       }
+
+       public void setArea(double area) {
+               if (MathUtil.equals(getArea(), area))
+                       return;
+               
+               double ratio = Math.max(getAspectRatio(), 0.01);
+               stripWidth = Math.sqrt(area / ratio);
+               stripLength = ratio * stripWidth;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       
+       @Override
+       public double getComponentCD(double mach) {
+               double density = this.getMaterial().getDensity();
+               double cd;
+               
+               cd = 0.034 * ((density + 0.025)/0.105) * (stripLength+1) / stripLength;
+               cd = MathUtil.min(cd, MAX_COMPUTED_CD);
+               return cd;
+       }
+
+       @Override
+       public String getComponentName() {
+               return "Streamer";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java b/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java
new file mode 100644 (file)
index 0000000..ff6afd4
--- /dev/null
@@ -0,0 +1,29 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.Prefs;
+
+public abstract class StructuralComponent extends InternalComponent {
+
+       private Material material;
+       
+       public StructuralComponent() {
+               super();
+               material = Prefs.getDefaultComponentMaterial(this.getClass(), Material.Type.BULK);
+       }
+       
+
+       public final Material getMaterial() {
+               return material;
+       }
+       
+       public final void setMaterial(Material mat) {
+               if (mat.getType() != Material.Type.BULK) {
+                       throw new IllegalArgumentException("Attempted to set non-bulk material "+mat);
+               }
+               if (mat.equals(material))
+                       return;
+               this.material = mat;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java b/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java
new file mode 100644 (file)
index 0000000..ed0bffb
--- /dev/null
@@ -0,0 +1,552 @@
+package net.sf.openrocket.rocketcomponent;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * Class for an axially symmetric rocket component generated by rotating
+ * a function y=f(x) >= 0 around the x-axis (eg. tube, cone, etc.)
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class SymmetricComponent extends BodyComponent implements RadialParent {
+       public static final double DEFAULT_RADIUS = 0.025;
+       public static final double DEFAULT_THICKNESS = 0.002;
+       
+       private static final int DIVISIONS = 100;  // No. of divisions when integrating
+       
+       protected boolean filled = false;
+       protected double thickness = DEFAULT_THICKNESS;
+       
+       
+       // Cached data, default values signify not calculated
+       private double wetArea = -1;
+       private double planArea = -1;
+       private double planCenter = -1;
+       private double volume = -1;
+       private double fullVolume = -1;
+       private double longitudalInertia = -1;
+       private double rotationalInertia = -1;
+       private Coordinate cg = null;
+       
+       
+       
+       public SymmetricComponent() {
+               super();
+       }
+
+       
+       /**
+        * Return the component radius at position x.
+        * @param x Position on x-axis.
+        * @return  Radius of the component at the given position, or 0 if outside
+        *          the component.
+        */
+       public abstract double getRadius(double x);
+       public abstract double getInnerRadius(double x);
+
+       public abstract double getForeRadius();
+       public abstract boolean isForeRadiusAutomatic();
+       public abstract double getAftRadius();
+       public abstract boolean isAftRadiusAutomatic();
+       
+       
+       // Implement the Radial interface:
+       public final double getOuterRadius(double x) {
+               return getRadius(x);
+       }
+       
+       
+       @Override
+       public final double getRadius(double x, double theta) {
+               return getRadius(x);
+       }
+       
+       @Override
+       public final double getInnerRadius(double x, double theta) {
+               return getInnerRadius(x);
+       }
+       
+       
+       
+       /**
+        * Return the component wall thickness.
+        */
+       public double getThickness() {
+               if (filled)
+                       return Math.max(getForeRadius(),getAftRadius());
+               return Math.min(thickness,Math.max(getForeRadius(),getAftRadius()));
+       }
+       
+       
+       /**
+        * Set the component wall thickness.  Values greater than the maximum radius are not
+        * allowed, and will result in setting the thickness to the maximum radius.
+        */
+       public void setThickness(double thickness) {
+               if ((this.thickness == thickness) && !filled)
+                       return;
+               this.thickness = MathUtil.clamp(thickness,0,Math.max(getForeRadius(),getAftRadius()));
+               filled = false;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+       /**
+        * Returns whether the component is set as filled.  If it is set filled, then the
+        * wall thickness will have no effect. 
+        */
+       public boolean isFilled() {
+               return filled;
+       }
+       
+       
+       /**
+        * Sets whether the component is set as filled.  If the component is filled, then
+        * the wall thickness will have no effect.
+        */
+       public void setFilled(boolean filled) {
+               if (this.filled == filled)
+                       return;
+               this.filled = filled;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+
+       /**
+        * Adds component bounds at a number of points between 0...length.
+        */
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               List<Coordinate> list = new ArrayList<Coordinate>(20);
+               for (int n=0; n<=5; n++) {
+                       double x = n*length/5;
+                       double r = getRadius(x);
+                       addBound(list,x,r);
+               }
+               return list;
+       }
+       
+       
+       
+       /**
+        * Calculate volume of the component by integrating over the length of the component.
+        * The method caches the result, so subsequent calls are instant.  Subclasses may
+        * override this method for simple shapes and use this method as necessary.
+        * 
+        * @return  The volume of the component.
+        */
+       @Override
+       public double getComponentVolume() {
+               if (volume < 0)
+                       integrate();
+               return volume;
+       }
+       
+       
+       /**
+        * Calculate full (filled) volume of the component by integrating over the length
+        * of the component.  The method caches the result, so subsequent calls are instant.  
+        * Subclasses may override this method for simple shapes and use this method as 
+        * necessary.
+        * 
+        * @return  The filled volume of the component.
+        */
+       public double getFullVolume() {
+               if (fullVolume < 0)
+                       integrate();
+               return fullVolume;
+       }
+       
+       
+       /**
+        * Calculate the wetted area of the component by integrating over the length 
+        * of the component.  The method caches the result, so subsequent calls are instant. 
+        * Subclasses may override this method for simple shapes and use this method as 
+        * necessary.
+        *  
+        * @return  The wetted area of the component.
+        */
+       public double getComponentWetArea() {
+               if (wetArea < 0)
+                       integrate();
+               return wetArea;
+       }
+       
+       
+       /**
+        * Calculate the planform area of the component by integrating over the length of 
+        * the component.  The method caches the result, so subsequent calls are instant.  
+        * Subclasses may override this method for simple shapes and use this method as 
+        * necessary.
+        *  
+        * @return  The planform area of the component.
+        */
+       public double getComponentPlanformArea() {
+               if (planArea < 0)
+                       integrate();
+               return planArea;
+       }
+       
+       
+       /**
+        * Calculate the planform center X-coordinate of the component by integrating over 
+        * the length of the component.  The planform center is defined as 
+        * <pre>   integrate(x*2*r(x)) / planform area  </pre>
+        * The method caches the result, so subsequent calls are instant.  Subclasses may 
+        * override this method for simple shapes and use this method as necessary.
+        *  
+        * @return  The planform center of the component.
+        */
+       public double getComponentPlanformCenter() {
+               if (planCenter < 0)
+                       integrate();
+               return planCenter;
+       }
+       
+       
+       /**
+        * Calculate CG of the component by integrating over the length of the component.
+        * The method caches the result, so subsequent calls are instant.  Subclasses may
+        * override this method for simple shapes and use this method as necessary.
+        * 
+        * @return  The CG+mass of the component.
+        */
+       @Override
+       public Coordinate getComponentCG() {
+               if (cg == null)
+                       integrate();
+               return cg;
+       }
+       
+       
+       @Override
+       public double getLongitudalUnitInertia() {
+               if (longitudalInertia < 0) {
+                       if (getComponentVolume() > 0.0000001)  // == 0.1cm^3
+                               integrateInertiaVolume();
+                       else
+                               integrateInertiaSurface();
+               }
+               return longitudalInertia;
+       }
+       
+       
+       @Override
+       public double getRotationalUnitInertia() {
+               if (rotationalInertia < 0) {
+                       if (getComponentVolume() > 0.0000001)
+                               integrateInertiaVolume();
+                       else
+                               integrateInertiaSurface();
+               }
+               return rotationalInertia;
+       }
+       
+       
+
+       /**
+        * Performs integration over the length of the component and updates the cached variables.
+        */
+       private void integrate() {
+               double x,r1,r2;
+               double cgx;
+               
+               // Check length > 0
+               if (length <= 0) {
+                       wetArea = 0;
+                       planArea = 0;
+                       planCenter = 0;
+                       volume = 0;
+                       cg = Coordinate.NUL;
+                       return;
+               }
+               
+               
+               // Integrate for volume, CG, wetted area and planform area
+               
+               final double l = length/DIVISIONS;
+               final double pil  = Math.PI*l;    // PI * l
+               final double pil3 = Math.PI*l/3;  // PI * l/3
+               r1 = getRadius(0);
+               x = 0;
+               wetArea = 0;
+               planArea = 0;
+               planCenter = 0;
+               fullVolume = 0;
+               volume = 0;
+               cgx = 0;
+               
+               for (int n=1; n<=DIVISIONS; n++) {
+                       /*
+                        * r1 and r2 are the two radii
+                        * x is the position of r1
+                        * hyp is the length of the hypotenuse from r1 to r2
+                        * height if the y-axis height of the component if not filled
+                        */
+                       
+                       r2 = getRadius(x+l);
+                       final double hyp = MathUtil.hypot(r2-r1, l);
+                       
+                       
+                       // Volume differential elements
+                       final double dV;
+                       final double dFullV;
+                       
+                       dFullV = pil3*(r1*r1 + r1*r2 + r2*r2);
+                       if (filled || r1<thickness || r2<thickness) {
+                               // Filled piece
+                               dV = dFullV;
+                       } else {
+                               // Hollow piece
+                               final double height = thickness*hyp/l;
+                               dV = pil*height*(r1+r2-height);
+                       }
+
+                       // Add to the volume-related components
+                       volume += dV;
+                       fullVolume += dFullV;
+                       cgx += (x+l/2)*dV;
+                       
+                       // Wetted area ( * PI at the end)
+                       wetArea += hyp*(r1+r2);
+                       
+                       // Planform area & center
+                       final double p = l*(r1+r2);
+                       planArea += p;
+                       planCenter += (x+l/2)*p;
+                       
+                       // Update for next iteration
+                       r1 = r2;
+                       x += l;
+               }
+               
+               wetArea *= Math.PI;
+               
+               if (planArea > 0)
+                       planCenter /= planArea;
+               
+               if (volume == 0) {
+                       cg = Coordinate.NUL;
+               } else {
+                       // getComponentMass is safe now
+                       cg = new Coordinate(cgx/volume,0,0,getComponentMass());
+               }
+       }
+       
+       
+       /**
+        * Integrate the longitudal and rotational inertia based on component volume.
+        * This method may be used only if the total volume is zero.
+        */
+       private void integrateInertiaVolume() {
+               double x, r1, r2;
+
+               final double l = length/DIVISIONS;
+               final double pil  = Math.PI*l;    // PI * l
+               final double pil3 = Math.PI*l/3;  // PI * l/3
+
+               r1 = getRadius(0);
+               x = 0;
+               longitudalInertia = 0;
+               rotationalInertia = 0;
+               
+               double volume = 0;
+               
+               for (int n=1; n<=DIVISIONS; n++) {
+                       /*
+                        * r1 and r2 are the two radii, outer is their average
+                        * x is the position of r1
+                        * hyp is the length of the hypotenuse from r1 to r2
+                        * height if the y-axis height of the component if not filled
+                        */
+                       r2 = getRadius(x+l);
+                       final double outer = (r1 + r2)/2;
+                       
+                       
+                       // Volume differential elements
+                       final double inner;
+                       final double dV;
+                       
+                       if (filled || r1<thickness || r2<thickness) {
+                               inner = 0;
+                               dV = pil3*(r1*r1 + r1*r2 + r2*r2);
+                       } else {
+                               final double hyp = MathUtil.hypot(r2-r1, l);
+                               final double height = thickness*hyp/l;
+                               dV = pil*height*(r1+r2-height);
+                               inner = Math.max(outer-height, 0);
+                       }
+                       
+                       rotationalInertia += dV * (pow2(outer) + pow2(inner))/2;
+                       longitudalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l))/12 
+                                       + pow2(x+l/2));
+                       
+                       volume += dV;
+                       
+                       // Update for next iteration
+                       r1 = r2;
+                       x += l;
+               }
+               
+               if (MathUtil.equals(volume,0)) {
+                       integrateInertiaSurface();
+                       return;
+               }
+               
+               rotationalInertia /= volume;
+               longitudalInertia /= volume;
+
+               // Shift longitudal inertia to CG
+               longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
+       }
+       
+       
+
+       /**
+        * Integrate the longitudal and rotational inertia based on component surface area.
+        * This method may be used only if the total volume is zero.
+        */
+       private void integrateInertiaSurface() {
+               double x, r1, r2;
+
+               final double l = length/DIVISIONS;
+
+               r1 = getRadius(0);
+               x = 0;
+               longitudalInertia = 0;
+               rotationalInertia = 0;
+               
+               double surface = 0;
+               
+               for (int n=1; n<=DIVISIONS; n++) {
+                       /*
+                        * r1 and r2 are the two radii, outer is their average
+                        * x is the position of r1
+                        * hyp is the length of the hypotenuse from r1 to r2
+                        * height if the y-axis height of the component if not filled
+                        */
+                       r2 = getRadius(x+l);
+                       final double hyp = MathUtil.hypot(r2-r1, l);
+                       final double outer = (r1 + r2)/2;
+                       
+                       final double dS = hyp * (r1+r2) * Math.PI;
+
+                       rotationalInertia += dS * pow2(outer);
+                       longitudalInertia += dS * ((6 * pow2(outer) + pow2(l))/12 + pow2(x+l/2));
+                       
+                       surface += dS;
+                       
+                       // Update for next iteration
+                       r1 = r2;
+                       x += l;
+               }
+
+               if (MathUtil.equals(surface, 0)) {
+                       longitudalInertia = 0;
+                       rotationalInertia = 0;
+                       return;
+               }
+               
+               longitudalInertia /= surface;
+               rotationalInertia /= surface;
+               
+               // Shift longitudal inertia to CG
+               longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
+       }
+       
+       
+       
+       
+       /**
+        * Invalidates the cached volume and CG information.
+        */
+       @Override
+       protected void componentChanged(ComponentChangeEvent e) {
+               super.componentChanged(e);
+               if (!e.isOtherChange()) {
+                       wetArea = -1;
+                       planArea = -1;
+                       planCenter = -1;
+                       volume = -1;
+                       fullVolume = -1;
+                       longitudalInertia = -1;
+                       rotationalInertia = -1;
+                       cg = null;
+               }
+       }
+       
+       
+       
+       ///////////   Auto radius helper methods
+       
+       
+       /**
+        * Returns the automatic radius for this component towards the 
+        * front of the rocket.  The automatics will not search towards the
+        * rear of the rocket for a suitable radius.  A positive return value
+        * indicates a preferred radius, a negative value indicates that a
+        * match was not found.
+        */
+       protected abstract double getFrontAutoRadius();
+
+       /**
+        * Returns the automatic radius for this component towards the
+        * end of the rocket.  The automatics will not search towards the
+        * front of the rocket for a suitable radius.  A positive return value
+        * indicates a preferred radius, a negative value indicates that a
+        * match was not found.
+        */
+       protected abstract double getRearAutoRadius();
+       
+       
+       
+       /**
+        * Return the previous symmetric component, or null if none exists.
+        * NOTE: This method currently assumes that there are no external
+        * "pods".
+        * 
+        * @return      the previous SymmetricComponent, or null.
+        */
+       protected final SymmetricComponent getPreviousSymmetricComponent() {
+               RocketComponent c;
+               for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
+                       if (c instanceof SymmetricComponent) {
+                               return (SymmetricComponent)c;
+                       }
+                       if (!(c instanceof Stage) &&
+                                (c.relativePosition == RocketComponent.Position.AFTER))
+                               return null;   // Bad component type as "parent"
+               }
+               return null;
+       }
+       
+       /**
+        * Return the next symmetric component, or null if none exists.
+        * NOTE: This method currently assumes that there are no external
+        * "pods".
+        * 
+        * @return      the next SymmetricComponent, or null.
+        */
+       protected final SymmetricComponent getNextSymmetricComponent() {
+               RocketComponent c;
+               for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
+                       if (c instanceof SymmetricComponent) {
+                               return (SymmetricComponent)c;
+                       }
+                       if (!(c instanceof Stage) &&
+                                (c.relativePosition == RocketComponent.Position.AFTER))
+                               return null;   // Bad component type as "parent"
+               }
+               return null;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java b/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java
new file mode 100644 (file)
index 0000000..4bded70
--- /dev/null
@@ -0,0 +1,82 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * An inner component that consists of a hollow cylindrical component.  This can be
+ * an inner tube, tube coupler, centering ring, bulkhead etc.
+ * 
+ * The properties include the inner and outer radii, length and radial position.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class ThicknessRingComponent extends RingComponent {
+
+       protected double outerRadius = 0;
+       protected double thickness = 0;
+       
+       
+       @Override
+       public double getOuterRadius() {
+               if (isOuterRadiusAutomatic() && getParent() instanceof RadialParent) {
+                       RocketComponent parent = getParent();
+                       double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x;
+                       double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
+                       pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
+                       pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
+                       outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1),
+                                       ((RadialParent)parent).getInnerRadius(pos2));
+               }
+                               
+               return outerRadius;
+       }
+
+       
+       @Override
+       public void setOuterRadius(double r) {
+               r = Math.max(r,0);
+               if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic())
+                       return;
+               
+               outerRadius = r;
+               outerRadiusAutomatic = false;
+
+               if (thickness > outerRadius)
+                       thickness = outerRadius;
+               
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       
+
+       @Override
+       public double getThickness() {
+               return Math.min(thickness, getOuterRadius());
+       }
+       @Override
+       public void setThickness(double thickness) {
+               double outer = getOuterRadius();
+               
+               thickness = MathUtil.clamp(thickness, 0, outer);
+               if (MathUtil.equals(getThickness(), thickness))
+                       return;
+               
+               this.thickness = thickness;
+               
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       
+       @Override
+       public double getInnerRadius() {
+               return Math.max(getOuterRadius()-thickness, 0);
+       }
+       @Override
+       public void setInnerRadius(double r) {
+               r = Math.max(r,0);
+               setThickness(getOuterRadius() - r);
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/ThrustCurveMotor.java b/src/net/sf/openrocket/rocketcomponent/ThrustCurveMotor.java
new file mode 100644 (file)
index 0000000..a29f557
--- /dev/null
@@ -0,0 +1,121 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+
+/**
+ * A class of motors specified by a fixed thrust curve.  This is the most
+ * accurate for solid rocket motors.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ThrustCurveMotor extends Motor {
+
+       private final double[] time;
+       private final double[] thrust;
+       private final Coordinate[] cg;
+       
+       private final double totalTime;
+       private final double maxThrust;
+       
+
+       /**
+        * Sole constructor.  Sets all the properties of the motor.
+        * 
+        * @param manufacturer  the manufacturer of the motor.
+        * @param designation   the designation of the motor.
+        * @param description   extra description of the motor.
+        * @param diameter      diameter of the motor.
+        * @param length        length of the motor.
+        * @param time          the time points for the thrust curve.
+        * @param thrust        thrust at the time points.
+        * @param cg            cg at the time points.
+        */
+       public ThrustCurveMotor(String 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 max = -1;
+
+               // Check argument validity
+               if ((time.length != thrust.length) || (time.length != cg.length)) {
+                       throw new IllegalArgumentException("Array lengths do not match, " +
+                                       "time:" + time.length + " thrust:" + thrust.length +
+                                       " cg:" + cg.length);
+               }
+               if (time.length < 2) {
+                       throw new IllegalArgumentException("Too short thrust-curve, length=" + 
+                                       time.length);
+               }
+               for (int i=0; i < time.length-1; i++) {
+                       if (time[i+1] < time[i]) {
+                               throw new IllegalArgumentException("Time goes backwards, " +
+                                               "time[" + i + "]=" + time[i] + " " +
+                                               "time[" + (i+1) + "]=" + time[i+1]);
+                       }
+               }
+               if (time[0] != 0) {
+                       throw new IllegalArgumentException("Curve starts at time=" + time[0]);
+               }
+               for (double t: thrust) {
+                       if (t < 0) {
+                               throw new IllegalArgumentException("Negative thrust.");
+                       }
+                       if (t > max)
+                               max = t;
+               }
+
+               this.maxThrust = max;
+               this.time = time.clone();
+               this.thrust = thrust.clone();
+               this.cg = cg.clone();
+               this.totalTime = time[time.length-1];
+       }
+
+
+       @Override
+       public double getTotalTime() {
+               return totalTime;
+       }
+
+       @Override
+       public double getMaxThrust() {
+               return maxThrust;
+       }
+       
+       @Override
+       public double getThrust(double t) {
+               if ((t < 0) || (t > totalTime))
+                       return 0;
+
+               for (int i=0; i < time.length-1; i++) {
+                       if ((t >= time[i]) && (t <= time[i+1])) {
+                               double delta = time[i+1] - time[i];
+                               t = t - time[i];
+                               return thrust[i] * (1 - t/delta) + thrust[i+1] * (t/delta);
+                       }
+               }
+               assert false : "Should not be reached.";
+               return 0;
+       }
+
+
+       @Override
+       public Coordinate getCG(double t) {
+               if (t <= 0)
+                       return cg[0];
+               if (t >= totalTime)
+                       return cg[cg.length-1];
+               
+               for (int i=0; i < time.length-1; i++) {
+                       if ((t >= time[i]) && (t <= time[i+1])) {
+                               double delta = time[i+1] - time[i];
+                               t = t - time[i];
+                               return cg[i].multiply(1 - t/delta).add(cg[i+1].multiply(t/delta));
+                       }
+               }
+               assert false : "Should not be reached.";
+               return cg[cg.length-1];
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/Transition.java b/src/net/sf/openrocket/rocketcomponent/Transition.java
new file mode 100644 (file)
index 0000000..7155a79
--- /dev/null
@@ -0,0 +1,834 @@
+package net.sf.openrocket.rocketcomponent;
+
+import static java.lang.Math.sin;
+import static java.lang.Math.sqrt;
+import static net.sf.openrocket.util.MathUtil.pow2;
+import static net.sf.openrocket.util.MathUtil.pow3;
+
+import java.util.Collection;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class Transition extends SymmetricComponent {
+       private static final double CLIP_PRECISION = 0.0001;
+       
+       
+       private Shape type;
+       private double shapeParameter;
+       private boolean clipped;    // Not to be read - use isClipped(), which may be overriden
+       
+       private double radius1, radius2;
+       private boolean autoRadius1, autoRadius2;   // Whether the start radius is automatic
+       
+       
+       private double foreShoulderRadius;
+       private double foreShoulderThickness;
+       private double foreShoulderLength;
+       private boolean foreShoulderCapped;
+       private double aftShoulderRadius;
+       private double aftShoulderThickness;
+       private double aftShoulderLength;
+       private boolean aftShoulderCapped;
+       
+       
+       // Used to cache the clip length
+       private double clipLength=-1;
+       
+       public Transition() {
+               super();
+               
+               this.radius1 = DEFAULT_RADIUS;
+               this.radius2 = DEFAULT_RADIUS;
+               this.length = DEFAULT_RADIUS * 3;
+               this.autoRadius1 = true;
+               this.autoRadius2 = true;
+               
+               this.type = Shape.CONICAL;
+               this.shapeParameter = 0;
+               this.clipped = true;
+       }
+       
+       
+       
+       
+       ////////  Fore radius  ////////
+       
+       
+       @Override
+       public double getForeRadius() {
+               if (isForeRadiusAutomatic()) {
+                       // Get the automatic radius from the front
+                       double r = -1;
+                       SymmetricComponent c = this.getPreviousSymmetricComponent();
+                       if (c != null) {
+                               r = c.getFrontAutoRadius();
+                       }
+                       if (r < 0)
+                               r = DEFAULT_RADIUS;
+                       return r;
+               }
+               return radius1;
+       }
+       
+       public void setForeRadius(double radius) {
+               if ((this.radius1 == radius) && (autoRadius1 == false))
+                       return;
+               
+               this.autoRadius1 = false;
+               this.radius1 = Math.max(radius,0);
+
+               if (this.thickness > this.radius1 && this.thickness > this.radius2)
+                       this.thickness = Math.max(this.radius1, this.radius2);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       @Override
+       public boolean isForeRadiusAutomatic() {
+               return autoRadius1;
+       }
+       
+       public void setForeRadiusAutomatic(boolean auto) {
+               if (autoRadius1 == auto)
+                       return;
+               
+               autoRadius1 = auto;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+
+       ////////  Aft radius  /////////
+       
+       @Override
+       public double getAftRadius() {
+               if (isAftRadiusAutomatic()) {
+                       // Return the auto radius from the rear
+                       double r = -1;
+                       SymmetricComponent c = this.getNextSymmetricComponent();
+                       if (c != null) {
+                               r = c.getRearAutoRadius();
+                       }
+                       if (r < 0)
+                               r = DEFAULT_RADIUS;
+                       return r;
+               }
+               return radius2;
+       }
+       
+       
+       
+       public void setAftRadius(double radius) {
+               if ((this.radius2 == radius) && (autoRadius2 == false))
+                       return;
+               
+               this.autoRadius2 = false;
+               this.radius2 = Math.max(radius,0);
+
+               if (this.thickness > this.radius1 && this.thickness > this.radius2)
+                       this.thickness = Math.max(this.radius1, this.radius2);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       @Override
+       public boolean isAftRadiusAutomatic() {
+               return autoRadius2;
+       }
+       
+       public void setAftRadiusAutomatic(boolean auto) {
+               if (autoRadius2 == auto)
+                       return;
+               
+               autoRadius2 = auto;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       
+       
+       //// Radius automatics
+       
+       @Override
+       protected double getFrontAutoRadius() {
+               if (isAftRadiusAutomatic())
+                       return -1;
+               return getAftRadius();
+       }
+
+
+       @Override
+       protected double getRearAutoRadius() {
+               if (isForeRadiusAutomatic())
+                       return -1;
+               return getForeRadius();
+       }
+
+
+       
+       
+       ////////  Type & shape  /////////
+       
+       public Shape getType() {
+               return type;
+       }
+       
+       public void setType(Shape type) {
+               if (this.type == type)
+                       return;
+               this.type = type;
+               this.clipped = type.isClippable();
+               this.shapeParameter = type.defaultParameter();
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       public double getShapeParameter() {
+               return shapeParameter;
+       }
+       
+       public void setShapeParameter(double n) {
+               if (shapeParameter == n)
+                       return;
+               this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       public boolean isClipped() {
+               if (!type.isClippable())
+                       return false;
+               return clipped;
+       }
+       
+       public void setClipped(boolean c) {
+               if (clipped == c)
+                       return;
+               clipped = c;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       public boolean isClippedEnabled() {
+               return type.isClippable();
+       }
+
+       public double getShapeParameterMin() {
+               return type.minParameter();
+       }
+       
+       public double getShapeParameterMax() {
+               return type.maxParameter();
+       }
+       
+       
+       ////////  Shoulders  ////////
+       
+       public double getForeShoulderRadius() {
+               return foreShoulderRadius;
+       }
+
+       public void setForeShoulderRadius(double foreShoulderRadius) {
+               if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
+                       return;
+               this.foreShoulderRadius = foreShoulderRadius;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       public double getForeShoulderThickness() {
+               return foreShoulderThickness;
+       }
+
+       public void setForeShoulderThickness(double foreShoulderThickness) {
+               if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
+                       return;
+               this.foreShoulderThickness = foreShoulderThickness;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       public double getForeShoulderLength() {
+               return foreShoulderLength;
+       }
+
+       public void setForeShoulderLength(double foreShoulderLength) {
+               if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
+                       return;
+               this.foreShoulderLength = foreShoulderLength;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       public boolean isForeShoulderCapped() {
+               return foreShoulderCapped;
+       }
+       
+       public void setForeShoulderCapped(boolean capped) {
+               if (this.foreShoulderCapped == capped)
+                       return;
+               this.foreShoulderCapped = capped;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+
+       
+       
+       public double getAftShoulderRadius() {
+               return aftShoulderRadius;
+       }
+
+       public void setAftShoulderRadius(double aftShoulderRadius) {
+               if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
+                       return;
+               this.aftShoulderRadius = aftShoulderRadius;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       public double getAftShoulderThickness() {
+               return aftShoulderThickness;
+       }
+
+       public void setAftShoulderThickness(double aftShoulderThickness) {
+               if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
+                       return;
+               this.aftShoulderThickness = aftShoulderThickness;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+       public double getAftShoulderLength() {
+               return aftShoulderLength;
+       }
+
+       public void setAftShoulderLength(double aftShoulderLength) {
+               if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
+                       return;
+               this.aftShoulderLength = aftShoulderLength;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+       
+       public boolean isAftShoulderCapped() {
+               return aftShoulderCapped;
+       }
+       
+       public void setAftShoulderCapped(boolean capped) {
+               if (this.aftShoulderCapped == capped)
+                       return;
+               this.aftShoulderCapped = capped;
+               fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+       }
+
+
+       
+       
+       ///////////   Shape implementations   ////////////
+       
+
+
+       /**
+        * Return the radius at point x of the transition.
+        */
+       @Override
+       public double getRadius(double x) {
+               if (x<0 || x>length)
+                       return 0;
+               
+               double r1=getForeRadius();
+               double r2=getAftRadius();
+
+               if (r1 == r2)
+                       return r1;
+               
+               if (r1 > r2) {
+                       x = length-x;
+                       double tmp = r1;
+                       r1 = r2;
+                       r2 = tmp;
+               }
+               
+               if (isClipped()) {
+                       // Check clip calculation
+                       if (clipLength < 0)
+                               calculateClip(r1,r2);
+                       return type.getRadius(clipLength+x, r2, clipLength+length, shapeParameter);
+               } else {
+                       // Not clipped
+                       return r1 + type.getRadius(x, r2-r1, length, shapeParameter);
+               }
+       }
+
+       /**
+        * Numerically solve clipLength from the equation
+        *     r1 == type.getRadius(clipLength,r2,clipLength+length)
+        * using a binary search.  It assumes getRadius() to be monotonically increasing.
+        */
+       private void calculateClip(double r1, double r2) {
+               double min=0, max=length;
+               
+               if (r1 >= r2) {
+                       double tmp=r1;
+                       r1 = r2;
+                       r2 = tmp;
+               }
+               
+               if (r1==0) {
+                       clipLength = 0;
+                       return;
+               }
+               
+               if (length <= 0) {
+                       clipLength = 0;
+                       return;
+               }
+               
+               // Required:
+               //    getR(min,min+length,r2) - r1 < 0
+               //    getR(max,max+length,r2) - r1 > 0
+
+               int n=0;
+               while (type.getRadius(max, r2, max+length, shapeParameter) - r1 < 0) {
+                       min = max;
+                       max *= 2;
+                       n++;
+                       if (n>10)
+                               break;
+               }
+
+               while (true) {
+                       clipLength = (min+max)/2;
+                       if ((max-min)<CLIP_PRECISION)
+                               return;
+                       double val = type.getRadius(clipLength, r2, clipLength+length, shapeParameter);
+                       if (val-r1 > 0) {
+                               max = clipLength;
+                       } else {
+                               min = clipLength;
+                       }
+               }
+       }
+       
+       
+       @Override
+       public double getInnerRadius(double x) {
+               return Math.max(getRadius(x)-thickness,0);
+       }
+               
+       
+
+       @Override
+       public Collection<Coordinate> getComponentBounds() {
+               Collection<Coordinate> bounds = super.getComponentBounds();
+               if (foreShoulderLength > 0.001)
+                       addBound(bounds, -foreShoulderLength, foreShoulderRadius);
+               if (aftShoulderLength > 0.001)
+                       addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
+               return bounds;
+       }
+       
+       @Override
+       public double getComponentMass() {
+               double mass = super.getComponentMass();
+               if (getForeShoulderLength() > 0.001) {
+                       final double or = getForeShoulderRadius();
+                       final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+                       mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
+               }
+               if (isForeShoulderCapped()) {
+                       final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+                       mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
+               }
+               
+               if (getAftShoulderLength() > 0.001) {
+                       final double or = getAftShoulderRadius();
+                       final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+                       mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
+               }
+               if (isAftShoulderCapped()) {
+                       final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+                       mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
+               }
+               
+               return mass;
+       }
+
+       @Override
+       public Coordinate getComponentCG() {
+               Coordinate cg = super.getComponentCG();
+               if (getForeShoulderLength() > 0.001) {
+                       final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+                       cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
+                                       getMaterial().getDensity()));
+               }
+               if (isForeShoulderCapped()) {
+                       final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+                       cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(), 
+                                       getForeShoulderThickness()-getForeShoulderLength(),
+                                       getMaterial().getDensity()));
+               }
+               
+               if (getAftShoulderLength() > 0.001) {
+                       final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+                       cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(), 
+                                       getLength()+getAftShoulderLength(), getMaterial().getDensity()));
+               }
+               if (isAftShoulderCapped()) {
+                       final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+                       cg = cg.average(ringCG(ir, 0, 
+                                       getLength()+getAftShoulderLength()-getAftShoulderThickness(), 
+                                       getLength()+getAftShoulderLength(), getMaterial().getDensity()));
+               }
+               return cg;
+       }
+
+       
+       /*
+        * The moments of inertia are not explicitly corrected for the shoulders.
+        * However, since the mass is corrected, the inertia is automatically corrected
+        * to very nearly the correct value.
+        */
+
+
+
+       /**
+        * Returns the name of the component ("Transition").
+        */
+       @Override
+       public String getComponentName() {
+               return "Transition";
+       }
+       
+       @Override
+       protected void componentChanged(ComponentChangeEvent e) {
+               super.componentChanged(e);
+               clipLength = -1;
+       }
+       
+       
+       
+       /**
+        * An enumeration listing the possible shapes of transitions.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       public static enum Shape {
+
+               /**
+                * Conical shape.
+                */
+               CONICAL("Conical",
+                               "A conical nose cone has a profile of a triangle.",
+                               "A conical transition has straight sides.") {
+                       @Override
+                       public double getRadius(double x, double radius, double length, double param) {
+                               assert x >= 0;
+                               assert x <= length;
+                               assert radius >= 0;
+                               return radius*x/length;
+                       }
+               },
+
+               /**
+                * Ogive shape.  The shape parameter is the portion of an extended tangent ogive
+                * that will be used.  That is, for param==1 a tangent ogive will be produced, and
+                * for smaller values the shape straightens out into a cone at param==0.
+                */
+               OGIVE("Ogive",
+                               "An ogive nose cone has a profile that is a segment of a circle.  " +
+                               "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
+                               "a smooth transition to the body tube, values less than 1 produce "+
+                               "<b>secant ogives</b>.",
+                               "An ogive transition has a profile that is a segment of a circle.  " +
+                               "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
+                               "a smooth transition to the body tube at the aft end, values less than 1 " +
+                               "produce <b>secant ogives</b>.") {
+                       @Override
+                       public boolean usesParameter() {
+                               return true;   // Range 0...1 is default
+                       }
+                       @Override
+                       public double defaultParameter() {
+                               return 1.0;    // Tangent ogive by default
+                       }
+                       @Override
+                       public double getRadius(double x, double radius, double length, double param) {
+                               assert x >= 0;
+                               assert x <= length;
+                               assert radius >= 0;
+                               assert param >= 0;
+                               assert param <= 1;
+                               
+                               // Impossible to calculate ogive for length < radius, scale instead
+                               // TODO: LOW: secant ogive could be calculated lower
+                               if (length < radius) {
+                                       x = x * radius / length;
+                                       length = radius;
+                               }
+                               
+                               if (param < 0.001)
+                                       return CONICAL.getRadius(x, radius, length, param);
+                               
+                               // Radius of circle is:
+                               double R = sqrt((pow2(length)+pow2(radius)) *
+                                               (pow2((2-param)*length) + pow2(param*radius))/(4*pow2(param*radius)));
+                               double L = length/param;
+//                             double R = (radius + length*length/(radius*param*param))/2;
+                               double y0 = sqrt(R*R - L*L);
+                               return sqrt(R*R - (L-x)*(L-x)) - y0;
+                       }
+               },
+
+               /**
+                * Ellipsoidal shape.
+                */
+               ELLIPSOID("Ellipsoid",
+                               "An ellipsoidal nose cone has a profile of a half-ellipse "+
+                               "with major axes of lengths 2&times;<i>Length</i> and <i>Diameter</i>.",
+                               "An ellipsoidal transition has a profile of a half-ellipse "+
+                               "with major axes of lengths 2&times;<i>Length</i> and <i>Diameter</i>.  If the "+
+                               "transition is not clipped, then the profile is extended at the center by the "+
+                               "corresponding radius.",true) {
+                       @Override
+                       public double getRadius(double x, double radius, double length, double param) {
+                               assert x >= 0;
+                               assert x <= length;
+                               assert radius >= 0;
+                               x = x*radius/length;
+                               return sqrt(2*radius*x-x*x);  // radius/length * sphere
+                       }
+               },
+
+               POWER("Power series",
+                               "A power series nose cone has a profile of "+
+                               "<i>Radius</i>&nbsp;&times;&nbsp;(<i>x</i>&nbsp;/&nbsp;<i>Length</i>)" +
+                               "<sup><i>k</i></sup> "+
+                               "where <i>k</i> is the shape parameter.  For <i>k</i>=0.5 this is a "+
+                               "<b>½-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a "+
+                               "<b>¾-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
+                               "A power series transition has a profile of "+
+                               "<i>Radius</i>&nbsp;&times;&nbsp;(<i>x</i>&nbsp;/&nbsp;<i>Length</i>)" +
+                               "<sup><i>k</i></sup> "+
+                               "where <i>k</i> is the shape parameter.  For <i>k</i>=0.5 the transition is "+
+                               "<b>½-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a <b>¾-power</b>, and for " +
+                               "<i>k</i>=1 <b>conical</b>.",true) {
+                       @Override
+                       public boolean usesParameter() {  // Range 0...1
+                               return true;
+                       }
+                       @Override
+                       public double defaultParameter() {
+                               return 0.5;
+                       }
+                       @Override
+                       public double getRadius(double x, double radius, double length, double param) {
+                               assert x >= 0;
+                               assert x <= length;
+                               assert radius >= 0;
+                               assert param >= 0;
+                               assert param <= 1;
+                               if (param<=0.00001) {
+                                       if (x<=0.00001)
+                                               return 0;
+                                       else
+                                               return radius;
+                               }
+                               return radius*Math.pow(x/length, param);
+                       }
+                       
+               },
+               
+               PARABOLIC("Parabolic series",
+                               "A parabolic series nose cone has a profile of a parabola.  The shape "+
+                               "parameter defines the segment of the parabola to utilize.  The shape " +
+                               "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
+                               "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
+                               "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
+                               "A parabolic series transition has a profile of a parabola.  The shape "+
+                               "parameter defines the segment of the parabola to utilize.  The shape " +
+                               "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
+                               "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
+                               "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
+                       
+                       // In principle a parabolic transition is clippable, but the difference is
+                       // negligible.
+                       
+                       @Override
+                       public boolean usesParameter() {  // Range 0...1
+                               return true;
+                       }
+                       @Override
+                       public double defaultParameter() {
+                               return 1.0;
+                       }
+                       @Override
+                       public double getRadius(double x, double radius, double length, double param) {
+                               assert x >= 0;
+                               assert x <= length;
+                               assert radius >= 0;
+                               assert param >= 0;
+                               assert param <= 1;
+
+                               return radius * ((2*x/length - param*pow2(x/length))/(2-param));
+                       }
+               },
+               
+               
+               
+               HAACK("Haack series",
+                               "The Haack series nose cones are designed to minimize drag.  The shape parameter " +
+                               "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
+                               "drag for fixed length and diameter, while a value of 0.333 produces an " +
+                               "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
+                               "The Haack series <i>nose cones</i> are designed to minimize drag.  " +
+                               "These transition shapes are their equivalents, but do not necessarily produce " +
+                               "optimal drag for transitions.  " +
+                               "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
+                               "while a value of 0.333 produces an <b>LV-Haack</b> shape.",true) {
+                       @Override
+                       public boolean usesParameter() {
+                               return true;
+                       }
+                       @Override 
+                       public double maxParameter() {
+                               return 1.0/3.0;  // Range 0...1/3
+                       }
+                       @Override
+                       public double getRadius(double x, double radius, double length, double param) {
+                               assert x >= 0;
+                               assert x <= length;
+                               assert radius >= 0;
+                               assert param >= 0;
+                               assert param <= 2;
+
+                               double theta = Math.acos(1-2*x/length); 
+                               if (param==0) {
+                                       return radius*sqrt((theta-sin(2*theta)/2)/Math.PI);
+                               }
+                               return radius*sqrt((theta-sin(2*theta)/2+param*pow3(sin(theta)))/Math.PI);
+                       }
+               },
+               
+//             POLYNOMIAL("Smooth polynomial",
+//                             "A polynomial is fitted such that the nose cone profile is horizontal "+
+//                             "at the aft end of the transition.  The angle at the tip is defined by "+
+//                             "the shape parameter.",
+//                             "A polynomial is fitted such that the transition profile is horizontal "+
+//                             "at the aft end of the transition.  The angle at the fore end is defined "+
+//                             "by the shape parameter.") {
+//                     @Override
+//                     public boolean usesParameter() {
+//                             return true;
+//                     }
+//                     @Override
+//                     public double maxParameter() {
+//                             return 3.0;   //  Range 0...3
+//                     }
+//                     @Override
+//                     public double defaultParameter() {
+//                             return 0.0;
+//                     }
+//                     public double getRadius(double x, double radius, double length, double param) {
+//                             assert x >= 0;
+//                             assert x <= length;
+//                             assert radius >= 0;
+//                             assert param >= 0;
+//                             assert param <= 3;
+//                             // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
+//                             x = x/length;
+//                             return radius*((((param-2)*x + (3-2*param))*x + param)*x);
+//                     }
+//             }
+               ;
+                                               
+               // Privete fields of the shapes
+               private final String name;
+               private final String transitionDesc;
+               private final String noseconeDesc;
+               private final boolean canClip;
+
+               // Non-clippable constructor
+               Shape(String name, String noseconeDesc, String transitionDesc) {
+                       this(name,noseconeDesc,transitionDesc,false);
+               }
+
+               // Clippable constructor
+               Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
+                       this.name = name;
+                       this.canClip = canClip;
+                       this.noseconeDesc = noseconeDesc;
+                       this.transitionDesc = transitionDesc;
+               }
+               
+               
+               /**
+                * Return the name of the transition shape name.
+                */
+               public String getName() {
+                       return name;
+               }
+               
+               /**
+                * Get a description of the Transition shape.
+                */
+               public String getTransitionDescription() {
+                       return transitionDesc;
+               }
+               
+               /**
+                * Get a description of the NoseCone shape.
+                */
+               public String getNoseConeDescription() {
+                       return noseconeDesc;
+               }
+
+               /**
+                * Check whether the shape differs in clipped mode.  The clipping should be
+                * enabled by default if possible.
+                */
+               public boolean isClippable() {
+                       return canClip;
+               }
+               
+               /**
+                * Return whether the shape uses the shape parameter.  (Default false.)
+                */
+               public boolean usesParameter() {
+                       return false;
+               }
+
+               /**
+                * Return the minimum value of the shape parameter.  (Default 0.)
+                */
+               public double minParameter() {
+                       return 0.0;
+               }
+               
+               /**
+                * Return the maximum value of the shape parameter.  (Default 1.)
+                */
+               public double maxParameter() {
+                       return 1.0;
+               }
+               
+               /**
+                * Return the default value of the shape parameter.  (Default 0.)
+                */
+               public double defaultParameter() {
+                       return 0.0;
+               }
+
+               /**
+                * Calculate the basic radius of a transition with the given radius, length and
+                * shape parameter at the point x from the tip of the component.  It is assumed
+                * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
+                * Boattails are achieved by reversing the component.
+                * 
+                * @param x      Position from the tip of the component.
+                * @param radius Aft end radius >= 0.
+                * @param length Length of the transition >= 0.
+                * @param param  Valid shape parameter.
+                * @return       The basic radius at the given position.
+                */
+               public abstract double getRadius(double x, double radius, double length, double param);
+
+               
+               /**
+                * Returns the name of the shape (same as getName()).
+                */
+               @Override
+               public String toString() {
+                       return name;
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java b/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java
new file mode 100644 (file)
index 0000000..c92c65e
--- /dev/null
@@ -0,0 +1,169 @@
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+
+/**
+ * A set of trapezoidal fins.  The root and tip chords are perpendicular to the rocket
+ * base line, while the leading and aft edges may be slanted.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class TrapezoidFinSet extends FinSet {
+       public static final double MAX_SWEEP_ANGLE=(89*Math.PI/180.0);
+
+       /*
+        *           sweep   tipChord
+        *           |    |___________
+        *           |   /            |
+        *           |  /             | 
+        *           | /              |  height
+        *            /               |
+        * __________/________________|_____________
+        *                length
+        *              == rootChord
+        */
+       
+       // rootChord == length
+       private double tipChord = 0;
+       private double height = 0;
+       private double sweep = 0;
+       
+       
+       public TrapezoidFinSet() {
+               this (3, 0.05, 0.05, 0.025, 0.05);
+       }
+       
+       // TODO: HIGH:  height=0 -> CP = NaN
+       public TrapezoidFinSet(int fins, double rootChord, double tipChord, double sweep, 
+                       double height) {
+               super();
+               
+               this.setFinCount(fins);
+               this.length = rootChord;
+               this.tipChord = tipChord;
+               this.sweep = sweep;
+               this.height = height;
+       }
+       
+       
+       public void setFinShape(double rootChord, double tipChord, double sweep, double height,
+                       double thickness) {
+               if (this.length==rootChord && this.tipChord==tipChord && this.sweep==sweep &&
+                               this.height==height && this.thickness==thickness)
+                       return;
+               this.length=rootChord;
+               this.tipChord=tipChord;
+               this.sweep=sweep;
+               this.height=height;
+               this.thickness=thickness;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       public double getRootChord() {
+               return length;
+       }
+       public void setRootChord(double r) {
+               if (length == r)
+                       return;
+               length = Math.max(r,0);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       public double getTipChord() {
+               return tipChord;
+       }
+       public void setTipChord(double r) {
+               if (tipChord == r)
+                       return;
+               tipChord = Math.max(r,0);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       /**
+        * Get the sweep length.
+        */
+       public double getSweep() {
+               return sweep;
+       }
+       /**
+        * Set the sweep length.
+        */
+       public void setSweep(double r) {
+               if (sweep == r)
+                       return;
+               sweep = r;
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+       
+       /**
+        * Get the sweep angle.  This is calculated from the true sweep and height, and is not
+        * stored separetely.
+        */
+       public double getSweepAngle() {
+               if (height == 0) {
+                       if (sweep > 0)
+                               return Math.PI/2;
+                       if (sweep < 0)
+                               return -Math.PI/2;
+                       return 0;
+               }
+               return Math.atan(sweep/height);
+       }
+       /**
+        * Sets the sweep by the sweep angle.  The sweep is calculated and set by this method,
+        * and the angle itself is not stored.
+        */
+       public void setSweepAngle(double r) {
+               if (r > MAX_SWEEP_ANGLE)
+                       r = MAX_SWEEP_ANGLE;
+               if (r < -MAX_SWEEP_ANGLE)
+                       r = -MAX_SWEEP_ANGLE;
+               double sweep = Math.tan(r) * height;
+               if (Double.isNaN(sweep) || Double.isInfinite(sweep))
+                       return;
+               setSweep(sweep);
+       }
+
+       public double getHeight() {
+               return height;
+       }
+       public void setHeight(double r) {
+               if (height == r)
+                       return;
+               height = Math.max(r,0);
+               fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+       }
+
+       
+
+       /**
+        * Returns the geometry of a trapezoidal fin.
+        */
+       @Override
+       public Coordinate[] getFinPoints() {
+               Coordinate[] c = new Coordinate[4];
+               
+               c[0] = Coordinate.NUL;
+               c[1] = new Coordinate(sweep,height);
+               c[2] = new Coordinate(sweep+tipChord,height);
+               c[3] = new Coordinate(length,0);
+               
+               return c;
+       }
+       
+       /**
+        * Returns the span of a trapezoidal fin.
+        */
+       @Override
+       public double getSpan() {
+               return height;
+       }
+       
+
+       @Override
+       public String getComponentName() {
+               return "Trapezoidal fin set";
+       }
+
+}
diff --git a/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java b/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java
new file mode 100644 (file)
index 0000000..bb19921
--- /dev/null
@@ -0,0 +1,30 @@
+package net.sf.openrocket.rocketcomponent;
+
+
+public class TubeCoupler extends ThicknessRingComponent {
+
+       public TubeCoupler() {
+               setOuterRadiusAutomatic(true);
+               setThickness(0.002);
+               setLength(0.06);
+       }
+       
+       
+       // Make setter visible
+       @Override
+       public void setOuterRadiusAutomatic(boolean auto) {
+               super.setOuterRadiusAutomatic(auto);
+       }
+
+       
+       @Override
+       public String getComponentName() {
+               return "Tube coupler";
+       }
+
+       @Override
+       public boolean isCompatible(Class<? extends RocketComponent> type) {
+               return false;
+       }
+}
+
diff --git a/src/net/sf/openrocket/simulation/EulerSimulator.java b/src/net/sf/openrocket/simulation/EulerSimulator.java
new file mode 100644 (file)
index 0000000..3f62137
--- /dev/null
@@ -0,0 +1,373 @@
+package net.sf.openrocket.simulation;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.GravityModel;
+import net.sf.openrocket.aerodynamics.WindSimulator;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Quaternion;
+
+
+
+
+/**
+ * A flight simulator based on Euler integration.  This class is out of date and
+ * deprecated in favor of the Runge-Kutta simulator RK4Simulator.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+@Deprecated
+public class EulerSimulator extends FlightSimulator {
+       /**
+        * Maximum roll step allowed.  This is selected as an uneven division of the full
+        * circle so that the simulation will sample the most wind directions
+        */
+       private static final double MAX_ROLL_STEP_ANGLE = 28.32 * Math.PI/180;
+       
+       private static final boolean DEBUG = false;
+       
+       
+       private FlightConditions flightConditions = null;
+       private double currentStep;
+       private double lateralPitchRate = 0;  // set by calculateFlightConditions 
+       
+       public EulerSimulator(AerodynamicCalculator calculator) {
+               super(calculator);
+       }
+
+       
+
+       protected FlightDataBranch newFlightData(String name) {
+               return new FlightDataBranch(name, FlightDataBranch.TYPE_TIME);  // TODO: ???
+       }
+
+       
+
+       @Override
+       protected RK4SimulationStatus initializeSimulation(Configuration configuration, 
+                       SimulationConditions simulation) {
+
+               RK4SimulationStatus status = new RK4SimulationStatus();
+               
+               status.startConditions = simulation;
+               
+               status.configuration = configuration;
+               status.flightData = newFlightData("Main");
+               status.launchRod = true;
+               status.time = 0.0;
+               
+               
+               status.launchRodDirection = new Coordinate(
+                               Math.sin(simulation.getLaunchRodAngle()) * 
+                               Math.cos(simulation.getLaunchRodDirection()),
+                               Math.sin(simulation.getLaunchRodAngle()) *
+                               Math.sin(simulation.getLaunchRodDirection()),
+                               Math.cos(simulation.getLaunchRodAngle())
+               );
+               status.launchRodLength = simulation.getLaunchRodLength();
+               // TODO: take into account launch lug positions
+               
+               
+               Quaternion o = new Quaternion();
+               o.multiplyLeft(Quaternion.rotation(
+                               new Coordinate(0, simulation.getLaunchRodAngle(), 0)));
+               o.multiplyLeft(Quaternion.rotation(
+                               new Coordinate(0, 0, simulation.getLaunchRodDirection())));
+               status.orientation = o;
+               status.position = Coordinate.NUL;
+               status.velocity = Coordinate.NUL;
+               status.rotation = Coordinate.NUL;
+               
+               status.windSimulator = new WindSimulator();
+               status.windSimulator.setAverage(simulation.getWindSpeedAverage());
+               status.windSimulator.setStandardDeviation(simulation.getWindSpeedDeviation());
+//             status.windSimulator.reset();
+               
+               currentStep = simulation.getTimeStep();
+               
+               status.gravityModel = new GravityModel(simulation.getLaunchLatitude());
+
+               flightConditions = null;
+               
+               return status;
+       }
+       
+       
+
+       @Override
+       protected Collection<FlightEvent> step(SimulationConditions simulation, 
+                       SimulationStatus simulationStatus) {
+               
+               RK4SimulationStatus status = (RK4SimulationStatus)simulationStatus;
+               FlightDataBranch data = status.flightData;
+               
+               
+               // Add data point and values
+               data.addPoint();
+               data.setValue(FlightDataBranch.TYPE_TIME, status.time);
+
+               if (DEBUG)
+                       System.out.println("original direction: "+status.orientation.rotateZ());
+               
+               // Calculate current flight conditions
+               if (flightConditions == null) {
+                       flightConditions = new FlightConditions(status.configuration);
+               }
+               calculateFlightConditions(flightConditions, status);
+
+               if (DEBUG)
+                       System.out.println("flightConditions="+flightConditions);
+               
+               
+               // Calculate time step
+               double timestep;
+               
+//             double normalStep = simulation.getTimeStep();
+//             double lateralStep = simulation.getMaximumStepAngle() / lateralPitchRate;
+//             double rotationStep = Math.abs(MAX_ROLL_STEP_ANGLE / flightConditions.getRollRate());
+//             if (lateralStep < normalStep || rotationStep < normalStep) {
+//                     System.out.printf("   t=%.3f STEP normal %.3f lateral %.3f rotation %.3f\n",
+//                                     status.time, normalStep, lateralStep, rotationStep);
+//             }
+
+               
+               timestep = MathUtil.min(simulation.getTimeStep(), 
+                               simulation.getMaximumStepAngle() / lateralPitchRate,
+                               Math.abs(MAX_ROLL_STEP_ANGLE / flightConditions.getRollRate()));
+               if (timestep < 0.0001)
+                       timestep = 0.0001;
+               
+               
+               
+               // Calculate aerodynamic forces  (only axial if still on launch rod)
+               AerodynamicForces forces;
+               calculator.setConfiguration(status.configuration);
+
+               if (status.launchRod) {
+                       forces = calculator.getAxialForces(status.time, flightConditions, status.warnings);
+               } else {
+                       forces = calculator.getAerodynamicForces(status.time,
+                                       flightConditions, status.warnings);
+               }
+
+               
+               // Check in case of NaN
+               assert(!Double.isNaN(forces.CD));
+               assert(!Double.isNaN(forces.CN));
+               assert(!Double.isNaN(forces.Caxial));
+               assert(!Double.isNaN(forces.Cm));
+               assert(!Double.isNaN(forces.Cyaw));
+               assert(!Double.isNaN(forces.Cside));
+               assert(!Double.isNaN(forces.Croll));
+               
+
+               //// Calculate forces and accelerations
+               
+               double dynP = (0.5 * flightConditions.getAtmosphericConditions().getDensity() *
+                               MathUtil.pow2(flightConditions.getVelocity()));
+               double refArea = flightConditions.getRefArea();
+               double refLength = flightConditions.getRefLength();
+               
+               
+               // Linear forces
+               double thrust = calculateThrust(status, currentStep);
+               double dragForce = forces.Caxial * dynP * refArea;
+               double fN = forces.CN * dynP * refArea;
+               double fSide = forces.Cside * dynP * refArea;
+               
+               double sin = Math.sin(flightConditions.getTheta());
+               double cos = Math.cos(flightConditions.getTheta());
+               
+               double forceX = - fN * cos - fSide * sin;
+               double forceY = - fN * sin - fSide * cos;
+               double forceZ = thrust - dragForce;
+
+               
+               Coordinate acceleration = new Coordinate(forceX / forces.cg.weight,
+                               forceY / forces.cg.weight, forceZ / forces.cg.weight);
+               
+               if (DEBUG)
+                       System.out.println("   acceleration before rotation: "+acceleration);
+               acceleration = status.orientation.rotate(acceleration);
+               if (DEBUG)
+                       System.out.println("   acceleration after  rotation: "+acceleration);
+
+               acceleration = acceleration.sub(0, 0, status.gravityModel.getGravity());
+               
+               
+               // Convert momenta
+               double Cm = forces.Cm - forces.CN * forces.cg.x / refLength;
+               double momX = (-Cm * sin - forces.Cyaw * cos) * dynP * refArea * refLength;
+               double momY = ( Cm * cos - forces.Cyaw * sin) * dynP * refArea * refLength;
+               double momZ = forces.Croll * dynP * refArea * refLength;
+               
+               assert(!Double.isNaN(momX));
+               assert(!Double.isNaN(momY));
+               assert(!Double.isNaN(momZ));
+               assert(!Double.isNaN(forces.longitudalInertia));
+               assert(!Double.isNaN(forces.rotationalInertia));
+               
+               Coordinate rotAcc = new Coordinate(momX / forces.longitudalInertia,
+                               momY / forces.longitudalInertia, momZ / forces.rotationalInertia);
+               rotAcc = status.orientation.rotate(rotAcc);
+               
+//             System.out.println("   rotAcc="+rotAcc);
+//             System.out.println("   momY="+momY+" lI="+forces.longitudalInertia
+//                             +" Cm="+Cm+" forces.Cm="+forces.Cm+" cg="+forces.cg);
+//             System.out.println("   orient before update:"+status.orientation);
+               
+
+               
+               // Perform position integration
+               Coordinate avgVel = status.velocity.add(acceleration.multiply(currentStep/2));
+               status.velocity = status.velocity.add(acceleration.multiply(currentStep));
+               
+               if (status.launchRod) {
+                       // Project velocity onto launch rod direction
+                       status.velocity = status.launchRodDirection.multiply(
+                                       status.velocity.dot(status.launchRodDirection));
+                       avgVel = status.launchRodDirection.multiply(avgVel.dot(status.launchRodDirection));
+               }
+               
+               status.position = status.position.add(avgVel.multiply(currentStep));
+
+               if (status.launchRod) {
+                       // Avoid sinking into ground when on the launch rod 
+                       if (status.position.z < 0) {
+//                             System.out.println("Corrected sinking from pos:"+status.position+
+//                                             " vel:"+status.velocity);
+                               status.position = Coordinate.NUL;
+                               status.velocity = Coordinate.NUL;
+                       }
+               }
+
+               
+               if (!status.launchRod) {
+                       // Integrate rotation when off launch rod
+                       Coordinate avgRot = status.rotation.add(rotAcc.multiply(currentStep/2));
+                       status.rotation = status.rotation.add(rotAcc.multiply(currentStep));
+                       Quaternion stepRotation = Quaternion.rotation(avgRot.multiply(currentStep));
+                       status.orientation.multiplyLeft(stepRotation);
+
+                       status.orientation.normalize();
+                       
+                       if (DEBUG)
+                               System.out.println("   step rotation "+ 
+                                               (avgRot.length()*currentStep*180/Math.PI) +"\u00b0 " +
+                                               "step="+currentStep+" after: "+status.orientation.rotateZ());
+               }
+
+               status.time += currentStep;
+               
+               
+               
+               // Check rotation angle step length and correct time step if necessary
+               double rot = status.rotation.length() * currentStep;
+               if (rot > simulation.getMaximumStepAngle()) {
+                       currentStep /= 2;
+                       if (DEBUG)
+                               System.out.println("  *** Step division to: "+currentStep);
+               }
+               if ((rot < simulation.getMaximumStepAngle()/3) &&
+                               (currentStep < simulation.getTimeStep() - 0.000001)) {
+                       currentStep *= 2;
+                       if (DEBUG)
+                               System.out.println("  *** Step multiplication to: "+currentStep);
+               }
+               
+               
+               
+               // Store values
+               data.setValue(FlightDataBranch.TYPE_ACCELERATION_Z, acceleration.z);
+               data.setValue(FlightDataBranch.TYPE_ACCELERATION_TOTAL, acceleration.length());
+               data.setValue(FlightDataBranch.TYPE_VELOCITY_TOTAL, status.velocity.length());
+               
+               data.setValue(FlightDataBranch.TYPE_AXIAL_DRAG_COEFF, forces.CD);
+               data.setValue(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, forces.frictionCD);
+               data.setValue(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, forces.pressureCD);
+               data.setValue(FlightDataBranch.TYPE_BASE_DRAG_COEFF, forces.baseCD);
+               
+               data.setValue(FlightDataBranch.TYPE_CP_LOCATION, forces.cp.x);
+               data.setValue(FlightDataBranch.TYPE_CG_LOCATION, forces.cg.x);
+               
+               data.setValue(FlightDataBranch.TYPE_MASS, forces.cg.weight);
+               
+               data.setValue(FlightDataBranch.TYPE_THRUST_FORCE, thrust);
+               data.setValue(FlightDataBranch.TYPE_DRAG_FORCE, dragForce);
+
+               
+               Coordinate c = status.orientation.rotateZ();
+               double theta = Math.atan2(c.z, MathUtil.hypot(c.x, c.y));
+               double phi = Math.atan2(c.y, c.x);
+               data.setValue(FlightDataBranch.TYPE_ORIENTATION_THETA, theta);
+               data.setValue(FlightDataBranch.TYPE_ORIENTATION_PHI, phi);
+               
+               return null;
+       }
+
+       
+       
+       /*
+        * TODO: MEDIUM:  Many parameters are stored one time step late, how to fix?
+        */
+       private void calculateFlightConditions(FlightConditions flightConditions,
+                       RK4SimulationStatus status) {
+
+               // Atmospheric conditions
+               AtmosphericConditions cond = status.startConditions.getAtmosphericModel().
+                       getConditions(status.position.z + status.startConditions.getLaunchAltitude());
+               flightConditions.setAtmosphericConditions(cond);
+               status.flightData.setValue(FlightDataBranch.TYPE_AIR_TEMPERATURE, cond.temperature);
+               status.flightData.setValue(FlightDataBranch.TYPE_AIR_PRESSURE, cond.pressure);
+               status.flightData.setValue(FlightDataBranch.TYPE_SPEED_OF_SOUND, cond.getMachSpeed());
+
+               
+               // Local wind speed and direction
+               double wind = status.windSimulator.getWindSpeed(status.time);
+               status.flightData.setValue(FlightDataBranch.TYPE_WIND_VELOCITY, wind);
+               
+               Coordinate windSpeed = status.velocity.sub(wind, 0, 0);
+               windSpeed = status.orientation.invRotate(windSpeed);
+               
+               double theta = Math.atan2(windSpeed.y, windSpeed.x);
+               double velocity = windSpeed.length();
+               double aoa = Math.acos(windSpeed.z / velocity);
+
+               if (Double.isNaN(theta) || Double.isInfinite(theta))
+                       theta = 0;
+               if (Double.isNaN(velocity) || Double.isInfinite(velocity))
+                       velocity = 0;
+               if (Double.isNaN(aoa) || Double.isInfinite(aoa))
+                       aoa = 0;
+               
+               flightConditions.setTheta(theta);
+               flightConditions.setAOA(aoa);
+               flightConditions.setVelocity(velocity);
+               
+               status.flightData.setValue(FlightDataBranch.TYPE_AOA, aoa);
+               
+               // Roll, pitch and yaw rate
+               Coordinate rot = status.orientation.invRotate(status.rotation);
+               flightConditions.setRollRate(rot.z);
+               status.flightData.setValue(FlightDataBranch.TYPE_ROLL_RATE, rot.z);
+               double len = MathUtil.hypot(windSpeed.x, windSpeed.y);
+               if (len < 0.001) {
+                       flightConditions.setPitchRate(0);
+                       flightConditions.setYawRate(0);
+                       lateralPitchRate = 0;
+               } else {
+                       double sinTheta = windSpeed.x / len;
+                       double cosTheta = windSpeed.y / len;
+                       flightConditions.setPitchRate(cosTheta*rot.x + sinTheta*rot.y);
+                       flightConditions.setYawRate(sinTheta*rot.x + cosTheta*rot.y);
+                       lateralPitchRate = MathUtil.hypot(rot.x, rot.y);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/FlightData.java b/src/net/sf/openrocket/simulation/FlightData.java
new file mode 100644 (file)
index 0000000..8d0307c
--- /dev/null
@@ -0,0 +1,206 @@
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class FlightData {
+       
+       /**
+        * An immutable FlightData object with NaN data.
+        */
+       public static final FlightData NaN_DATA;
+       static {
+               FlightData data = new FlightData();
+               data.immute();
+               NaN_DATA = data;
+       }
+
+       private boolean mutable = true;
+       private final ArrayList<FlightDataBranch> branches = new ArrayList<FlightDataBranch>();
+       
+       private final WarningSet warnings = new WarningSet();
+       
+       private double maxAltitude = Double.NaN;
+       private double maxVelocity = Double.NaN;
+       private double maxAcceleration = Double.NaN;
+       private double maxMachNumber = Double.NaN;
+       private double timeToApogee = Double.NaN;
+       private double flightTime = Double.NaN;
+       private double groundHitVelocity = Double.NaN;
+
+       
+       /**
+        * Create a FlightData object with no content.  The resulting object is mutable.
+        */
+       public FlightData() {
+               
+       }
+       
+       
+       /**
+        * Construct an immutable FlightData object with no data branches but the specified
+        * summary information.
+        * 
+        * @param maxAltitude                   maximum altitude.
+        * @param maxVelocity                   maximum velocity.
+        * @param maxAcceleration               maximum acceleration.
+        * @param maxMachNumber                 maximum Mach number.
+        * @param timeToApogee                  time to apogee.
+        * @param flightTime                    total flight time.
+        * @param groundHitVelocity             ground hit velocity.
+        */
+       public FlightData(double maxAltitude, double maxVelocity, double maxAcceleration,
+                       double maxMachNumber, double timeToApogee, double flightTime,
+                       double groundHitVelocity) {
+               this.maxAltitude = maxAltitude;
+               this.maxVelocity = maxVelocity;
+               this.maxAcceleration = maxAcceleration;
+               this.maxMachNumber = maxMachNumber;
+               this.timeToApogee = timeToApogee;
+               this.flightTime = flightTime;
+               this.groundHitVelocity = groundHitVelocity;
+               
+               this.immute();
+       }
+
+
+       /**
+        * Create an immutable FlightData object with the specified branches.
+        * 
+        * @param branches      the branches.
+        */
+       public FlightData(FlightDataBranch ... branches) {
+               this();
+               
+               for (FlightDataBranch b: branches)
+                       this.addBranch(b);
+               
+               calculateIntrestingValues();
+               this.immute();
+       }
+       
+       
+       
+       
+       /**
+        * Returns the warning set associated with this object.  This WarningSet cannot be
+        * set, so simulations must use this warning set to store their warnings.
+        * The returned WarningSet should not be modified otherwise.
+        * 
+        * @return      the warnings generated during this simulation.
+        */
+       public WarningSet getWarningSet() {
+               return warnings;
+       }
+       
+       
+       public void addBranch(FlightDataBranch branch) {
+               if (!mutable)
+                       throw new IllegalStateException("FlightData has been made immutable");
+
+               branch.immute();
+               branches.add(branch);
+               
+               if (branches.size() == 1) {
+                       calculateIntrestingValues();
+               }
+       }
+
+       public int getBranchCount() {
+               return branches.size();
+       }
+       
+       public FlightDataBranch getBranch(int n) {
+               return branches.get(n);
+       }
+       
+       
+       
+       public double getMaxAltitude() {
+               return maxAltitude;
+       }
+       
+       public double getMaxVelocity() {
+               return maxVelocity;
+       }
+       
+       public double getMaxAcceleration() {
+               return maxAcceleration;
+       }
+       
+       public double getMaxMachNumber() {
+               return maxMachNumber;
+       }
+       
+       public double getTimeToApogee() {
+               return timeToApogee;
+       }
+       
+       public double getFlightTime() {
+               return flightTime;
+       }
+       
+       public double getGroundHitVelocity() {
+               return groundHitVelocity;
+       }
+       
+       
+       
+       /**
+        * Calculate the max. altitude/velocity/acceleration, time to apogee, flight time
+        * and ground hit velocity.
+        */
+       private void calculateIntrestingValues() {
+               if (branches.isEmpty())
+                       return;
+               
+               FlightDataBranch branch = branches.get(0);
+               maxAltitude = branch.getMaximum(FlightDataBranch.TYPE_ALTITUDE);
+               maxVelocity = branch.getMaximum(FlightDataBranch.TYPE_VELOCITY_TOTAL);
+               maxAcceleration = branch.getMaximum(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
+               maxMachNumber = branch.getMaximum(FlightDataBranch.TYPE_MACH_NUMBER);
+
+               flightTime = branch.getLast(FlightDataBranch.TYPE_TIME);
+               if (branch.getLast(FlightDataBranch.TYPE_ALTITUDE) < 10) {
+                       groundHitVelocity = branch.getLast(FlightDataBranch.TYPE_VELOCITY_TOTAL);
+               } else {
+                       groundHitVelocity = Double.NaN;
+               }
+               
+               // Time to apogee
+               List<Double> time = branch.get(FlightDataBranch.TYPE_TIME);
+               List<Double> altitude = branch.get(FlightDataBranch.TYPE_ALTITUDE);
+               
+               if (time == null || altitude == null) {
+                       timeToApogee = Double.NaN;
+                       return;
+               }
+               int index = 0;
+               for (Double alt: altitude) {
+                       if (alt != null) {
+                               if (MathUtil.equals(alt, maxAltitude))
+                                       break;
+                       }
+
+                       index++;
+               }
+               if (index < time.size())
+                       timeToApogee = time.get(index);
+               else
+                       timeToApogee = Double.NaN;
+       }
+
+
+       public void immute() {
+               mutable = false;
+       }
+       public boolean isMutable() {
+               return mutable;
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/simulation/FlightDataBranch.java b/src/net/sf/openrocket/simulation/FlightDataBranch.java
new file mode 100644 (file)
index 0000000..c80c268
--- /dev/null
@@ -0,0 +1,429 @@
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Pair;
+
+
+public class FlightDataBranch {
+       
+       //// Time
+       public static final Type TYPE_TIME = 
+               new Type("Time", UnitGroup.UNITS_FLIGHT_TIME, 1);
+       
+       
+       //// Vertical position and motion
+       public static final Type TYPE_ALTITUDE = 
+               new Type("Altitude", UnitGroup.UNITS_DISTANCE, 10);
+       public static final Type TYPE_VELOCITY_Z = 
+               new Type("Vertical velocity", UnitGroup.UNITS_VELOCITY, 11);
+       public static final Type TYPE_ACCELERATION_Z = 
+               new Type("Vertical acceleration", UnitGroup.UNITS_ACCELERATION, 12);
+
+       
+       //// Total motion
+       public static final Type TYPE_VELOCITY_TOTAL =
+               new Type("Total velocity", UnitGroup.UNITS_VELOCITY, 20);
+       public static final Type TYPE_ACCELERATION_TOTAL =
+               new Type("Total acceleration", UnitGroup.UNITS_ACCELERATION, 21);
+
+       
+       //// Lateral position and motion
+       
+       public static final Type TYPE_POSITION_X =
+               new Type("Position upwind", UnitGroup.UNITS_DISTANCE, 30);
+       public static final Type TYPE_POSITION_Y =
+               new Type("Position parallel to wind", UnitGroup.UNITS_DISTANCE, 31);
+       public static final Type TYPE_POSITION_XY =
+               new Type("Lateral distance", UnitGroup.UNITS_DISTANCE, 32);
+       public static final Type TYPE_POSITION_DIRECTION =
+               new Type("Lateral direction", UnitGroup.UNITS_ANGLE, 33);
+
+       public static final Type TYPE_VELOCITY_XY =
+               new Type("Lateral velocity", UnitGroup.UNITS_VELOCITY, 34);
+       public static final Type TYPE_ACCELERATION_XY =
+               new Type("Lateral acceleration", UnitGroup.UNITS_ACCELERATION, 35);
+
+       
+       //// Angular motion
+       public static final Type TYPE_AOA = new Type("Angle of attack", UnitGroup.UNITS_ANGLE, 40);
+       public static final Type TYPE_ROLL_RATE = new Type("Roll rate", UnitGroup.UNITS_ROLL, 41);
+       public static final Type TYPE_PITCH_RATE = new Type("Pitch rate", UnitGroup.UNITS_ROLL, 42);
+       public static final Type TYPE_YAW_RATE = new Type("Yaw rate", UnitGroup.UNITS_ROLL, 43);
+
+       
+       //// Stability information
+       public static final Type TYPE_MASS = 
+               new Type("Mass", UnitGroup.UNITS_MASS, 50);
+       public static final Type TYPE_CP_LOCATION = 
+               new Type("CP location", UnitGroup.UNITS_LENGTH, 51);
+       public static final Type TYPE_CG_LOCATION = 
+               new Type("CG location", UnitGroup.UNITS_LENGTH, 52);
+       public static final Type TYPE_STABILITY = 
+               new Type("Stability margin calibers", UnitGroup.UNITS_COEFFICIENT, 53);
+
+       
+       //// Characteristic numbers
+       public static final Type TYPE_MACH_NUMBER =
+               new Type("Mach number", UnitGroup.UNITS_COEFFICIENT, 60);
+       public static final Type TYPE_REYNOLDS_NUMBER =
+               new Type("Reynolds number", UnitGroup.UNITS_COEFFICIENT, 61);
+       
+
+       //// Thrust and drag
+       public static final Type TYPE_THRUST_FORCE = 
+               new Type("Thrust", UnitGroup.UNITS_FORCE, 70);
+       public static final Type TYPE_DRAG_FORCE = 
+               new Type("Drag force", UnitGroup.UNITS_FORCE, 71);
+
+       public static final Type TYPE_DRAG_COEFF = 
+               new Type("Drag coefficient", UnitGroup.UNITS_COEFFICIENT, 72);
+       public static final Type TYPE_AXIAL_DRAG_COEFF = 
+               new Type("Axial drag coefficient", UnitGroup.UNITS_COEFFICIENT, 73);
+
+       
+       ////  Component drag coefficients
+       public static final Type TYPE_FRICTION_DRAG_COEFF = 
+               new Type("Friction drag coefficient", UnitGroup.UNITS_COEFFICIENT, 80);
+       public static final Type TYPE_PRESSURE_DRAG_COEFF = 
+               new Type("Pressure drag coefficient", UnitGroup.UNITS_COEFFICIENT, 81);
+       public static final Type TYPE_BASE_DRAG_COEFF = 
+               new Type("Base drag coefficient", UnitGroup.UNITS_COEFFICIENT, 82);
+
+
+       ////  Other coefficients
+       public static final Type TYPE_NORMAL_FORCE_COEFF =
+               new Type("Normal force coefficient", UnitGroup.UNITS_COEFFICIENT, 90);
+       public static final Type TYPE_PITCH_MOMENT_COEFF =
+               new Type("Pitch moment coefficient", UnitGroup.UNITS_COEFFICIENT, 91);
+       public static final Type TYPE_YAW_MOMENT_COEFF =
+               new Type("Yaw moment coefficient", UnitGroup.UNITS_COEFFICIENT, 92);
+       public static final Type TYPE_SIDE_FORCE_COEFF =
+               new Type("Side force coefficient", UnitGroup.UNITS_COEFFICIENT, 93);
+       public static final Type TYPE_ROLL_MOMENT_COEFF =
+               new Type("Roll moment coefficient", UnitGroup.UNITS_COEFFICIENT, 94);
+       public static final Type TYPE_ROLL_FORCING_COEFF =
+               new Type("Roll forcing coefficient", UnitGroup.UNITS_COEFFICIENT, 95);
+       public static final Type TYPE_ROLL_DAMPING_COEFF =
+               new Type("Roll damping coefficient", UnitGroup.UNITS_COEFFICIENT, 96);
+       
+       public static final Type TYPE_PITCH_DAMPING_MOMENT_COEFF =
+               new Type("Pitch damping coefficient", UnitGroup.UNITS_COEFFICIENT, 97);
+       public static final Type TYPE_YAW_DAMPING_MOMENT_COEFF =
+               new Type("Yaw damping coefficient", UnitGroup.UNITS_COEFFICIENT, 98);
+       
+       
+       ////  Reference length + area
+       public static final Type TYPE_REFERENCE_LENGTH = 
+               new Type("Reference length", UnitGroup.UNITS_LENGTH, 100);
+       public static final Type TYPE_REFERENCE_AREA = 
+               new Type("Reference area", UnitGroup.UNITS_AREA, 101);
+       
+
+       ////  Orientation
+       public static final Type TYPE_ORIENTATION_THETA = 
+               new Type("Vertical orientation (zenith)", UnitGroup.UNITS_ANGLE, 106);
+       public static final Type TYPE_ORIENTATION_PHI =
+               new Type("Lateral orientation (azimuth)", UnitGroup.UNITS_ANGLE, 107);
+       
+       
+       ////  Atmospheric conditions
+       public static final Type TYPE_WIND_VELOCITY = new Type("Wind velocity", 
+                       UnitGroup.UNITS_VELOCITY, 110);
+       public static final Type TYPE_AIR_TEMPERATURE = new Type("Air temperature",
+                       UnitGroup.UNITS_TEMPERATURE, 111);
+       public static final Type TYPE_AIR_PRESSURE = new Type("Air pressure",
+                       UnitGroup.UNITS_PRESSURE, 112);
+       public static final Type TYPE_SPEED_OF_SOUND = new Type("Speed of sound",
+                       UnitGroup.UNITS_VELOCITY, 113);
+
+
+       ////  Simulation information
+       public static final Type TYPE_TIME_STEP = new Type("Simulation time step",
+                       UnitGroup.UNITS_TIME_STEP, 200);
+       public static final Type TYPE_COMPUTATION_TIME = new Type("Computation time",
+                       UnitGroup.UNITS_SHORT_TIME, 201);
+
+       
+       /**
+        * Array of known data types for String -> Type conversion.
+        */
+       private static final Type[] TYPES = {
+               TYPE_TIME, 
+               TYPE_ALTITUDE, TYPE_VELOCITY_Z, TYPE_ACCELERATION_Z,
+               TYPE_VELOCITY_TOTAL, TYPE_ACCELERATION_TOTAL,
+               TYPE_POSITION_X, TYPE_POSITION_Y, TYPE_POSITION_XY, TYPE_POSITION_DIRECTION,
+               TYPE_VELOCITY_XY, TYPE_ACCELERATION_XY,
+               TYPE_AOA, TYPE_ROLL_RATE, TYPE_PITCH_RATE, TYPE_YAW_RATE,
+               TYPE_MASS, TYPE_CP_LOCATION, TYPE_CG_LOCATION, TYPE_STABILITY,
+               TYPE_MACH_NUMBER, TYPE_REYNOLDS_NUMBER,
+               TYPE_THRUST_FORCE, TYPE_DRAG_FORCE,
+               TYPE_DRAG_COEFF, TYPE_AXIAL_DRAG_COEFF,
+               TYPE_FRICTION_DRAG_COEFF, TYPE_PRESSURE_DRAG_COEFF, TYPE_BASE_DRAG_COEFF,
+               TYPE_NORMAL_FORCE_COEFF, TYPE_PITCH_MOMENT_COEFF, TYPE_YAW_MOMENT_COEFF, TYPE_SIDE_FORCE_COEFF,
+               TYPE_ROLL_MOMENT_COEFF, TYPE_ROLL_FORCING_COEFF, TYPE_ROLL_DAMPING_COEFF,
+               TYPE_PITCH_DAMPING_MOMENT_COEFF, TYPE_YAW_DAMPING_MOMENT_COEFF,
+               TYPE_REFERENCE_LENGTH, TYPE_REFERENCE_AREA, 
+               TYPE_ORIENTATION_THETA, TYPE_ORIENTATION_PHI,
+               TYPE_WIND_VELOCITY, TYPE_AIR_TEMPERATURE, TYPE_AIR_PRESSURE, TYPE_SPEED_OF_SOUND,
+               TYPE_TIME_STEP, TYPE_COMPUTATION_TIME
+       };
+       
+       /**
+        * Return a {@link Type} based on a string description.  This returns known data types
+        * if possible, or a new type otherwise.
+        * 
+        * @param s             the string description of the type.
+        * @param u             the unit group the new type should belong to if a new group is created.
+        * @return              a data type.
+        */
+       public static Type getType(String s, UnitGroup u) {
+               for (Type t: TYPES) {
+                       if (t.getName().equalsIgnoreCase(s))
+                               return t;
+               }
+               return new Type(s, u);
+       }
+       
+       
+       
+       public static class Type implements Comparable<Type> {
+               private final String name;
+               private final UnitGroup units;
+               private final int priority;
+               private final int hashCode;
+               
+               private Type(String typeName, UnitGroup units) {
+                       this(typeName, units, 999);
+               }
+               
+               public Type(String typeName, UnitGroup units, int priority) {
+                       if (typeName == null)
+                               throw new IllegalArgumentException("typeName is null");
+                       this.name = typeName;
+                       this.units = units;
+                       this.priority = priority;
+                       this.hashCode = this.name.toLowerCase().hashCode();
+               }
+               
+               public String getName() {
+                       return name;
+               }
+               
+               public UnitGroup getUnitGroup() {
+                       return units;
+               }
+               
+               @Override
+               public String toString() {
+                       return name;
+               }
+               
+               @Override
+               public boolean equals(Object other) {
+                       if (!(other instanceof Type))
+                               return false;
+                       return this.name.equalsIgnoreCase(((Type)other).name);
+               }
+               @Override
+               public int hashCode() {
+                       return hashCode;
+               }
+
+               @Override
+               public int compareTo(Type o) {
+                       return this.priority - o.priority;
+               }
+       }
+       
+
+       
+       
+       /** The name of this flight data branch. */
+       private final String branchName;
+       
+       private final Map<Type, ArrayList<Double>> values = 
+               new LinkedHashMap<Type, ArrayList<Double>>();
+       
+       private final Map<Type, Double> maxValues = new HashMap<Type, Double>();
+       private final Map<Type, Double> minValues = new HashMap<Type, Double>();
+       
+       
+       private final ArrayList<Pair<Double,FlightEvent>> events =
+               new ArrayList<Pair<Double,FlightEvent>>();
+       
+       private boolean mutable = true;
+       
+       
+       public FlightDataBranch(String name, Type... types) {
+               if (types.length == 0) {
+                       throw new IllegalArgumentException("Must specify at least one data type.");
+               }
+               
+               this.branchName = name;
+
+               for (Type t: types) {
+                       if (values.containsKey(t)) {
+                               throw new IllegalArgumentException("Value type "+t+" specified multiple " +
+                               "times in constructor.");
+                       }
+                       
+                       values.put(t, new ArrayList<Double>());
+                       minValues.put(t, Double.NaN);
+                       maxValues.put(t, Double.NaN);
+               }
+       }
+       
+       
+       public String getBranchName() {
+               return branchName;
+       }
+       
+       public Type[] getTypes() {
+               Type[] array = values.keySet().toArray(new Type[0]);
+               Arrays.sort(array);
+               return array;
+       }
+       
+       public int getLength() {
+               for (Type t: values.keySet()) {
+                       return values.get(t).size();
+               }
+               return 0;
+       }
+       
+       
+       
+       public void addPoint() {
+               if (!mutable)
+                       throw new IllegalStateException("FlightDataBranch has been made immutable.");
+               for (Type t: values.keySet()) {
+                       values.get(t).add(Double.NaN);
+               }
+       }
+       
+       public void setValue(Type type, double value) {
+               if (!mutable)
+                       throw new IllegalStateException("FlightDataBranch has been made immutable.");
+               ArrayList<Double> list = values.get(type);
+               if (list == null) {
+                       
+                       list = new ArrayList<Double>();
+                       int n = getLength();
+                       for (int i=0; i < n; i++) {
+                               list.add(Double.NaN);
+                       }
+                       values.put(type, list);
+                       minValues.put(type, value);
+                       maxValues.put(type, value);
+                       
+               }
+               list.set(list.size()-1, value);
+               double min = minValues.get(type);
+               double max = maxValues.get(type);
+               
+               if (Double.isNaN(min) || (value < min)) {
+                       minValues.put(type, value);
+               }
+               if (Double.isNaN(max) || (value > max)) {
+                       maxValues.put(type, value);
+               }
+       }
+       
+       
+       @SuppressWarnings("unchecked")
+       public List<Double> get(Type type) {
+               ArrayList<Double> list = values.get(type);
+               if (list==null)
+                       return null;
+               return (List<Double>)list.clone();
+       }
+       
+       
+       public double get(Type type, int index) {
+               ArrayList<Double> list = values.get(type);
+               if (list==null)
+                       return Double.NaN;
+               return list.get(index);
+       }
+       
+
+       /**
+        * Return the last value of the specified type in the branch, or NaN if the type is
+        * unavailable.
+        * 
+        * @param type  the parameter type.
+        * @return              the last value in this branch, or NaN.
+        */
+       public double getLast(Type type) {
+               ArrayList<Double> list = values.get(type);
+               if (list==null || list.isEmpty())
+                       return Double.NaN;
+               return list.get(list.size()-1);
+       }
+
+       /**
+        * Return the minimum value of the specified type in the branch, or NaN if the type
+        * is unavailable.
+        * 
+        * @param type  the parameter type.
+        * @return              the minimum value in this branch, or NaN.
+        */
+       public double getMinimum(Type type) {
+               Double v = minValues.get(type);
+               if (v==null)
+                       return Double.NaN;
+               return v;
+       }
+       
+       /**
+        * Return the maximum value of the specified type in the branch, or NaN if the type
+        * is unavailable.
+        * 
+        * @param type  the parameter type.
+        * @return              the maximum value in this branch, or NaN.
+        */
+       public double getMaximum(Type type) {
+               Double v = maxValues.get(type);
+               if (v==null)
+                       return Double.NaN;
+               return v;
+       }
+       
+       
+       public void addEvent(double time, FlightEvent event) {
+               if (!mutable)
+                       throw new IllegalStateException("FlightDataBranch has been made immutable.");
+               events.add(new Pair<Double,FlightEvent>(time,event));
+       }
+       
+       
+       /**
+        * Return the list of events.  The list is a list of (time, event) pairs.
+        * 
+        * @return      the list of events during the flight.
+        */
+       @SuppressWarnings("unchecked")
+       public List<Pair<Double, FlightEvent>> getEvents() {
+               return (List<Pair<Double, FlightEvent>>) events.clone();
+       }
+
+       
+       /**
+        * Make this FlightDataBranch immutable.  Any calls to the set methods that would
+        * modify this object will after this call throw an <code>IllegalStateException</code>.
+        */
+       public void immute() {
+               mutable = false;
+       }
+       
+       public boolean isMutable() {
+               return mutable;
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/FlightEvent.java b/src/net/sf/openrocket/simulation/FlightEvent.java
new file mode 100644 (file)
index 0000000..d4c1984
--- /dev/null
@@ -0,0 +1,135 @@
+package net.sf.openrocket.simulation;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+/**
+ * A class that defines an event during the flight of a rocket.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class FlightEvent implements Comparable<FlightEvent> {
+
+       /**
+        * The type of the flight event.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       public enum Type {
+               /** 
+                * Rocket launch.
+                */
+               LAUNCH,
+               /**
+                * When the motor has lifted off the ground.
+                */
+               LIFTOFF,
+               /**
+                * Launch rod has been cleared.
+                */
+               LAUNCHROD,
+               /** 
+                * Ignition of a motor.  Source is the motor mount the motor of which has ignited. 
+                */
+               IGNITION,
+               /** 
+                * Burnout of a motor.  Source is the motor mount the motor of which has burnt out. 
+                */
+               BURNOUT,
+               /** 
+                * Ejection charge of a motor fired.  Source is the motor mount the motor of
+                * which has exploded its ejection charge. 
+                */
+               EJECTION_CHARGE,
+               /** 
+                * Opening of a recovery device.  Source is the RecoveryComponent which has opened. 
+                */
+               RECOVERY_DEVICE_DEPLOYMENT,
+               /** 
+                * Separation of a stage.  Source is the stage which has separated all lower stages. 
+                */
+               STAGE_SEPARATION,
+               /** 
+                * Apogee has been reached.
+                */
+               APOGEE,
+               /** 
+                * Ground has been hit after flight.
+                */
+               GROUND_HIT,
+               
+               /**
+                * End of simulation.  Placing this to the queue will end the simulation.
+                */
+               SIMULATION_END,
+               
+               /**
+                * A change in altitude has occurred.  Data is a <code>Pair<Double,Double></code>
+                * which contains the old and new altitudes.
+                */
+               ALTITUDE
+       }
+
+       private final Type type;
+       private final double time;
+       private final RocketComponent source;
+       private final Object data;
+
+       
+       public FlightEvent(Type type, double time) {
+               this(type, time, null);
+       }
+       
+       public FlightEvent(Type type, double time, RocketComponent source) {
+               this(type,time,source,null);
+       }
+
+       public FlightEvent(Type type, double time, RocketComponent source, Object data) {
+               this.type = type;
+               this.time = time;
+               this.source = source;
+               this.data = data;
+       }
+       
+
+       
+       public Type getType() {
+               return type;
+       }
+       
+       public double getTime() {
+               return time;
+       }
+       
+       public RocketComponent getSource() {
+               return source;
+       }
+       
+       public Object getData() {
+               return data;
+       }
+       
+       
+       public FlightEvent resetSource() {
+               return new FlightEvent(type, time, null, data);
+       }
+
+       /**
+        * Compares this event to another event depending on the event time.  Secondary
+        * sorting is performed based on the event type ordinal.
+        */
+       @Override
+       public int compareTo(FlightEvent o) {
+               if (this.time < o.time)
+                       return -1;
+               if (this.time > o.time)
+                       return 1;
+               
+               return this.type.ordinal() - o.type.ordinal();
+       }
+       
+       @Override
+       public String toString() {
+               return "FlightEvent[type="+type.toString()+",time="+time+",source="+source+"]";
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/FlightSimulator.java b/src/net/sf/openrocket/simulation/FlightSimulator.java
new file mode 100644 (file)
index 0000000..2b67530
--- /dev/null
@@ -0,0 +1,573 @@
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.AtmosphericModel;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Clusterable;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationLaunchException;
+import net.sf.openrocket.simulation.exception.SimulationNotSupportedException;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+
+
+
+
+/**
+ * Abstract class that implements a flight simulation using a specific
+ * {@link AerodynamicCalculator}.  The simulation methods are the <code>simulate</code>
+ * methods.
+ * <p>
+ * This class contains the event flight event handling mechanisms common to all
+ * simulations.  The simulator calls the {@link #step(SimulationConditions, SimulationStatus)}
+ * method periodically to take time steps.  Concrete subclasses of this class specify 
+ * how the actual time steps are taken (e.g. Euler or Runge-Kutta integration).
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class FlightSimulator {
+       
+       public static final double RECOVERY_TIME_STEP = 0.5;
+       
+       /** The {@link AerodynamicCalculator} to use to calculate the aerodynamic forces. */
+       protected AerodynamicCalculator calculator = null;
+
+       /** The {@link AtmosphericModel} used to model the atmosphere. */
+       protected AtmosphericModel atmosphericModel;
+       
+       /** Listener list. */
+       protected final List<SimulationListener> listeners = new ArrayList<SimulationListener>();
+
+       
+       private PriorityQueue<FlightEvent> eventQueue;
+       private WarningSet warnings;
+       
+       
+       public FlightSimulator() {
+               
+       }
+               
+       public FlightSimulator(AerodynamicCalculator calculator) {
+               this.calculator = calculator;
+       }
+       
+       
+       
+       
+       public AerodynamicCalculator getCalculator() {
+               return calculator;
+       }
+
+       public void setCalculator(AerodynamicCalculator calc) {
+               this.calculator = calc;
+       }
+       
+       
+       /**
+        * Will be removed!  Use {@link #simulate(SimulationConditions)} instead.
+        */
+       @Deprecated
+       public FlightData simulate(SimulationConditions simulation, 
+                       boolean simulateBranches, WarningSet warnings) 
+                       throws SimulationNotSupportedException {
+               try {
+                       return simulate(simulation);
+               } catch (SimulationException e) {
+                       throw new SimulationNotSupportedException(e);
+               }
+       }
+       
+
+       public FlightData simulate(SimulationConditions simulation) 
+                       throws SimulationException {
+
+               // Set up flight data
+               FlightData flightData = new FlightData();
+               
+               // Set up rocket configuration
+               Configuration configuration = calculator.getConfiguration();
+               configuration.setAllStages();
+               configuration.setMotorConfigurationID(simulation.getMotorConfigurationID());
+               
+               if (!configuration.hasMotors()) {
+                       throw new SimulationLaunchException("No motors defined.");
+               }
+               
+               // Set up the event queue
+               eventQueue = new PriorityQueue<FlightEvent>();
+               eventQueue.add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulation.getRocket()));
+
+               // Initialize the simulation
+               SimulationStatus status = initializeSimulation(configuration, simulation);
+               status.warnings = flightData.getWarningSet();
+               warnings = flightData.getWarningSet();
+               
+
+               // Start the simulation
+               while (handleEvents(eventQueue, status)) {
+                       
+                       // Take the step
+                       double oldAlt = status.position.z;
+                       
+                       if (status.deployedRecoveryDevices.isEmpty()) {
+                               step(simulation, status);
+                       } else {
+                               recoveryStep(simulation, status);
+                       }
+                       
+                       
+                       // Add appropriate events
+                                               
+                       if (!status.liftoff) {
+                               
+                               // Avoid sinking into ground before liftoff
+                               if (status.position.z < 0) {
+                                       status.position = Coordinate.NUL;
+                                       status.velocity = Coordinate.NUL;
+                               }
+                               // Detect liftoff
+                               if (status.position.z > 0.01) {
+                                       eventQueue.add(new FlightEvent(FlightEvent.Type.LIFTOFF, status.time));
+                                       status.liftoff = true;
+                               }
+                               
+                       } else {
+
+                               // Check ground hit after liftoff
+                               if (status.position.z < 0) {
+                                       status.position = status.position.setZ(0);
+                                       eventQueue.add(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.time));
+                                       eventQueue.add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.time));
+                               }
+
+                       }
+
+                       
+                       // Add altitude event
+                       eventQueue.add(new FlightEvent(FlightEvent.Type.ALTITUDE, status.time, 
+                                       status.configuration.getRocket(), 
+                                       new Pair<Double,Double>(oldAlt,status.position.z)));
+
+
+                       // Check for launch guide clearance
+                       if (status.launchRod && status.position.length() > status.launchRodLength) {
+                               eventQueue.add(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.time, null));
+                               status.launchRod = false;
+                       }
+                       
+                       
+                       // Check for apogee
+                       if (!status.apogeeReached && status.position.z < oldAlt - 0.001) {
+                               eventQueue.add(new FlightEvent(FlightEvent.Type.APOGEE, status.time,
+                                               status.configuration.getRocket()));
+                               status.apogeeReached = true;
+                       }
+
+                       
+                       // Call listeners
+                       SimulationListener[] array = listeners.toArray(new SimulationListener[0]);
+                       for (SimulationListener l: array) {
+                               addListenerEvents(l.stepTaken(status));
+                       }
+               }
+               
+               flightData.addBranch(status.flightData);
+               
+               System.out.println("Warnings at the end:  "+flightData.getWarningSet());
+               
+               // TODO: HIGH: Simulate branches
+               return flightData;
+       }
+       
+       
+       
+
+       /**
+        * Handles events occurring during the flight from the <code>eventQueue</code>.
+        * Each event that has occurred before or at the current simulation time is
+        * processed.  Suitable events are also added to the flight data.
+        * 
+        * @param data                          the FlightData to add events to.
+        * @param endEvent                      the event at which to end this simulation.
+        * @param simulateBranches      whether to invoke a separate simulation of separated lower
+        *                                                      stages
+        * @throws SimulationException 
+        */
+       private boolean handleEvents(PriorityQueue<FlightEvent> queue, SimulationStatus status)
+       throws SimulationException {
+               FlightEvent e;
+               boolean ret = true;
+               
+               e = queue.peek();
+               // Skip to events if no motor has ignited yet
+               if (!status.motorIgnited) {
+                       if (e == null || Double.isNaN(e.getTime()) || e.getTime() > 1000000) {
+                               throw new SimulationLaunchException("No motors ignited.");
+                       }
+                       status.time = e.getTime();
+               }
+               
+               while ((e != null) && (e.getTime() <= status.time)) {
+                       e = queue.poll();
+
+                       // If no motor has ignited and no events are occurring, abort
+                       if (!status.motorIgnited) {
+                               if (e == null || Double.isNaN(e.getTime()) || e.getTime() > 1000000) {
+                                       throw new SimulationLaunchException("No motors ignited.");
+                               }
+                       }
+                       
+                       // If event is motor burnout without liftoff, abort
+                       if (e.getType() == FlightEvent.Type.BURNOUT  &&  !status.liftoff) {
+                               throw new SimulationLaunchException("Motor burnout without liftoff.");
+                       }
+
+                       // Add event to flight data
+                       if (e.getType() != FlightEvent.Type.ALTITUDE) {
+                               status.flightData.addEvent(status.time, e.resetSource());
+                       }
+                       
+                       // Check for motor ignition events, add ignition events to queue
+                       Iterator<MotorMount> iterator = status.configuration.motorIterator();
+                       while (iterator.hasNext()) {
+                               MotorMount mount = iterator.next();
+                               if (mount.getIgnitionEvent().isActivationEvent(e, (RocketComponent)mount)) {
+                                       queue.add(new FlightEvent(FlightEvent.Type.IGNITION, 
+                                                       status.time + mount.getIgnitionDelay(), (RocketComponent)mount));
+                               }
+                       }
+                       
+                       // Handle motor ignition events, add burnout events
+                       if (e.getType() == FlightEvent.Type.IGNITION) {
+                               status.motorIgnited = true;
+                               
+                               String id = status.configuration.getMotorConfigurationID();
+                               MotorMount mount = (MotorMount) e.getSource();
+                               Motor motor = mount.getMotor(id);
+                               
+                               status.configuration.setIgnitionTime(mount, e.getTime());
+                               queue.add(new FlightEvent(FlightEvent.Type.BURNOUT, 
+                                               e.getTime() + motor.getTotalTime(), (RocketComponent)mount));
+                               queue.add(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE,
+                                               e.getTime() + motor.getTotalTime() + mount.getMotorDelay(id), 
+                                               (RocketComponent)mount));
+                       }
+                       
+                       
+                       // Handle stage separation on motor ignition
+                       if (e.getType() == FlightEvent.Type.IGNITION) {
+                               RocketComponent mount = (RocketComponent) e.getSource();
+                               int n = mount.getStageNumber();
+                               if (n < mount.getRocket().getStageCount()-1) {
+                                       if (status.configuration.isStageActive(n+1)) {
+                                               queue.add(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, e.getTime(),
+                                                               mount.getStage()));
+                                       }
+                               }
+                       }
+                       if (e.getType() == FlightEvent.Type.STAGE_SEPARATION) {
+                               RocketComponent stage = (RocketComponent) e.getSource();
+                               int n = stage.getStageNumber();
+                               status.configuration.setToStage(n);
+                       }
+                       
+                       
+                       // Handle recovery device deployment
+                       Iterator<RocketComponent> iterator1 = status.configuration.iterator();
+                       while (iterator1.hasNext()) {
+                               RocketComponent c = iterator1.next();
+                               if (!(c instanceof RecoveryDevice))
+                                       continue;
+                               if (((RecoveryDevice)c).getDeployEvent().isActivationEvent(e, c)) {
+                                       // Delay event by at least 1ms to allow stage separation to occur first
+                                       queue.add(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
+                                                       e.getTime() + Math.max(0.001, ((RecoveryDevice)c).getDeployDelay()), c));
+                               }
+                       }
+                       if (e.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
+                               RocketComponent c = e.getSource();
+                               int n = c.getStageNumber();
+                               // Ignore event if stage not active
+                               if (status.configuration.isStageActive(n)) {
+                                       
+                                       // Check whether any motor is active anymore
+                                       iterator = status.configuration.motorIterator();
+                                       while (iterator.hasNext()) {
+                                               MotorMount mount = iterator.next();
+                                               Motor motor = mount.getMotor(status.configuration.getMotorConfigurationID());
+                                               if (motor == null)
+                                                       continue;
+                                               if (status.configuration.getIgnitionTime(mount) + motor.getAverageTime()
+                                                               > status.time) {
+                                                       warnings.add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
+                                               }
+                                       }
+                                       
+                                       // Check for launch rod
+                                       if (status.launchRod) {
+                                               warnings.add(Warning.fromString("Recovery device device deployed while on " +
+                                                               "the launch guide."));
+                                       }
+                                       
+                                       // Check current velocity
+                                       if (status.velocity.length() > 20) {
+                                               // TODO: LOW: Custom warning.
+                                               warnings.add(Warning.fromString("Recovery device deployment at high " +
+                                                               "speed (" 
+                                                               + UnitGroup.UNITS_VELOCITY.toStringUnit(status.velocity.length())
+                                                               + ")."));
+                                       }
+                                       
+                                       status.liftoff = true;
+                                       status.deployedRecoveryDevices.add((RecoveryDevice)c);
+                               }
+                       }
+                       
+                       
+                       
+                       // Simulation end
+                       if (e.getType() == FlightEvent.Type.SIMULATION_END) {
+                               ret = false;
+                       }
+                       
+                       
+                       // Call listeners
+                       SimulationListener[] array = listeners.toArray(new SimulationListener[0]);
+                       for (SimulationListener l: array) {
+                               addListenerEvents(l.handleEvent(e, status));
+                       }
+                       
+                       
+                       e = queue.peek();
+                       // Skip to events if no motor has ignited yet
+                       if (!status.motorIgnited) {
+                               if (e == null || Double.isNaN(e.getTime()) || e.getTime() > 1000000) {
+                                       throw new SimulationLaunchException("No motors ignited.");
+                               }
+                               status.time = e.getTime();
+                       }
+               }
+               return ret;
+       }
+       
+       
+       // TODO: MEDIUM: Create method storeData() which is overridden by simulators
+       
+       
+       /**
+        * Perform a step during recovery.  This is a 3-DOF simulation using simple Euler
+        * integration.
+        * 
+        * @param conditions    the simulation conditions.
+        * @param status                the current simulation status.
+        */
+       protected void recoveryStep(SimulationConditions conditions, SimulationStatus status) {
+               double totalCD = 0;
+               double refArea = status.configuration.getReferenceArea();
+               
+               // TODO: MEDIUM: Call listeners during recovery phase
+               
+               // Get the atmospheric conditions
+               AtmosphericConditions atmosphere = conditions.getAtmosphericModel().getConditions(
+                               conditions.getLaunchAltitude() + status.position.z);
+
+               //// Local wind speed and direction
+               double windSpeed = status.windSimulator.getWindSpeed(status.time);
+               Coordinate airSpeed = status.velocity.add(windSpeed, 0, 0);
+
+               // Get total CD
+               double mach = airSpeed.length() / atmosphere.getMachSpeed();
+               for (RecoveryDevice c: status.deployedRecoveryDevices) {
+                       totalCD += c.getCD(mach) * c.getArea() / refArea;
+               }
+               
+               // Compute drag force
+               double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2());
+               double dragForce = totalCD * dynP * refArea;
+               double mass = calculator.getCG(status.time).weight;
+               
+               
+               // Compute drag acceleration
+               Coordinate linearAcceleration;
+               if (airSpeed.length() > 0.001) {
+                       linearAcceleration = airSpeed.normalize().multiply(-dragForce/mass);
+               } else {
+                       linearAcceleration = Coordinate.NUL;
+               }
+               
+               // Add effect of gravity
+               linearAcceleration = linearAcceleration.sub(0, 0, status.gravityModel.getGravity());
+
+               
+               // Select time step
+               double timeStep = MathUtil.min(0.5/linearAcceleration.length(), RECOVERY_TIME_STEP);
+               
+               // Perform Euler integration
+               status.position = (status.position.add(status.velocity.multiply(timeStep)).
+                               add(linearAcceleration.multiply(MathUtil.pow2(timeStep)/2)));
+               status.velocity = status.velocity.add(linearAcceleration.multiply(timeStep));
+               status.time += timeStep;
+
+               
+               // Store data
+               FlightDataBranch data = status.flightData;
+               boolean extra = status.startConditions.getCalculateExtras();
+               data.addPoint();
+
+               data.setValue(FlightDataBranch.TYPE_TIME, status.time);
+               data.setValue(FlightDataBranch.TYPE_ALTITUDE, status.position.z);
+               data.setValue(FlightDataBranch.TYPE_POSITION_X, status.position.x);
+               data.setValue(FlightDataBranch.TYPE_POSITION_Y, status.position.y);
+               if (extra) {
+                       data.setValue(FlightDataBranch.TYPE_POSITION_XY, 
+                                       MathUtil.hypot(status.position.x, status.position.y));
+                       data.setValue(FlightDataBranch.TYPE_POSITION_DIRECTION, 
+                                       Math.atan2(status.position.y, status.position.x));
+                       
+                       data.setValue(FlightDataBranch.TYPE_VELOCITY_XY, 
+                                       MathUtil.hypot(status.velocity.x, status.velocity.y));
+                       data.setValue(FlightDataBranch.TYPE_ACCELERATION_XY, 
+                                       MathUtil.hypot(linearAcceleration.x, linearAcceleration.y));
+                       
+                       data.setValue(FlightDataBranch.TYPE_ACCELERATION_TOTAL,linearAcceleration.length());
+                       
+                       double Re = airSpeed.length() * 
+                                       calculator.getConfiguration().getLength() / 
+                                       atmosphere.getKinematicViscosity();
+                       data.setValue(FlightDataBranch.TYPE_REYNOLDS_NUMBER, Re);
+               }
+               
+               data.setValue(FlightDataBranch.TYPE_VELOCITY_Z, status.velocity.z);
+               data.setValue(FlightDataBranch.TYPE_ACCELERATION_Z, linearAcceleration.z);
+               
+               data.setValue(FlightDataBranch.TYPE_VELOCITY_TOTAL, airSpeed.length());
+               data.setValue(FlightDataBranch.TYPE_MACH_NUMBER, mach);
+               
+               data.setValue(FlightDataBranch.TYPE_MASS, mass);
+
+               data.setValue(FlightDataBranch.TYPE_THRUST_FORCE, 0);
+               data.setValue(FlightDataBranch.TYPE_DRAG_FORCE, dragForce);
+
+               data.setValue(FlightDataBranch.TYPE_WIND_VELOCITY, windSpeed);
+               data.setValue(FlightDataBranch.TYPE_AIR_TEMPERATURE, atmosphere.temperature);
+               data.setValue(FlightDataBranch.TYPE_AIR_PRESSURE, atmosphere.pressure);
+               data.setValue(FlightDataBranch.TYPE_SPEED_OF_SOUND, atmosphere.getMachSpeed());
+               
+               data.setValue(FlightDataBranch.TYPE_TIME_STEP, timeStep);
+               if (status.simulationStartTime != Long.MIN_VALUE)
+                       data.setValue(FlightDataBranch.TYPE_COMPUTATION_TIME,
+                                       (System.nanoTime() - status.simulationStartTime)/1000000000.0);
+       }
+       
+       
+       
+       
+       /**
+        * Add events that listeners have returned, and add a Warning to the 
+        * simulation if necessary.
+        * 
+        * @param events        a collection of the events, or <code>null</code>.
+        */
+       protected final void addListenerEvents(Collection<FlightEvent> events) {
+               if (events == null)
+                       return;
+               for (FlightEvent e: events) {
+                       if (e != null && e.getTime() < 1000000) {
+                               warnings.add(Warning.LISTENERS_AFFECTED);
+                               eventQueue.add(e);
+                       }
+               }
+       }
+       
+       
+       
+       /**
+        * Calculate the average thrust produced by the motors in the current configuration.
+        * The average is taken between <code>status.time</code> and 
+        * <code>status.time + timestep</code>.
+        * <p>
+        * Note:  Using this method does not take into account any moments generated by
+        * off-center motors.
+        *  
+        * @param status        the current simulation status.
+        * @param timestep      the time step of the current iteration.
+        * @return                      the average thrust during the time step.
+        */
+       protected double calculateThrust(SimulationStatus status, double timestep) {
+               double thrust = 0;
+               Iterator<MotorMount> iterator = status.configuration.motorIterator();
+               
+               while (iterator.hasNext()) {
+                       MotorMount mount = iterator.next();
+                       
+                       // Count the number of motors in a cluster
+                       int count = 1;
+                       for (RocketComponent c = (RocketComponent)mount; c != null; c = c.getParent()) {
+                               if (c instanceof Clusterable) 
+                                       count *= ((Clusterable)c).getClusterConfiguration().getClusterCount();
+                       }
+                       
+                       Motor motor = mount.getMotor(status.configuration.getMotorConfigurationID());
+                       double ignitionTime = status.configuration.getIgnitionTime(mount);
+                       double time = status.time - ignitionTime;
+                       thrust += count * motor.getThrust(time, time + timestep);
+                       // TODO: MEDIUM: Moment generated by motors
+               }
+               
+               return thrust;
+       }
+       
+       
+       
+       /**
+        * Initialize a new {@link SimulationStatus} object for simulation using this simulator.
+        * 
+        * @param configuration the starting configuration of the rocket.
+        * @param simulation    the simulation conditions.
+        * @return                              a {@link SimulationStatus} object for the simulation.
+        */
+       protected abstract SimulationStatus initializeSimulation(Configuration configuration, 
+                       SimulationConditions simulation);
+       
+       /**
+        * Make a time step.  The current status of the simulation is stored in the
+        * variable <code>status</code> and must be updated by this call.
+        *
+        * @param simulation    the simulation conditions.
+        * @param status                the current simulation status, received originally from
+        *                                              {@link #initializeSimulation(Configuration, SimulationConditions)}
+        * @return      a collection of flight events to handle, or null for none.
+        */
+       
+       
+       protected abstract Collection<FlightEvent> step(SimulationConditions simulation, 
+                       SimulationStatus status) throws SimulationException;
+
+
+       
+       public void addSimulationListener(SimulationListener l) {
+               listeners.add(l);
+       }
+       public void removeSimulationListener(SimulationListener l) {
+               listeners.remove(l);
+       }
+       public void resetSimulationListeners() {
+               listeners.clear();
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/RK4SimulationStatus.java b/src/net/sf/openrocket/simulation/RK4SimulationStatus.java
new file mode 100644 (file)
index 0000000..b34a9b0
--- /dev/null
@@ -0,0 +1,23 @@
+package net.sf.openrocket.simulation;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Quaternion;
+
+public class RK4SimulationStatus extends SimulationStatus {
+       public Quaternion orientation;
+       public Coordinate rotation;
+       
+       public Coordinate launchRodDirection;
+       
+       
+       /**
+        * Provides a copy of the simulation status.  The orientation quaternion is
+        * cloned as well, so changing it does not affect other simulation status objects.
+        */
+       @Override
+       public RK4SimulationStatus clone() {
+               RK4SimulationStatus copy = (RK4SimulationStatus) super.clone();
+               copy.orientation = this.orientation.clone();
+               return copy;
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/RK4Simulator.java b/src/net/sf/openrocket/simulation/RK4Simulator.java
new file mode 100644 (file)
index 0000000..dd5ec49
--- /dev/null
@@ -0,0 +1,642 @@
+package net.sf.openrocket.simulation;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.GravityModel;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.aerodynamics.WindSimulator;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Quaternion;
+import net.sf.openrocket.util.Rotation2D;
+
+
+public class RK4Simulator extends FlightSimulator {
+       
+       /**
+        * A recommended reasonably accurate time step.
+        */
+       public static final double RECOMMENDED_TIME_STEP = 0.05;
+       
+       /**
+        * A recommended maximum angle step value.
+        */
+       public static final double RECOMMENDED_ANGLE_STEP = 3*Math.PI/180;
+       
+       /**
+        * Maximum roll step allowed.  This is selected as an uneven division of the full
+        * circle so that the simulation will sample the most wind directions
+        */
+       private static final double MAX_ROLL_STEP_ANGLE = 28.32 * Math.PI/180;
+//     private static final double MAX_ROLL_STEP_ANGLE = 8.32 * Math.PI/180;
+       
+       private static final double MAX_ROLL_RATE_CHANGE = 2 * Math.PI/180;
+       private static final double MAX_PITCH_CHANGE = 2 * Math.PI/180;
+
+       
+       private static final boolean DEBUG = false;
+
+       
+       /* Single instance so it doesn't have to be created each semi-step. */
+       private final FlightConditions flightConditions = new FlightConditions(null);
+       
+       
+       private Coordinate linearAcceleration;
+       private Coordinate angularAcceleration;
+       
+       // set by calculateFlightConditions and calculateAcceleration:
+       private double timestep;
+       private double oldTimestep;
+       private AerodynamicForces forces;
+       private double windSpeed;
+       private double thrustForce, dragForce;
+       private double lateralPitchRate = 0;
+       
+       private double rollAcceleration = 0;
+       private double lateralPitchAcceleration = 0;
+       
+       private double maxVelocityZ = 0;
+       private double startWarningTime = -1;
+       
+       private Rotation2D thetaRotation;
+
+       
+       public RK4Simulator() {
+               super();
+       }
+       
+       
+       public RK4Simulator(AerodynamicCalculator calculator) {
+               super(calculator);
+       }
+
+       
+
+       
+
+       @Override
+       protected RK4SimulationStatus initializeSimulation(Configuration configuration, 
+                       SimulationConditions simulation) {
+
+               RK4SimulationStatus status = new RK4SimulationStatus();
+               
+               status.startConditions = simulation;
+               
+               status.configuration = configuration;
+               // TODO: LOW: Branch names
+               status.flightData = new FlightDataBranch("Main", FlightDataBranch.TYPE_TIME);
+               status.launchRod = true;
+               status.time = 0.0;
+               status.simulationStartTime = System.nanoTime();
+               
+               status.launchRodDirection = new Coordinate(
+                               Math.sin(simulation.getLaunchRodAngle()) * 
+                               Math.cos(simulation.getLaunchRodDirection()),
+                               Math.sin(simulation.getLaunchRodAngle()) *
+                               Math.sin(simulation.getLaunchRodDirection()),
+                               Math.cos(simulation.getLaunchRodAngle())
+               );
+               status.launchRodLength = simulation.getLaunchRodLength();
+               
+               // Take into account launch lug positions
+               double lugPosition = Double.NaN;
+               for (RocketComponent c: configuration) {
+                       if (c instanceof LaunchLug) {
+                               double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x;
+                               if (Double.isNaN(lugPosition) || pos > lugPosition) {
+                                       lugPosition = pos;
+                               }
+                       }
+               }
+               if (!Double.isNaN(lugPosition)) {
+                       double maxX = 0;
+                       for (Coordinate c: configuration.getBounds()) {
+                               if (c.x > maxX)
+                                       maxX = c.x;
+                       }
+                       if (maxX >= lugPosition) {
+                               status.launchRodLength = Math.max(0,
+                                               status.launchRodLength - (maxX - lugPosition));
+                       }
+               }
+               
+               
+               Quaternion o = new Quaternion();
+               o.multiplyLeft(Quaternion.rotation(
+                               new Coordinate(0, simulation.getLaunchRodAngle(), 0)));
+               o.multiplyLeft(Quaternion.rotation(
+                               new Coordinate(0, 0, simulation.getLaunchRodDirection())));
+               status.orientation = o;
+               status.position = Coordinate.NUL;
+               status.velocity = Coordinate.NUL;
+               status.rotation = Coordinate.NUL;
+               
+               /*
+                * Force a very small deviation to the wind speed to avoid insanely
+                * perfect conditions (rocket dropping at exactly 180 deg AOA).
+                */
+               status.windSimulator = new WindSimulator();
+               status.windSimulator.setAverage(simulation.getWindSpeedAverage());
+               status.windSimulator.setStandardDeviation(
+                               Math.max(simulation.getWindSpeedDeviation(), 0.005));
+//             status.windSimulator.reset();
+               
+               status.gravityModel = new GravityModel(simulation.getLaunchLatitude());
+
+               rollAcceleration = 0;
+               lateralPitchAcceleration = 0;
+               oldTimestep = -1;
+               maxVelocityZ = 0;
+               startWarningTime = -1;
+               
+               return status;
+       }
+       
+       
+
+       @Override
+       protected Collection<FlightEvent> step(SimulationConditions simulation, 
+                       SimulationStatus simulationStatus) throws SimulationException {
+               
+               RK4SimulationStatus status = (RK4SimulationStatus)simulationStatus;
+               
+               ////////  Perform RK4 integration:  ////////
+               
+               Coordinate k1a, k1v, k1ra, k1rv; // Acceleration, velocity, rot.acc, rot.vel
+               Coordinate k2a, k2v, k2ra, k2rv;
+               Coordinate k3a, k3v, k3ra, k3rv;
+               Coordinate k4a, k4v, k4ra, k4rv;
+               RK4SimulationStatus status2;
+               
+               
+               // Calculate time step and store data after first call to calculateFlightConditions
+               calculateFlightConditions(status);
+
+               
+               /*
+                * Select the time step to use.  It is the minimum of the following:
+                *  1. the user-specified time step
+                *  2. the maximum pitch step angle limit
+                *  3. the maximum roll step angle limit
+                *  4. the maximum roll rate change limit (using previous step acceleration)
+                *  5. the maximum pitch change limit (using previous step acceleration)
+                * 
+                * The last two are required since near the steady-state roll rate the roll rate
+                * may oscillate significantly even between the sub-steps of the RK4 integration.
+                * 
+                * Additionally a low-pass filter is applied to the time step selectively 
+                * if the new time step is longer than the previous time step.
+                */
+               double dt1 = simulation.getTimeStep();
+               double dt2 = simulation.getMaximumStepAngle() / lateralPitchRate;
+               double dt3 = Math.abs(MAX_ROLL_STEP_ANGLE / flightConditions.getRollRate());
+               double dt4 = Math.abs(MAX_ROLL_RATE_CHANGE / rollAcceleration);
+               double dt5 = Math.abs(MAX_PITCH_CHANGE / lateralPitchAcceleration);
+               timestep = MathUtil.min(dt1,dt2,dt3);
+               timestep = MathUtil.min(timestep,dt4,dt5);
+               
+               if (oldTimestep > 0 && oldTimestep < timestep) {
+                       timestep = 0.3*timestep + 0.7*oldTimestep;
+               }
+               
+               if (timestep < 0.001)
+                       timestep = 0.001;
+               
+               oldTimestep = timestep;
+               if (DEBUG)
+                       System.out.printf("Time step: %.3f  dt1=%.3f dt2=%.3f dt3=%.3f dt4=%.3f dt5=%.3f\n",
+                       timestep,dt1,dt2,dt3,dt4,dt5);
+
+               
+               
+               //// First position, k1 = f(t, y)
+
+               //calculateFlightConditions already called
+               calculateAcceleration(status);
+               k1a = linearAcceleration;
+               k1ra = angularAcceleration;
+               k1v = status.velocity;
+               k1rv = status.rotation;
+               
+
+               // Store the flight information
+               storeData(status);
+               
+               
+               
+               //// Second position, k2 = f(t + h/2, y + k1*h/2)
+               
+               status2 = status.clone();
+               status2.time = status.time + timestep/2;
+               status2.position = status.position.add(k1v.multiply(timestep/2));
+               status2.velocity = status.velocity.add(k1a.multiply(timestep/2));
+               status2.orientation.multiplyLeft(Quaternion.rotation(k1rv.multiply(timestep/2)));
+               status2.rotation = status.rotation.add(k1ra.multiply(timestep/2));
+               
+               calculateFlightConditions(status2);
+               calculateAcceleration(status2);
+               k2a = linearAcceleration;
+               k2ra = angularAcceleration;
+               k2v = status2.velocity;
+               k2rv = status2.rotation;
+               
+               
+               //// Third position, k3 = f(t + h/2, y + k2*h/2)
+               
+               status2.orientation = status.orientation.clone();  // All others are set explicitly
+               status2.position = status.position.add(k2v.multiply(timestep/2));
+               status2.velocity = status.velocity.add(k2a.multiply(timestep/2));
+               status2.orientation.multiplyLeft(Quaternion.rotation(k2rv.multiply(timestep/2)));
+               status2.rotation = status.rotation.add(k2ra.multiply(timestep/2));
+               
+               calculateFlightConditions(status2);
+               calculateAcceleration(status2);
+               k3a = linearAcceleration;
+               k3ra = angularAcceleration;
+               k3v = status2.velocity;
+               k3rv = status2.rotation;
+
+               
+               
+               //// Fourth position, k4 = f(t + h, y + k3*h)
+               
+               status2.orientation = status.orientation.clone();  // All others are set explicitly
+               status2.time = status.time + timestep;
+               status2.position = status.position.add(k3v.multiply(timestep));
+               status2.velocity = status.velocity.add(k3a.multiply(timestep));
+               status2.orientation.multiplyLeft(Quaternion.rotation(k3rv.multiply(timestep)));
+               status2.rotation = status.rotation.add(k3ra.multiply(timestep));
+               
+               calculateFlightConditions(status2);
+               calculateAcceleration(status2);
+               k4a = linearAcceleration;
+               k4ra = angularAcceleration;
+               k4v = status2.velocity;
+               k4rv = status2.rotation;
+               
+               
+               
+               //// Sum all together,  y(n+1) = y(n) + h*(k1 + 2*k2 + 2*k3 + k4)/6
+
+               Coordinate deltaV, deltaP, deltaR, deltaO;
+               deltaV = k2a.add(k3a).multiply(2).add(k1a).add(k4a).multiply(timestep/6);
+               deltaP = k2v.add(k3v).multiply(2).add(k1v).add(k4v).multiply(timestep/6);
+               deltaR = k2ra.add(k3ra).multiply(2).add(k1ra).add(k4ra).multiply(timestep/6);
+               deltaO = k2rv.add(k3rv).multiply(2).add(k1rv).add(k4rv).multiply(timestep/6);
+               
+               if (DEBUG)
+                       System.out.println("Rot.Acc: "+deltaR+"  k1:"+k1ra+"  k2:"+k2ra+"  k3:"+k3ra+
+                                       "  k4:"+k4ra);
+
+               status.velocity = status.velocity.add(deltaV);
+               status.position = status.position.add(deltaP);
+               status.rotation = status.rotation.add(deltaR);
+               status.orientation.multiplyLeft(Quaternion.rotation(deltaO));
+
+
+               status.orientation.normalizeIfNecessary();
+               
+               status.time = status.time + timestep;
+
+               
+               return null;
+       }
+
+
+       
+       /**
+        * Calculate the linear and angular acceleration at the given status.  The results
+        * are stored in the fields {@link #linearAcceleration} and {@link #angularAcceleration}.
+        *  
+        * @param status   the status of the rocket.
+        * @throws SimulationException 
+        */
+       private void calculateAcceleration(RK4SimulationStatus status) throws SimulationException {
+               
+               /**
+                * Check whether to store warnings or not.  Warnings are ignored when on the 
+                * launch rod or 0.25 seconds after departure, and when the velocity has dropped
+                * below 20% of the max. velocity.
+                */
+               WarningSet warnings = status.warnings;
+               maxVelocityZ = MathUtil.max(maxVelocityZ, status.velocity.z);
+               if (status.launchRod) {
+                       warnings = null;
+               } else {
+                       if (status.velocity.z < 0.2 * maxVelocityZ)
+                               warnings = null;
+                       if (startWarningTime < 0)
+                               startWarningTime = status.time + 0.25;
+               }
+               if (status.time < startWarningTime)
+                       warnings = null;
+               
+               
+               // Calculate aerodynamic forces  (only axial if still on launch rod)
+               calculator.setConfiguration(status.configuration);
+
+               if (status.launchRod) {
+                       forces = calculator.getAxialForces(status.time, flightConditions, warnings);
+               } else {
+                       forces = calculator.getAerodynamicForces(status.time, flightConditions, warnings);
+               }
+               
+               
+               // Allow listeners to modify the forces
+               int mod = flightConditions.getModCount();
+               SimulationListener[] list = listeners.toArray(new SimulationListener[0]);
+               for (SimulationListener l: list) {
+                       l.forceCalculation(status, flightConditions, forces);
+               }
+               if (flightConditions.getModCount() != mod) {
+                       status.warnings.add(Warning.LISTENERS_AFFECTED);
+               }
+
+               
+               assert(!Double.isNaN(forces.CD));
+               assert(!Double.isNaN(forces.CN));
+               assert(!Double.isNaN(forces.Caxial));
+               assert(!Double.isNaN(forces.Cm));
+               assert(!Double.isNaN(forces.Cyaw));
+               assert(!Double.isNaN(forces.Cside));
+               assert(!Double.isNaN(forces.Croll));
+               
+
+               ////////  Calculate forces and accelerations  ////////
+               
+               double dynP = (0.5 * flightConditions.getAtmosphericConditions().getDensity() *
+                               MathUtil.pow2(flightConditions.getVelocity()));
+               double refArea = flightConditions.getRefArea();
+               double refLength = flightConditions.getRefLength();
+               
+               
+               // Linear forces
+               thrustForce = calculateThrust(status, timestep);
+               dragForce = forces.Caxial * dynP * refArea;
+               double fN = forces.CN * dynP * refArea;
+               double fSide = forces.Cside * dynP * refArea;
+               
+//             double sin = Math.sin(flightConditions.getTheta());
+//             double cos = Math.cos(flightConditions.getTheta());
+               
+//             double forceX = - fN * cos - fSide * sin;
+//             double forceY = - fN * sin - fSide * cos;
+               double forceZ = thrustForce - dragForce;
+
+               
+//             linearAcceleration = new Coordinate(forceX / forces.cg.weight,
+//                             forceY / forces.cg.weight, forceZ / forces.cg.weight);
+               linearAcceleration = new Coordinate(-fN / forces.cg.weight, -fSide / forces.cg.weight,
+                               forceZ / forces.cg.weight);
+               
+               linearAcceleration = thetaRotation.rotateZ(linearAcceleration);
+               linearAcceleration = status.orientation.rotate(linearAcceleration);
+
+               linearAcceleration = linearAcceleration.sub(0, 0, status.gravityModel.getGravity());
+               
+               
+               // If still on launch rod, project acceleration onto launch rod direction and
+               // set angular acceleration to zero.
+               if (status.launchRod) {
+                       linearAcceleration = status.launchRodDirection.multiply(
+                                       linearAcceleration.dot(status.launchRodDirection));
+                       angularAcceleration = Coordinate.NUL;
+                       rollAcceleration = 0;
+                       lateralPitchAcceleration = 0;
+                       return;
+               }
+                               
+               
+               // Convert momenta
+               double Cm = forces.Cm - forces.CN * forces.cg.x / refLength;
+               double Cyaw = forces.Cyaw - forces.Cside * forces.cg.x / refLength;
+               
+//             double momX = (-Cm * sin - Cyaw * cos) * dynP * refArea * refLength;
+//             double momY = ( Cm * cos - Cyaw * sin) * dynP * refArea * refLength;
+               double momX = -Cyaw * dynP * refArea * refLength;
+               double momY = Cm * dynP * refArea * refLength;
+               
+               double momZ = forces.Croll * dynP * refArea * refLength;
+               if (DEBUG)
+                       System.out.printf("Croll:  %.3f  dynP=%.3f  momZ=%.3f\n",forces.Croll,dynP,momZ);
+               
+               assert(!Double.isNaN(momX));
+               assert(!Double.isNaN(momY));
+               assert(!Double.isNaN(momZ));
+               assert(!Double.isNaN(forces.longitudalInertia));
+               assert(!Double.isNaN(forces.rotationalInertia));
+               
+               angularAcceleration = new Coordinate(momX / forces.longitudalInertia,
+                               momY / forces.longitudalInertia, momZ / forces.rotationalInertia);
+
+               rollAcceleration = angularAcceleration.z;
+               // TODO: LOW: This should be hypot, but does it matter?
+               lateralPitchAcceleration = MathUtil.max(Math.abs(angularAcceleration.x), 
+                               Math.abs(angularAcceleration.y));
+               
+               if (DEBUG)
+                       System.out.println("rot.inertia = "+forces.rotationalInertia);
+               
+               angularAcceleration = thetaRotation.rotateZ(angularAcceleration);
+               
+               angularAcceleration = status.orientation.rotate(angularAcceleration);
+       }
+       
+       
+       /**
+        * Calculate the flight conditions for the current rocket status.  The conditions
+        * are stored in the field {@link #flightConditions}.  Additional information that
+        * is calculated and will be stored in the flight data is also computed into the
+        * suitable fields.
+        * @throws SimulationException 
+        */
+       private void calculateFlightConditions(RK4SimulationStatus status) throws SimulationException {
+
+               flightConditions.setReference(status.configuration);
+
+               
+               //// Atmospheric conditions
+               AtmosphericConditions atmosphere = status.startConditions.getAtmosphericModel().
+                       getConditions(status.position.z + status.startConditions.getLaunchAltitude());
+               flightConditions.setAtmosphericConditions(atmosphere);
+
+               
+               //// Local wind speed and direction
+               windSpeed = status.windSimulator.getWindSpeed(status.time);
+               Coordinate airSpeed = status.velocity.add(windSpeed, 0, 0);
+               airSpeed = status.orientation.invRotate(airSpeed);
+               
+               
+        // Lateral direction:
+        double len = MathUtil.hypot(airSpeed.x, airSpeed.y);
+        if (len > 0.0001) {
+            thetaRotation = new Rotation2D(airSpeed.y/len, airSpeed.x/len);
+            flightConditions.setTheta(Math.atan2(airSpeed.y, airSpeed.x));
+        } else {
+            thetaRotation = Rotation2D.ID;
+            flightConditions.setTheta(0);
+        }
+
+               double velocity = airSpeed.length();
+        flightConditions.setVelocity(velocity);
+        if (velocity > 0.01) {
+            // aoa must be calculated from the monotonous cosine
+            // sine can be calculated by a simple division
+            flightConditions.setAOA(Math.acos(airSpeed.z / velocity), len / velocity);
+        } else {
+            flightConditions.setAOA(0);
+        }
+               
+               
+               // Roll, pitch and yaw rate
+               Coordinate rot = status.orientation.invRotate(status.rotation);
+               rot = thetaRotation.invRotateZ(rot);
+               
+               flightConditions.setRollRate(rot.z);
+               if (len < 0.001) {
+                       flightConditions.setPitchRate(0);
+                       flightConditions.setYawRate(0);
+                       lateralPitchRate = 0;
+               } else {
+                       flightConditions.setPitchRate(rot.y);
+                       flightConditions.setYawRate(rot.x);
+                       // TODO: LOW: set this as power of two?
+                       lateralPitchRate = MathUtil.hypot(rot.x, rot.y);
+               }
+               
+               
+               // Allow listeners to modify the conditions
+               int mod = flightConditions.getModCount();
+               SimulationListener[] list = listeners.toArray(new SimulationListener[0]);
+               for (SimulationListener l: list) {
+                       l.flightConditions(status, flightConditions);
+               }
+               if (mod != flightConditions.getModCount()) {
+                       // Re-calculate cached values
+                       thetaRotation = new Rotation2D(flightConditions.getTheta());
+                       lateralPitchRate = MathUtil.hypot(flightConditions.getPitchRate(),
+                                       flightConditions.getYawRate());
+                       status.warnings.add(Warning.LISTENERS_AFFECTED);
+               }
+               
+       }
+       
+       
+       
+       private void storeData(RK4SimulationStatus status) {
+               FlightDataBranch data = status.flightData;
+               boolean extra = status.startConditions.getCalculateExtras();
+               
+               data.addPoint();
+               data.setValue(FlightDataBranch.TYPE_TIME, status.time);
+               data.setValue(FlightDataBranch.TYPE_ALTITUDE, status.position.z);
+               data.setValue(FlightDataBranch.TYPE_POSITION_X, status.position.x);
+               data.setValue(FlightDataBranch.TYPE_POSITION_Y, status.position.y);
+               
+               if (extra) {
+                       data.setValue(FlightDataBranch.TYPE_POSITION_XY, 
+                                       MathUtil.hypot(status.position.x, status.position.y));
+                       data.setValue(FlightDataBranch.TYPE_POSITION_DIRECTION, 
+                                       Math.atan2(status.position.y, status.position.x));
+                       
+                       data.setValue(FlightDataBranch.TYPE_VELOCITY_XY, 
+                                       MathUtil.hypot(status.velocity.x, status.velocity.y));
+                       data.setValue(FlightDataBranch.TYPE_ACCELERATION_XY, 
+                                       MathUtil.hypot(linearAcceleration.x, linearAcceleration.y));
+                       
+                       data.setValue(FlightDataBranch.TYPE_ACCELERATION_TOTAL,linearAcceleration.length());
+                       
+                       double Re = flightConditions.getVelocity() * 
+                                       calculator.getConfiguration().getLength() / 
+                                       flightConditions.getAtmosphericConditions().getKinematicViscosity();
+                       data.setValue(FlightDataBranch.TYPE_REYNOLDS_NUMBER, Re);
+               }
+               
+               data.setValue(FlightDataBranch.TYPE_VELOCITY_Z, status.velocity.z);
+               data.setValue(FlightDataBranch.TYPE_ACCELERATION_Z, linearAcceleration.z);
+               
+               data.setValue(FlightDataBranch.TYPE_VELOCITY_TOTAL, flightConditions.getVelocity());
+               data.setValue(FlightDataBranch.TYPE_MACH_NUMBER, flightConditions.getMach());
+
+               if (!status.launchRod) {
+                       data.setValue(FlightDataBranch.TYPE_CP_LOCATION, forces.cp.x);
+                       data.setValue(FlightDataBranch.TYPE_CG_LOCATION, forces.cg.x);
+                       data.setValue(FlightDataBranch.TYPE_STABILITY, 
+                                       (forces.cp.x - forces.cg.x) / flightConditions.getRefLength());
+               }
+               data.setValue(FlightDataBranch.TYPE_MASS, forces.cg.weight);
+               
+               data.setValue(FlightDataBranch.TYPE_THRUST_FORCE, thrustForce);
+               data.setValue(FlightDataBranch.TYPE_DRAG_FORCE, dragForce);
+               
+               if (!status.launchRod) {
+                       data.setValue(FlightDataBranch.TYPE_PITCH_MOMENT_COEFF,
+                                       forces.Cm - forces.CN * forces.cg.x / flightConditions.getRefLength());
+                       data.setValue(FlightDataBranch.TYPE_YAW_MOMENT_COEFF, 
+                                       forces.Cyaw - forces.Cside * forces.cg.x / flightConditions.getRefLength());
+                       data.setValue(FlightDataBranch.TYPE_NORMAL_FORCE_COEFF, forces.CN);
+                       data.setValue(FlightDataBranch.TYPE_SIDE_FORCE_COEFF, forces.Cside);
+                       data.setValue(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, forces.Croll);
+                       data.setValue(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, forces.CrollForce);
+                       data.setValue(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, forces.CrollDamp);
+                       data.setValue(FlightDataBranch.TYPE_PITCH_DAMPING_MOMENT_COEFF, 
+                                       forces.pitchDampingMoment);
+               }
+                               
+               data.setValue(FlightDataBranch.TYPE_DRAG_COEFF, forces.CD);
+               data.setValue(FlightDataBranch.TYPE_AXIAL_DRAG_COEFF, forces.Caxial);
+               data.setValue(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, forces.frictionCD);
+               data.setValue(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, forces.pressureCD);
+               data.setValue(FlightDataBranch.TYPE_BASE_DRAG_COEFF, forces.baseCD);
+               
+               data.setValue(FlightDataBranch.TYPE_REFERENCE_LENGTH, flightConditions.getRefLength());
+               data.setValue(FlightDataBranch.TYPE_REFERENCE_AREA, flightConditions.getRefArea());
+               
+               
+               data.setValue(FlightDataBranch.TYPE_PITCH_RATE, flightConditions.getPitchRate());
+               data.setValue(FlightDataBranch.TYPE_YAW_RATE, flightConditions.getYawRate());
+               
+
+               
+               if (extra) {
+                       Coordinate c = status.orientation.rotateZ();
+                       double theta = Math.atan2(c.z, MathUtil.hypot(c.x, c.y));
+                       double phi = Math.atan2(c.y, c.x);
+                       if (phi < -(Math.PI-0.0001))
+                               phi = Math.PI;
+                       data.setValue(FlightDataBranch.TYPE_ORIENTATION_THETA, theta);
+                       data.setValue(FlightDataBranch.TYPE_ORIENTATION_PHI, phi);
+               }
+               
+               data.setValue(FlightDataBranch.TYPE_AOA, flightConditions.getAOA());
+               data.setValue(FlightDataBranch.TYPE_ROLL_RATE, flightConditions.getRollRate());
+
+               data.setValue(FlightDataBranch.TYPE_WIND_VELOCITY, windSpeed);
+               data.setValue(FlightDataBranch.TYPE_AIR_TEMPERATURE, 
+                               flightConditions.getAtmosphericConditions().temperature);
+               data.setValue(FlightDataBranch.TYPE_AIR_PRESSURE, 
+                               flightConditions.getAtmosphericConditions().pressure);
+               data.setValue(FlightDataBranch.TYPE_SPEED_OF_SOUND, 
+                               flightConditions.getAtmosphericConditions().getMachSpeed());
+
+               
+               data.setValue(FlightDataBranch.TYPE_TIME_STEP, timestep);
+               data.setValue(FlightDataBranch.TYPE_COMPUTATION_TIME, 
+                               (System.nanoTime() - status.simulationStartTime)/1000000000.0);
+               
+               
+//             data.setValue(FlightDataBranch.TYPE_, 0);
+
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/simulation/SimulationConditions.java b/src/net/sf/openrocket/simulation/SimulationConditions.java
new file mode 100644 (file)
index 0000000..4d932b2
--- /dev/null
@@ -0,0 +1,402 @@
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.aerodynamics.AtmosphericModel;
+import net.sf.openrocket.aerodynamics.ExtendedISAModel;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class SimulationConditions implements ChangeSource, Cloneable {
+
+       public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI/3;
+       
+       /**
+        * The ISA standard atmosphere.
+        */
+       private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
+       
+       
+       private final Rocket rocket;
+       private String motorID = null;
+
+       
+       /*
+        * NOTE:  When adding/modifying parameters, they must also be added to the
+        * equals and copyFrom methods!!
+        */
+
+       // TODO: HIGH: Fetch default values from Prefs!
+       
+       private double launchRodLength = 1;
+       
+       /** Launch rod angle > 0, radians from vertical */ 
+       private double launchRodAngle = 0;
+       
+       /** Launch rod direction, 0 = upwind, PI = downwind. */
+       private double launchRodDirection = 0;
+       
+
+       private double windAverage = 2.0;
+       private double windTurbulence = 0.1;
+       
+       private double launchAltitude = 0;
+       private double launchLatitude = 45;
+       
+       private boolean useISA = true;
+       private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE;
+       private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE;
+       private AtmosphericModel atmosphericModel = null;
+       
+       
+       private double timeStep = RK4Simulator.RECOMMENDED_TIME_STEP;
+       private double maximumAngle = RK4Simulator.RECOMMENDED_ANGLE_STEP;
+       
+       private boolean calculateExtras = true;
+       
+
+       private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
+       
+       
+       
+       public SimulationConditions(Rocket rocket) {
+               this.rocket = rocket;
+       }
+       
+       
+       
+       public Rocket getRocket() {
+               return rocket;
+       }
+               
+       
+       public String getMotorConfigurationID() {
+               return motorID;
+       }
+       
+       public void setMotorConfigurationID(String id) {
+               if (id != null)
+                       id = id.intern();
+               if (id == motorID)
+                       return;
+               motorID = id;
+               fireChangeEvent();
+       }
+       
+
+       public double getLaunchRodLength() {
+               return launchRodLength;
+       }
+
+       public void setLaunchRodLength(double launchRodLength) {
+               if (MathUtil.equals(this.launchRodLength, launchRodLength))
+                       return;
+               this.launchRodLength = launchRodLength;
+               fireChangeEvent();
+       }
+
+       
+       public double getLaunchRodAngle() {
+               return launchRodAngle;
+       }
+
+       public void setLaunchRodAngle(double launchRodAngle) {
+               launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE);
+               if (MathUtil.equals(this.launchRodAngle, launchRodAngle))
+                       return;
+               this.launchRodAngle = launchRodAngle;
+               fireChangeEvent();
+       }
+
+       
+       public double getLaunchRodDirection() {
+               return launchRodDirection;
+       }
+
+       public void setLaunchRodDirection(double launchRodDirection) {
+               launchRodDirection = MathUtil.reduce180(launchRodDirection);
+               if (MathUtil.equals(this.launchRodDirection, launchRodDirection))
+                       return;
+               this.launchRodDirection = launchRodDirection;
+               fireChangeEvent();
+       }
+
+       
+       
+       public double getWindSpeedAverage() {
+               return windAverage;
+       }
+
+       public void setWindSpeedAverage(double windAverage) {
+               if (MathUtil.equals(this.windAverage, windAverage))
+                       return;
+               this.windAverage = MathUtil.max(windAverage, 0);
+               fireChangeEvent();
+       }
+
+       
+       public double getWindSpeedDeviation() {
+               return windAverage * windTurbulence;
+       }
+
+       public void setWindSpeedDeviation(double windDeviation) {
+               if (windAverage < 0.1) {
+                       windAverage = 0.1;
+               }
+               setWindTurbulenceIntensity(windDeviation / windAverage);
+       }
+
+       
+       /**
+     * Return the wind turbulence intensity (standard deviation / average).
+     * 
+     * @return  the turbulence intensity
+     */
+    public double getWindTurbulenceIntensity() {
+        return windTurbulence;
+    }
+
+    /**
+     * Set the wind standard deviation to match the given turbulence intensity.
+     * 
+     * @param intensity   the turbulence intensity
+     */
+    public void setWindTurbulenceIntensity(double intensity) {
+       // Does not check equality so that setWindSpeedDeviation can be sure of event firing
+       this.windTurbulence = intensity;
+       fireChangeEvent();
+    }
+    
+
+       
+       
+       
+       public double getLaunchAltitude() {
+               return launchAltitude;
+       }
+
+       public void setLaunchAltitude(double altitude) {
+               if (MathUtil.equals(this.launchAltitude, altitude))
+                       return;
+               this.launchAltitude = altitude;
+               fireChangeEvent();
+       }
+       
+       
+       public double getLaunchLatitude() {
+               return launchLatitude;
+       }
+
+       public void setLaunchLatitude(double launchLatitude) {
+               launchLatitude = MathUtil.clamp(launchLatitude, -90, 90);
+               if (MathUtil.equals(this.launchLatitude, launchLatitude))
+                       return;
+               this.launchLatitude = launchLatitude;
+               fireChangeEvent();
+       }
+
+
+       
+       
+       
+       public boolean isISAAtmosphere() {
+               return useISA;
+       }
+       
+       public void setISAAtmosphere(boolean isa) {
+               if (isa == useISA)
+                       return;
+               useISA = isa;
+               fireChangeEvent();
+       }
+       
+
+       public double getLaunchTemperature() {
+               return launchTemperature;
+       }
+
+
+
+       public void setLaunchTemperature(double launchTemperature) {
+               if (MathUtil.equals(this.launchTemperature, launchTemperature))
+                       return;
+               this.launchTemperature = launchTemperature;
+               this.atmosphericModel = null;
+               fireChangeEvent();
+       }
+
+
+
+       public double getLaunchPressure() {
+               return launchPressure;
+       }
+
+
+
+       public void setLaunchPressure(double launchPressure) {
+               if (MathUtil.equals(this.launchPressure, launchPressure))
+                       return;
+               this.launchPressure = launchPressure;
+               this.atmosphericModel = null;
+               fireChangeEvent();
+       }
+
+       
+       /**
+        * Returns an atmospheric model corresponding to the launch conditions.  The
+        * atmospheric models may be shared between different calls.
+        * 
+        * @return      an AtmosphericModel object.
+        */
+       public AtmosphericModel getAtmosphericModel() {
+               if (useISA) {
+                       return ISA_ATMOSPHERIC_MODEL;
+               }
+               if (atmosphericModel == null) {
+                       atmosphericModel = new ExtendedISAModel(launchAltitude, 
+                                       launchTemperature, launchPressure);
+               }
+               return atmosphericModel;
+       }
+       
+       
+       public double getTimeStep() {
+               return timeStep;
+       }
+
+       public void setTimeStep(double timeStep) {
+               if (MathUtil.equals(this.timeStep, timeStep))
+                       return;
+               this.timeStep = timeStep;
+               fireChangeEvent();
+       }
+
+       public double getMaximumStepAngle() {
+               return maximumAngle;
+       }
+
+       public void setMaximumStepAngle(double maximumAngle) {
+               maximumAngle = MathUtil.clamp(maximumAngle, 1*Math.PI/180, 20*Math.PI/180);
+               if (MathUtil.equals(this.maximumAngle, maximumAngle))
+                       return;
+               this.maximumAngle = maximumAngle;
+               fireChangeEvent();
+       }
+
+
+
+       public boolean getCalculateExtras() {
+               return calculateExtras;
+       }
+
+
+
+       public void setCalculateExtras(boolean calculateExtras) {
+               if (this.calculateExtras == calculateExtras)
+                       return;
+               this.calculateExtras = calculateExtras;
+               fireChangeEvent();
+       }
+
+
+       
+       @Override
+       public SimulationConditions clone() {
+               try {
+                       SimulationConditions copy = (SimulationConditions)super.clone();
+                       copy.listeners = new ArrayList<ChangeListener>();
+                       return copy;
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException(e);
+               }
+       }
+       
+       
+       public void copyFrom(SimulationConditions src) {
+               if (this.rocket != src.rocket) {
+                       throw new IllegalArgumentException("Unable to copy simulation conditions of "+
+                                       "a difference rocket");
+               }
+               if (this.equals(src))
+                       return;
+               
+               this.motorID = src.motorID;
+               this.launchAltitude = src.launchAltitude;
+               this.launchLatitude = src.launchLatitude;
+               this.launchPressure = src.launchPressure;
+               this.launchRodAngle = src.launchRodAngle;
+               this.launchRodDirection = src.launchRodDirection;
+               this.launchRodLength = src.launchRodLength;
+               this.launchTemperature = src.launchTemperature;
+               this.maximumAngle = src.maximumAngle;
+               this.timeStep = src.timeStep;
+               this.windAverage = src.windAverage;
+               this.windTurbulence = src.windTurbulence;
+               this.calculateExtras = src.calculateExtras;
+               
+               fireChangeEvent();
+       }
+       
+       
+       
+       /**
+        * Compares whether the two simulation conditions are equal.  The two are considered
+        * equal if the rocket, motor id and all variables are equal.
+        */
+       @Override
+       public boolean equals(Object other) {
+               if (!(other instanceof SimulationConditions))
+                       return false;
+               SimulationConditions o = (SimulationConditions)other;
+               return ((this.rocket == o.rocket) &&
+                               this.motorID == o.motorID &&
+                               MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
+                               MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
+                               MathUtil.equals(this.launchPressure, o.launchPressure) &&
+                               MathUtil.equals(this.launchRodAngle, o.launchRodAngle) &&
+                               MathUtil.equals(this.launchRodDirection, o.launchRodDirection) &&
+                               MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
+                               MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
+                               MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
+                               MathUtil.equals(this.timeStep, o.timeStep) &&
+                               MathUtil.equals(this.windAverage, o.windAverage) &&
+                               MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
+                               this.calculateExtras == o.calculateExtras);
+       }
+       
+       /**
+        * Hashcode method compatible with {@link #equals(Object)}.
+        */
+       @Override
+       public int hashCode() {
+               if (motorID == null)
+                       return rocket.hashCode();
+               return rocket.hashCode() + motorID.hashCode();
+       }
+
+       @Override
+       public void addChangeListener(ChangeListener listener) {
+               listeners.add(listener);
+       }
+
+       @Override
+       public void removeChangeListener(ChangeListener listener) {
+               listeners.remove(listener);
+       }
+       
+       private final ChangeEvent event = new ChangeEvent(this);
+       private void fireChangeEvent() {
+               ChangeListener[] array = listeners.toArray(new ChangeListener[0]);
+               
+               for (int i=array.length-1; i >=0; i--) {
+                       array[i].stateChanged(event);
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/simulation/SimulationListener.java b/src/net/sf/openrocket/simulation/SimulationListener.java
new file mode 100644 (file)
index 0000000..0f427a9
--- /dev/null
@@ -0,0 +1,50 @@
+package net.sf.openrocket.simulation;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.simulation.exception.SimulationException;
+
+
+
+public interface SimulationListener {
+
+       
+       public void flightConditions(SimulationStatus status, FlightConditions conditions)
+               throws SimulationException;
+       
+       
+       public void forceCalculation(SimulationStatus status, FlightConditions conditions,
+                       AerodynamicForces forces) throws SimulationException;
+       
+       
+       /**
+        * Called every time a simulation step has been taken.  The parameter contains the
+        * simulation status.  This method may abort the simulation by returning a
+        * <code>SIMULATION_END</code> event.  Note that this event and all others within
+        * the current time are still handled, so be careful not to create an infinite loop
+        * of events.
+        * 
+        * @param status        the current flight status.
+        * @return                      new flight events to handle, or <code>null</code> for none.
+        */
+       public Collection<FlightEvent> stepTaken(SimulationStatus status)
+               throws SimulationException;
+       
+       
+       /**
+        * Called every time an event is handled by the simulation system.  The parameters
+        * contain the event and current simulation status.  This method may abort the 
+        * simulation by returning a <code>SIMULATION_END</code> event.  Note that this 
+        * event and all others within the current time are still handled, so be careful 
+        * not to create an infinite loop of events.
+        * 
+        * @param event         the event that triggered this call.
+        * @param status        the current flight status.
+        * @return                      new flight events to handle, or <code>null</code> for none.
+        */
+       public Collection<FlightEvent> handleEvent(FlightEvent event, SimulationStatus status)
+               throws SimulationException;
+       
+}
diff --git a/src/net/sf/openrocket/simulation/SimulationStatus.java b/src/net/sf/openrocket/simulation/SimulationStatus.java
new file mode 100644 (file)
index 0000000..4d05cbf
--- /dev/null
@@ -0,0 +1,72 @@
+package net.sf.openrocket.simulation;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import net.sf.openrocket.aerodynamics.GravityModel;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.aerodynamics.WindSimulator;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.util.Coordinate;
+
+
+public class SimulationStatus implements Cloneable {
+       
+       public SimulationConditions startConditions;
+
+       public double time;
+       public Configuration configuration;
+       public FlightDataBranch flightData;
+       
+       public Coordinate position;
+       public Coordinate velocity;
+       
+       public WindSimulator windSimulator;
+       public GravityModel gravityModel;
+       
+       public double launchRodLength;
+       
+       
+       /** Nanosecond time when the simulation was started. */
+       public long simulationStartTime = Long.MIN_VALUE;
+       
+       
+       /** Set to true when a motor has ignited. */
+       public boolean motorIgnited = false;
+       
+       /** Set to true when the rocket has risen from the ground. */
+       public boolean liftoff = false;
+       
+       /** <code>true</code> while the rocket is on the launch rod. */
+       public boolean launchRod = true;
+
+       /** Set to true when apogee has been detected. */
+       public boolean apogeeReached = false;
+        
+       /** Contains a list of deployed recovery devices. */
+       public final Set<RecoveryDevice> deployedRecoveryDevices = new HashSet<RecoveryDevice>();
+       
+       
+       public WarningSet warnings;
+       
+       
+       /** Available for special purposes by the listeners. */
+       public Object extra = null;
+       
+       
+       /**
+        * Returns a (shallow) copy of this object.  The general purpose is that the 
+        * conditions, flight data etc. point to the same objects.  However, subclasses 
+        * are allowed to deep-clone specific objects, such as those pertaining to the 
+        * current orientation of the rocket.
+        */
+       @Override
+       public SimulationStatus clone() {
+               try {
+                       return (SimulationStatus) super.clone();
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("BUG:  CloneNotSupportedException?!?",e);
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java b/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java
new file mode 100644 (file)
index 0000000..e229343
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.simulation.exception;
+
+
+/**
+ * An exception signifying that a simulation was cancelled.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SimulationCancelledException extends SimulationException {
+
+       public SimulationCancelledException() {
+
+       }
+
+       public SimulationCancelledException(String message) {
+               super(message);
+       }
+
+       public SimulationCancelledException(Throwable cause) {
+               super(cause);
+       }
+
+       public SimulationCancelledException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/exception/SimulationException.java b/src/net/sf/openrocket/simulation/exception/SimulationException.java
new file mode 100644 (file)
index 0000000..f180f89
--- /dev/null
@@ -0,0 +1,21 @@
+package net.sf.openrocket.simulation.exception;
+
+public class SimulationException extends Exception {
+
+       public SimulationException() {
+
+       }
+
+       public SimulationException(String message) {
+               super(message);
+       }
+
+       public SimulationException(Throwable cause) {
+               super(cause);
+       }
+
+       public SimulationException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java b/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java
new file mode 100644 (file)
index 0000000..1c840e3
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.simulation.exception;
+
+/**
+ * An exception signifying that a problem occurred at launch, for example
+ * that no motors were defined or no motors ignited.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SimulationLaunchException extends SimulationException {
+
+       public SimulationLaunchException() {
+
+       }
+
+       public SimulationLaunchException(String message) {
+               super(message);
+       }
+
+       public SimulationLaunchException(Throwable cause) {
+               super(cause);
+       }
+
+       public SimulationLaunchException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java b/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java
new file mode 100644 (file)
index 0000000..d6bd737
--- /dev/null
@@ -0,0 +1,21 @@
+package net.sf.openrocket.simulation.exception;
+
+
+public class SimulationListenerException extends SimulationException {
+
+       public SimulationListenerException() {
+       }
+
+       public SimulationListenerException(String message) {
+               super(message);
+       }
+
+       public SimulationListenerException(Throwable cause) {
+               super(cause);
+       }
+
+       public SimulationListenerException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java b/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java
new file mode 100644 (file)
index 0000000..6661825
--- /dev/null
@@ -0,0 +1,30 @@
+package net.sf.openrocket.simulation.exception;
+
+
+/**
+ * A exception that signifies that the attempted simulation is not supported.  
+ * The reason for not being supported may be due to unsupported combination of
+ * simulator/calculator, unsupported rocket structure or other reasons.
+ * <p>
+ * This exception signifies a fatal problem in the simulation; for non-fatal conditions
+ * add a warning to the simulation results.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SimulationNotSupportedException extends SimulationException {
+
+       public SimulationNotSupportedException() {
+       }
+
+       public SimulationNotSupportedException(String message) {
+               super(message);
+       }
+
+       public SimulationNotSupportedException(Throwable cause) {
+               super(cause);
+       }
+
+       public SimulationNotSupportedException(String message, Throwable cause) {
+               super(message, cause);
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java
new file mode 100644 (file)
index 0000000..d662450
--- /dev/null
@@ -0,0 +1,41 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationException;
+
+
+public abstract class AbstractSimulationListener implements SimulationListener {
+
+       @Override 
+       public void flightConditions(SimulationStatus status, FlightConditions conditions)
+               throws SimulationException {
+               // No-op
+       }
+       
+       @Override
+       public void forceCalculation(SimulationStatus status, FlightConditions conditions,
+                       AerodynamicForces forces) throws SimulationException {
+               // No-op
+       }
+
+       @Override
+       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                       SimulationStatus status) throws SimulationException {
+               // No-op
+               return null;
+       }
+
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status)
+               throws SimulationException {
+               // No-op
+               return null;
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/ApogeeEndListener.java b/src/net/sf/openrocket/simulation/listeners/ApogeeEndListener.java
new file mode 100644 (file)
index 0000000..7f5f8fd
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationException;
+
+
+/**
+ * A simulation listeners that ends the simulation when apogee is reached.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ApogeeEndListener extends AbstractSimulationListener {
+
+       public static final ApogeeEndListener INSTANCE = new ApogeeEndListener();
+       
+       @Override
+       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                       SimulationStatus status) throws SimulationException {
+
+               if (event.getType() == FlightEvent.Type.APOGEE) {
+                       return Collections.singleton(new FlightEvent(FlightEvent.Type.SIMULATION_END, 
+                                       status.time));
+               }
+               return null;
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/CSVSaveListener.java b/src/net/sf/openrocket/simulation/listeners/CSVSaveListener.java
new file mode 100644 (file)
index 0000000..dea0d01
--- /dev/null
@@ -0,0 +1,309 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.Iterator;
+
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class CSVSaveListener extends AbstractSimulationListener {
+
+       private static enum Types {
+               TIME {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.time;
+                       }
+               },
+               POSITION_X {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.position.x;
+                       }
+               },
+               POSITION_Y {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.position.y;
+                       }
+               },
+               ALTITUDE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.position.z;
+                       }
+               },
+               VELOCITY_X {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.velocity.x;
+                       }
+               },
+               VELOCITY_Y {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.velocity.y;
+                       }
+               },
+               VELOCITY_Z {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.velocity.z;
+                       }
+               },
+               THETA {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ORIENTATION_THETA);
+                       }
+               },
+               PHI {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ORIENTATION_PHI);
+                       }
+               },
+               AOA {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_AOA);
+                       }
+               },
+               ROLLRATE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_RATE);
+                       }
+               },
+               PITCHRATE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_PITCH_RATE);
+                       }
+               },
+               
+               PITCHMOMENT {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_PITCH_MOMENT_COEFF);
+                       }
+               },
+               YAWMOMENT {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_YAW_MOMENT_COEFF);
+                       }
+               },
+               ROLLMOMENT {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF);
+                       }
+               },
+               NORMALFORCE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_NORMAL_FORCE_COEFF);
+                       }
+               },
+               SIDEFORCE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_SIDE_FORCE_COEFF);
+                       }
+               },
+               AXIALFORCE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_DRAG_FORCE);
+                       }
+               },
+               WINDSPEED {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_WIND_VELOCITY);
+                       }
+               },
+               PITCHDAMPING {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.
+                                               TYPE_PITCH_DAMPING_MOMENT_COEFF);
+                       }
+               },
+               CA {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_AXIAL_DRAG_COEFF);
+                       }
+               },
+               CD {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_DRAG_COEFF);
+                       }
+               },
+               CDpressure {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF);
+                       }
+               },
+               CDfriction {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF);
+                       }
+               },
+               CDbase {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_BASE_DRAG_COEFF);
+                       }
+               },
+               MACH {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_MACH_NUMBER);
+                       }
+               },
+               RE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_REYNOLDS_NUMBER);
+                       }
+               },
+               
+               CONTROL_ANGLE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               Iterator<RocketComponent> iterator = 
+                                       status.configuration.getRocket().deepIterator();
+                               FinSet fin = null;
+                               
+                               while (iterator.hasNext()) {
+                                       RocketComponent c = iterator.next();
+                                       if (c instanceof FinSet && c.getName().equals("CONTROL")) {
+                                               fin = (FinSet)c;
+                                               break;
+                                       }
+                               }
+                               if (fin==null)
+                                       return 0;
+                               return fin.getCantAngle();
+                       }
+               },
+               
+               EXTRA {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               if (status.extra instanceof Double)
+                                       return (Double)status.extra;
+                               else
+                                       return Double.NaN;
+                       }
+               },
+               
+               MASS {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_MASS);
+                       }
+               }
+               
+               ;
+               
+               public abstract double getValue(SimulationStatus status);
+       }
+       
+       
+       public static final String FILENAME_FORMAT = "simulation-%03d.csv";
+       
+       private File file;
+       private PrintStream output = null;
+       
+               
+       
+       @Override
+       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                       SimulationStatus status) {
+
+               if (event.getType() == FlightEvent.Type.LAUNCH) {
+                       int n = 1;
+
+                       if (output != null) {
+                               System.err.println("WARNING: Ending simulation logging to CSV file " +
+                                               "(SIMULATION_END not encountered).");
+                               output.close();
+                               output = null;
+                       }
+                       
+                       do {
+                               file = new File(String.format(FILENAME_FORMAT, n));
+                               n++;
+                       } while (file.exists());
+                       
+                       System.err.println("Opening file "+file+" for CSV output.");
+                       try {
+                               output = new PrintStream(file);
+                       } catch (FileNotFoundException e) {
+                               System.err.println("ERROR OPENING FILE: "+e);
+                       }
+                       
+                       final Types[] types = Types.values();
+                       StringBuilder s = new StringBuilder("# " + types[0].toString());
+                       for (int i=1; i<types.length; i++) {
+                               s.append("," + types[i].toString());
+                       }
+                       output.println(s);
+                       
+               } else if (event.getType() == FlightEvent.Type.SIMULATION_END && output != null) {
+                       
+                       System.err.println("Ending simulation logging to CSV file: "+file);
+                       output.close();
+                       output = null;
+                       
+               } else if (event.getType() != FlightEvent.Type.ALTITUDE){
+                       
+                       if (output != null) {
+                               output.println("# Event "+event);
+                       } else {
+                               System.err.println("WARNING: Event "+event+" encountered without open file");
+                       }
+                       
+               }
+               
+               return null;
+       }
+
+       
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+               final Types[] types = Types.values();
+               StringBuilder s;
+               
+               if (output != null) {
+                       
+                       s = new StringBuilder("" + types[0].getValue(status));
+                       for (int i=1; i<types.length; i++) {
+                               s.append("," + types[i].getValue(status));
+                       }
+                       output.println(s);
+               
+               } else {
+                       
+                       System.err.println("WARNING: stepTaken called with no open file " +
+                                       "(t="+status.time+")");
+               }
+
+               return null;
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/InterruptListener.java b/src/net/sf/openrocket/simulation/listeners/InterruptListener.java
new file mode 100644 (file)
index 0000000..44c2a25
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+
+
+/**
+ * A simulation listener that throws a {@link SimulationCancelledException} if
+ * this thread has been interrupted.  The conditions is checked every time a step
+ * is taken.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class InterruptListener extends AbstractSimulationListener {
+       
+       public static final InterruptListener INSTANCE = new InterruptListener();
+
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status) 
+       throws SimulationCancelledException {
+
+               if (Thread.interrupted()) {
+                       throw new SimulationCancelledException("The simulation was interrupted.");
+               }
+
+               return null;
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/PrintSimulationListener.java b/src/net/sf/openrocket/simulation/listeners/PrintSimulationListener.java
new file mode 100644 (file)
index 0000000..85717ee
--- /dev/null
@@ -0,0 +1,38 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class PrintSimulationListener extends AbstractSimulationListener {
+
+       @Override
+       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                       SimulationStatus status) {
+
+               System.out.println("*** handleEvent *** "+event.toString() + 
+                               " position="+status.position + " velocity="+status.velocity);
+               return null;
+       }
+
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+               FlightDataBranch data = status.flightData;
+               System.out.printf("*** stepTaken *** time=%.3f position="+status.position+
+                               " velocity="+status.velocity+"=%.3f\n", status.time, status.velocity.length());
+               System.out.printf("                  thrust=%.3fN drag==%.3fN mass=%.3fkg " +
+                               "accZ=%.3fm/s2 acc=%.3fm/s2\n", 
+                               data.getLast(FlightDataBranch.TYPE_THRUST_FORCE),
+                               data.getLast(FlightDataBranch.TYPE_DRAG_FORCE),
+                               data.getLast(FlightDataBranch.TYPE_MASS),
+                               data.getLast(FlightDataBranch.TYPE_ACCELERATION_Z),
+                               data.getLast(FlightDataBranch.TYPE_ACCELERATION_TOTAL));
+               
+               return null;
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/RollSaveListener.java b/src/net/sf/openrocket/simulation/listeners/RollSaveListener.java
new file mode 100644 (file)
index 0000000..d80f423
--- /dev/null
@@ -0,0 +1,159 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.util.Collection;
+
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class RollSaveListener extends AbstractSimulationListener {
+
+       private static enum Types {
+               TIME {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.time;
+                       }
+               },
+               ALTITUDE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.position.z;
+                       }
+               },
+               VELOCITY_Z {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.velocity.z;
+                       }
+               },
+               THETA {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ORIENTATION_THETA);
+                       }
+               },
+               AOA {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_AOA);
+                       }
+               },
+               ROLLRATE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_RATE);
+                       }
+               },
+               PITCHRATE {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_PITCH_RATE);
+                       }
+               },
+               ROLLMOMENT {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF);
+                       }
+               },
+               MACH {
+                       @Override
+                       public double getValue(SimulationStatus status) {
+                               return status.flightData.getLast(FlightDataBranch.TYPE_MACH_NUMBER);
+                       }
+               },
+
+               ;
+               
+               public abstract double getValue(SimulationStatus status);
+       }
+       
+       
+       public static final String FILENAME_FORMAT = "simulation-%03d.csv";
+       
+       private File file;
+       private PrintStream output = null;
+       
+               
+       
+       @Override
+       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                       SimulationStatus status) {
+
+               if (event.getType() == FlightEvent.Type.LAUNCH) {
+                       int n = 1;
+
+                       if (output != null) {
+                               System.err.println("WARNING: Ending simulation logging to CSV file " +
+                                               "(SIMULATION_END not encountered).");
+                               output.close();
+                               output = null;
+                       }
+                       
+                       do {
+                               file = new File(String.format(FILENAME_FORMAT, n));
+                               n++;
+                       } while (file.exists());
+                       
+                       System.err.println("Opening file "+file+" for CSV output.");
+                       try {
+                               output = new PrintStream(file);
+                       } catch (FileNotFoundException e) {
+                               System.err.println("ERROR OPENING FILE: "+e);
+                       }
+                       
+                       final Types[] types = Types.values();
+                       StringBuilder s = new StringBuilder("# " + types[0].toString());
+                       for (int i=1; i<types.length; i++) {
+                               s.append("," + types[i].toString());
+                       }
+                       output.println(s);
+                       
+               } else if (event.getType() == FlightEvent.Type.SIMULATION_END && output != null) {
+                       
+                       System.err.println("Ending simulation logging to CSV file: "+file);
+                       output.close();
+                       output = null;
+                       
+               } else if (event.getType() != FlightEvent.Type.ALTITUDE){
+                       
+                       if (output != null) {
+                               output.println("# Event "+event);
+                       } else {
+                               System.err.println("WARNING: Event "+event+" encountered without open file");
+                       }
+                       
+               }
+               
+               return null;
+       }
+
+       
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+               final Types[] types = Types.values();
+               StringBuilder s;
+               
+               if (output != null) {
+                       
+                       s = new StringBuilder("" + types[0].getValue(status));
+                       for (int i=1; i<types.length; i++) {
+                               s.append("," + types[i].getValue(status));
+                       }
+                       output.println(s);
+               
+               } else {
+                       
+                       System.err.println("WARNING: stepTaken called with no open file " +
+                                       "(t="+status.time+")");
+               }
+
+               return null;
+       }
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/StopSimulationListener.java b/src/net/sf/openrocket/simulation/listeners/StopSimulationListener.java
new file mode 100644 (file)
index 0000000..ece85e2
--- /dev/null
@@ -0,0 +1,59 @@
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class StopSimulationListener extends AbstractSimulationListener {
+
+       private final int REPORT = 500;
+       
+       private final double stopTime;
+       private final int stopStep;
+
+       private int step = 0;
+
+       private long startTime = -1;
+       private long time = -1;
+       
+       public StopSimulationListener(double t, int n) {
+               stopTime = t;
+               stopStep = n;
+       }
+       
+       
+       @Override
+       public Collection<FlightEvent> handleEvent(FlightEvent event,
+                       SimulationStatus status) {
+
+               if (event.getType() == FlightEvent.Type.LAUNCH) {
+                       System.out.println("Simulation starting.");
+                       time = System.nanoTime();
+                       startTime = System.nanoTime();
+               }
+               
+               return null;
+       }
+
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+               step ++;
+               if ((step%REPORT) == 0) {
+                       long t = System.nanoTime();
+                       
+                       System.out.printf("Step %4d, time=%.3f, took %d us/step (avg. %d us/step)\n",
+                                       step,status.time,(t-time)/1000/REPORT,(t-startTime)/1000/step);
+                       time = t;
+               }
+               if (status.time >= stopTime || step >= stopStep) {
+                       System.out.printf("Stopping simulation, step=%d time=%.3f\n",step,status.time);
+                       return Collections.singleton(new FlightEvent(FlightEvent.Type.SIMULATION_END,
+                                       status.time, null));
+               }
+               return null;
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/haisu/HaisuCatoListener.java b/src/net/sf/openrocket/simulation/listeners/haisu/HaisuCatoListener.java
new file mode 100644 (file)
index 0000000..2790d92
--- /dev/null
@@ -0,0 +1,38 @@
+package net.sf.openrocket.simulation.listeners.haisu;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+
+public class HaisuCatoListener extends AbstractSimulationListener {
+
+       private static final double POSITION = 0.8;
+       private static final double CNA = 5.16;
+       
+       private final double alpha;
+
+       public HaisuCatoListener(double alpha) {
+               this.alpha = alpha;
+       }
+       
+       @Override
+       public void forceCalculation(SimulationStatus status, FlightConditions conditions, 
+                       AerodynamicForces forces) {
+
+               double cn = CNA * alpha;
+               double cm = cn * POSITION / conditions.getRefLength();
+               
+               double theta = conditions.getTheta();
+               double costheta = Math.cos(theta);
+               
+               forces.CN += cn * costheta;
+               forces.Cm += cm * costheta;
+               
+               if (Math.abs(costheta) < 0.99) {
+                       System.err.println("THETA = "+(theta*180/Math.PI)+ " aborting...");
+                       System.exit(1);
+               }
+       }
+
+}
diff --git a/src/net/sf/openrocket/simulation/listeners/haisu/RollControlListener.java b/src/net/sf/openrocket/simulation/listeners/haisu/RollControlListener.java
new file mode 100644 (file)
index 0000000..139996a
--- /dev/null
@@ -0,0 +1,124 @@
+package net.sf.openrocket.simulation.listeners.haisu;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class RollControlListener extends AbstractSimulationListener {
+
+       private static final double DELTA_T = 0.01;
+       private static final double START_TIME = 0.5;
+       
+       private static final double MACH = 0.9;
+       
+       private static final double SETPOINT = 0.0;
+       
+       private static final double TURNRATE = 10 * Math.PI/180;  // per second
+
+       
+       /*
+        * At M=0.3 KP oscillation threshold between 0.35 and 0.4.    Good KI=3
+        * At M=0.6 KP oscillation threshold between 0.07 and 0.08    Good KI=2
+        * At M=0.9 KP oscillation threshold between 0.013 and 0.014  Good KI=0.5
+        */
+       private static final double KP = 0.007;
+       private static final double KI = 0.2;
+       
+       
+       private static final double MAX_ANGLE = 15 * Math.PI/180;
+       
+       
+       
+       
+       private double rollrate;
+       
+       private double intState = 0;
+       
+       private double finPosition = 0;
+       
+       
+       public RollControlListener() {
+       }
+
+       @Override
+       public void flightConditions(SimulationStatus status, FlightConditions conditions) {
+               // Limit movement:
+               
+//             conditions.setAOA(0);
+//             conditions.setTheta(0);
+//             conditions.setMach(MACH);
+//             conditions.setPitchRate(0);
+//             conditions.setYawRate(0);
+//             status.position = new Coordinate(0,0,100);
+//             status.velocity = Coordinate.NUL;
+
+               
+               rollrate = conditions.getRollRate();
+       }
+       
+       @Override
+       public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+               
+               if (status.time < START_TIME)
+                       return null;
+
+               // PID controller
+               FinSet finset = null;
+               for (RocketComponent c: status.configuration) {
+                       if ((c instanceof FinSet) && (c.getName().equals("CONTROL"))) {
+                               finset = (FinSet)c;
+                               break;
+                       }
+               }
+               if (finset==null) {
+                       throw new RuntimeException("CONTROL fin not found");
+               }
+               
+               
+               double error = SETPOINT - rollrate;
+               
+               
+               error = Math.signum(error) * error * error;    ////  pow2(error)
+
+               double p = KP * error;
+               intState += error * DELTA_T;
+               double i = KI * intState;
+               
+               double value = p+i;
+               
+                               
+               if (Math.abs(value) > MAX_ANGLE) {
+                       System.err.printf("Attempting to set angle %.1f at t=%.3f, clamping.\n", 
+                                       value*180/Math.PI, status.time);
+                       value = MathUtil.clamp(value, -MAX_ANGLE, MAX_ANGLE);
+               }
+               
+               
+               if (finPosition < value) {
+                       finPosition = Math.min(finPosition + TURNRATE*DELTA_T, value);
+               } else {
+                       finPosition = Math.max(finPosition - TURNRATE*DELTA_T, value);
+               }
+
+               if (MathUtil.equals(status.time*10, Math.rint(status.time*10))) {
+                       System.err.printf("t=%.3f  angle=%.1f  current=%.1f\n",status.time, 
+                                       value*180/Math.PI, finPosition*180/Math.PI);
+               }
+
+               finset.setCantAngle(finPosition);
+                               
+               return null;
+       }
+       
+       
+       
+       
+       
+}
diff --git a/src/net/sf/openrocket/unit/CaliberUnit.java b/src/net/sf/openrocket/unit/CaliberUnit.java
new file mode 100644 (file)
index 0000000..4d9784c
--- /dev/null
@@ -0,0 +1,102 @@
+package net.sf.openrocket.unit;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class CaliberUnit extends GeneralUnit {
+
+       public static final double DEFAULT_CALIBER = 0.01;
+       
+       private final Configuration configuration;
+       private final Rocket rocket;
+       
+       private double caliber = -1;
+       
+       
+       /* Listener for rocket and configuration, resets the caliber to -1. */
+       private final ChangeListener listener = new ChangeListener() {
+               @Override
+               public void stateChanged(ChangeEvent e) {
+                       caliber = -1;
+               }
+       };
+       
+       
+       
+       public CaliberUnit(Configuration configuration) {
+               super(1.0, "cal");
+               this.configuration = configuration;
+               
+               if (configuration == null) {
+                       this.rocket = null;
+               } else {
+                       this.rocket = configuration.getRocket();
+                       configuration.addChangeListener(listener);
+               }
+       }
+       
+       public CaliberUnit(Rocket rocket) {
+               super(1.0, "cal");
+               this.configuration = null;
+               this.rocket = rocket;
+               if (rocket != null) {
+                       rocket.addChangeListener(listener);
+               }
+       }
+       
+       
+       @Override
+       public double fromUnit(double value) {
+               if (caliber < 0)
+                       calculateCaliber();
+               
+               return value * caliber;
+       }
+
+       @Override
+       public double toUnit(double value) {
+               if (caliber < 0)
+                       calculateCaliber();
+               
+               return value / caliber;
+       }
+
+       
+       // TODO: HIGH:  Check caliber calculation method...
+       private void calculateCaliber() {
+               caliber = 0;
+               
+               Iterator<RocketComponent> iterator;
+               if (configuration != null) {
+                       iterator = configuration.iterator();
+               } else if (rocket != null) {
+                       iterator = rocket.deepIterator();
+               } else {
+                       Collection<RocketComponent> set = Collections.emptyList();
+                       iterator = set.iterator();
+               }
+               
+               while (iterator.hasNext()) {
+                       RocketComponent c = iterator.next();
+                       if (c instanceof SymmetricComponent) {
+                               double r1 = ((SymmetricComponent)c).getForeRadius() * 2;
+                               double r2 = ((SymmetricComponent)c).getAftRadius() * 2;
+                               caliber = MathUtil.max(caliber, r1, r2);
+                       }
+               }
+               
+               if (caliber <= 0)
+                       caliber = DEFAULT_CALIBER;
+       }
+}
diff --git a/src/net/sf/openrocket/unit/DegreeUnit.java b/src/net/sf/openrocket/unit/DegreeUnit.java
new file mode 100644 (file)
index 0000000..e694aa6
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.unit;
+
+import java.text.DecimalFormat;
+
+public class DegreeUnit extends GeneralUnit {
+
+       public DegreeUnit() {
+               super(Math.PI/180.0,"\u00b0");
+       }
+
+       @Override
+       public boolean hasSpace() {
+               return false;
+       }
+
+       @Override
+       public double round(double v) {
+               return Math.rint(v);
+       }
+
+       private final DecimalFormat decFormat = new DecimalFormat("0.#");
+       @Override
+       public String toString(double value) {
+               double val = toUnit(value);
+               return decFormat.format(val);
+       }
+}
diff --git a/src/net/sf/openrocket/unit/FixedPrecisionUnit.java b/src/net/sf/openrocket/unit/FixedPrecisionUnit.java
new file mode 100644 (file)
index 0000000..2946dc7
--- /dev/null
@@ -0,0 +1,137 @@
+package net.sf.openrocket.unit;
+
+import java.util.ArrayList;
+
+public class FixedPrecisionUnit extends Unit {
+       
+       private final double precision;
+       private final String formatString;
+
+       public FixedPrecisionUnit(String unit, double precision) {
+               this(unit, precision, 1.0);
+       }
+       
+       public FixedPrecisionUnit(String unit, double precision, double multiplier) {
+               super(multiplier, unit);
+               
+               this.precision = precision;
+               
+               int decimals = 0;
+               double p = precision;
+               while ((p - Math.floor(p)) > 0.0000001) {
+                       p *= 10;
+                       decimals++;
+               }
+               formatString = "%." + decimals + "f";
+       }
+       
+
+       @Override
+       public double getNextValue(double value) {
+               return round(value + precision);
+       }
+
+       @Override
+       public double getPreviousValue(double value) {
+               return round(value - precision);
+       }
+
+
+       @Override
+       public double round(double value) {
+               return Math.rint(value/precision)*precision;
+       }
+
+       
+       
+
+       @Override
+       public String toString(double value) {
+               return String.format(formatString, value);
+       }
+       
+       
+       
+       // TODO: LOW: This is copied from GeneralUnit, perhaps combine
+       @Override
+       public Tick[] getTicks(double start, double end, double minor, double major) {
+               // Convert values
+               start = toUnit(start);
+               end = toUnit(end);
+               minor = toUnit(minor);
+               major = toUnit(major);
+               
+               if (minor <= 0 || major <= 0 || major < minor) {
+                       throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major);
+               }
+               
+               ArrayList<Tick> ticks = new ArrayList<Tick>();
+               
+               int mod2,mod3,mod4;  // Moduli for minor-notable, major-nonnotable, major-notable
+               double minstep;
+
+               // Find the smallest possible step size
+               double one=1;
+               while (one > minor)
+                       one /= 10;
+               while (one < minor)
+                       one *= 10;
+               // one is the smallest round-ten that is larger than minor
+               if (one/2 >= minor) {
+                       // smallest step is round-five
+                       minstep = one/2;
+                       mod2 = 2;  // Changed later if clashes with major ticks
+               } else {
+                       minstep = one;
+                       mod2 = 10;  // Changed later if clashes with major ticks
+               }
+               
+               // Find step size for major ticks
+               one = 1;
+               while (one > major)
+                       one /= 10;
+               while (one < major)
+                       one *= 10;
+               if (one/2 >= major) {
+                       // major step is round-five, major-notable is next round-ten
+                       double majorstep = one/2;
+                       mod3 = (int)Math.round(majorstep/minstep);
+                       mod4 = mod3*2;
+               } else {
+                       // major step is round-ten, major-notable is next round-ten
+                       mod3 = (int)Math.round(one/minstep);
+                       mod4 = mod3*10;
+               }
+               // Check for clashes between minor-notable and major-nonnotable
+               if (mod3 == mod2) {
+                       if (mod2==2)
+                               mod2 = 1;  // Every minor tick is notable
+                       else
+                               mod2 = 5;  // Every fifth minor tick is notable
+               }
+
+
+               // Calculate starting position
+               int pos = (int)Math.ceil(start/minstep);
+//             System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4);
+               while (pos*minstep <= end) {
+                       double unitValue = pos*minstep;
+                       double value = fromUnit(unitValue);
+                       
+                       if (pos%mod4 == 0)
+                               ticks.add(new Tick(value,unitValue,true,true));
+                       else if (pos%mod3 == 0)
+                               ticks.add(new Tick(value,unitValue,true,false));
+                       else if (pos%mod2 == 0)
+                               ticks.add(new Tick(value,unitValue,false,true));
+                       else
+                               ticks.add(new Tick(value,unitValue,false,false));
+                       
+                       pos++;
+               }
+               
+               return ticks.toArray(new Tick[0]);
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/unit/GeneralUnit.java b/src/net/sf/openrocket/unit/GeneralUnit.java
new file mode 100644 (file)
index 0000000..d604fe6
--- /dev/null
@@ -0,0 +1,228 @@
+package net.sf.openrocket.unit;
+
+import java.util.ArrayList;
+
+public class GeneralUnit extends Unit {
+
+       private final int significantNumbers;
+       private final int decimalRounding;
+       
+       // Values smaller that this are rounded using decimal rounding
+       // [pre-calculated as 10^(significantNumbers-1)]
+       private final double decimalLimit; 
+       
+       // Pre-calculated as 10^significantNumbers
+       private final double significantNumbersLimit;
+       
+       
+       public GeneralUnit(double multiplier, String unit) {
+               this(multiplier, unit, 2, 10);
+       }
+       
+       public GeneralUnit(double multiplier, String unit, int significantNumbers) {
+               this(multiplier, unit, significantNumbers, 10);
+       }
+       
+       public GeneralUnit(double multiplier, String unit, int significantNumbers, int decimalRounding) {
+               super(multiplier, unit);
+               assert(significantNumbers>0);
+               assert(decimalRounding>0);
+               
+               this.significantNumbers = significantNumbers;
+               this.decimalRounding = decimalRounding;
+               
+               double d=1;
+               double e=10;
+               for (int i=1; i<significantNumbers; i++) {
+                       d *= 10.0;
+                       e *= 10.0;
+               }
+               decimalLimit = d;
+               significantNumbersLimit = e;
+       }
+
+       @Override
+       public double round(double value) {
+               if (value < decimalLimit) {
+                       // Round to closest 1/decimalRounding
+                       return Math.rint(value*decimalRounding)/decimalRounding;
+               } else {
+                       // Round to given amount of significant numbers
+                       double m = 1;
+                       while (value >= significantNumbersLimit) {
+                               m *= 10.0;
+                               value /= 10.0;
+                       }
+                       return Math.rint(value)*m;
+               }
+       }
+
+       
+       
+
+       // TODO: LOW: untested
+       // start, end and scale in this units
+//     @Override
+       public ArrayList<Tick> getTicks(double start, double end, double scale) {
+               ArrayList<Tick> ticks = new ArrayList<Tick>();
+               double delta;
+               int normal, major;
+
+               // TODO: LOW: more fine-grained (e.g.  0||||5||||10||||15||||20)
+               if (scale <= 1.0/decimalRounding) {
+                       delta = 1.0/decimalRounding;
+                       normal = 1;
+                       major = decimalRounding;
+               } else if (scale <= 1.0) {
+                       delta = 1.0/decimalRounding;
+                       normal = decimalRounding;
+                       major = decimalRounding*10;
+               } else {
+                       double r = scale;
+                       delta = 1;
+                       while (r > 10) {
+                               r /= 10;
+                               delta *= 10;
+                       }
+                       normal = 10;
+                       major = 100;   // TODO: LOW: More fine-grained with 5
+               }
+               
+               double v = Math.ceil(start/delta)*delta;
+               int n = (int)Math.round(v/delta);
+               
+//             while (v <= end) {
+//                     if (n%major == 0)
+//                             ticks.add(new Tick(v,Tick.MAJOR));
+//                     else if (n%normal == 0)
+//                             ticks.add(new Tick(v,Tick.NORMAL));
+//                     else
+//                             ticks.add(new Tick(v,Tick.MINOR));
+//                     v += delta;
+//                     n++;
+//             }
+               
+               return ticks;
+       }
+       
+       @Override
+       public Tick[] getTicks(double start, double end, double minor, double major) {
+               // Convert values
+               start = toUnit(start);
+               end = toUnit(end);
+               minor = toUnit(minor);
+               major = toUnit(major);
+               
+               if (minor <= 0 || major <= 0 || major < minor) {
+                       throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major);
+               }
+               
+               ArrayList<Tick> ticks = new ArrayList<Tick>();
+               
+               int mod2,mod3,mod4;  // Moduli for minor-notable, major-nonnotable, major-notable
+               double minstep;
+
+               // Find the smallest possible step size
+               double one=1;
+               while (one > minor)
+                       one /= 10;
+               while (one < minor)
+                       one *= 10;
+               // one is the smallest round-ten that is larger than minor
+               if (one/2 >= minor) {
+                       // smallest step is round-five
+                       minstep = one/2;
+                       mod2 = 2;  // Changed later if clashes with major ticks
+               } else {
+                       minstep = one;
+                       mod2 = 10;  // Changed later if clashes with major ticks
+               }
+               
+               // Find step size for major ticks
+               one = 1;
+               while (one > major)
+                       one /= 10;
+               while (one < major)
+                       one *= 10;
+               if (one/2 >= major) {
+                       // major step is round-five, major-notable is next round-ten
+                       double majorstep = one/2;
+                       mod3 = (int)Math.round(majorstep/minstep);
+                       mod4 = mod3*2;
+               } else {
+                       // major step is round-ten, major-notable is next round-ten
+                       mod3 = (int)Math.round(one/minstep);
+                       mod4 = mod3*10;
+               }
+               // Check for clashes between minor-notable and major-nonnotable
+               if (mod3 == mod2) {
+                       if (mod2==2)
+                               mod2 = 1;  // Every minor tick is notable
+                       else
+                               mod2 = 5;  // Every fifth minor tick is notable
+               }
+
+
+               // Calculate starting position
+               int pos = (int)Math.ceil(start/minstep);
+//             System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4);
+               while (pos*minstep <= end) {
+                       double unitValue = pos*minstep;
+                       double value = fromUnit(unitValue);
+                       
+                       if (pos%mod4 == 0)
+                               ticks.add(new Tick(value,unitValue,true,true));
+                       else if (pos%mod3 == 0)
+                               ticks.add(new Tick(value,unitValue,true,false));
+                       else if (pos%mod2 == 0)
+                               ticks.add(new Tick(value,unitValue,false,true));
+                       else
+                               ticks.add(new Tick(value,unitValue,false,false));
+                       
+                       pos++;
+               }
+               
+               return ticks.toArray(new Tick[0]);
+       }
+       
+       
+       @Override
+       public double getNextValue(double value) {
+               // TODO: HIGH: Auto-generated method stub
+               return value+1;
+       }
+
+       @Override
+       public double getPreviousValue(double value) {
+               // TODO: HIGH: Auto-generated method stub
+               return value-1;
+       }
+       
+       
+       ///// TESTING:
+       
+       private static void printTicks(double start, double end, double minor, double major) {
+               Tick[] ticks = Unit.NOUNIT2.getTicks(start, end, minor, major);
+               String str = "Ticks for ("+start+","+end+","+minor+","+major+"):";
+               for (int i=0; i<ticks.length; i++) {
+                       str += " "+ticks[i].value;
+                       if (ticks[i].major) {
+                               if (ticks[i].notable)
+                                       str += "*";
+                               else
+                                       str += "o";
+                       } else {
+                               if (ticks[i].notable)
+                                       str += "_";
+                               else
+                                       str += " ";
+                       }
+               }
+               System.out.println(str);
+       }
+       public static void main(String[] arg) {
+               printTicks(0,100,1,10);
+               printTicks(4.7,11.0,0.15,0.7);
+       }
+       
+}
diff --git a/src/net/sf/openrocket/unit/RadianUnit.java b/src/net/sf/openrocket/unit/RadianUnit.java
new file mode 100644 (file)
index 0000000..18f1aff
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.unit;
+
+import java.text.DecimalFormat;
+
+public class RadianUnit extends GeneralUnit {
+
+       public RadianUnit() {
+               super(1,"rad");
+       }
+
+       @Override
+       public double round(double v) {
+               return Math.rint(v*10.0)/10.0;
+       }
+
+       private final DecimalFormat decFormat = new DecimalFormat("0.0");
+       @Override
+       public String toString(double value) {
+               double val = toUnit(value);
+               return decFormat.format(val);
+       }
+}
diff --git a/src/net/sf/openrocket/unit/TemperatureUnit.java b/src/net/sf/openrocket/unit/TemperatureUnit.java
new file mode 100644 (file)
index 0000000..23645ce
--- /dev/null
@@ -0,0 +1,27 @@
+package net.sf.openrocket.unit;
+
+public class TemperatureUnit extends FixedPrecisionUnit {
+
+       protected final double addition;
+       
+       public TemperatureUnit(double multiplier, double addition, String unit) {
+               super(unit, 1, multiplier);
+
+               this.addition = addition;
+       }
+       
+       @Override
+       public boolean hasSpace() {
+               return false;
+       }
+       
+       @Override
+       public double toUnit(double value) {
+               return value/multiplier - addition;
+       }
+       
+       @Override
+       public double fromUnit(double value) {
+               return (value + addition)*multiplier;
+       }
+}
diff --git a/src/net/sf/openrocket/unit/Tick.java b/src/net/sf/openrocket/unit/Tick.java
new file mode 100644 (file)
index 0000000..4448aac
--- /dev/null
@@ -0,0 +1,28 @@
+package net.sf.openrocket.unit;
+
+public final class Tick {
+       public final double value;
+       public final double unitValue;
+       public final boolean major;
+       public final boolean notable;
+       
+       public Tick(double value, double unitValue, boolean major, boolean notable) {
+               this.value = value;
+               this.unitValue = unitValue;
+               this.major = major;
+               this.notable = notable;
+       }
+       
+       @Override
+       public String toString() {
+               String s = "Tick[value="+value;
+               if (major)
+                       s += ",major";
+               else
+                       s += ",minor";
+               if (notable)
+                       s += ",notable";
+               s+= "]";
+               return s;
+       }
+}
diff --git a/src/net/sf/openrocket/unit/Unit.java b/src/net/sf/openrocket/unit/Unit.java
new file mode 100644 (file)
index 0000000..3686bed
--- /dev/null
@@ -0,0 +1,223 @@
+package net.sf.openrocket.unit;
+
+import java.text.DecimalFormat;
+
+public abstract class Unit {
+       
+       /** No unit with 2 digit precision */
+       public static final Unit NOUNIT2 = new GeneralUnit(1,"\u200b", 2);  // zero-width space
+
+       protected final double multiplier;   // meters = units * multiplier
+       protected final String unit;
+
+       /**
+        * Creates a new Unit with a given multiplier and unit name.
+        * 
+        * Multiplier e.g. 1 in = 0.0254 meter
+        * 
+        * @param multiplier  The multiplier to use on the value, 1 this unit == multiplier SI units
+        * @param unit        The unit's short form.
+        */
+       public Unit(double multiplier, String unit) {
+               if (multiplier == 0)
+                       throw new IllegalArgumentException("Unit has multiplier=0");
+               this.multiplier = multiplier;
+               this.unit = unit;
+       }
+
+       /**
+        * Converts from SI units to this unit.  The default implementation simply divides by the
+        * multiplier.
+        * 
+        * @param value  Value in SI unit
+        * @return       Value in these units
+        */
+       public double toUnit(double value) {
+               return value/multiplier;
+       }
+
+       /**
+        * Convert from this type of units to SI units.  The default implementation simply 
+        * multiplies by the multiplier.
+        * 
+        * @param value  Value in these units
+        * @return       Value in SI units
+        */
+       public double fromUnit(double value) {
+               return value*multiplier;
+       }
+
+       
+       /**
+        * Return the unit name.
+        * 
+        * @return      the unit.
+        */
+       public String getUnit() {
+               return unit;
+       }
+       
+       /**
+        * Whether the value and unit should be separated by a whitespace.  This method 
+        * returns true as most units have a space between the value and unit, but may be 
+        * overridden.
+        * 
+        * @return  true if the value and unit should be separated
+        */
+       public boolean hasSpace() {
+               return true;
+       }
+       
+       
+       // Testcases for toString(double)
+       public static void main(String arg[]) {
+               System.out.println(NOUNIT2.toString(0.0049));
+               System.out.println(NOUNIT2.toString(0.0050));
+               System.out.println(NOUNIT2.toString(0.0051));
+               System.out.println(NOUNIT2.toString(0.00123));
+               System.out.println(NOUNIT2.toString(0.0123));
+               System.out.println(NOUNIT2.toString(0.1234));
+               System.out.println(NOUNIT2.toString(1.2345));
+               System.out.println(NOUNIT2.toString(12.345));
+               System.out.println(NOUNIT2.toString(123.456));
+               System.out.println(NOUNIT2.toString(1234.5678));
+               System.out.println(NOUNIT2.toString(12345.6789));
+               System.out.println(NOUNIT2.toString(123456.789));
+               System.out.println(NOUNIT2.toString(1234567.89));
+               System.out.println(NOUNIT2.toString(12345678.9));
+               
+               System.out.println(NOUNIT2.toString(-0.0049));
+               System.out.println(NOUNIT2.toString(-0.0050));
+               System.out.println(NOUNIT2.toString(-0.0051));
+               System.out.println(NOUNIT2.toString(-0.00123));
+               System.out.println(NOUNIT2.toString(-0.0123));
+               System.out.println(NOUNIT2.toString(-0.1234));
+               System.out.println(NOUNIT2.toString(-1.2345));
+               System.out.println(NOUNIT2.toString(-12.345));
+               System.out.println(NOUNIT2.toString(-123.456));
+               System.out.println(NOUNIT2.toString(-1234.5678));
+               System.out.println(NOUNIT2.toString(-12345.6789));
+               System.out.println(NOUNIT2.toString(-123456.789));
+               System.out.println(NOUNIT2.toString(-1234567.89));
+               System.out.println(NOUNIT2.toString(-12345678.9));
+               
+       }
+       
+       
+       @Override
+       public String toString() {
+               return unit;
+       }
+       
+       private static final DecimalFormat intFormat = new DecimalFormat("#");
+       private static final DecimalFormat decFormat = new DecimalFormat("0.##");
+       private static final DecimalFormat expFormat = new DecimalFormat("0.00E0");
+
+       /**
+        * Format the given value (in SI units) to a string representation of the value in this
+        * units.  An suitable amount of decimals for the unit are used in the representation.
+        * The unit is not appended to the numerical value.
+        *  
+        * @param value  Value in SI units.
+        * @return       A string representation of the number in these units.
+        */
+       public String toString(double value) {
+               double val = toUnit(value);
+
+               if (Math.abs(val) > 1E6) {
+                       return expFormat.format(val);
+               }
+               if (Math.abs(val) >= 100) {
+                       return intFormat.format(val);
+               }
+               if (Math.abs(val) <= 0.005) {
+                       return "0";
+               }
+
+               double sign = Math.signum(val);
+               val = Math.abs(val);
+               double mul = 1.0;
+               while (val < 100) {
+                       mul *= 10;
+                       val *= 10;
+               }
+               val = Math.rint(val)/mul * sign;
+               
+               return decFormat.format(val);
+       }
+       
+       
+       /**
+        * Return a string with the specified value and unit.  The value is converted into
+        * this unit.  If <code>value</code> is NaN, returns "N/A" (not applicable).
+        * 
+        * @param value         the value to print in SI units.
+        * @return                      the value and unit, or "N/A".
+        */
+       public String toStringUnit(double value) {
+               if (Double.isNaN(value))
+                       return "N/A";
+               
+               String s = toString(value);
+               if (hasSpace())
+                       s += " ";
+               s += unit;
+               return s;
+       }
+       
+       
+       
+
+       /**
+        * Round the value (in the current units) to a precision suitable for rough valuing
+        * (approximately 2 significant numbers).
+        * 
+        * @param value  Value in current units
+        * @return       Rounded value.
+        */
+       public abstract double round(double value);
+
+       /**
+        * Return the next rounded value after the given value.
+        * @param value  Value in these units.
+        * @return       The next suitable rounded value.
+        */
+       public abstract double getNextValue(double value);
+       
+       /**
+        * Return the previous rounded value before the given value.
+        * @param value  Value in these units.
+        * @return       The previous suitable rounded value.
+        */
+       public abstract double getPreviousValue(double value);
+       
+       //public abstract ArrayList<Tick> getTicks(double start, double end, double scale);
+       
+       /**
+        * Return ticks in the range start - end (in current units).  minor is the minimum
+        * distance between minor, non-notable ticks and major the minimum distance between
+        * major non-notable ticks.  The values are in current units, i.e. no conversion is
+        * performed.
+        */
+       public abstract Tick[] getTicks(double start, double end, double minor, double major);
+       
+       /**
+        * Compares whether the two units are equal.  Equality requires the unit classes,
+        * multiplier values and units to be equal.
+        */
+       @Override
+       public boolean equals(Object other) {
+               if (other == null)
+                       return false;
+               if (this.getClass() != other.getClass())
+                       return false;
+               return ((this.multiplier == ((Unit)other).multiplier) && 
+                               this.unit.equals(((Unit)other).unit));
+       }
+       
+       @Override
+       public int hashCode() {
+               return this.getClass().hashCode() + this.unit.hashCode();
+       }
+
+}
diff --git a/src/net/sf/openrocket/unit/UnitGroup.java b/src/net/sf/openrocket/unit/UnitGroup.java
new file mode 100644 (file)
index 0000000..091a25e
--- /dev/null
@@ -0,0 +1,525 @@
+package net.sf.openrocket.unit;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+
+
+/**
+ * A group of units (eg. length, mass etc.).  Contains a list of different units of a same
+ * quantity.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class UnitGroup {
+
+       public static final UnitGroup UNITS_NONE;
+       
+       public static final UnitGroup UNITS_MOTOR_DIMENSIONS;
+       public static final UnitGroup UNITS_LENGTH;
+       public static final UnitGroup UNITS_DISTANCE;
+       
+       public static final UnitGroup UNITS_AREA;
+       public static final UnitGroup UNITS_STABILITY;
+       public static final UnitGroup UNITS_VELOCITY;
+       public static final UnitGroup UNITS_ACCELERATION;
+       public static final UnitGroup UNITS_MASS;
+       public static final UnitGroup UNITS_ANGLE;
+       public static final UnitGroup UNITS_DENSITY_BULK;
+       public static final UnitGroup UNITS_DENSITY_SURFACE;
+       public static final UnitGroup UNITS_DENSITY_LINE;
+       public static final UnitGroup UNITS_FORCE;
+       public static final UnitGroup UNITS_IMPULSE;
+       
+       /** Time in the order of less than a second (time step etc). */
+       public static final UnitGroup UNITS_TIME_STEP;
+       
+       /** Time in the order of seconds (motor delay etc). */
+       public static final UnitGroup UNITS_SHORT_TIME;
+       
+       /** Time in the order of the flight time of a rocket. */
+       public static final UnitGroup UNITS_FLIGHT_TIME;
+       public static final UnitGroup UNITS_ROLL;
+       public static final UnitGroup UNITS_TEMPERATURE;
+       public static final UnitGroup UNITS_PRESSURE;
+       public static final UnitGroup UNITS_RELATIVE;
+       public static final UnitGroup UNITS_ROUGHNESS;
+       
+       public static final UnitGroup UNITS_COEFFICIENT;
+       
+       
+       public static final Map<String, UnitGroup> UNITS;
+       
+       
+       /*
+        * Note:  Units may not use HTML tags.
+        */
+       static {
+               UNITS_NONE = new UnitGroup();
+               UNITS_NONE.addUnit(Unit.NOUNIT2);
+               
+               UNITS_LENGTH = new UnitGroup();
+               UNITS_LENGTH.addUnit(new GeneralUnit(0.001,"mm"));
+               UNITS_LENGTH.addUnit(new GeneralUnit(0.01,"cm"));
+               UNITS_LENGTH.addUnit(new GeneralUnit(1,"m"));
+               UNITS_LENGTH.addUnit(new GeneralUnit(0.0254,"in"));
+               UNITS_LENGTH.addUnit(new GeneralUnit(0.3048,"ft"));
+               UNITS_LENGTH.setDefaultUnit(1);
+               
+               UNITS_MOTOR_DIMENSIONS = new UnitGroup();
+               UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.001,"mm"));
+               UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.01,"cm"));
+               UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.0254,"in"));
+               UNITS_MOTOR_DIMENSIONS.setDefaultUnit(0);
+               
+               UNITS_DISTANCE = new UnitGroup();
+               UNITS_DISTANCE.addUnit(new GeneralUnit(1,"m"));
+               UNITS_DISTANCE.addUnit(new GeneralUnit(1000,"km"));
+               UNITS_DISTANCE.addUnit(new GeneralUnit(0.3048,"ft"));
+               UNITS_DISTANCE.addUnit(new GeneralUnit(0.9144,"yd"));
+               UNITS_DISTANCE.addUnit(new GeneralUnit(1609.344,"mi"));
+               
+               UNITS_AREA = new UnitGroup();
+               UNITS_AREA.addUnit(new GeneralUnit(pow2(0.001),"mm\u00b2"));
+               UNITS_AREA.addUnit(new GeneralUnit(pow2(0.01),"cm\u00b2"));
+               UNITS_AREA.addUnit(new GeneralUnit(1,"m\u00b2"));
+               UNITS_AREA.addUnit(new GeneralUnit(pow2(0.0254),"in\u00b2"));
+               UNITS_AREA.addUnit(new GeneralUnit(pow2(0.3048),"ft\u00b2"));
+               UNITS_AREA.setDefaultUnit(1);
+               
+               
+               UNITS_STABILITY = new UnitGroup();
+               UNITS_STABILITY.addUnit(new GeneralUnit(0.001,"mm"));
+               UNITS_STABILITY.addUnit(new GeneralUnit(0.01,"cm"));
+               UNITS_STABILITY.addUnit(new GeneralUnit(0.0254,"in"));
+               UNITS_STABILITY.addUnit(new CaliberUnit((Rocket)null));
+               UNITS_STABILITY.setDefaultUnit(3);
+               
+               UNITS_VELOCITY = new UnitGroup();
+               UNITS_VELOCITY.addUnit(new GeneralUnit(1, "m/s"));
+               UNITS_VELOCITY.addUnit(new GeneralUnit(1/3.6, "km/h"));
+               UNITS_VELOCITY.addUnit(new GeneralUnit(1/0.3048, "ft/s"));
+               UNITS_VELOCITY.addUnit(new GeneralUnit(0.44704, "mph"));
+               
+               UNITS_ACCELERATION = new UnitGroup();
+               UNITS_ACCELERATION.addUnit(new GeneralUnit(1, "m/s\u00b2"));
+               UNITS_ACCELERATION.addUnit(new GeneralUnit(1/0.3048, "ft/s\00b2"));
+               
+
+               UNITS_MASS = new UnitGroup();
+               UNITS_MASS.addUnit(new GeneralUnit(0.001,"g"));
+               UNITS_MASS.addUnit(new GeneralUnit(1,"kg"));
+               UNITS_MASS.addUnit(new GeneralUnit(0.0283495,"oz"));
+               UNITS_MASS.addUnit(new GeneralUnit(0.0453592,"lb"));
+               
+               UNITS_ANGLE = new UnitGroup();
+               UNITS_ANGLE.addUnit(new DegreeUnit());
+               UNITS_ANGLE.addUnit(new FixedPrecisionUnit("rad",0.01));
+               
+               UNITS_DENSITY_BULK = new UnitGroup();
+               UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1000,"g/cm\u00b3"));
+               UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1,"kg/m\u00b3"));
+               UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1729.004,"oz/in\u00b3"));
+               UNITS_DENSITY_BULK.addUnit(new GeneralUnit(16.01846,"lb/ft\u00b3"));
+
+               UNITS_DENSITY_SURFACE = new UnitGroup();
+               UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(10,"g/cm\u00b2"));
+               UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.001,"g/m\u00b2"));
+               UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(1,"kg/m\u00b2"));
+               UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(43.9418,"oz/in\u00b2"));
+               UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.30515173,"oz/ft\u00b2"));
+               UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(4.88243,"lb/ft\u00b2"));
+               UNITS_DENSITY_SURFACE.setDefaultUnit(1);
+
+               UNITS_DENSITY_LINE = new UnitGroup();
+               UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.001,"g/m"));
+               UNITS_DENSITY_LINE.addUnit(new GeneralUnit(1,"kg/m"));
+               UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.0930102,"oz/ft"));
+
+               UNITS_FORCE = new UnitGroup();
+               UNITS_FORCE.addUnit(new GeneralUnit(1,"N"));
+               UNITS_FORCE.addUnit(new GeneralUnit(4.448222,"lbf"));
+               UNITS_FORCE.addUnit(new GeneralUnit(9.80665,"kgf"));
+
+               UNITS_IMPULSE = new UnitGroup();
+               UNITS_IMPULSE.addUnit(new GeneralUnit(1,"Ns"));
+               UNITS_IMPULSE.addUnit(new GeneralUnit(4.448222, "lbf\u00b7s"));
+
+               UNITS_TIME_STEP = new UnitGroup();
+               UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("ms", 1, 0.001));
+               UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("s", 0.01));
+               UNITS_TIME_STEP.setDefaultUnit(1);
+
+               UNITS_SHORT_TIME = new UnitGroup();
+               UNITS_SHORT_TIME.addUnit(new GeneralUnit(1,"s"));
+
+               UNITS_FLIGHT_TIME = new UnitGroup();
+               UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(1,"s"));
+               UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(60,"min"));
+               
+               UNITS_ROLL = new UnitGroup();
+               UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s"));
+               UNITS_ROLL.addUnit(new GeneralUnit(2*Math.PI, "r/s"));
+               UNITS_ROLL.addUnit(new GeneralUnit(2*Math.PI/60, "rpm"));
+               UNITS_ROLL.setDefaultUnit(1);
+
+               UNITS_TEMPERATURE = new UnitGroup();
+               UNITS_TEMPERATURE.addUnit(new FixedPrecisionUnit("K", 1));
+               UNITS_TEMPERATURE.addUnit(new TemperatureUnit(1, 273.15, "\u00b0C"));
+               UNITS_TEMPERATURE.addUnit(new TemperatureUnit(5.0/9.0, 459.67, "\u00b0F"));
+               UNITS_TEMPERATURE.setDefaultUnit(1);
+               
+               UNITS_PRESSURE = new UnitGroup();
+               UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("mbar", 1, 1.0e2));
+               UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("bar", 0.001, 1.0e5));
+               UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("atm", 0.001, 1.01325e5));
+               UNITS_PRESSURE.addUnit(new GeneralUnit(133.322, "mmHg"));
+               UNITS_PRESSURE.addUnit(new GeneralUnit(3386.389, "inHg"));
+               UNITS_PRESSURE.addUnit(new GeneralUnit(6894.757, "psi"));
+               UNITS_PRESSURE.addUnit(new GeneralUnit(1, "Pa"));
+
+               UNITS_RELATIVE = new UnitGroup();
+               UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("\u200b", 0.01));
+               UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("%", 1, 0.01));
+               UNITS_RELATIVE.setDefaultUnit(1);
+
+               
+               UNITS_ROUGHNESS = new UnitGroup();
+               UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.000001, "\u03bcm"));
+               UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.0000254, "mil"));
+               
+               
+               UNITS_COEFFICIENT = new UnitGroup();
+               UNITS_COEFFICIENT.addUnit(new FixedPrecisionUnit("\u200b", 0.01));  // zero-width space
+               
+
+               HashMap<String,UnitGroup> map = new HashMap<String,UnitGroup>();
+               map.put("NONE", UNITS_NONE);
+               map.put("LENGTH", UNITS_LENGTH);
+               map.put("MOTOR_DIMENSIONS", UNITS_MOTOR_DIMENSIONS);
+               map.put("DISTANCE", UNITS_DISTANCE);
+               map.put("VELOCITY", UNITS_VELOCITY);
+               map.put("ACCELERATION", UNITS_ACCELERATION);
+               map.put("AREA", UNITS_AREA);
+               map.put("STABILITY", UNITS_STABILITY);
+               map.put("MASS", UNITS_MASS);
+               map.put("ANGLE", UNITS_ANGLE);
+               map.put("DENSITY_BULK", UNITS_DENSITY_BULK);
+               map.put("DENSITY_SURFACE", UNITS_DENSITY_SURFACE);
+               map.put("DENSITY_LINE", UNITS_DENSITY_LINE);
+               map.put("FORCE", UNITS_FORCE);
+               map.put("IMPULSE", UNITS_IMPULSE);
+               map.put("TIME_STEP", UNITS_TIME_STEP);
+               map.put("SHORT_TIME", UNITS_SHORT_TIME);
+               map.put("FLIGHT_TIME", UNITS_FLIGHT_TIME);
+               map.put("ROLL", UNITS_ROLL);
+               map.put("TEMPERATURE", UNITS_TEMPERATURE);
+               map.put("PRESSURE", UNITS_PRESSURE);
+               map.put("RELATIVE", UNITS_RELATIVE);
+               map.put("ROUGHNESS", UNITS_ROUGHNESS);
+               map.put("COEFFICIENT", UNITS_COEFFICIENT);
+
+               UNITS = Collections.unmodifiableMap(map);
+       }
+       
+       public static void setDefaultMetricUnits() {
+               UNITS_LENGTH.setDefaultUnit("cm");
+               UNITS_MOTOR_DIMENSIONS.setDefaultUnit("mm");
+               UNITS_DISTANCE.setDefaultUnit("m");
+               UNITS_AREA.setDefaultUnit("cm\u00b2");
+               UNITS_STABILITY.setDefaultUnit("cal");
+               UNITS_VELOCITY.setDefaultUnit("m/s");
+               UNITS_ACCELERATION.setDefaultUnit("m/s\u00b2");
+               UNITS_MASS.setDefaultUnit("g");
+               UNITS_ANGLE.setDefaultUnit(0);
+               UNITS_DENSITY_BULK.setDefaultUnit("g/cm\u00b3");
+               UNITS_DENSITY_SURFACE.setDefaultUnit("g/m\u00b2");
+               UNITS_DENSITY_LINE.setDefaultUnit("g/m");
+               UNITS_FORCE.setDefaultUnit("N");
+               UNITS_IMPULSE.setDefaultUnit("Ns");
+               UNITS_TIME_STEP.setDefaultUnit("s");
+               UNITS_FLIGHT_TIME.setDefaultUnit("s");
+               UNITS_ROLL.setDefaultUnit("r/s");
+               UNITS_TEMPERATURE.setDefaultUnit(1);
+               UNITS_PRESSURE.setDefaultUnit("mbar");
+               UNITS_RELATIVE.setDefaultUnit("%");
+               UNITS_ROUGHNESS.setDefaultUnit("\u03bcm");
+       }
+       
+       public static void setDefaultImperialUnits() {
+               UNITS_LENGTH.setDefaultUnit("in");
+               UNITS_MOTOR_DIMENSIONS.setDefaultUnit("in");
+               UNITS_DISTANCE.setDefaultUnit("ft");
+               UNITS_AREA.setDefaultUnit("in\u00b2");
+               UNITS_STABILITY.setDefaultUnit("cal");
+               UNITS_VELOCITY.setDefaultUnit("ft/s");
+               UNITS_ACCELERATION.setDefaultUnit("ft/s\u00b2");
+               UNITS_MASS.setDefaultUnit("oz");
+               UNITS_ANGLE.setDefaultUnit(0);
+               UNITS_DENSITY_BULK.setDefaultUnit("oz/in\u00b3");
+               UNITS_DENSITY_SURFACE.setDefaultUnit("oz/ft\u00b2");
+               UNITS_DENSITY_LINE.setDefaultUnit("oz/ft");
+               UNITS_FORCE.setDefaultUnit("N");
+               UNITS_IMPULSE.setDefaultUnit("Ns");
+               UNITS_TIME_STEP.setDefaultUnit("s");
+               UNITS_FLIGHT_TIME.setDefaultUnit("s");
+               UNITS_ROLL.setDefaultUnit("r/s");
+               UNITS_TEMPERATURE.setDefaultUnit(2);
+               UNITS_PRESSURE.setDefaultUnit("mbar");
+               UNITS_RELATIVE.setDefaultUnit("%");
+               UNITS_ROUGHNESS.setDefaultUnit("mil");
+       }
+       
+       
+       
+       public static UnitGroup stabilityUnits(Rocket rocket) {
+               return new StabilityUnitGroup(rocket);
+       }
+       
+       
+       public static UnitGroup stabilityUnits(Configuration config) {
+               return new StabilityUnitGroup(config);
+       }
+       
+
+       //////////////////////////////////////////////////////
+
+       
+       private ArrayList<Unit> units = new ArrayList<Unit>();
+       private int defaultUnit = 0;
+       
+       public int getUnitCount() {
+               return units.size();
+       }
+
+       public Unit getDefaultUnit() {
+               return units.get(defaultUnit);
+       }
+       
+       public int getDefaultUnitIndex() {
+               return defaultUnit;
+       }
+       
+       public void setDefaultUnit(int n) {
+               if (n<0 || n>=units.size()) {
+                       throw new IllegalArgumentException("index out of range: "+n);
+               }
+               defaultUnit = n;
+       }
+       
+       /**
+        * Set the default unit based on the unit name.  Does nothing if the name
+        * does not match any of the units.
+        * 
+        * @param name  the unit name (<code>null</code> ok).
+        * @return              <code>true</code> if the the default was set, 
+        *                              <code>false</code> if a matching unit was not found.  
+        */
+       public boolean setDefaultUnit(String name) {
+               if (name == null)
+                       return false;
+               
+               for (int i=0; i < units.size(); i++) {
+                       if (name.equals(units.get(i).getUnit())) {
+                               setDefaultUnit(i);
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+       
+       public Unit getUnit(int n) {
+               return units.get(n);
+       }
+       
+       public int getUnitIndex(Unit u) {
+               return units.indexOf(u);
+       }
+       
+       public void addUnit(Unit u) {
+               units.add(u);
+       }
+       
+       public void addUnit(int n, Unit u) {
+               units.add(n,u);
+       }
+       
+       public void removeUnit(int n) {
+               units.remove(n);
+       }
+       
+       public boolean contains(Unit u) {
+               return units.contains(u);
+       }
+       
+       public Unit[] getUnits() {
+               return units.toArray(new Unit[0]);
+       }
+       
+       
+       /**
+        * Return the value formatted by the default unit of this group.
+        * It is the same as calling <code>getDefaultUnit().toString(value)</code>.
+        * 
+        * @param value         the SI value to format.
+        * @return                      the formatted string.
+        * @see                         Unit#toString(double)
+        */
+       public String toString(double value) {
+               return this.getDefaultUnit().toString(value);
+       }
+       
+       
+       /**
+        * Return the value formatted by the default unit of this group including the unit.
+        * It is the same as calling <code>getDefaultUnit().toStringUnit(value)</code>.
+        * 
+        * @param value         the SI value to format.
+        * @return                      the formatted string.
+        * @see                         Unit#toStringUnit(double)
+        */
+       public String toStringUnit(double value) {
+               return this.getDefaultUnit().toStringUnit(value);
+       }
+       
+       
+       private static final Pattern STRING_PATTERN = Pattern.compile("^\\s*([0-9.,-]+)(.*?)$");
+       /**
+        * Converts a string into an SI value.  If the string has one of the units in this
+        * group appended to it, that unit will be used in conversion.  Otherwise the default
+        * unit will be used.  If an unknown unit is specified or the value does not parse
+        * with <code>Double.parseDouble</code> then a <code>NumberFormatException</code> 
+        * is thrown.
+        * <p>
+        * This method is applicable only for simple units without e.g. powers.
+        * 
+        * @param str   the string to parse.
+        * @return              the SI value.
+        * @throws NumberFormatException   if the string cannot be parsed.
+        */
+       public double fromString(String str) {
+               Matcher matcher = STRING_PATTERN.matcher(str);
+               
+               if (!matcher.matches()) {
+                       throw new NumberFormatException("string did not match required pattern");
+               }
+               
+               double value = Double.parseDouble(matcher.group(1));
+               String unit = matcher.group(2).trim();
+               
+               if (unit.equals("")) {
+                       value = this.getDefaultUnit().fromUnit(value);
+               } else {
+                       int i;
+                       for (i=0; i < units.size(); i++) {
+                               Unit u = units.get(i);
+                               if (unit.equalsIgnoreCase(u.getUnit())) {
+                                       value = u.fromUnit(value);
+                                       break;
+                               }
+                       }
+                       if (i >= units.size()) {
+                               throw new NumberFormatException("unknown unit "+unit);
+                       }
+               }
+               
+               return value;
+       }
+       
+       
+       ///////////////////////////
+       
+       
+       /**
+        * A private class that switches the CaliberUnit to a rocket-specific CaliberUnit.
+        * All other methods are passed through to UNITS_STABILITY.
+        */
+       private static class StabilityUnitGroup extends UnitGroup {
+               
+               private final CaliberUnit caliberUnit;
+               
+               
+               public StabilityUnitGroup(Rocket rocket) {
+                       caliberUnit = new CaliberUnit(rocket);
+               }
+               
+               public StabilityUnitGroup(Configuration config) {
+                       caliberUnit = new CaliberUnit(config);
+               }
+               
+
+               ////  Modify CaliberUnit to use local variable
+               
+               @Override
+               public Unit getDefaultUnit() {
+                       return getUnit(UNITS_STABILITY.getDefaultUnitIndex());
+               }
+
+               @Override
+               public Unit getUnit(int n) {
+                       Unit u = UNITS_STABILITY.getUnit(n);
+                       if (u instanceof CaliberUnit) {
+                               return caliberUnit;
+                       }
+                       return u;
+               }
+
+               @Override
+               public int getUnitIndex(Unit u) {
+                       if (u instanceof CaliberUnit) {
+                               for (int i=0; i < UNITS_STABILITY.getUnitCount(); i++) {
+                                       if (UNITS_STABILITY.getUnit(i) instanceof CaliberUnit)
+                                               return i;
+                               }
+                       }
+                       return UNITS_STABILITY.getUnitIndex(u);
+               }
+
+               
+
+               ////  Pass on to UNITS_STABILITY
+               
+               @Override
+               public int getDefaultUnitIndex() {
+                       return UNITS_STABILITY.getDefaultUnitIndex();
+               }
+
+               @Override
+               public void setDefaultUnit(int n) {
+                       UNITS_STABILITY.setDefaultUnit(n);
+               }
+
+               @Override
+               public int getUnitCount() {
+                       return UNITS_STABILITY.getUnitCount();
+               }
+
+
+               ////  Unsupported methods
+               
+               @Override
+               public void addUnit(int n, Unit u) {
+                       throw new UnsupportedOperationException("StabilityUnitGroup must not be modified");
+               }
+
+               @Override
+               public void addUnit(Unit u) {
+                       throw new UnsupportedOperationException("StabilityUnitGroup must not be modified");
+               }
+
+               @Override
+               public void removeUnit(int n) {
+                       throw new UnsupportedOperationException("StabilityUnitGroup must not be modified");
+               }
+       }
+}
diff --git a/src/net/sf/openrocket/util/Analysis.java b/src/net/sf/openrocket/util/Analysis.java
new file mode 100644 (file)
index 0000000..505d979
--- /dev/null
@@ -0,0 +1,203 @@
+package net.sf.openrocket.util;
+
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.GAMMA;
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.R;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.Arrays;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.ExactAtmosphericConditions;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.file.GeneralRocketLoader;
+import net.sf.openrocket.file.RocketLoadException;
+import net.sf.openrocket.file.RocketLoader;
+import net.sf.openrocket.rocketcomponent.Configuration;
+
+public class Analysis {
+       
+       private static final double MACH_MIN = 0.01;
+       private static final double MACH_MAX = 5.00001;
+       private static final double MACH_STEP = 0.02;
+       
+       private static final double AOA_MACH = 0.6;
+       private static final double AOA_MIN = 0;
+       private static final double AOA_MAX = 15.00001*Math.PI/180;
+       private static final double AOA_STEP = 0.5*Math.PI/180;
+       
+       private static final double REYNOLDS = 9.8e6;
+       private static final double STAG_TEMP = 330;
+       
+       
+       private final RocketLoader loader = new GeneralRocketLoader();
+       private final AerodynamicCalculator calculator = new BarrowmanCalculator();
+       
+       private final FlightConditions conditions;
+       private final double length;
+       
+       private final Configuration config;
+       
+       private final AtmosphericConditions atmosphere;
+       
+       
+       
+       private Analysis(String filename) throws RocketLoadException {
+
+               OpenRocketDocument doc = loader.load(new File(filename));
+               config = doc.getRocket().getDefaultConfiguration();
+               
+               calculator.setConfiguration(config);
+               
+               conditions = new FlightConditions(config);
+               System.out.println("Children: " + Arrays.toString(config.getRocket().getChildren()));
+               System.out.println("Children: " + Arrays.toString(config.getRocket().getChild(0).getChildren()));
+               length = config.getLength();
+               System.out.println("Rocket length: " + (length*1000)+"mm");
+               
+               atmosphere = new ExactAtmosphericConditions();
+               
+       }
+       
+       
+       private double computeVelocityAndAtmosphere(double mach, double reynolds, double stagTemp) {
+               final double temperature;
+               final double pressure;
+               
+               
+               temperature = stagTemp / (1 + (GAMMA-1)/2 * MathUtil.pow2(mach));
+               
+               // Speed of sound
+               double c = 331.3 * Math.sqrt(1 + (temperature - 273.15)/273.15);
+               
+               // Free-stream velocity
+               double v0 = c * mach;
+               
+//             kin.visc. = (3.7291e-06 + 4.9944e-08 * temperature) / density
+               pressure = reynolds * (3.7291e-06 + 4.9944e-08 * temperature) * R * temperature / 
+                                       (v0 * length);
+               
+               atmosphere.pressure = pressure;
+               atmosphere.temperature = temperature;
+               conditions.setAtmosphericConditions(atmosphere);
+               conditions.setVelocity(v0);
+               
+               if (Math.abs(conditions.getMach() - mach) > 0.001) {
+                       System.err.println("Computed mach: "+conditions.getMach() + " requested "+mach);
+//                     System.exit(1);
+               }
+               
+               return v0;
+       }
+       
+       
+       
+       private void computeVsMach(PrintStream stream) {
+               
+               conditions.setAOA(0);
+               conditions.setTheta(45*Math.PI/180);
+               stream.println("% Mach, Caxial, CP, , CNa, Croll");
+               
+               for (double mach = MACH_MIN; mach <= MACH_MAX; mach += MACH_STEP) {
+                       
+                       computeVelocityAndAtmosphere(mach, REYNOLDS, STAG_TEMP);
+//                     conditions.setMach(mach);
+                       
+                       
+                       AerodynamicForces forces = calculator.getAerodynamicForces(0, conditions, null);
+                       
+
+                       double Re = conditions.getVelocity() * 
+                                       calculator.getConfiguration().getLength() / 
+                                       conditions.getAtmosphericConditions().getKinematicViscosity();
+                       if (Math.abs(Re - REYNOLDS) > 1) {
+                               throw new RuntimeException("Re="+Re);
+                       }
+                       stream.printf("%f, %f, %f, %f, %f\n", mach, forces.Caxial, forces.cp.x, forces.CNa, 
+                                       forces.Croll);
+               }
+               
+       }
+
+       
+       
+       private void computeVsAOA(PrintStream stream, double thetaDeg) {
+               
+               computeVelocityAndAtmosphere(AOA_MACH, REYNOLDS, STAG_TEMP);
+               conditions.setTheta(thetaDeg * Math.PI/180);
+               stream.println("% AOA, CP, CN, Cm   at theta = "+thetaDeg);
+               
+               for (double aoa = AOA_MIN; aoa <= AOA_MAX; aoa += AOA_STEP) {
+
+                       conditions.setAOA(aoa);
+                       AerodynamicForces forces = calculator.getAerodynamicForces(0, conditions, null);
+                       
+
+                       double Re = conditions.getVelocity() * 
+                                       calculator.getConfiguration().getLength() / 
+                                       conditions.getAtmosphericConditions().getKinematicViscosity();
+                       if (Math.abs(Re - REYNOLDS) > 1) {
+                               throw new RuntimeException("Re="+Re);
+                       }
+                       stream.printf("%f, %f, %f, %f\n", aoa*180/Math.PI, forces.cp.x, forces.CN, forces.Cm);
+               }
+               
+       }
+       
+       
+       
+
+       public static void main(String arg[]) throws Exception {
+               
+               if (arg.length != 2) {
+                       System.err.println("Arguments:  <rocket file> <output prefix>");
+                       System.exit(1);
+               }
+
+               Analysis a = new Analysis(arg[0]);
+               final String prefix = arg[1];
+               
+
+               String name;
+               double v0 = a.computeVelocityAndAtmosphere(0.6, 9.8e6, 322);
+               System.out.printf("Sanity test: mach = %.1f v=%.1f temp=%.1f pres=%.0f c=%.1f " +
+                               "ref.length=%.1fmm\n",
+                               a.conditions.getMach(), v0, a.atmosphere.temperature, a.atmosphere.pressure, 
+                               a.atmosphere.getMachSpeed(), a.conditions.getRefLength()*1000);
+               System.out.println();
+               
+               
+               // CA, CP, Croll vs. Mach  at AOA=0
+               name = prefix + "-CA-CP-CNa-Croll-vs-Mach.csv";
+               System.out.println("Computing CA, CP, CNa, Croll vs. Mach to file "+name);
+               a.computeVsMach(new PrintStream(name));
+
+               
+               // CN & Cm vs. AOA  at M=0.6
+               name = prefix + "-CP-CN-Cm-vs-AOA-0.csv";
+               System.out.println("Computing CP, CN, Cm vs. AOA at theta=0 to file "+name);
+               a.computeVsAOA(new PrintStream(name), 0);
+
+               // CN & Cm vs. AOA  at M=0.6
+               name = prefix + "-CP-CN-Cm-vs-AOA-22.5.csv";
+               System.out.println("Computing CP, CN, Cm vs. AOA at theta=22.5 to file "+name);
+               a.computeVsAOA(new PrintStream(name), 0);
+
+               // CN & Cm vs. AOA  at M=0.6
+               name = prefix + "-CP-CN-Cm-vs-AOA-45.csv";
+               System.out.println("Computing CP, CN, Cm vs. AOA at theta=45 to file "+name);
+               a.computeVsAOA(new PrintStream(name), 0);
+
+               
+               System.out.println("Done.");
+       }
+       
+       
+       
+       
+       
+}
diff --git a/src/net/sf/openrocket/util/Base64.java b/src/net/sf/openrocket/util/Base64.java
new file mode 100644 (file)
index 0000000..fb8490a
--- /dev/null
@@ -0,0 +1,220 @@
+package net.sf.openrocket.util;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+public class Base64 {
+
+       public static final int DEFAULT_CHARS_PER_LINE = 72;
+       
+       private static final char[] ALPHABET = new char[] {
+                       'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
+                       'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
+                       'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
+                       'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
+       };
+       private static final char PAD = '=';
+       
+       private static final Map<Character,Integer> REVERSE = new HashMap<Character,Integer>();
+       static {
+               for (int i=0; i<64; i++) {
+                       REVERSE.put(ALPHABET[i], i);
+               }
+               REVERSE.put('-', 62);
+               REVERSE.put('_', 63);
+               REVERSE.put(PAD, 0);
+       }
+       
+       
+       public static String encode(byte[] data) {
+               return encode(data, DEFAULT_CHARS_PER_LINE);
+       }
+       
+       public static String encode(byte[] data, int maxColumn) {
+               StringBuilder builder = new StringBuilder();
+               int column = 0;
+               
+               for (int position=0; position < data.length; position+=3) {
+                       if (column+4 > maxColumn) {
+                               builder.append('\n');
+                               column = 0;
+                       }
+                       builder.append(encodeGroup(data, position));
+                       column += 4;
+               }
+               builder.append('\n');
+               return builder.toString();
+       }
+       
+       
+
+       
+       public static byte[] decode(String data) {
+               byte[] array = new byte[data.length()*3/4];
+               char[] block = new char[4];
+               int length = 0;
+               
+               for (int position=0; position < data.length(); ) {
+                       int p;
+                       for (p=0; p<4 && position < data.length(); position++) {
+                               char c = data.charAt(position);
+                               if (!Character.isWhitespace(c)) {
+                                       block[p] = c;
+                                       p++;
+                               }
+                       }
+                       
+                       if (p==0)
+                               break;
+                       if (p!=4) {
+                               throw new IllegalArgumentException("Data ended when decoding Base64, p="+p);
+                       }
+                       
+                       int l = decodeGroup(block, array, length);
+                       length += l;
+                       if (l < 3)
+                               break;
+               }
+               return Arrays.copyOf(array, length);
+       }
+       
+       
+       ////  Helper methods
+       
+       
+       /**
+        * Encode three bytes of data into four characters.
+        */
+       private static char[] encodeGroup(byte[] data, int position) {
+               char[] c = new char[] { '=','=','=','=' };
+               int b1=0, b2=0, b3=0;
+               int length = data.length - position;
+               
+               if (length == 0)
+                       return c;
+               
+               if (length >= 1) {
+                       b1 = ((int)data[position])&0xFF;
+               }
+               if (length >= 2) {
+                       b2 = ((int)data[position+1])&0xFF;
+               }
+               if (length >= 3) {
+                       b3 = ((int)data[position+2])&0xFF;
+               }
+               
+               c[0] = ALPHABET[b1>>2];
+               c[1] = ALPHABET[(b1 & 3)<<4 | (b2>>4)];
+               if (length == 1)
+                       return c;
+               c[2] = ALPHABET[(b2 & 15)<<2 | (b3>>6)];
+               if (length == 2)
+                       return c;
+               c[3] = ALPHABET[b3 & 0x3f];
+               return c;
+       }
+       
+       
+       /**
+        * Decode four chars from data into 0-3 bytes of data starting at position in array.
+        * @return      the number of bytes decoded.
+        */
+       private static int decodeGroup(char[] data, byte[] array, int position) {
+               int b1, b2, b3, b4;
+               
+               try {
+                       b1 = REVERSE.get(data[0]);
+                       b2 = REVERSE.get(data[1]);
+                       b3 = REVERSE.get(data[2]);
+                       b4 = REVERSE.get(data[3]);
+               } catch (NullPointerException e) {
+                       // If auto-boxing fails
+                       throw new IllegalArgumentException("Illegal characters in the sequence to be "+
+                                       "decoded: "+Arrays.toString(data));
+               }
+               
+               array[position]   = (byte)((b1 << 2) | (b2 >> 4)); 
+               array[position+1] = (byte)((b2 << 4) | (b3 >> 2)); 
+               array[position+2] = (byte)((b3 << 6) | (b4)); 
+               
+               // Check the amount of data decoded
+               if (data[0] == PAD)
+                       return 0;
+               if (data[1] == PAD) {
+                       throw new IllegalArgumentException("Illegal character padding in sequence to be "+
+                                       "decoded: "+Arrays.toString(data));
+               }
+               if (data[2] == PAD)
+                       return 1;
+               if (data[3] == PAD)
+                       return 2;
+               
+               return 3;
+       }
+       
+       
+       
+       public static void main(String[] arg) {
+               Random rnd = new Random();
+               
+               for (int round=0; round < 1000; round++) {
+                       int n = rnd.nextInt(1000);
+                       n = 100000;
+                       
+                       byte[] array = new byte[n];
+                       rnd.nextBytes(array);
+
+                       String encoded = encode(array);
+                       
+                       System.out.println(encoded);
+                       System.exit(0);
+//                     for (int i=0; i<1000; i++) {
+//                             int pos = rnd.nextInt(encoded.length());
+//                             String s1 = encoded.substring(0, pos);
+//                             String s2 = encoded.substring(pos);
+//                             switch (rnd.nextInt(15)) {
+//                             case 0:
+//                                     encoded = s1 + " " + s2;
+//                                     break;
+//                             case 1:
+//                                     encoded = s1 + "\u0009" + s2;
+//                                     break;
+//                             case 2:
+//                                     encoded = s1 + "\n" + s2;
+//                                     break;
+//                             case 3:
+//                                     encoded = s1 + "\u000B" + s2;
+//                                     break;
+//                             case 4:
+//                                     encoded = s1 + "\r" + s2;
+//                                     break;
+//                             case 5:
+//                                     encoded = s1 + "\u000C" + s2;
+//                                     break;
+//                             case 6:
+//                                     encoded = s1 + "\u001C" + s2;
+//                                     break;
+//                             }
+//                     }
+                       
+                       byte[] decoded = null;
+                       try {
+                               decoded = decode(encoded);
+                       } catch (IllegalArgumentException e) {
+                               e.printStackTrace();
+                               System.err.println("Bad data:\n"+encoded);
+                               System.exit(1);
+                       }
+                       
+                       if (!Arrays.equals(array, decoded)) {
+                               System.err.println("Data differs!  n="+n);
+                               System.exit(1);
+                       }
+                       System.out.println("n="+n+" ok!");
+               }
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/util/ChangeSource.java b/src/net/sf/openrocket/util/ChangeSource.java
new file mode 100644 (file)
index 0000000..5d166cd
--- /dev/null
@@ -0,0 +1,15 @@
+package net.sf.openrocket.util;
+
+import javax.swing.event.ChangeListener;
+
+/**
+ * An interface defining an object firing ChangeEvents.  Why isn't this included in the Java API??
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface ChangeSource {
+
+       public void addChangeListener(ChangeListener listener);
+       public void removeChangeListener(ChangeListener listener);
+       
+}
diff --git a/src/net/sf/openrocket/util/Coordinate.java b/src/net/sf/openrocket/util/Coordinate.java
new file mode 100644 (file)
index 0000000..5b88f08
--- /dev/null
@@ -0,0 +1,291 @@
+package net.sf.openrocket.util;
+
+import java.io.Serializable;
+
+/**
+ * An immutable class of weighted coordinates.  The weights are non-negative.
+ * 
+ * Can also be used as non-weighted coordinates with weight=0.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public final class Coordinate implements Serializable {
+       public static final Coordinate NUL = new Coordinate(0,0,0,0);
+       public static final Coordinate NaN = new Coordinate(Double.NaN,Double.NaN,
+                       Double.NaN,Double.NaN);
+       public static final double COMPARISON_DELTA = 0.000001;
+       public final double x,y,z;
+       public final double weight;
+       
+       
+       private double length = -1;  /* Cached when calculated */
+       
+       
+       /* Count and report the number of times a Coordinate is constructed: */
+//     private static int count=0;
+//     {
+//             count++;
+//             if ((count % 1000) == 0) {
+//                     System.err.println("Coordinate instantiated "+count+" times");
+//             }
+//     }
+       
+       
+
+       public Coordinate() {
+               this(0,0,0,0);
+       }
+       
+       public Coordinate(double x) {
+               this(x,0,0,0);
+       }
+       
+       public Coordinate(double x, double y) {
+               this(x,y,0,0);
+       }
+       
+       public Coordinate(double x, double y, double z) {
+               this(x,y,z,0);
+       }
+       public Coordinate(double x, double y, double z, double w) {
+               this.x = x;
+               this.y = y;
+               this.z = z;
+               this.weight=w;
+       }
+
+       
+       public boolean isWeighted() {
+               return (weight != 0);
+       }
+       
+       public boolean isNaN() {
+               return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight);
+       }
+       
+       public Coordinate setX(double x) {
+               return new Coordinate(x,this.y,this.z,this.weight);
+       }
+       
+       public Coordinate setY(double y) {
+               return new Coordinate(this.x,y,this.z,this.weight);
+       }
+       
+       public Coordinate setZ(double z) {
+               return new Coordinate(this.x,this.y,z,this.weight);
+       }
+       
+       public Coordinate setWeight(double weight) {
+               return new Coordinate(this.x, this.y, this.z, weight);
+       }
+       
+       public Coordinate setXYZ(Coordinate c) {
+               return new Coordinate(c.x, c.y, c.z, this.weight);
+       }
+
+       
+       /**
+        * Add the coordinate and weight of two coordinates.
+        * 
+        * @param other  the other <code>Coordinate</code>
+        * @return               the sum of the coordinates
+        */
+       public Coordinate add(Coordinate other) {
+               return new Coordinate(this.x+other.x, this.y+other.y, this.z+other.z, 
+                               this.weight+other.weight);
+       }
+       
+       public Coordinate add(double x, double y, double z) {
+               return new Coordinate(this.x+x, this.y+y, this.z+z, this.weight);
+       }
+
+       public Coordinate add(double x, double y, double z, double weight) {
+               return new Coordinate(this.x+x, this.y+y, this.z+z, this.weight+weight);
+       }
+
+       /**
+        * Subtract a Coordinate from this Coordinate.  The weight of the resulting Coordinate
+        * is the same as of this Coordinate, the weight of the argument is ignored.
+        * 
+        * @param other  Coordinate to subtract from this.
+        * @return  The result
+        */
+       public Coordinate sub(Coordinate other) {
+               return new Coordinate(this.x-other.x, this.y-other.y, this.z-other.z, this.weight);
+       }
+
+       /**
+        * Subtract the specified values from this Coordinate.  The weight of the result
+        * is the same as the weight of this Coordinate.
+        * 
+        * @param x     x value to subtract
+        * @param y             y value to subtract
+        * @param z             z value to subtract
+        * @return              the result.
+        */
+       public Coordinate sub(double x, double y, double z) {
+               return new Coordinate(this.x - x, this.y - y, this.z - z, this.weight);
+       }
+       
+       
+       /**
+        * Multiply the <code>Coordinate</code> with a scalar.  All coordinates and the
+        * weight are multiplied by the given scalar.
+
+        * @param m  Factor to multiply by.
+        * @return   The product. 
+        */
+       public Coordinate multiply(double m) {
+               return new Coordinate(this.x*m, this.y*m, this.z*m, this.weight*m);
+       }
+
+       /**
+        * Dot product of two Coordinates, taken as vectors.  Equal to
+        * x1*x2+y1*y2+z1*z2
+        * @param other  Coordinate to take product with.
+        * @return   The dot product.
+        */
+       public double dot(Coordinate other) {
+               return this.x*other.x + this.y*other.y + this.z*other.z;
+       }
+       /**
+        * Dot product of two Coordinates.
+        */
+       public static double dot(Coordinate v1, Coordinate v2) {
+               return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
+       }
+
+       /**
+        * Distance from the origin to the Coordinate.
+        */
+       public double length() {
+               if (length < 0) {
+                       length = Math.sqrt(x*x+y*y+z*z); 
+               }
+               return length;
+       }
+       
+       /**
+        * Square of the distance from the origin to the Coordinate.
+        */
+       public double length2() {
+               return x*x+y*y+z*z;
+       }
+       
+       /**
+        * Returns a new coordinate which has the same direction from the origin as this
+        * coordinate but is at a distance of one.  If this coordinate is the origin,
+        * this method throws an <code>IllegalStateException</code>.  The weight of the
+        * coordinate is unchanged.
+        * 
+        * @return   the coordinate normalized to distance one of the origin.
+        * @throws   IllegalStateException  if this coordinate is the origin.
+        */
+       public Coordinate normalize() {
+               double l = length();
+               if (l < 0.0000001) {
+                       throw new IllegalStateException("Cannot normalize zero coordinate");
+               }
+               return new Coordinate(x/l, y/l, z/l, weight);
+       }
+       
+       
+       
+       
+       /**
+        * Weighted average of two coordinates.  If either of the weights are positive,
+        * the result is the weighted average of the coordinates and the weight is the sum
+        * of the original weights.  If the sum of the weights is zero (and especially if
+        * both of the weights are zero), the result is the unweighted average of the 
+        * coordinates with weight zero.
+        * <p>
+        * If <code>other</code> is <code>null</code> then this <code>Coordinate</code> is
+        * returned.
+        */
+       public Coordinate average(Coordinate other) {
+               double x,y,z,w;
+               
+               if (other == null)
+                       return this;
+               
+               w = this.weight + other.weight;
+               if (Math.abs(w) < MathUtil.pow2(MathUtil.EPSILON)) {
+                       x = (this.x+other.x)/2;
+                       y = (this.y+other.y)/2;
+                       z = (this.z+other.z)/2;
+                       w = 0;
+               } else {
+                       x = (this.x*this.weight + other.x*other.weight)/w;
+                       y = (this.y*this.weight + other.y*other.weight)/w;
+                       z = (this.z*this.weight + other.z*other.weight)/w;
+               }
+               return new Coordinate(x,y,z,w);
+       }
+       
+       
+       /**
+        * Tests whether the coordinates (not weight!) are the same.
+        * 
+        * Compares only the (x,y,z) coordinates, NOT the weight.  Coordinate comparison is
+        * done to the precision of COMPARISON_DELTA.
+        * 
+        * @param other  Coordinate to compare to.
+        * @return  true if the coordinates are equal
+        */
+       @Override
+       public boolean equals(Object other) {
+               if (!(other instanceof Coordinate))
+                       return false;
+               
+               final Coordinate c = (Coordinate)other;
+               return (MathUtil.equals(this.x, c.x) &&
+                               MathUtil.equals(this.y, c.y) &&
+                               MathUtil.equals(this.z, c.z));
+       }
+       
+       /**
+        * Hash code method compatible with {@link #equals(Object)}.
+        */
+       @Override
+       public int hashCode() {
+               return (int)((x+y+z)*100000);
+       }
+       
+       
+       @Override
+       public String toString() {
+               if (isWeighted())
+                       return String.format("(%.3f,%.3f,%.3f,w=%.3f)", x,y,z,weight);
+               else
+                       return String.format("(%.3f,%.3f,%.3f)", x,y,z);
+       }
+       
+       
+       
+       public static void main(String[] arg) {
+               double a=1.2;
+               double x;
+               Coordinate c;
+               long t1, t2;
+               
+               x = 0;
+               t1 = System.nanoTime();
+               for (int i=0; i < 100000000; i++) {
+                       x = x + a;
+               }
+               t2 = System.nanoTime();
+               System.out.println("Value: "+x);
+               System.out.println("Plain addition: "+ ((t2-t1+500000)/1000000) + " ms");
+               
+               c = Coordinate.NUL;
+               t1 = System.nanoTime();
+               for (int i=0; i < 100000000; i++) {
+                       c = c.add(a,0,0);
+               }
+               t2 = System.nanoTime();
+               System.out.println("Value: "+c.x);
+               System.out.println("Coordinate addition: "+ ((t2-t1+500000)/1000000) + " ms");
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/GUIUtil.java b/src/net/sf/openrocket/util/GUIUtil.java
new file mode 100644 (file)
index 0000000..eb0eccf
--- /dev/null
@@ -0,0 +1,80 @@
+package net.sf.openrocket.util;
+
+import java.awt.Component;
+import java.awt.KeyboardFocusManager;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowEvent;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JRootPane;
+import javax.swing.KeyStroke;
+import javax.swing.RootPaneContainer;
+import javax.swing.SwingUtilities;
+
+public class GUIUtil {
+
+       private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+       private static final String CLOSE_ACTION_KEY =  "escape:WINDOW_CLOSING"; 
+       
+       
+       /**
+        * Add the correct action to close a JDialog when the ESC key is pressed.
+        * The dialog is closed by sending is a WINDOW_CLOSING event.
+        * 
+        * @param dialog        the dialog for which to install the action.
+        */
+       public static void installEscapeCloseOperation(final JDialog dialog) { 
+           Action dispatchClosing = new AbstractAction() { 
+               public void actionPerformed(ActionEvent event) { 
+                   dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING)); 
+               } 
+           }; 
+           JRootPane root = dialog.getRootPane(); 
+           root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY); 
+           root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing); 
+       }
+       
+       
+       /**
+        * Set the given button as the default button of the frame/dialog it is in.  The button
+        * must be first attached to the window component hierarchy.
+        * 
+        * @param button        the button to set as the default button.
+        */
+       public static void setDefaultButton(JButton button) {
+               Window w = SwingUtilities.windowForComponent(button);
+               if (w == null) {
+                       throw new IllegalArgumentException("Attach button to a window first.");
+               }
+               if (!(w instanceof RootPaneContainer)) {
+                       throw new IllegalArgumentException("Button not attached to RootPaneContainer, w="+w);
+               }
+               ((RootPaneContainer)w).getRootPane().setDefaultButton(button);
+       }
+
+       
+       
+       /**
+        * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
+        * the components.  This is necessary for e.g. <code>JTextArea</code>.
+        * 
+        * @param c             the component to modify
+        */
+    public static void setTabToFocusing(Component c) {
+        Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
+        c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
+        strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
+        c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
+    }
+
+       
+}
diff --git a/src/net/sf/openrocket/util/Icons.java b/src/net/sf/openrocket/util/Icons.java
new file mode 100644 (file)
index 0000000..96a32b7
--- /dev/null
@@ -0,0 +1,56 @@
+package net.sf.openrocket.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+import net.sf.openrocket.document.Simulation;
+
+
+public class Icons {
+
+       /**
+        * Icons used for showing the status of a simulation (up to date, out of date, etc).
+        */
+       public static final Map<Simulation.Status, Icon> SIMULATION_STATUS_ICON_MAP;
+       static {
+               HashMap<Simulation.Status, Icon> map = new HashMap<Simulation.Status, Icon>();
+               map.put(Simulation.Status.NOT_SIMULATED, new ImageIcon("pix/spheres/gray-16x16.png", "Not simulated"));
+               map.put(Simulation.Status.UPTODATE, new ImageIcon("pix/spheres/green-16x16.png", "Up to date"));
+               map.put(Simulation.Status.LOADED, new ImageIcon("pix/spheres/yellow-16x16.png", "Loaded from file"));
+               map.put(Simulation.Status.OUTDATED, new ImageIcon("pix/spheres/red-16x16.png", "Out-of-date"));
+               map.put(Simulation.Status.EXTERNAL, new ImageIcon("pix/spheres/blue-16x16.png", "Imported data"));
+               SIMULATION_STATUS_ICON_MAP = Collections.unmodifiableMap(map);
+       }
+       
+       public static final Icon SIMULATION_LISTENER_OK;
+       public static final Icon SIMULATION_LISTENER_ERROR;
+       static {
+               SIMULATION_LISTENER_OK = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.UPTODATE);
+               SIMULATION_LISTENER_ERROR = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.OUTDATED);
+       }
+
+
+       public static final Icon FILE_NEW = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-new.png"), "New document");
+       public static final Icon FILE_OPEN = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-open.png"), "Open document");
+       public static final Icon FILE_SAVE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-save.png"), "Save document");
+       public static final Icon FILE_SAVE_AS = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-save-as.png"), "Save document as");
+       public static final Icon FILE_CLOSE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-close.png"), "Close document");
+       public static final Icon FILE_QUIT = new ImageIcon(ClassLoader.getSystemResource("pix/icons/application-exit.png"), "Quit OpenRocket");
+       
+       public static final Icon EDIT_UNDO = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-undo.png"), "Undo");
+       public static final Icon EDIT_REDO = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-redo.png"), "Redo");
+       public static final Icon EDIT_CUT = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-cut.png"), "Cut");
+       public static final Icon EDIT_COPY = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-copy.png"), "Copy");
+       public static final Icon EDIT_PASTE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-paste.png"), "Paste");
+       public static final Icon EDIT_DELETE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-delete.png"), "Delete");
+
+       public static final Icon ZOOM_IN = new ImageIcon(ClassLoader.getSystemResource("pix/icons/zoom-in.png"), "Zoom in");
+       public static final Icon ZOOM_OUT = new ImageIcon(ClassLoader.getSystemResource("pix/icons/zoom-out.png"), "Zoom out");
+
+       public static final Icon PREFERENCES = new ImageIcon(ClassLoader.getSystemResource("pix/icons/preferences.png"), "Preferences");
+
+}
diff --git a/src/net/sf/openrocket/util/LineStyle.java b/src/net/sf/openrocket/util/LineStyle.java
new file mode 100644 (file)
index 0000000..efc946b
--- /dev/null
@@ -0,0 +1,31 @@
+package net.sf.openrocket.util;
+
+import java.util.Arrays;
+
+/**
+ * An enumeration of line styles.  The line styles are defined by an array of
+ * floats suitable for <code>BasicStroke</code>.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public enum LineStyle {
+       SOLID("Solid",new float[] { 10f, 0f }),
+       DASHED("Dashed",new float[] { 6f, 4f }),
+       DOTTED("Dotted",new float[] { 2f, 3f }),
+       DASHDOT("Dash-dotted",new float[] { 8f, 3f, 2f, 3f})
+       ;
+       
+       private final String name;
+       private final float[] dashes;
+       LineStyle(String name, float[] dashes) {
+               this.name = name;
+               this.dashes = dashes;
+       }
+       public float[] getDashes() {
+               return Arrays.copyOf(dashes, dashes.length);
+       }
+       @Override
+       public String toString() {
+               return name;
+       }
+}
\ No newline at end of file
diff --git a/src/net/sf/openrocket/util/LinearInterpolator.java b/src/net/sf/openrocket/util/LinearInterpolator.java
new file mode 100644 (file)
index 0000000..00efe3a
--- /dev/null
@@ -0,0 +1,128 @@
+package net.sf.openrocket.util;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class LinearInterpolator implements Cloneable {
+
+       private TreeMap<Double, Double> sortMap = new TreeMap<Double,Double>();
+
+       /**
+        * Construct a <code>LinearInterpolator</code> with no points.  Some points must be
+        * added using {@link #addPoints(double[], double[])} before using the interpolator.
+        */
+       public LinearInterpolator() {
+       }
+       
+       /**
+        * Construct a <code>LinearInterpolator</code> with the given points.
+        * 
+        * @param x             the x-coordinates of the points.
+        * @param y             the y-coordinates of the points.
+        * @throws IllegalArgumentException             if the lengths of <code>x</code> and <code>y</code>
+        *                                                                              are not equal.
+        * @see #addPoints(double[], double[])
+        */
+       public LinearInterpolator(double[] x, double[] y) {
+               addPoints(x,y);
+       }
+       
+       
+       /**
+        * Add the point to the linear interpolation.
+        * 
+        * @param x             the x-coordinate of the point.
+        * @param y             the y-coordinate of the point.
+        */
+       public void addPoint(double x, double y) {
+               sortMap.put(x, y);
+       }
+       
+       /**
+        * Add the points to the linear interpolation.
+        * 
+        * @param x             the x-coordinates of the points.
+        * @param y             the y-coordinates of the points.
+        * @throws IllegalArgumentException             if the lengths of <code>x</code> and <code>y</code>
+        *                                                                              are not equal.
+        */
+       public void addPoints(double[] x, double[] y) {
+               if (x.length != y.length) {
+                       throw new IllegalArgumentException("Array lengths do not match, x="+x.length +
+                                       " y="+y.length);
+               }
+               for (int i=0; i < x.length; i++) {
+                       sortMap.put(x[i],y[i]);
+               }
+       }
+       
+       
+       
+       public double getValue(double x) {
+               Map.Entry<Double,Double> e1, e2;
+               double x1, x2;
+               double y1, y2;
+               
+               e1 = sortMap.floorEntry(x);
+               
+               if (e1 == null) {
+                       // x smaller than any value in the set
+                       e1 = sortMap.firstEntry();
+                       if (e1 == null) {
+                               throw new IllegalStateException("No points added yet to the interpolator.");
+                       }
+                       return e1.getValue();
+               }
+               
+               x1 = e1.getKey();
+               e2 = sortMap.higherEntry(x1);
+
+               if (e2 == null) {
+                       // x larger than any value in the set
+                       return e1.getValue();
+               }
+               
+               x2 = e2.getKey();
+               y1 = e1.getValue();
+               y2 = e2.getValue();
+               
+               return (x - x1)/(x2-x1) * (y2-y1) + y1;
+       }
+       
+       
+       public double[] getXPoints() {
+               double[] x = new double[sortMap.size()];
+               Iterator<Double> iter = sortMap.keySet().iterator();
+               for (int i=0; iter.hasNext(); i++) {
+                       x[i] = iter.next();
+               }
+               return x;
+       }
+       
+       
+       @SuppressWarnings("unchecked")
+       @Override
+       public LinearInterpolator clone() {
+               try {
+                       LinearInterpolator other = (LinearInterpolator)super.clone();
+                       other.sortMap = (TreeMap<Double,Double>)this.sortMap.clone();
+                       return other;
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("CloneNotSupportedException?!",e);
+               }
+       }
+
+       
+       public static void main(String[] args) {
+               LinearInterpolator interpolator = new LinearInterpolator(
+                               new double[] {1, 1.5, 2, 4, 5},
+                               new double[] {0, 1,   0, 2, 2}
+               );
+               
+               for (double x=0; x < 6; x+=0.1) {
+                       System.out.printf("%.1f:  %.2f\n", x, interpolator.getValue(x));
+               }
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/MathUtil.java b/src/net/sf/openrocket/util/MathUtil.java
new file mode 100644 (file)
index 0000000..a714f81
--- /dev/null
@@ -0,0 +1,217 @@
+package net.sf.openrocket.util;
+
+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;
+       }
+       
+       /**
+        * The cube of x (x^3).
+        * @param x  x
+        * @return   x^3
+        */
+       public static double pow3(double x) {
+               return x*x*x;
+       }
+       
+       public static double pow4(double x) {
+               return (x*x)*(x*x);
+       }
+       
+       /**
+        * Clamps the value x to the range min - max.  
+        * @param x    Original value.
+        * @param min  Minimum value to return.
+        * @param max  Maximum value to return.
+        * @return     The clamped value.
+        */
+       public static double clamp(double x, double min, double max) {
+               if (x < min)
+                       return min;
+               if (x > max)
+                       return max;
+               return x;
+       }
+       
+       public static float clamp(float x, float min, float max) {
+               if (x < min)
+                       return min;
+               if (x > max)
+                       return max;
+               return x;
+       }
+       
+       public static int clamp(int x, int min, int max) {
+               if (x < min)
+                       return min;
+               if (x > max)
+                       return max;
+               return x;
+       }
+       
+       
+       /**
+        * Maps a value from one value range to another.
+        * 
+        * @param value         the value to map.
+        * @param fromMin       the minimum of the starting range.
+        * @param fromMax       the maximum of the starting range.
+        * @param toMin         the minimum of the destination range.
+        * @param toMax         the maximum of the destination range.
+        * @return                      the mapped value.
+        * @throws      IllegalArgumentException  if fromMin == fromMax, but toMin != toMax.
+        */
+       public static double map(double value, double fromMin, double fromMax,
+                       double toMin, double toMax) {
+               if (equals(toMin, toMax))
+                       return toMin;
+               if (equals(fromMin, fromMax)) {
+                       throw new IllegalArgumentException("from range is singular an to range is not.");
+               }
+               return (value - fromMin)/(fromMax-fromMin) * (toMax - toMin) + toMin;
+       }
+       
+       /**
+        * Compute the minimum of two values.  This is performed by direct comparison. 
+        * However, if one of the values is NaN and the other is not, the non-NaN value is
+        * returned.
+        */
+       public static double min(double x, double y) {
+               if (Double.isNaN(y))
+                       return x;
+               return (x < y) ? x : y;
+       }
+       
+       /**
+        * Compute the maximum of two values.  This is performed by direct comparison. 
+        * However, if one of the values is NaN and the other is not, the non-NaN value is
+        * returned.
+        */
+       public static double max(double x, double y) {
+               if (Double.isNaN(x))
+                       return y;
+               return (x < y) ? y : x;
+       }
+       
+       /**
+        * Compute the minimum of three values.  This is performed by direct comparison. 
+        * However, if one of the values is NaN and the other is not, the non-NaN value is
+        * returned.
+        */
+       public static double min(double x, double y, double z) {
+               if (x < y || Double.isNaN(y)) {
+                       return min(x,z);
+               } else {
+                       return min(y,z);
+               }
+       }
+       
+       /**
+        * Compute the maximum of three values.  This is performed by direct comparison. 
+        * However, if one of the values is NaN and the other is not, the non-NaN value is
+        * returned.
+        */
+       public static double max(double x, double y, double z) {
+               if (x > y || Double.isNaN(y)) {
+                       return max(x,z);
+               } else {
+                       return max(y,z);
+               }
+       }
+       
+       /**
+        * Calculates the hypotenuse <code>sqrt(x^2+y^2)</code>.  This method is SIGNIFICANTLY
+        * faster than <code>Math.hypot(x,y)</code>.
+        */
+       public static double hypot(double x, double 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;
+       }
+
+       /**
+        * Reduce the angle x to the range -PI - PI.
+        * 
+        * Either -PI and PI might be returned, depending on the rounding function. 
+        * 
+        * @param x  Original angle.
+        * @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;
+       }
+       
+       
+       public static boolean equals(double a, double b) {
+               double absb = Math.abs(b);
+               
+               if (absb < EPSILON/2) {
+                       // Near zero
+                       return Math.abs(a) < EPSILON/2;
+               }
+               return Math.abs(a-b) < EPSILON*absb;
+       }
+       
+       public static double sign(double x) {
+               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 void main(String[] arg) {
+               double nan = Double.NaN;
+               System.out.println("min(5,6)     = " + min(5, 6));
+               System.out.println("min(5,nan)   = " + min(5, nan));
+               System.out.println("min(nan,6)   = " + min(nan, 6));
+               System.out.println("min(nan,nan) = " + min(nan, nan));
+               System.out.println();
+               System.out.println("max(5,6)     = " + max(5, 6));
+               System.out.println("max(5,nan)   = " + max(5, nan));
+               System.out.println("max(nan,6)   = " + max(nan, 6));
+               System.out.println("max(nan,nan) = " + max(nan, nan));
+               System.out.println();
+               System.out.println("min(5,6,7)       = " + min(5, 6, 7));
+               System.out.println("min(5,6,nan)     = " + min(5, 6, nan));
+               System.out.println("min(5,nan,7)     = " + min(5, nan, 7));
+               System.out.println("min(5,nan,nan)   = " + min(5, nan, nan));
+               System.out.println("min(nan,6,7)     = " + min(nan, 6, 7));
+               System.out.println("min(nan,6,nan)   = " + min(nan, 6, nan));
+               System.out.println("min(nan,nan,7)   = " + min(nan, nan, 7));
+               System.out.println("min(nan,nan,nan) = " + min(nan, nan, nan));
+               System.out.println();
+               System.out.println("max(5,6,7)       = " + max(5, 6, 7));
+               System.out.println("max(5,6,nan)     = " + max(5, 6, nan));
+               System.out.println("max(5,nan,7)     = " + max(5, nan, 7));
+               System.out.println("max(5,nan,nan)   = " + max(5, nan, nan));
+               System.out.println("max(nan,6,7)     = " + max(nan, 6, 7));
+               System.out.println("max(nan,6,nan)   = " + max(nan, 6, nan));
+               System.out.println("max(nan,nan,7)   = " + max(nan, nan, 7));
+               System.out.println("max(nan,nan,nan) = " + max(nan, nan, nan));
+               System.out.println();
+               
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/MutableCoordinate.java b/src/net/sf/openrocket/util/MutableCoordinate.java
new file mode 100644 (file)
index 0000000..02af935
--- /dev/null
@@ -0,0 +1,312 @@
+package net.sf.openrocket.util;
+
+import java.io.Serializable;
+
+/**
+ * An immutable class of weighted coordinates.  The weights are non-negative.
+ * 
+ * Can also be used as non-weighted coordinates with weight=0.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public final class MutableCoordinate implements Serializable {
+       public static final MutableCoordinate NUL = new MutableCoordinate(0,0,0,0);
+       public static final MutableCoordinate NaN = new MutableCoordinate(Double.NaN,Double.NaN,
+                       Double.NaN,Double.NaN);
+       public static final double COMPARISON_DELTA = 0.000001;
+       private double x,y,z;
+       private double weight;
+       
+       
+       /* Count and report the number of times a Coordinate is constructed: */
+//     private static int count=0;
+//     {
+//             count++;
+//             if ((count % 1000) == 0) {
+//                     System.out.println("Coordinate instantiated "+count+" times");
+//             }
+//     }
+       
+
+       public MutableCoordinate() {
+               x=0;
+               y=0;
+               z=0;
+               weight=0;
+       }
+       
+       public MutableCoordinate(double x) {
+               this.x = x;
+               this.y = 0;
+               this.z = 0;
+               weight = 0;
+       }
+       
+       public MutableCoordinate(double x, double y) {
+               this.x = x;
+               this.y = y;
+               this.z = 0;
+               weight = 0;
+       }
+       
+       public MutableCoordinate(double x, double y, double z) {
+               this.x = x;
+               this.y = y;
+               this.z = z;
+               weight = 0;
+       }
+       public MutableCoordinate(double x, double y, double z, double w) {
+               this.x = x;
+               this.y = y;
+               this.z = z;
+               this.weight=w;
+       }
+
+       
+       public boolean isWeighted() {
+               return (weight != 0);
+       }
+       
+       public boolean isNaN() {
+               return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight);
+       }
+       
+       public MutableCoordinate setX(double x) {
+               return new MutableCoordinate(x,this.y,this.z,this.weight);
+       }
+       
+       public MutableCoordinate setY(double y) {
+               return new MutableCoordinate(this.x,y,this.z,this.weight);
+       }
+       
+       public MutableCoordinate setZ(double z) {
+               return new MutableCoordinate(this.x,this.y,z,this.weight);
+       }
+       
+       public MutableCoordinate setWeight(double weight) {
+               return new MutableCoordinate(this.x, this.y, this.z, weight);
+       }
+       
+       public MutableCoordinate setXYZ(MutableCoordinate c) {
+               return new MutableCoordinate(c.x, c.y, c.z, this.weight);
+       }
+       
+       public double getX() {
+               return x;
+       }
+       public double getY() {
+               return y;
+       }
+       public double getZ() {
+               return z;
+       }
+
+       
+       /**
+        * Add the coordinate and weight of two coordinates.
+        * 
+        * @param other  the other <code>Coordinate</code>
+        * @return               the sum of the coordinates
+        */
+       public MutableCoordinate add(MutableCoordinate other) {
+               this.x += other.x;
+               this.y += other.y;
+               this.z += other.z;
+               this.weight += other.weight;
+               return this;
+       }
+       
+       public MutableCoordinate add(double x, double y, double z) {
+               this.x += x;
+               this.y += y;
+               this.z += z;
+               return this;
+       }
+
+       public MutableCoordinate add(double x, double y, double z, double weight) {
+               return new MutableCoordinate(this.x+x, this.y+y, this.z+z, this.weight+weight);
+       }
+
+       /**
+        * Subtract a Coordinate from this Coordinate.  The weight of the resulting Coordinate
+        * is the same as of this Coordinate, the weight of the argument is ignored.
+        * 
+        * @param other  Coordinate to subtract from this.
+        * @return  The result
+        */
+       public MutableCoordinate sub(MutableCoordinate other) {
+               return new MutableCoordinate(this.x-other.x, this.y-other.y, this.z-other.z, this.weight);
+       }
+
+       /**
+        * Subtract the specified values from this Coordinate.  The weight of the result
+        * is the same as the weight of this Coordinate.
+        * 
+        * @param x     x value to subtract
+        * @param y             y value to subtract
+        * @param z             z value to subtract
+        * @return              the result.
+        */
+       public MutableCoordinate sub(double x, double y, double z) {
+               return new MutableCoordinate(this.x - x, this.y - y, this.z - z, this.weight);
+       }
+       
+       
+       /**
+        * Multiply the <code>Coordinate</code> with a scalar.  All coordinates and the
+        * weight are multiplied by the given scalar.
+
+        * @param m  Factor to multiply by.
+        * @return   The product. 
+        */
+       public MutableCoordinate multiply(double m) {
+               return new MutableCoordinate(this.x*m, this.y*m, this.z*m, this.weight*m);
+       }
+
+       /**
+        * Dot product of two Coordinates, taken as vectors.  Equal to
+        * x1*x2+y1*y2+z1*z2
+        * @param other  Coordinate to take product with.
+        * @return   The dot product.
+        */
+       public double dot(MutableCoordinate other) {
+               return this.x*other.x + this.y*other.y + this.z*other.z;
+       }
+       /**
+        * Dot product of two Coordinates.
+        */
+       public static double dot(MutableCoordinate v1, MutableCoordinate v2) {
+               return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
+       }
+
+       /**
+        * Distance from the origin to the Coordinate.
+        */
+       public double length() {
+               return Math.sqrt(x*x+y*y+z*z);
+       }
+       
+       /**
+        * Square of the distance from the origin to the Coordinate.
+        */
+       public double length2() {
+               return x*x+y*y+z*z;
+       }
+       
+       /**
+        * Returns a new coordinate which has the same direction from the origin as this
+        * coordinate but is at a distance of one.  If this coordinate is the origin,
+        * this method throws an <code>IllegalStateException</code>.  The weight of the
+        * coordinate is unchanged.
+        * 
+        * @return   the coordinate normalized to distance one of the origin.
+        * @throws   IllegalStateException  if this coordinate is the origin.
+        */
+       public MutableCoordinate normalize() {
+               double l = length();
+               if (l < 0.0000001) {
+                       throw new IllegalStateException("Cannot normalize zero coordinate");
+               }
+               return new MutableCoordinate(x/l, y/l, z/l, weight);
+       }
+       
+       
+       
+       
+       /**
+        * Weighted average of two coordinates.  If either of the weights are positive,
+        * the result is the weighted average of the coordinates and the weight is the sum
+        * of the original weights.  If the sum of the weights is zero (and especially if
+        * both of the weights are zero), the result is the unweighted average of the 
+        * coordinates with weight zero.
+        * <p>
+        * If <code>other</code> is <code>null</code> then this <code>Coordinate</code> is
+        * returned.
+        */
+       public MutableCoordinate average(MutableCoordinate other) {
+               double x,y,z,w;
+               
+               if (other == null)
+                       return this;
+               
+               w = this.weight + other.weight;
+               if (MathUtil.equals(w, 0)) {
+                       x = (this.x+other.x)/2;
+                       y = (this.y+other.y)/2;
+                       z = (this.z+other.z)/2;
+                       w = 0;
+               } else {
+                       x = (this.x*this.weight + other.x*other.weight)/w;
+                       y = (this.y*this.weight + other.y*other.weight)/w;
+                       z = (this.z*this.weight + other.z*other.weight)/w;
+               }
+               return new MutableCoordinate(x,y,z,w);
+       }
+       
+       
+       /**
+        * Tests whether the coordinates (not weight!) are the same.
+        * 
+        * Compares only the (x,y,z) coordinates, NOT the weight.  Coordinate comparison is
+        * done to the precision of COMPARISON_DELTA.
+        * 
+        * @param other  Coordinate to compare to.
+        * @return  true if the coordinates are equal
+        */
+       @Override
+       public boolean equals(Object other) {
+               if (!(other instanceof MutableCoordinate))
+                       return false;
+               
+               final MutableCoordinate c = (MutableCoordinate)other;
+               return (MathUtil.equals(this.x, c.x) &&
+                               MathUtil.equals(this.y, c.y) &&
+                               MathUtil.equals(this.z, c.z));
+       }
+       
+       /**
+        * Hash code method compatible with {@link #equals(Object)}.
+        */
+       @Override
+       public int hashCode() {
+               return (int)((x+y+z)*100000);
+       }
+       
+       
+       @Override
+       public String toString() {
+               if (isWeighted())
+                       return String.format("(%.3f,%.3f,%.3f,w=%.3f)", x,y,z,weight);
+               else
+                       return String.format("(%.3f,%.3f,%.3f)", x,y,z);
+       }
+       
+       
+       
+       public static void main(String[] arg) {
+               double a=1.2;
+               double x;
+               MutableCoordinate c;
+               long t1, t2;
+               
+               x = 0;
+               t1 = System.nanoTime();
+               for (int i=0; i < 100000000; i++) {
+                       x = x + a;
+               }
+               t2 = System.nanoTime();
+               System.out.println("Value: "+x);
+               System.out.println("Plain addition: "+ ((t2-t1+500000)/1000000) + " ms");
+               
+               c = MutableCoordinate.NUL;
+               t1 = System.nanoTime();
+               for (int i=0; i < 100000000; i++) {
+                       c = c.add(a,0,0);
+               }
+               t2 = System.nanoTime();
+               System.out.println("Value: "+c.x);
+               System.out.println("Coordinate addition: "+ ((t2-t1+500000)/1000000) + " ms");
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/Pair.java b/src/net/sf/openrocket/util/Pair.java
new file mode 100644 (file)
index 0000000..8a4cec2
--- /dev/null
@@ -0,0 +1,22 @@
+package net.sf.openrocket.util;
+
+public class Pair<U,V> {
+
+       private final U u;
+       private final V v;
+       
+       
+       public Pair(U u, V v) {
+               this.u = u;
+               this.v = v;
+       }
+       
+       public U getU() {
+               return u;
+       }
+       
+       public V getV() {
+               return v;
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/PinkNoise.java b/src/net/sf/openrocket/util/PinkNoise.java
new file mode 100644 (file)
index 0000000..572ad03
--- /dev/null
@@ -0,0 +1,140 @@
+package net.sf.openrocket.util;
+import java.util.Random;
+
+
+/**
+ * A class that provides a source of pink noise with a power spectrum density 
+ * proportional to 1/f^alpha.  The values are computed by applying an IIR filter to
+ * generated Gaussian random numbers.  The number of poles used in the filter may be
+ * specified.  Values as low as 3 produce good results, but using a larger number of
+ * poles allows lower frequencies to be amplified.  Below the cutoff frequency the
+ * power spectrum density if constant.
+ * <p>
+ * The IIR filter use by this class is presented by N. Jeremy Kasdin, Proceedings of 
+ * the IEEE, Vol. 83, No. 5, May 1995, p. 822.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class PinkNoise {
+       private final int poles;
+       private final double[] multipliers;
+       
+       private final double[] values;
+       private final Random rnd;
+
+       
+       /**
+        * Generate pink noise with alpha=1.0 using a five-pole IIR.
+        */
+       public PinkNoise() {
+               this(1.0, 5, new Random());
+       }
+       
+       
+       /**
+        * Generate a specific pink noise using a five-pole IIR.
+        * 
+        * @param alpha         the exponent of the pink noise, 1/f^alpha.
+        */
+       public PinkNoise(double alpha) {
+               this(alpha, 5, new Random());
+       }
+       
+       
+       /**
+        * Generate pink noise specifying alpha and the number of poles.  The larger the
+        * number of poles, the lower are the lowest frequency components that are amplified.
+        * 
+        * @param alpha         the exponent of the pink noise, 1/f^alpha.
+        * @param poles         the number of poles to use.
+        */
+       public PinkNoise(double alpha, int poles) {
+               this(alpha, poles, new Random());
+       }
+       
+       
+       /**
+        * Generate pink noise specifying alpha, the number of poles and the randomness source.
+        * 
+        * @param alpha    the exponent of the pink noise, 1/f^alpha.
+        * @param poles    the number of poles to use.
+        * @param random   the randomness source.
+        */
+       public PinkNoise(double alpha, int poles, Random random) {
+               this.rnd = random;
+               this.poles = poles;
+               this.multipliers = new double[poles];
+               this.values = new double[poles];
+               
+               double a = 1;
+               for (int i=0; i < poles; i++) {
+                       a = (i - alpha/2) * a / (i+1);
+                       multipliers[i] = a;
+               }
+               
+               // Fill the history with random values
+               for (int i=0; i < 5*poles; i++)
+                       this.nextValue();
+       }
+       
+       
+       
+       public double nextValue() {
+               double x = rnd.nextGaussian();
+//             double x = rnd.nextDouble()-0.5;
+               
+               for (int i=0; i < poles; i++) {
+                       x -= multipliers[i] * values[i];
+               }
+               System.arraycopy(values, 0, values, 1, values.length-1);
+               values[0] = x;
+               
+               return x;
+       }
+       
+       
+       public static void main(String[] arg) {
+               
+               PinkNoise source;
+               
+               source = new PinkNoise(1.0, 100);
+               double std = 0;
+               for (int i=0; i < 1000000; i++) {
+                       
+               }
+               
+
+//             int n = 5000000;
+//             double avgavg=0;
+//             double avgstd = 0; 
+//             double[] val = new double[n];
+//
+//             for (int j=0; j < 10; j++) {
+//                     double avg=0, std=0;
+//                     source = new PinkNoise(5.0/3.0, 2);
+//
+//                     for (int i=0; i < n; i++) {
+//                             val[i] = source.nextValue();
+//                             avg += val[i];
+//                     }
+//                     avg /= n;
+//                     for (int i=0; i < n; i++) {
+//                             std += (val[i]-avg)*(val[i]-avg);
+//                     }
+//                     std /= n;
+//                     std = Math.sqrt(std);
+//                     
+//                     System.out.println("avg:"+avg+" stddev:"+std);
+//                     avgavg += avg;
+//                     avgstd += std;
+//             }
+//             avgavg /= 10;
+//             avgstd /= 10;
+//             System.out.println("Average avg:"+avgavg+" std:"+avgstd);
+//             
+               // Two poles:
+
+       }
+       
+       
+}
diff --git a/src/net/sf/openrocket/util/PolyInterpolator.java b/src/net/sf/openrocket/util/PolyInterpolator.java
new file mode 100644 (file)
index 0000000..f97d45c
--- /dev/null
@@ -0,0 +1,262 @@
+package net.sf.openrocket.util;
+
+import java.util.Arrays;
+
+/**
+ * A class for polynomial interpolation.  The interpolation constraints can be specified
+ * either as function values or values of the n'th derivative of the function.
+ * Using an interpolation consists of three steps:
+ * <p>
+ * 1. constructing a <code>PolyInterpolator</code> using the interpolation x coordinates <br>
+ * 2. generating the interpolation polynomial using the function and derivative values <br>
+ * 3. evaluating the polynomial at the desired point
+ * <p>
+ * The constructor takes an array of double arrays.  The first array defines x coordinate
+ * values for the function values, the second array x coordinate values for the function's
+ * derivatives, the third array for second derivatives and so on.  Constructing the
+ * <code>PolyInterpolator</code> is relatively slow, O(n^3) where n is the order of the
+ * polynomial.  (It contains calculation of the inverse of an n x n matrix.)
+ * <p>
+ * Generating the interpolation polynomial is performed by the method 
+ * {@link #interpolator(double...)}, which takes as an argument the function and 
+ * derivative values.  This operation takes O(n^2) time.
+ * <p>
+ * Finally, evaluating the polynomial at different positions takes O(n) time.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class PolyInterpolator {
+
+       // Order of the polynomial
+       private final int count;
+       
+       private final double[][] interpolationMatrix;
+       
+       
+       /**
+        * Construct a polynomial interpolation generator.  All arguments to the constructor
+        * are the x coordinates of the interpolated function.  The first array correspond to
+        * the function values, the second to function derivatives, the third to second
+        * derivatives and so forth.  The order of the polynomial is automatically calculated
+        * from the total number of constraints.
+        * <p>
+        * The construction takes O(n^3) time.
+        * 
+        * @param points        an array of constraints, the first corresponding to function value
+        *                                      constraints, the second to derivative constraints etc.
+        */
+       public PolyInterpolator(double[] ... points) {
+               int count = 0;
+               for (int i=0; i < points.length; i++) {
+                       count += points[i].length;
+               }
+               if (count == 0) {
+                       throw new IllegalArgumentException("No interpolation points defined.");
+               }
+               
+               this.count = count;
+               
+               int[] mul = new int[count];
+               Arrays.fill(mul, 1);
+
+               double[][] matrix = new double[count][count];
+               int row = 0;
+               for (int j=0; j < points.length; j++) {
+                       
+                       for (int i=0; i < points[j].length; i++) {
+                               double x = 1;
+                               for (int col = count-1-j; col>= 0; col--) {
+                                       matrix[row][col] = x*mul[col];
+                                       x *= points[j][i];
+                               }
+                               row++;
+                       }
+                       
+                       for (int i=0; i < count; i++) {
+                               mul[i] *= (count-i-j-1);
+                       }
+               }
+               assert(row == count);
+               
+               interpolationMatrix = inverse(matrix);
+       }
+
+
+       /**
+        * Generates an interpolation polynomial.  The arguments supplied to this method
+        * are the function values, derivatives, second derivatives etc. in the order
+        * specified in the constructor (i.e. values first, then derivatives etc).
+        * <p>
+        * This method takes O(n^2) time.
+        * 
+        * @param values        the function values, derivatives etc. at positions defined in the
+        *                                      constructor.
+        * @return              the coefficients of the interpolation polynomial, the highest order
+        *                                      term first and the constant last.
+        */
+       public double[] interpolator(double... values) {
+               if (values.length != count) {
+                       throw new IllegalArgumentException("Wrong number of arguments "+values.length+
+                                       " expected "+count);
+               }
+               
+               double[] ret = new double[count];
+               
+               for (int j=0; j < count; j++) {
+                       for (int i=0; i < count; i++) {
+                               ret[j] += interpolationMatrix[j][i] * values[i];
+                       }
+               }
+               
+               return ret;
+       }
+
+
+       /**
+        * Interpolate the given values at the point <code>x</code>.  This is equivalent
+        * to generating an interpolation polynomial and evaluating the polynomial at the
+        * specified point.
+        * 
+        * @param x                     point at which to evaluate the interpolation polynomial.
+        * @param values        the function, derivatives etc. at position defined in the
+        *                                      constructor.
+        * @return                      the value of the interpolation.
+        */
+       public double interpolate(double x, double... values) {
+               return eval(x, interpolator(values));
+       }
+
+       
+       /**
+        * Evaluate a polynomial at the specified point <code>x</code>.  The coefficients are
+        * assumed to have the highest order coefficient first and the constant term last.
+        * 
+        * @param x                             position at which to evaluate the polynomial.
+        * @param coefficients  polynomial coefficients, highest term first and constant last.
+        * @return                              the value of the polynomial.
+        */
+       public static double eval(double x, double[] coefficients) {
+               double v = 1;
+               double result = 0;
+               for (int i = coefficients.length-1; i >= 0; i--) {
+                       result += coefficients[i] * v;
+                       v *= x;
+               }
+               return result;
+       }
+       
+       
+       
+       
+       private static double[][] inverse(double[][] matrix) {
+               int n = matrix.length;
+               
+               double x[][] = new double[n][n];
+               double b[][] = new double[n][n];
+               int index[] = new int[n];
+               for (int i=0; i<n; ++i) 
+                       b[i][i] = 1;
+
+               // Transform the matrix into an upper triangle
+               gaussian(matrix, index);
+
+               // Update the matrix b[i][j] with the ratios stored
+               for (int i=0; i<n-1; ++i)
+                       for (int j=i+1; j<n; ++j)
+                               for (int k=0; k<n; ++k)
+                                       b[index[j]][k] -= matrix[index[j]][i]*b[index[i]][k];
+
+               // Perform backward substitutions
+               for (int i=0; i<n; ++i) {
+                       x[n-1][i] = b[index[n-1]][i]/matrix[index[n-1]][n-1];
+                       for (int j=n-2; j>=0; --j) {
+                               x[j][i] = b[index[j]][i];
+                               for (int k=j+1; k<n; ++k) {
+                                       x[j][i] -= matrix[index[j]][k]*x[k][i];
+                               }
+                               x[j][i] /= matrix[index[j]][j];
+                       }
+               }
+               return x;
+       }
+
+       private static void gaussian(double a[][],
+                       int index[]) {
+               int n = index.length;
+               double c[] = new double[n];
+
+               // Initialize the index
+               for (int i=0; i<n; ++i) index[i] = i;
+
+               // Find the rescaling factors, one from each row
+               for (int i=0; i<n; ++i) {
+                       double c1 = 0;
+                       for (int j=0; j<n; ++j) {
+                               double c0 = Math.abs(a[i][j]);
+                               if (c0 > c1) c1 = c0;
+                       }
+                       c[i] = c1;
+               }
+
+               // Search the pivoting element from each column
+               int k = 0;
+               for (int j=0; j<n-1; ++j) {
+                       double pi1 = 0;
+                       for (int i=j; i<n; ++i) {
+                               double pi0 = Math.abs(a[index[i]][j]);
+                               pi0 /= c[index[i]];
+                               if (pi0 > pi1) {
+                                       pi1 = pi0;
+                                       k = i;
+                               }
+                       }
+
+                       // Interchange rows according to the pivoting order
+                       int itmp = index[j];
+                       index[j] = index[k];
+                       index[k] = itmp;
+                       for (int i=j+1; i<n; ++i) {
+                               double pj = a[index[i]][j]/a[index[j]][j];
+
+                               // Record pivoting ratios below the diagonal
+                               a[index[i]][j] = pj;
+
+                               // Modify other elements accordingly
+                               for (int l=j+1; l<n; ++l)
+                                       a[index[i]][l] -= pj*a[index[j]][l];
+                       }
+               }
+       }
+
+
+
+
+       public static void main(String[] arg) {
+
+               PolyInterpolator p0 = new PolyInterpolator(
+                               new double[] {0.6, 1.1},
+                               new double[] {0.6, 1.1}
+               );
+               double[] r0 = p0.interpolator(1.5, 1.6, 2, -3);
+               
+               PolyInterpolator p1 = new PolyInterpolator(
+                               new double[] {0.6, 1.1},
+                               new double[] {0.6, 1.1},
+                               new double[] {0.6}
+               );
+               double[] r1 = p1.interpolator(1.5, 1.6, 2, -3, 0);
+               
+               PolyInterpolator p2 = new PolyInterpolator(
+                               new double[] {0.6, 1.1},
+                               new double[] {0.6, 1.1},
+                               new double[] {0.6, 1.1}
+               );
+               double[] r2 = p2.interpolator(1.5, 1.6, 2, -3, 0, 0);
+               
+
+               for (double x=0.6; x <= 1.11; x += 0.01) {
+                       System.out.println(x + " " + eval(x,r0) + " " + eval(x,r1) + " " + eval(x,r2));
+               }
+               
+       }
+}
diff --git a/src/net/sf/openrocket/util/Prefs.java b/src/net/sf/openrocket/util/Prefs.java
new file mode 100644 (file)
index 0000000..dbb31c2
--- /dev/null
@@ -0,0 +1,457 @@
+package net.sf.openrocket.util;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.InternalComponent;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassObject;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.RK4Simulator;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class Prefs {
+
+       /**
+        * Whether to use the debug-node instead of the normal node.
+        */
+       public static final boolean DEBUG = false;
+       
+       /**
+        * Whether to clear all preferences at application startup.  This has an effect only
+        * if DEBUG is true.
+        */
+       public static final boolean CLEARPREFS = true;
+       
+       /**
+        * The node name to use in the Java preferences storage.
+        */
+       public static final String NODENAME = (DEBUG?"OpenRocket-debug":"OpenRocket");
+       
+       
+       
+       private static final String VERSION = "0.9.0";
+       
+       
+       public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
+       
+       
+       public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
+
+       
+       /**
+        * Node to this application's preferences.
+        */
+       public static final Preferences NODE;
+       
+       
+       static {
+               Preferences root = Preferences.userRoot();
+               if (DEBUG && CLEARPREFS) {
+                       try {
+                               if (root.nodeExists(NODENAME)) {
+                                       root.node(NODENAME).removeNode();
+                               }
+                       } catch (BackingStoreException e) {
+                               throw new RuntimeException("Unable to clear preference node",e);
+                       }
+               }
+               NODE = root.node(NODENAME);
+       }
+       
+       
+       
+       
+       /////////  Default component attributes
+       
+       private static final HashMap<Class<?>,String> DEFAULT_COLORS = 
+               new HashMap<Class<?>,String>();
+       static {
+               DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
+               DEFAULT_COLORS.put(FinSet.class, "0,0,200");
+               DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
+               DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
+               DEFAULT_COLORS.put(MassObject.class, "0,0,0");
+               DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
+       }
+       
+       
+       private static final HashMap<Class<?>,String> DEFAULT_LINE_STYLES = 
+               new HashMap<Class<?>,String>();
+       static {
+               DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
+               DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
+       }
+       
+       
+       private static final Material DEFAULT_LINE_MATERIAL = 
+               Databases.findMaterial(Material.Type.LINE, "Elastic cord (round 2mm, 1/16 in)", 0.0018);
+       private static final Material DEFAULT_SURFACE_MATERIAL = 
+               Databases.findMaterial(Material.Type.SURFACE, "Ripstop nylon", 0.067);
+       private static final Material DEFAULT_BULK_MATERIAL = 
+               Databases.findMaterial(Material.Type.BULK, "Cardboard", 680);
+
+       
+       //////////////////////
+       
+       
+       public static String getVersion() {
+               return VERSION;
+       }
+       
+       
+       
+       public static void storeVersion() {
+               NODE.put("OpenRocketVersion", getVersion());
+       }
+       
+       
+       /**
+        * Returns a limited-range integer value from the preferences.  If the value 
+        * in the preferences is negative or greater than max, then the default value 
+        * is returned.
+        * 
+        * @param key  The preference to retrieve.
+        * @param max  Maximum allowed value for the choice.
+        * @param def  Default value.
+        * @return   The preference value.
+        */
+       public static int getChoise(String key, int max, int def) {
+               int v = NODE.getInt(key, def);
+               if ((v<0) || (v>max))
+                       return def;
+               return v;
+       }
+       
+       
+       /**
+        * Helper method that puts an integer choice value into the preferences.
+        * 
+        * @param key     the preference key.
+        * @param value   the value to store.
+        */
+       public static void putChoise(String key, int value) {
+               NODE.putInt(key, value);
+               storeVersion();
+       }
+       
+       
+       
+       public static String getString(String key, String def) {
+               return NODE.get(key, def);
+       }
+       
+       public static void putString(String key, String value) {
+               NODE.put(key, value);
+               storeVersion();
+       }
+       
+
+       
+       
+       //////////////////
+       
+       public static File getDefaultDirectory() {
+               String file = NODE.get("defaultDirectory", null);
+               if (file == null)
+                       return null;
+               return new File(file);
+       }
+       
+       public static void setDefaultDirectory(File dir) {
+               String d;
+               if (dir == null) {
+                       d = null;
+               } else {
+                       d = dir.getAbsolutePath();
+               }
+               NODE.put("defaultDirectory", d);
+               storeVersion();
+       }
+       
+       
+       
+       public static Color getDefaultColor(Class<? extends RocketComponent> c) {
+               String color = get("componentColors", c, DEFAULT_COLORS);
+               if (color == null)
+                       return Color.BLACK;
+
+               String[] rgb = color.split(",");
+               if (rgb.length==3) {
+                       try {
+                               int red = MathUtil.clamp(Integer.parseInt(rgb[0]),0,255);
+                               int green = MathUtil.clamp(Integer.parseInt(rgb[1]),0,255);
+                               int blue = MathUtil.clamp(Integer.parseInt(rgb[2]),0,255);
+                               return new Color(red,green,blue);
+                       } catch (NumberFormatException ignore) { }
+               }
+
+               return Color.BLACK;
+       }
+       
+       public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
+               if (color==null)
+                       return;
+               String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
+               set("componentColors", c, string);
+       }
+       
+       public static Color getMotorBorderColor() {
+               // TODO: MEDIUM:  Motor color (settable?)
+               return new Color(0,0,0,200);
+       }
+
+       
+       public static Color getMotorFillColor() {
+               // TODO: MEDIUM:  Motor fill color (settable?)
+               return new Color(0,0,0,100);
+       }
+       
+       
+       public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
+               String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
+               try {
+                       return LineStyle.valueOf(value);
+               } catch (Exception e) {
+                       return LineStyle.SOLID;
+               }
+       }
+       
+       public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
+                       LineStyle style) {
+               if (style == null)
+                       return;
+               set("componentStyle", c, style.name());
+       }
+       
+
+       /**
+        * Return the DPI setting of the monitor.  This is either the setting provided
+        * by the system or a user-specified DPI setting.
+        * 
+        * @return    the DPI setting to use.
+        */
+       public static double getDPI() {
+               int dpi = NODE.getInt("DPI", 0);  // Tenths of a dpi
+               
+               if (dpi < 10) {
+                       dpi = Toolkit.getDefaultToolkit().getScreenResolution()*10;
+               }
+               if (dpi < 10)
+                       dpi = 960;
+               
+               return ((double)dpi)/10.0;
+       }
+       
+       
+       public static double getDefaultMach() {
+               // TODO: HIGH: implement custom default mach number
+               return 0.3;
+       }
+       
+       
+       
+       
+       public static Material getDefaultComponentMaterial(
+                       Class<? extends RocketComponent> componentClass,
+                       Material.Type type) {
+               
+               String material = get("componentMaterials", componentClass, null);
+               if (material != null) {
+                       try {
+                               Material m = Material.fromStorableString(material);
+                               if (m.getType() == type)
+                                       return m;
+                       } catch (IllegalArgumentException ignore) { }
+               }
+               
+               switch (type) {
+               case LINE:
+                       return DEFAULT_LINE_MATERIAL;
+               case SURFACE:
+                       return DEFAULT_SURFACE_MATERIAL;
+               case BULK:
+                       return DEFAULT_BULK_MATERIAL;
+               }
+               throw new IllegalArgumentException("Unknown material type: "+type);
+       }
+       
+       public static void setDefaultComponentMaterial(
+                       Class<? extends RocketComponent> componentClass, Material material) {
+               
+               set("componentMaterials", componentClass, 
+                               material==null ? null : material.toStorableString());
+       }
+       
+       
+       public static int getMaxThreadCount() {
+               return Runtime.getRuntime().availableProcessors();
+       }
+       
+       
+       
+       public static Point getWindowPosition(Class<?> c) {
+               int x, y;
+               String pref = NODE.node("windows").get("position." + c.getCanonicalName(), null);
+               
+               if (pref == null)
+                       return null;
+               
+               if (pref.indexOf(',')<0)
+                       return null;
+               
+               try {
+                       x = Integer.parseInt(pref.substring(0,pref.indexOf(',')));
+                       y = Integer.parseInt(pref.substring(pref.indexOf(',')+1));
+               } catch (NumberFormatException e) {
+                       return null;
+               }
+               return new Point(x,y);
+       }
+       
+       public static void setWindowPosition(Class<?> c, Point p) {
+               NODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
+               storeVersion();
+       }
+       
+       
+       
+
+       public static Dimension getWindowSize(Class<?> c) {
+               int x, y;
+               String pref = NODE.node("windows").get("size." + c.getCanonicalName(), null);
+               
+               if (pref == null)
+                       return null;
+               
+               if (pref.indexOf(',')<0)
+                       return null;
+               
+               try {
+                       x = Integer.parseInt(pref.substring(0,pref.indexOf(',')));
+                       y = Integer.parseInt(pref.substring(pref.indexOf(',')+1));
+               } catch (NumberFormatException e) {
+                       return null;
+               }
+               return new Dimension(x,y);
+       }
+       
+       public static void setWindowSize(Class<?> c, Dimension d) {
+               NODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
+               storeVersion();
+       }
+       
+       
+       ////  Background flight data computation
+       
+       public static boolean computeFlightInBackground() {
+               return NODE.getBoolean("backgroundFlight", true);
+       }
+       
+       public static Simulation getBackgroundSimulation(Rocket rocket) {
+               Simulation s = new Simulation(rocket);
+               SimulationConditions cond = s.getConditions();
+               
+               cond.setTimeStep(RK4Simulator.RECOMMENDED_TIME_STEP*2);
+               cond.setWindSpeedAverage(1.0);
+               cond.setWindSpeedDeviation(0.1);
+               cond.setLaunchRodLength(5);
+               return s;
+       }
+       
+       
+       
+       
+       /////////  Default unit storage
+       
+       public static void loadDefaultUnits() {
+               Preferences prefs = NODE.node("units");
+               try {
+                       
+                       for (String key: prefs.keys()) {
+                               UnitGroup group = UnitGroup.UNITS.get(key);
+                               if (group == null)
+                                       continue;
+                               
+                               group.setDefaultUnit(prefs.get(key, null));
+                       }
+                       
+               } catch (BackingStoreException e) {
+                       System.err.println("BackingStoreException:");
+                       e.printStackTrace();
+               }
+       }
+       
+       public static void storeDefaultUnits() {
+               Preferences prefs = NODE.node("units");
+               
+               for (String key: UnitGroup.UNITS.keySet()) {
+                       UnitGroup group = UnitGroup.UNITS.get(key);
+                       if (group == null || group.getUnitCount() < 2)
+                               continue;
+                       
+                       prefs.put(key, group.getDefaultUnit().getUnit());
+               }
+       }
+       
+       
+       
+       ////  Helper methods
+       
+       private static String get(String directory, 
+                       Class<? extends RocketComponent> componentClass,
+                       Map<Class<?>, String> defaultMap) {
+
+               // Search preferences
+               Class<?> c = componentClass;
+               Preferences prefs = NODE.node(directory);
+               while (c!=null && RocketComponent.class.isAssignableFrom(c)) {
+                       String value = prefs.get(c.getSimpleName(), null);
+                       if (value != null)
+                               return value;
+                       c = c.getSuperclass();
+               }
+               
+               if (defaultMap == null)
+                       return null;
+
+               // Search defaults
+               c = componentClass;
+               while (RocketComponent.class.isAssignableFrom(c)) {
+                       String value = defaultMap.get(c);
+                       if (value != null)
+                               return value;
+                       c = c.getSuperclass();
+               }
+               
+               return null;
+       }
+
+
+       private static void set(String directory, Class<? extends RocketComponent> componentClass,
+                       String value) {
+               Preferences prefs = NODE.node(directory);
+               if (value == null)
+                       prefs.remove(componentClass.getSimpleName());
+               else
+                       prefs.put(componentClass.getSimpleName(), value);
+               storeVersion();
+       }
+
+}
diff --git a/src/net/sf/openrocket/util/Quaternion.java b/src/net/sf/openrocket/util/Quaternion.java
new file mode 100644 (file)
index 0000000..3c457ca
--- /dev/null
@@ -0,0 +1,292 @@
+package net.sf.openrocket.util;
+
+
+public class Quaternion implements Cloneable {
+
+       protected double w, x, y, z;
+       protected int modCount = 0;
+       
+       public Quaternion() {
+               this(1,0,0,0);
+       }
+       
+       public Quaternion(double w, double x, double y, double z) {
+               this.w = w;
+               this.x = x;
+               this.y = y;
+               this.z = z;
+       }
+       
+       
+       public static Quaternion rotation(Coordinate rotation) {
+               double length = rotation.length();
+               if (length < 0.000001) {
+                       return new Quaternion(1,0,0,0);
+               }
+               double sin = Math.sin(length/2);
+               double cos = Math.cos(length/2);
+               return new Quaternion(cos, 
+                               sin*rotation.x/length, sin*rotation.y/length, sin*rotation.z/length);
+       }
+       
+       public static Quaternion rotation(Coordinate axis, double angle) {
+               Coordinate a = axis.normalize();
+               double sin = Math.sin(angle);
+               double cos = Math.cos(angle);
+               return new Quaternion(cos, sin*a.x, sin*a.y, sin*a.z);
+       }
+
+       
+       public double getW() {
+               return w;
+       }
+
+       public void setW(double w) {
+               this.w = w;
+               modCount++;
+       }
+
+       public double getX() {
+               return x;
+       }
+
+       public void setX(double x) {
+               this.x = x;
+               modCount++;
+       }
+
+       public double getY() {
+               return y;
+       }
+
+       public void setY(double y) {
+               this.y = y;
+               modCount++;
+       }
+
+       public double getZ() {
+               return z;
+       }
+
+       public void setZ(double z) {
+               this.z = z;
+               modCount++;
+       }
+       
+       
+       public void setAll(double w, double x, double y, double z) {
+               this.w = w;
+               this.x = x;
+               this.y = y;
+               this.z = z;
+               modCount++;
+       }
+
+       
+       /**
+        * Multiply this quaternion by the other quaternion from the right side.  This
+        * calculates the product  <code>this = this * other</code>.
+        * 
+        * @param other   the quaternion to multiply this quaternion by.
+        * @return                this quaternion.
+        */
+       public Quaternion multiplyRight(Quaternion other) {
+               double w = (this.w*other.w - this.x*other.x - this.y*other.y - this.z*other.z);
+               double x = (this.w*other.x + this.x*other.w + this.y*other.z - this.z*other.y);
+               double y = (this.w*other.y + this.y*other.w + this.z*other.x - this.x*other.z);
+               double z = (this.w*other.z + this.z*other.w + this.x*other.y - this.y*other.x);
+               
+               this.w = w;
+               this.x = x;
+               this.y = y;
+               this.z = z;
+               return this;
+       }
+       
+       /**
+        * Multiply this quaternion by the other quaternion from the left side.  This
+        * calculates the product  <code>this = other * this</code>.
+        * 
+        * @param other   the quaternion to multiply this quaternion by.
+        * @return                this quaternion.
+        */
+       public Quaternion multiplyLeft(Quaternion other) {
+               /*  other(abcd) * this(wxyz)  */
+               
+               double w = (other.w*this.w - other.x*this.x - other.y*this.y - other.z*this.z);
+               double x = (other.w*this.x + other.x*this.w + other.y*this.z - other.z*this.y);
+               double y = (other.w*this.y + other.y*this.w + other.z*this.x - other.x*this.z);
+               double z = (other.w*this.z + other.z*this.w + other.x*this.y - other.y*this.x);
+               
+               this.w = w;
+               this.x = x;
+               this.y = y;
+               this.z = z;
+               return this;
+       }
+       
+       
+
+       
+
+
+       @Override
+       public Quaternion clone() {
+               try {
+                       return (Quaternion) super.clone();
+               } catch (CloneNotSupportedException e) {
+                       throw new RuntimeException("CloneNotSupportedException encountered");
+               }
+       }
+
+
+
+       /**
+        * Normalize this quaternion.  After the call the norm of the quaternion is exactly
+        * one.  If this quaternion is the zero quaternion, throws
+        * <code>IllegalStateException</code>.  Returns this quaternion.
+        * 
+        * @return   this quaternion.
+        * @throws   IllegalStateException  if the norm of this quaternion is zero.
+        */
+       public Quaternion normalize() {
+               double norm = norm();
+               if (norm < 0.0000001) {
+                       throw new IllegalStateException("attempting to normalize zero-quaternion");
+               }
+               x /= norm;
+               y /= norm;
+               z /= norm;
+               w /= norm;
+               return this;
+       }
+       
+       
+       /**
+        * Normalize this quaternion if the norm is more than 1ppm from one.
+        * 
+        * @return      this quaternion.
+        * @throws   IllegalStateException  if the norm of this quaternion is zero.
+        */
+       public Quaternion normalizeIfNecessary() {
+               double n2 = norm2();
+               if (n2 < 0.999999 || n2 > 1.000001)
+                       normalize();
+               return this;
+       }
+       
+       
+       
+       /**
+        * Return the norm of this quaternion.  
+        * 
+        * @return   the norm of this quaternion sqrt(w^2 + x^2 + y^2 + z^2).
+        */
+       public double norm() {
+               return Math.sqrt(x*x + y*y + z*z + w*w);
+       }
+       
+       /**
+        * Return the square of the norm of this quaternion.
+        * 
+        * @return      the square of the norm of this quaternion (w^2 + x^2 + y^2 + z^2).
+        */
+       public double norm2() {
+               return x*x + y*y + z*z + w*w;
+       }
+       
+       
+       public Coordinate rotate(Coordinate coord) {
+               double a,b,c,d;
+               
+               assert(Math.abs(norm2()-1) < 0.00001) : "Quaternion not unit length: "+this;
+               
+               a = - x * coord.x - y * coord.y - z * coord.z;  // w
+               b =   w * coord.x + y * coord.z - z * coord.y;  // x i
+               c =   w * coord.y - x * coord.z + z * coord.x;  // y j
+               d =   w * coord.z + x * coord.y - y * coord.x;  // z k
+               
+               assert(MathUtil.equals(a*w + b*x + c*y + d*z, 0)) : 
+                       ("Should be zero: " + (a*w - b*x - c*y - d*z) + " in " + this + " c=" + coord);
+                               
+               return new Coordinate(
+                               - a*x + b*w - c*z + d*y,
+                               - a*y + b*z + c*w - d*x,
+                               - a*z - b*y + c*x + d*w,
+                               coord.weight
+               );
+       }
+       
+       public Coordinate invRotate(Coordinate coord) {
+               double a,b,c,d;
+
+               assert(Math.abs(norm2()-1) < 0.00001) : "Quaternion not unit length: "+this;
+
+               a = + x * coord.x + y * coord.y + z * coord.z;
+               b =   w * coord.x - y * coord.z + z * coord.y;
+               c =   w * coord.y + x * coord.z - z * coord.x;
+               d =   w * coord.z - x * coord.y + y * coord.x;
+               
+               assert(MathUtil.equals(a*w - b*x - c*y - d*z, 0)): 
+                       ("Should be zero: " + (a*w - b*x - c*y - d*z) + " in " + this + " c=" + coord);
+               
+               return new Coordinate(
+                               a*x + b*w + c*z - d*y,
+                               a*y - b*z + c*w + d*x,
+                               a*z + b*y - c*x + d*w,
+                               coord.weight
+               );
+       }
+       
+       
+       /**
+        * Rotate the coordinate (0,0,1) using this quaternion.  The result is returned
+        * as a Coordinate.  This method is equivalent to calling
+        * <code>q.rotate(new Coordinate(0,0,1))</code> but requires only about half of the 
+        * multiplications.
+        * 
+        * @return      The coordinate (0,0,1) rotated using this quaternion.
+        */
+       public Coordinate rotateZ() {
+               return new Coordinate(
+                               2*(w*y + x*z),
+                               2*(y*z - w*x),
+                               w*w - x*x - y*y + z*z
+               );
+       }
+       
+       
+       @Override
+       public String toString() {
+               return String.format("Quaternion[%f,%f,%f,%f,norm=%f]",w,x,y,z,this.norm());
+       }
+       
+       public static void main(String[] arg) {
+               
+               Quaternion q = new Quaternion(Math.random()-0.5,Math.random()-0.5,
+                               Math.random()-0.5,Math.random()-0.5);
+               q.normalize();
+               
+               q = new Quaternion(-0.998717,0.000000,0.050649,-0.000000);
+               
+               Coordinate coord = new Coordinate(10*(Math.random()-0.5), 
+                               10*(Math.random()-0.5), 10*(Math.random()-0.5));
+               
+               System.out.println("Quaternion: "+q);
+               System.out.println("Coordinate: "+coord);
+               coord = q.invRotate(coord);
+               System.out.println("Rotated: "+ coord);
+               coord = q.rotate(coord);
+               System.out.println("Back:       "+coord);
+               
+//             Coordinate c = new Coordinate(0,1,0);
+//             Coordinate rot = new Coordinate(Math.PI/4,0,0);
+//             
+//             System.out.println("Before: "+c);
+//             c = rotation(rot).invRotate(c);
+//             System.out.println("After: "+c);
+               
+               
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/QuaternionMultiply.java b/src/net/sf/openrocket/util/QuaternionMultiply.java
new file mode 100644 (file)
index 0000000..099ecf8
--- /dev/null
@@ -0,0 +1,90 @@
+package net.sf.openrocket.util;
+
+public class QuaternionMultiply {
+
+       private static class Value {
+               public int sign = 1;
+               public String value;
+               
+               public Value multiply(Value other) {
+                       Value result = new Value();
+                       result.sign = this.sign * other.sign;
+                       if (this.value.compareTo(other.value) < 0)
+                               result.value = this.value + "*" + other.value;
+                       else
+                               result.value = other.value + "*" + this.value;
+                       return result;
+               }
+               @Override
+               public String toString() {
+                       String s;
+                       
+                       if (sign < 0)
+                               s = "-";
+                       else
+                               s = "+";
+                       
+                       if (sign == 0)
+                               s += " 0";
+                       else
+                               s += " " + value;
+                       
+                       return s;
+               }
+       }
+       
+       
+       
+       private static Value[] multiply(Value[] first, Value[] second) {
+               return null;
+       }
+       
+       
+       public static void main(String[] arg) {
+               if (arg.length % 4 != 0  || arg.length < 4) {
+                       System.out.println("Must have modulo 4 args, at least 4");
+                       return;
+               }
+               
+               Value[][] values = new Value[arg.length/4][4];
+               
+               for (int i=0; i<arg.length; i++) {
+                       Value value = new Value();
+                       
+                       if (arg[i].equals("")) {
+                               value.sign = 0;
+                       } else {
+                               if (arg[i].startsWith("-")) {
+                                       value.sign = -1;
+                                       value.value = arg[i].substring(1);
+                               } else if (arg[i].startsWith("+")) {
+                                       value.sign = 1;
+                                       value.value = arg[i].substring(1);
+                               } else {
+                                       value.sign = 1;
+                                       value.value = arg[i];
+                               }
+                       }
+                       
+                       values[i/4][i%4] = value;
+               }
+
+               System.out.println("Multiplying:");
+               for (int i=0; i < values.length; i++) {
+                       print(values[i]);
+               }
+               System.out.println("Result:");
+               
+               Value[] result = values[0];
+               for (int i=1; i < values.length; i++) {
+                       result = multiply(result, values[i]);
+               }
+               print(result);
+       }
+       
+       private static void print(Value[] q) {
+               System.out.println("   " + q[0] + " " + q[1] + " i " + q[2] + " j " + q[3] + " k");
+       }
+       
+}
+
diff --git a/src/net/sf/openrocket/util/Reflection.java b/src/net/sf/openrocket/util/Reflection.java
new file mode 100644 (file)
index 0000000..26c00be
--- /dev/null
@@ -0,0 +1,162 @@
+package net.sf.openrocket.util;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+public class Reflection {
+       
+       private static final String ROCKETCOMPONENT_PACKAGE = "net.sf.openrocket.rocketcomponent";
+       
+       /**
+        * Simple wrapper class that converts the Method.invoke() exceptions into suitable
+        * RuntimeExceptions.
+        * 
+        * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+        */
+       public static class Method {
+               private final java.lang.reflect.Method method;
+               public Method(java.lang.reflect.Method m) {
+                       method = m;
+               }
+               /**
+                * Same as Method.invoke(), but the possible exceptions are wrapped into 
+                * RuntimeExceptions.
+                */
+               public Object invoke(Object obj, Object... args) {
+                       try {
+                               return method.invoke(obj, args);
+                       } catch (IllegalArgumentException e) {
+                               throw new RuntimeException("Error while invoking method '"+method+"'. "+
+                                               "Please report this as a bug.",e);
+                       } catch (IllegalAccessException e) {
+                               throw new RuntimeException("Error while invoking method '"+method+"'. "+
+                                               "Please report this as a bug.",e);
+                       } catch (InvocationTargetException e) {
+                               throw new RuntimeException("Error while invoking method '"+method+"'. "+
+                                               "Please report this as a bug.",e);
+                       }
+               }
+               /**
+                * Invoke static method.  Equivalent to invoke(null, args...).
+                */
+               public Object invokeStatic(Object... args) {
+                       return invoke(null,args);
+               }
+               /**
+                * Same as Method.toString().
+                */
+               @Override
+               public String toString() {
+                       return method.toString();
+               }
+       }
+       
+       
+       /**
+        * Throws an exception if method not found.
+        */
+       public static Reflection.Method findMethodStatic(
+                       Class<? extends RocketComponent> componentClass,
+                       String method, Class<?>... params) {
+               Reflection.Method m = findMethod(ROCKETCOMPONENT_PACKAGE, componentClass, 
+                               "", method, params);
+               if (m == null) {
+                       throw new RuntimeException("Could not find method for componentClass="
+                                       +componentClass+" method="+method);
+               }
+               return m;
+       }
+       
+       
+       
+       public static Reflection.Method findMethod(String pack, RocketComponent component, 
+                       String method, Class<?>...params) {
+               return findMethod(pack,component.getClass(),"",method,params);
+       }
+       
+       
+       public static Reflection.Method findMethod(String pack, RocketComponent component, 
+                       String suffix, String method, Class<?>... params) {
+               return findMethod(pack, component.getClass(), suffix, method, params);
+       }
+
+       
+       public static Reflection.Method findMethod(String pack, 
+                       Class<? extends RocketComponent> componentClass, 
+                       String suffix, String method, Class<?>... params) {
+               Class<?> currentclass;
+               String name;
+               
+               currentclass = componentClass;
+               while ((currentclass != null) && (currentclass != Object.class)) {
+                       name = currentclass.getCanonicalName();
+                       if (name.lastIndexOf('.')>=0)
+                               name = name.substring(name.lastIndexOf(".")+1);
+                       name = pack + "." + name + suffix;
+                       
+                       try {
+                               Class<?> c = Class.forName(name);
+                               java.lang.reflect.Method m = c.getMethod(method,params);
+                               return new Reflection.Method(m);
+                       } catch (ClassNotFoundException ignore) {
+                       } catch (NoSuchMethodException ignore) {
+                       }
+
+                       currentclass = currentclass.getSuperclass();
+               }
+               return null;
+       }
+       
+       
+       public static Object construct(String pack, RocketComponent component, String suffix,
+                       Object... params) {
+               
+               Class<?> currentclass;
+               String name;
+               
+               currentclass = component.getClass();
+               while ((currentclass != null) && (currentclass != Object.class)) {
+                       name = currentclass.getCanonicalName();
+                       if (name.lastIndexOf('.')>=0)
+                               name = name.substring(name.lastIndexOf(".")+1);
+                       name = pack + "." + name + suffix;
+                       
+                       try {
+                               Class<?> c = Class.forName(name);
+                               Class<?>[] paramClasses = new Class<?>[params.length];
+                               for (int i=0; i < params.length; i++) {
+                                       paramClasses[i] = params[i].getClass();
+                               }
+                               
+                               // Constructors must be searched manually.  Why?!
+                               main: for (Constructor<?> constructor: c.getConstructors()) {
+                                       Class<?>[] parameterTypes = constructor.getParameterTypes();
+                                       if (params.length != parameterTypes.length)
+                                               continue;
+                                       for (int i=0; i < params.length; i++) {
+                                               if (!parameterTypes[i].isInstance(params[i])) 
+                                                       continue main;
+                                       }
+                                       // Matching constructor found
+                                       return constructor.newInstance(params);
+                               }
+                       } catch (ClassNotFoundException ignore) {
+                       } catch (IllegalArgumentException e) {
+                               throw new RuntimeException("Construction of "+name+" failed",e);
+                       } catch (InstantiationException e) {
+                               throw new RuntimeException("Construction of "+name+" failed",e);
+                       } catch (IllegalAccessException e) {
+                               throw new RuntimeException("Construction of "+name+" failed",e);
+                       } catch (InvocationTargetException e) {
+                               throw new RuntimeException("Construction of "+name+" failed",e);
+                       }
+
+                       currentclass = currentclass.getSuperclass();
+               }
+               throw new RuntimeException("Suitable constructor for component "+component+ 
+                               " not found");
+       }
+}
diff --git a/src/net/sf/openrocket/util/Rotation2D.java b/src/net/sf/openrocket/util/Rotation2D.java
new file mode 100644 (file)
index 0000000..f8f063e
--- /dev/null
@@ -0,0 +1,58 @@
+package net.sf.openrocket.util;
+
+public class Rotation2D {
+       
+       public static final Rotation2D ID = new Rotation2D(0.0, 1.0);
+
+       public final double sin, cos;
+
+       
+       public Rotation2D(double angle) {
+               this(Math.sin(angle), Math.cos(angle));
+       }
+       
+       public Rotation2D(double sin, double cos) {
+               this.sin = sin;
+               this.cos = cos;
+       }
+       
+       public Coordinate rotateX(Coordinate c) {
+               return new Coordinate(c.x, cos*c.y - sin*c.z, cos*c.z + sin*c.y, c.weight);
+       }
+       
+       public Coordinate rotateY(Coordinate c) {
+               return new Coordinate(cos*c.x + sin*c.z, c.y, cos*c.z - sin*c.x, c.weight);
+       }
+       
+       public Coordinate rotateZ(Coordinate c) {
+               return new Coordinate(cos*c.x - sin*c.y, cos*c.y + sin*c.x, c.z, c.weight);
+       }
+       
+
+       public Coordinate invRotateX(Coordinate c) {
+               return new Coordinate(c.x, cos*c.y + sin*c.z, cos*c.z - sin*c.y, c.weight);
+       }
+       
+       public Coordinate invRotateY(Coordinate c) {
+               return new Coordinate(cos*c.x - sin*c.z, c.y, cos*c.z + sin*c.x, c.weight);
+       }
+       
+       public Coordinate invRotateZ(Coordinate c) {
+               return new Coordinate(cos*c.x + sin*c.y, cos*c.y - sin*c.x, c.z, c.weight);
+       }
+       
+
+       
+       public static void main(String arg[]) {
+               Coordinate c = new Coordinate(1,1,1,2.5);
+               Rotation2D rot = new Rotation2D(Math.PI/4);
+               
+               System.out.println("X: "+rot.rotateX(c));
+               System.out.println("Y: "+rot.rotateY(c));
+               System.out.println("Z: "+rot.rotateZ(c));
+               System.out.println("invX: "+rot.invRotateX(c));
+               System.out.println("invY: "+rot.invRotateY(c));
+               System.out.println("invZ: "+rot.invRotateZ(c));
+       }
+       
+}
diff --git a/src/net/sf/openrocket/util/Test.java b/src/net/sf/openrocket/util/Test.java
new file mode 100644 (file)
index 0000000..8f03955
--- /dev/null
@@ -0,0 +1,411 @@
+package net.sf.openrocket.util;
+
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
+
+public class Test {
+
+       public static double noseconeLength=0.10,noseconeRadius=0.01;
+       public static double bodytubeLength=0.20,bodytubeRadius=0.01,bodytubeThickness=0.001;
+       
+       public static int finCount=3;
+       public static double finRootChord=0.04,finTipChord=0.05,finSweep=0.01,finThickness=0.003, finHeight=0.03;
+       
+       public static double materialDensity=1000;  // kg/m3
+       
+
+       public static Rocket makeRocket() {
+               Rocket rocket;
+               Stage stage,stage2;
+               NoseCone nosecone;
+               BodyTube bodytube, bt2;
+               Transition transition;
+               TrapezoidFinSet finset;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+               stage2 = new Stage();
+               stage2.setName("Stage2");
+               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
+               bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
+               transition = new Transition();
+               bt2 = new BodyTube(bodytubeLength,bodytubeRadius*2,bodytubeThickness);
+               bt2.setMotorMount(true);
+
+               finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
+               
+               
+               // Stage construction
+               rocket.addChild(stage);
+               rocket.addChild(stage2);
+
+               
+               // Component construction
+               stage.addChild(nosecone);
+               
+               stage.addChild(bodytube);
+
+               
+               stage2.addChild(transition);
+               
+               stage2.addChild(bt2);
+               
+               bodytube.addChild(finset);
+               
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               return rocket;
+       }
+       
+
+       public static Rocket makeSmallFlyable() {
+               Rocket rocket;
+               Stage stage;
+               NoseCone nosecone;
+               BodyTube bodytube;
+               TrapezoidFinSet finset;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+
+               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
+               bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
+
+               finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
+               
+               
+               // Stage construction
+               rocket.addChild(stage);
+
+               
+               // Component construction
+               stage.addChild(nosecone);
+               stage.addChild(bodytube);
+
+               bodytube.addChild(finset);
+               
+               Material material = Prefs.getDefaultComponentMaterial(null, Material.Type.BULK);
+               nosecone.setMaterial(material);
+               bodytube.setMaterial(material);
+               finset.setMaterial(material);
+               
+               String id = rocket.newMotorConfigurationID();
+               bodytube.setMotorMount(true);
+               
+               for (Motor m: Databases.MOTOR) {
+                       if (m.getDesignation().equals("B4")) {
+                               bodytube.setMotor(id, m);
+                               break;
+                       }
+               }
+               bodytube.setMotorOverhang(0.005);
+               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               return rocket;
+       }
+
+
+       public static Rocket makeBigBlue() {
+               Rocket rocket;
+               Stage stage;
+               NoseCone nosecone;
+               BodyTube bodytube;
+               FreeformFinSet finset;
+               MassComponent mcomp;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+
+               nosecone = new NoseCone(Transition.Shape.ELLIPSOID,0.105,0.033);
+               nosecone.setThickness(0.001);
+               bodytube = new BodyTube(0.69,0.033,0.001);
+
+               finset = new FreeformFinSet();
+               finset.setPoints(new Coordinate[] {
+                               new Coordinate(0, 0),
+                               new Coordinate(0.115, 0.072),
+                               new Coordinate(0.255, 0.072),
+                               new Coordinate(0.255, 0.037),
+                               new Coordinate(0.150, 0)
+               });
+               finset.setThickness(0.003);
+               finset.setFinCount(4);
+               
+               finset.setCantAngle(0*Math.PI/180);
+               System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
+               
+               mcomp = new MassComponent(0.2,0.03,0.045 + 0.060);
+               mcomp.setRelativePosition(Position.TOP);
+               mcomp.setPositionValue(0);
+               
+               // Stage construction
+               rocket.addChild(stage);
+               rocket.setPerfectFinish(false);
+
+               
+               // Component construction
+               stage.addChild(nosecone);
+               stage.addChild(bodytube);
+
+               bodytube.addChild(finset);
+               
+               bodytube.addChild(mcomp);
+               
+//             Material material = new Material("Test material", 500);
+//             nosecone.setMaterial(material);
+//             bodytube.setMaterial(material);
+//             finset.setMaterial(material);
+               
+               String id = rocket.newMotorConfigurationID();
+               bodytube.setMotorMount(true);
+               
+               for (Motor m: Databases.MOTOR) {
+                       if (m.getDesignation().equals("F12J")) {
+                               bodytube.setMotor(id, m);
+                               break;
+                       }
+               }
+               bodytube.setMotorOverhang(0.005);
+               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               return rocket;
+       }
+       
+       
+
+       public static Rocket makeIsoHaisu() {
+               Rocket rocket;
+               Stage stage;
+               NoseCone nosecone;
+               BodyTube tube1, tube2, tube3;
+               TrapezoidFinSet finset;
+               TrapezoidFinSet auxfinset;
+               MassComponent mcomp;
+               
+               final double R = 0.07;
+               
+               rocket = new Rocket();
+               stage = new Stage();
+               stage.setName("Stage1");
+
+               nosecone = new NoseCone(Transition.Shape.OGIVE,0.53,R);
+               nosecone.setThickness(0.005);
+               nosecone.setMassOverridden(true);
+               nosecone.setOverrideMass(0.588);
+               stage.addChild(nosecone);
+               
+               tube1 = new BodyTube(0.505,R,0.005);
+               tube1.setMassOverridden(true);
+               tube1.setOverrideMass(0.366);
+               stage.addChild(tube1);
+               
+               tube2 = new BodyTube(0.605,R,0.005);
+               tube2.setMassOverridden(true);
+               tube2.setOverrideMass(0.427);
+               stage.addChild(tube2);
+               
+               tube3 = new BodyTube(1.065,R,0.005);
+               tube3.setMassOverridden(true);
+               tube3.setOverrideMass(0.730);
+               stage.addChild(tube3);
+               
+               
+               LaunchLug lug = new LaunchLug();
+               tube1.addChild(lug);
+               
+               TubeCoupler coupler = new TubeCoupler();
+               coupler.setOuterRadiusAutomatic(true);
+               coupler.setThickness(0.005);
+               coupler.setLength(0.28);
+               coupler.setMassOverridden(true);
+               coupler.setOverrideMass(0.360);
+               coupler.setRelativePosition(Position.BOTTOM);
+               coupler.setPositionValue(-0.14);
+               tube1.addChild(coupler);
+               
+               
+               // Parachute
+               MassComponent mass = new MassComponent(0.05, 0.05, 0.280);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.2);
+               tube1.addChild(mass);
+               
+               // Cord
+               mass = new MassComponent(0.05, 0.05, 0.125);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.2);
+               tube1.addChild(mass);
+               
+               // Payload
+               mass = new MassComponent(0.40, R, 1.500);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.25);
+               tube1.addChild(mass);
+               
+               
+               auxfinset = new TrapezoidFinSet();
+               auxfinset.setName("CONTROL");
+               auxfinset.setFinCount(2);
+               auxfinset.setRootChord(0.05);
+               auxfinset.setTipChord(0.05);
+               auxfinset.setHeight(0.10);
+               auxfinset.setSweep(0);
+               auxfinset.setThickness(0.008);
+               auxfinset.setCrossSection(CrossSection.AIRFOIL);
+               auxfinset.setRelativePosition(Position.TOP);
+               auxfinset.setPositionValue(0.28);
+               auxfinset.setBaseRotation(Math.PI/2);
+               tube1.addChild(auxfinset);
+               
+               
+               
+               
+               coupler = new TubeCoupler();
+               coupler.setOuterRadiusAutomatic(true);
+               coupler.setLength(0.28);
+               coupler.setRelativePosition(Position.TOP);
+               coupler.setPositionValue(0.47);
+               coupler.setMassOverridden(true);
+               coupler.setOverrideMass(0.360);
+               tube2.addChild(coupler);
+               
+               
+               
+               // Parachute
+               mass = new MassComponent(0.1, 0.05, 0.028);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.14);
+               tube2.addChild(mass);
+               
+               Bulkhead bulk = new Bulkhead();
+               bulk.setOuterRadiusAutomatic(true);
+               bulk.setMassOverridden(true);
+               bulk.setOverrideMass(0.050);
+               bulk.setRelativePosition(Position.TOP);
+               bulk.setPositionValue(0.27);
+               tube2.addChild(bulk);
+               
+               // Chord
+               mass = new MassComponent(0.1, 0.05, 0.125);
+               mass.setRelativePosition(Position.TOP);
+               mass.setPositionValue(0.19);
+               tube2.addChild(mass);
+               
+               
+               
+               InnerTube inner = new InnerTube();
+               inner.setOuterRadius(0.08/2);
+               inner.setInnerRadius(0.0762/2);
+               inner.setLength(0.86);
+               inner.setMassOverridden(true);
+               inner.setOverrideMass(0.388);
+               tube3.addChild(inner);
+               
+               
+               CenteringRing center = new CenteringRing();
+               center.setInnerRadiusAutomatic(true);
+               center.setOuterRadiusAutomatic(true);
+               center.setLength(0.005);
+               center.setMassOverridden(true);
+               center.setOverrideMass(0.038);
+               center.setRelativePosition(Position.BOTTOM);
+               center.setPositionValue(0);
+               tube3.addChild(center);
+               
+               
+               center = new CenteringRing();
+               center.setInnerRadiusAutomatic(true);
+               center.setOuterRadiusAutomatic(true);
+               center.setLength(0.005);
+               center.setMassOverridden(true);
+               center.setOverrideMass(0.038);
+               center.setRelativePosition(Position.TOP);
+               center.setPositionValue(0.28);
+               tube3.addChild(center);
+               
+               
+               center = new CenteringRing();
+               center.setInnerRadiusAutomatic(true);
+               center.setOuterRadiusAutomatic(true);
+               center.setLength(0.005);
+               center.setMassOverridden(true);
+               center.setOverrideMass(0.038);
+               center.setRelativePosition(Position.TOP);
+               center.setPositionValue(0.83);
+               tube3.addChild(center);
+               
+               
+               
+               
+               
+               finset = new TrapezoidFinSet();
+               finset.setRootChord(0.495);
+               finset.setTipChord(0.1);
+               finset.setHeight(0.185);
+               finset.setThickness(0.005);
+               finset.setSweep(0.3);
+               finset.setRelativePosition(Position.BOTTOM);
+               finset.setPositionValue(-0.03);
+               finset.setBaseRotation(Math.PI/2);
+               tube3.addChild(finset);
+               
+               
+               finset.setCantAngle(0*Math.PI/180);
+               System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
+               
+               
+               // Stage construction
+               rocket.addChild(stage);
+               rocket.setPerfectFinish(false);
+
+               
+               
+               String id = rocket.newMotorConfigurationID();
+               tube3.setMotorMount(true);
+               
+               for (Motor m: Databases.MOTOR) {
+                       if (m.getDesignation().equals("L540")) {
+                               tube3.setMotor(id, m);
+                               break;
+                       }
+               }
+               tube3.setMotorOverhang(0.02);
+               rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+
+//             tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER);
+               
+               rocket.getDefaultConfiguration().setAllStages();
+               
+               
+               return rocket;
+       }
+       
+       
+       
+}
diff --git a/src/net/sf/openrocket/util/Transformation.java b/src/net/sf/openrocket/util/Transformation.java
new file mode 100644 (file)
index 0000000..d263c42
--- /dev/null
@@ -0,0 +1,263 @@
+package net.sf.openrocket.util;
+
+import java.util.*;
+
+/**
+ * Defines an affine transformation of the form  A*x+c,  where x and c are Coordinates and
+ * A is a 3x3 matrix.
+ * 
+ * The Transformations are immutable.  All modification methods return a new transformation.
+ * 
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class Transformation implements java.io.Serializable {
+
+       
+       public static final Transformation IDENTITY =
+               new Transformation();
+       
+       public static final Transformation PROJECT_XY = 
+               new Transformation(new double[][]{{1,0,0},{0,1,0},{0,0,0}});
+       public static final Transformation PROJECT_YZ = 
+               new Transformation(new double[][]{{0,0,0},{0,1,0},{0,0,1}});
+       public static final Transformation PROJECT_XZ = 
+               new Transformation(new double[][]{{1,0,0},{0,0,0},{0,0,1}});
+       
+       private static final int X = 0;
+       private static final int Y = 1;
+       private static final int Z = 2;
+       
+       private final Coordinate translate;
+       private final double[][] rotation = new double[3][3];
+       
+       /**
+        * Create identity transformation.
+        */
+       public Transformation() {
+               translate = new Coordinate(0,0,0);
+               rotation[X][X]=1;
+               rotation[Y][Y]=1;
+               rotation[Z][Z]=1;
+       }
+       
+       /**
+        * Create transformation with only translation.
+        * @param x Translation in x-axis.
+        * @param y Translation in y-axis.
+        * @param z Translation in z-axis.
+        */
+       public Transformation(double x,double y,double z) {
+               translate = new Coordinate(x,y,z);
+               rotation[X][X]=1;
+               rotation[Y][Y]=1;
+               rotation[Z][Z]=1;
+       }
+
+       /**
+        * Create transformation with only translation.
+        * @param translation  The translation term.
+        */
+       public Transformation(Coordinate translation) {
+               this.translate = translation;
+               rotation[X][X]=1;
+               rotation[Y][Y]=1;
+               rotation[Z][Z]=1;
+       }
+       
+       /**
+        * Create transformation with given rotation matrix and translation.
+        * @param rotation
+        * @param translation
+        */
+       public Transformation(double[][] rotation, Coordinate translation) {
+               for (int i=0; i<3; i++)
+                       for (int j=0; j<3; j++)
+                               this.rotation[i][j] = rotation[i][j];
+               this.translate = translation;
+       }
+       
+       
+       /**
+        * Create transformation with given rotation matrix and translation.
+        * @param rotation
+        * @param translation
+        */
+       public Transformation(double[][] rotation) {
+               for (int i=0; i<3; i++)
+                       for (int j=0; j<3; j++)
+                               this.rotation[i][j] = rotation[i][j];
+               this.translate = Coordinate.NUL;
+       }
+       
+
+       
+       
+       
+       /**
+        * Transform a coordinate according to this transformation.
+        * 
+        * @param orig  the coordinate to transform.
+        * @return              the result.
+        */
+       public Coordinate transform(Coordinate orig) {
+               final double x,y,z;
+
+               x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z + translate.x;
+               y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z + translate.y;
+               z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z + translate.z;
+               
+               return new Coordinate(x,y,z,orig.weight);
+       }
+       
+       
+       /**
+        * Transform an array of coordinates.  The transformed coordinates are stored
+        * in the same array, and the array is returned.
+        * 
+        * @param orig  the coordinates to transform.
+        * @return              <code>orig</code>, with the coordinates transformed.
+        */
+       public Coordinate[] transform(Coordinate[] orig) {
+               for (int i=0; i < orig.length; i++) {
+                       orig[i] = transform(orig[i]);
+               }
+               return orig;
+       }
+       
+       /**
+        * Transforms all coordinates in a Collection.  The original coordinate elements are
+        * removed from the set and replaced with the transformed ones.  The Collection given
+        * must implement the .clear() and .addAll() methods.
+        * 
+        * @param set  Collection of coordinates to transform.
+        */
+       public void transform(Collection<Coordinate> set) {
+               ArrayList<Coordinate> temp = new ArrayList<Coordinate>(set.size());
+               Iterator<Coordinate> iter = set.iterator();
+               while (iter.hasNext())
+                       temp.add(this.transform(iter.next()));
+               set.clear();
+               set.addAll(temp);
+       }
+
+       /**
+        * Applies only the linear transformation  A*x
+        * @param orig  Coordinate to transform.
+        */
+       public Coordinate linearTransform(Coordinate orig) {
+               final double x,y,z;
+
+               x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z;
+               y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z;
+               z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z;
+               
+               return new Coordinate(x,y,z,orig.weight);
+       }
+       
+       /**
+        * Applies the given transformation before this tranformation.  The resulting 
+        * transformation result.transform(c) will equal this.transform(other.transform(c)).
+        * 
+        * @param other  Transformation to apply
+        * @return   The new transformation
+        */
+       public Transformation applyTransformation(Transformation other) {
+               // other = Ax+b
+               // this = Cx+d
+               // C(Ax+b)+d = CAx + Cb+d
+               
+               // Translational portion
+               Transformation combined = new Transformation(
+                               this.linearTransform(other.translate).add(this.translate)
+               );
+               
+               // Linear portion
+               for (int i=0; i<3; i++) {
+                       final double x,y,z;
+                       x = rotation[i][X];
+                       y = rotation[i][Y];
+                       z = rotation[i][Z];
+                       combined.rotation[i][X] = 
+                               x*other.rotation[X][X] + y*other.rotation[Y][X] + z*other.rotation[Z][X];
+                       combined.rotation[i][Y] = 
+                               x*other.rotation[X][Y] + y*other.rotation[Y][Y] + z*other.rotation[Z][Y];
+                       combined.rotation[i][Z] = 
+                               x*other.rotation[X][Z] + y*other.rotation[Y][Z] + z*other.rotation[Z][Z];
+               }
+               return combined;
+       }
+       
+       
+       /**
+        * Rotate around x-axis a given angle.
+        * @param theta  The angle to rotate in radians.
+        * @return  The transformation.
+        */
+       public static Transformation rotate_x(double theta) {
+               return new Transformation(new double[][]{
+                               {1,0,0},
+                               {0,Math.cos(theta),-Math.sin(theta)},
+                               {0,Math.sin(theta),Math.cos(theta)}});
+       }
+       
+       /**
+        * Rotate around y-axis a given angle.
+        * @param theta  The angle to rotate in radians.
+        * @return  The transformation.
+        */
+       public static Transformation rotate_y(double theta) {
+               return new Transformation(new double[][]{
+                               {Math.cos(theta),0,Math.sin(theta)},
+                               {0,1,0},
+                               {-Math.sin(theta),0,Math.cos(theta)}});
+       }
+       
+       /**
+        * Rotate around z-axis a given angle.
+        * @param theta  The angle to rotate in radians.
+        * @return  The transformation.
+        */
+       public static Transformation rotate_z(double theta) {
+               return new Transformation(new double[][]{
+                               {Math.cos(theta),-Math.sin(theta),0},
+                               {Math.sin(theta),Math.cos(theta),0},
+                               {0,0,1}});
+       }
+       
+       
+       
+       public void print(String... str) {
+               for (String s: str) {
+                       System.out.println(s);
+               }
+               System.out.printf("[%3.2f %3.2f %3.2f]   [%3.2f]\n",
+                               rotation[X][X],rotation[X][Y],rotation[X][Z],translate.x);
+               System.out.printf("[%3.2f %3.2f %3.2f] + [%3.2f]\n",
+                               rotation[Y][X],rotation[Y][Y],rotation[Y][Z],translate.y);
+               System.out.printf("[%3.2f %3.2f %3.2f]   [%3.2f]\n",
+                               rotation[Z][X],rotation[Z][Y],rotation[Z][Z],translate.z);
+               System.out.println();
+       }
+       
+       
+       public static void main(String[] arg) {
+               Transformation t;
+               
+               t = new Transformation();
+               t.print("Empty");
+               t = new Transformation(1,2,3);
+               t.print("1,2,3");
+               t = new Transformation(new Coordinate(2,3,4));
+               t.print("coord 2,3 4");
+               
+               t = Transformation.rotate_y(0.01);
+               t.print("rotate_y 0.01");
+               
+               t = new Transformation(-1,0,0);
+               t = t.applyTransformation(Transformation.rotate_y(0.01));
+               t = t.applyTransformation(new Transformation(1,0,0));
+               t.print("shift-rotate-shift");
+       }
+       
+}