diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index d2dda908b..0cd7e30d5 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -537,7 +537,8 @@ simpanel.pop.plot = Plot / Export simpanel.pop.run = Run simpanel.pop.delete = Delete simpanel.pop.duplicate = Duplicate -simpanel.pop.exportToCSV = Export table as CSV file +simpanel.pop.exportSimTableToCSV = Export simulation table as CSV file +simpanel.pop.exportSelectedSimsToCSV = Export simulations as CSV file simpanel.pop.exportToCSV.save.dialog.title = Save as CSV file simpanel.dlg.no.simulation.table.rows = Simulation table has no entries\u2026 Please run a simulation first. simpanel.checkbox.donotask = Do not ask me again @@ -575,9 +576,11 @@ SimuRunDlg.lbl.Altitude = Altitude: SimuRunDlg.lbl.Velocity = Velocity: SimuRunDlg.msg.Unabletosim = Unable to simulate: SimuRunDlg.msg.errorOccurred = An error occurred during the simulation: +SimuRunDlg.msg.branchErrorOccurred = An error occurred during simulation branch BasicEventSimulationEngine.error.noMotorsDefined = No motors defined in the simulation. -BasicEventSimulationEngine.error.cantCalculateStability = Can't calculate rocket stability. +BasicEventSimulationEngine.error.activeLengthZero = Active airframe has length 0 +BasicEventSimulationEngine.error.cantCalculateStability = Can't calculate stability BasicEventSimulationEngine.error.earlyMotorBurnout = Motor burnout without liftoff. BasicEventSimulationEngine.error.noConfiguredIgnition = No motors configured to ignite at liftoff BasicEventSimulationEngine.error.noIgnition = No motors ignited. @@ -1364,6 +1367,7 @@ TCMotorSelPan.lbl.motorNameColumn.ttip = Select which motor property to display TCMotorSelPan.btn.commonName = Use common name TCMotorSelPan.btn.designation = Use manufacturer's designation TCMotorSelPan.lbl.nrOfMotors = Number of motors: +TCMotorSelPan.lbl.nrOfMotors.None = None TCMotorSelPan.lbl.ttip.nrOfMotors = Number of motors currently visible in the motor selection table TCMotorSelPan.checkbox.limitlength = Limit motor length to mount length TCMotorSelPan.checkbox.limitdiameter = Limit motor diameter to mount diameter @@ -1377,8 +1381,7 @@ TCMotorSelPan.MotorMountDimensions = Motor mount dimensions: TCMotorSelPan.lbl.Search = Search: TCMotorSelPan.lbl.Selectthrustcurve = Select thrust curve: TCMotorSelPan.lbl.Ejectionchargedelay = Ejection charge delay: -TCMotorSelPan.equalsIgnoreCase.None = None -TCMotorSelPan.lbl.NumberofsecondsorNone = (Number of seconds or \"None\") +TCMotorSelPan.lbl.Numberofseconds = (or type in desired delay in seconds) TCMotorSelPan.lbl.Designation = Designation: TCMotorSelPan.lbl.CommonName = Common name: TCMotorSelPan.lbl.Totalimpulse = Total impulse: @@ -1395,7 +1398,8 @@ TCMotorSelPan.lbl.Datapoints = Data points: TCMotorSelPan.lbl.Digest = Digest: TCMotorSelPan.title.Thrustcurve = Thrust curve: TCMotorSelPan.title.Thrust = Thrust -TCMotorSelPan.delayBox.None = Plugged (None) +TCMotorSelPan.delayBox.Plugged = Plugged +TCMotorSelPan.delayBox.PluggedNone = None TCMotorSelPan.noDescription = No description available. TCMotorSelPan.btn.checkAll = Select All TCMotorSelPan.btn.checkNone = Clear All @@ -1896,6 +1900,7 @@ Warning.PODSET_FORWARD = In-line podset forward of parent airframe component Warning.PODSET_OVERLAP = In-line podset overlaps parent airframe component Warning.THICK_FIN = Thick fins may not simulate accurately. Warning.JAGGED_EDGED_FIN = Jagged-edged fin predictions may be inaccurate. +Warning.ZERO_AREA_FIN = Fins with zero area will not affect aerodynamics Warning.LISTENERS_AFFECTED = Listeners modified the flight simulation Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING = Recovery device opened while motor still burning. Warning.FILE_INVALID_PARAMETER = Invalid parameter encountered, ignoring. diff --git a/core/resources/l10n/messages_ar.properties b/core/resources/l10n/messages_ar.properties index bd36758b4..5db732751 100644 --- a/core/resources/l10n/messages_ar.properties +++ b/core/resources/l10n/messages_ar.properties @@ -1335,8 +1335,7 @@ TCMotorSelPan.MotorMountDimensions = :أبعاد حامل المحرك TCMotorSelPan.lbl.Search = :بحث TCMotorSelPan.lbl.Selectthrustcurve = :حدد منحنى الدفع TCMotorSelPan.lbl.Ejectionchargedelay = :تأخير شحنة الإطلاق -TCMotorSelPan.equalsIgnoreCase.None = لا شئ -TCMotorSelPan.lbl.NumberofsecondsorNone = (\"لا شئ\" عدد الثواني أو) +TCMotorSelPan.lbl.Numberofseconds = (\"لا شئ\" عدد الثواني أو) TCMotorSelPan.lbl.Designation = :تسمية الشركة المصنعة TCMotorSelPan.lbl.Totalimpulse = :الدفع الكلي TCMotorSelPan.lbl.Avgthrust = :متوسط الدفع @@ -1351,7 +1350,7 @@ TCMotorSelPan.lbl.Datapoints = :نقاط البيانات TCMotorSelPan.lbl.Digest = :الإستيعاب TCMotorSelPan.title.Thrustcurve = :منحنى الدفع TCMotorSelPan.title.Thrust = الدفع -TCMotorSelPan.delayBox.None = لا شئ +TCMotorSelPan.delayBox.Plugged = لا شئ TCMotorSelPan.noDescription = .لا يوجد وصف متاح TCMotorSelPan.btn.checkAll = تحديد الكل TCMotorSelPan.btn.checkNone = مسح الكل diff --git a/core/resources/l10n/messages_cs.properties b/core/resources/l10n/messages_cs.properties index 3f747027a..08a09dffd 100644 --- a/core/resources/l10n/messages_cs.properties +++ b/core/resources/l10n/messages_cs.properties @@ -940,8 +940,7 @@ TCMotorSelPan.lbl.Motormountdia = Prumer uchycen TCMotorSelPan.lbl.Search = Hledej: TCMotorSelPan.lbl.Selectthrustcurve = Vyber vkonovou krivku: TCMotorSelPan.lbl.Ejectionchargedelay = Oddelovac zpo\u017Eden: -TCMotorSelPan.equalsIgnoreCase.None = Nic -TCMotorSelPan.lbl.NumberofsecondsorNone = (Pocet sekund nebo \"Nic\") +TCMotorSelPan.lbl.Numberofseconds = (Pocet sekund nebo \"Nic\") TCMotorSelPan.lbl.Designation = Pojmenovn: TCMotorSelPan.lbl.Totalimpulse = Celkov impulse: TCMotorSelPan.lbl.Avgthrust = Prumern tah: @@ -953,7 +952,7 @@ TCMotorSelPan.lbl.Datapoints = Datov TCMotorSelPan.lbl.Digest = Vber: TCMotorSelPan.title.Thrustcurve = Vkonov krivka: TCMotorSelPan.title.Thrust = Tah -TCMotorSelPan.delayBox.None = Nic +TCMotorSelPan.delayBox.Plugged = Nic ! PlotDialog diff --git a/core/resources/l10n/messages_de.properties b/core/resources/l10n/messages_de.properties index f1624bde5..0c68ebee2 100644 --- a/core/resources/l10n/messages_de.properties +++ b/core/resources/l10n/messages_de.properties @@ -998,8 +998,7 @@ TCMotorSelPan.lbl.Motormountdia = Durchmesser der Motorhalterung TCMotorSelPan.lbl.Search = Suchen: TCMotorSelPan.lbl.Selectthrustcurve = Schubkurve auswhlen: TCMotorSelPan.lbl.Ejectionchargedelay = Verzgerung der Ausstoladung: -TCMotorSelPan.equalsIgnoreCase.None = keine -TCMotorSelPan.lbl.NumberofsecondsorNone = (Anzahl der Sekunden oder \keine\) +TCMotorSelPan.lbl.Numberofseconds = (Anzahl der Sekunden oder \keine\) TCMotorSelPan.lbl.Designation = Bezeichnung: TCMotorSelPan.lbl.Totalimpulse = Gesamtimpuls: TCMotorSelPan.lbl.Avgthrust = Durchschn. Schub: @@ -1011,7 +1010,7 @@ TCMotorSelPan.lbl.Datapoints = Datenpunkte: TCMotorSelPan.lbl.Digest = bersicht: TCMotorSelPan.title.Thrustcurve = Schubkurve: TCMotorSelPan.title.Thrust = Schub: -TCMotorSelPan.delayBox.None = keine +TCMotorSelPan.delayBox.Plugged = keine ! PlotDialog diff --git a/core/resources/l10n/messages_es.properties b/core/resources/l10n/messages_es.properties index 9ce7011f4..3d989be9b 100644 --- a/core/resources/l10n/messages_es.properties +++ b/core/resources/l10n/messages_es.properties @@ -1125,8 +1125,7 @@ TCMotorSelPan.checkbox.hideSimilar = Borrar las curvas similares TCMotorSelPan.checkbox.hideUsed = Ocultar los motores que ya est\u00e1n instalados TCMotorSelPan.checkbox.limitdiameter = Limitar el di\u00e1metro de motor al di\u00e1metro del porta motor TCMotorSelPan.checkbox.limitlength = Limitar la longitud del motor a la longitud del porta motor -TCMotorSelPan.delayBox.None = Ninguno -TCMotorSelPan.equalsIgnoreCase.None = Ninguno +TCMotorSelPan.delayBox.Plugged = Ninguno TCMotorSelPan.lbl.Avgthrust = Empuje medio: TCMotorSelPan.lbl.Burntime = Tiempo de quemado: TCMotorSelPan.lbl.Datapoints = Muestras de la curva: @@ -1136,7 +1135,7 @@ TCMotorSelPan.lbl.Emptymass = Masa carcasa: TCMotorSelPan.lbl.Launchmass = Masa total: TCMotorSelPan.lbl.Maxthrust = Empuje m\u00e1ximo: TCMotorSelPan.lbl.Motormountdia = Di\u00e1metro del porta motor: -TCMotorSelPan.lbl.NumberofsecondsorNone = (segundos) +TCMotorSelPan.lbl.Numberofseconds = (segundos) TCMotorSelPan.lbl.Search = Buscar: TCMotorSelPan.lbl.Selectthrustcurve = Seleccione curva de empuje: TCMotorSelPan.lbl.Selrocketmotor = Seleccione el motor del cohete: diff --git a/core/resources/l10n/messages_fr.properties b/core/resources/l10n/messages_fr.properties index ebcc8276c..0eff3be76 100644 --- a/core/resources/l10n/messages_fr.properties +++ b/core/resources/l10n/messages_fr.properties @@ -1119,8 +1119,7 @@ TCMotorSelPan.checkbox.hideSimilar = Cacher les courbes de pouss\u00E9es si TCMotorSelPan.checkbox.hideUsed = Ne pas afficher les moteurs d\u00E9j\u00E0 utilis\u00E9s TCMotorSelPan.checkbox.limitdiameter = Afficher uniquement les moteurs dont le diametre est \u00E9gale au porte moteur TCMotorSelPan.checkbox.limitlength = Limiter la longueur du moteur \u00E0 celle du porte moteur -TCMotorSelPan.delayBox.None = Aucun -TCMotorSelPan.equalsIgnoreCase.None = Aucun +TCMotorSelPan.delayBox.Plugged = Aucun TCMotorSelPan.lbl.Avgthrust = Pouss\u00E9e moyenne: TCMotorSelPan.lbl.Burntime = Temps de combustion: TCMotorSelPan.lbl.Datapoints = Points de donn\u00E9es: @@ -1130,7 +1129,7 @@ TCMotorSelPan.lbl.Emptymass = Masse \u00E0 vide: TCMotorSelPan.lbl.Launchmass = Masse au lancement: TCMotorSelPan.lbl.Maxthrust = Pouss\u00E9e Max.: TCMotorSelPan.lbl.Motormountdia = Diam\u00E8tre du tube porte moteur: -TCMotorSelPan.lbl.NumberofsecondsorNone = (Nombre de secondes ou "Aucun") +TCMotorSelPan.lbl.Numberofseconds = (Nombre de secondes ou "Aucun") TCMotorSelPan.lbl.Search = Rechercher: TCMotorSelPan.lbl.Selectthrustcurve = Choisir la courbe de pouss\u00E9e: TCMotorSelPan.lbl.Selrocketmotor = Choisir le moteur fus\u00E9e: diff --git a/core/resources/l10n/messages_it.properties b/core/resources/l10n/messages_it.properties index 5b153d660..55c33cc87 100644 --- a/core/resources/l10n/messages_it.properties +++ b/core/resources/l10n/messages_it.properties @@ -999,8 +999,7 @@ TCMotorSelPan.lbl.Motormountdia = Diametro dell'alloggio del motore: TCMotorSelPan.lbl.Search = Cerca: TCMotorSelPan.lbl.Selectthrustcurve = Seleziona la curva di spinta: TCMotorSelPan.lbl.Ejectionchargedelay = Ritardo della carica di espulsione: -TCMotorSelPan.equalsIgnoreCase.None = Nessun -TCMotorSelPan.lbl.NumberofsecondsorNone = (Numero di secondi o \"Nessuno\") +TCMotorSelPan.lbl.Numberofseconds = (Numero di secondi o \"Nessuno\") TCMotorSelPan.lbl.Designation = Classe: TCMotorSelPan.lbl.Totalimpulse = Impulso totale: TCMotorSelPan.lbl.Avgthrust = Spinta media: @@ -1012,7 +1011,7 @@ TCMotorSelPan.lbl.Datapoints = Punti: TCMotorSelPan.lbl.Digest = Riassunto: TCMotorSelPan.title.Thrustcurve = Curva di spinta: TCMotorSelPan.title.Thrust = Spinta -TCMotorSelPan.delayBox.None = Nessuno +TCMotorSelPan.delayBox.Plugged = Nessuno ! PlotDialog diff --git a/core/resources/l10n/messages_ja.properties b/core/resources/l10n/messages_ja.properties index 79f13487b..947c2022a 100644 --- a/core/resources/l10n/messages_ja.properties +++ b/core/resources/l10n/messages_ja.properties @@ -1029,8 +1029,7 @@ TCMotorSelPan.lbl.Motormountdia = \u30E2\u30FC\u30BF\u30FC\u30DE\u30A6\u30F3\u3 TCMotorSelPan.lbl.Search = \u691C\u7D22\uFF1A TCMotorSelPan.lbl.Selectthrustcurve = \u63A8\u529B\u5C65\u6B74\uFF1A TCMotorSelPan.lbl.Ejectionchargedelay = Ejection charge delay: -TCMotorSelPan.equalsIgnoreCase.None = None -TCMotorSelPan.lbl.NumberofsecondsorNone = (Number of seconds or \"None\") +TCMotorSelPan.lbl.Numberofseconds = (Number of seconds or \"None\") TCMotorSelPan.lbl.Designation = \u8A18\u53F7 TCMotorSelPan.lbl.Totalimpulse = \u30C8\u30FC\u30BF\u30EB\u30A4\u30F3\u30D1\u30EB\u30B9\uFF1A TCMotorSelPan.lbl.Avgthrust = \u5E73\u5747\u63A8\u529B\uFF1A @@ -1042,7 +1041,7 @@ TCMotorSelPan.lbl.Datapoints = \u30C7\u30FC\u30BF\u70B9\uFF1A TCMotorSelPan.lbl.Digest = \u30C0\u30A4\u30B8\u30A7\u30B9\u30C8\uFF1A TCMotorSelPan.title.Thrustcurve = \u63A8\u529B\u5C65\u6B74\uFF1A TCMotorSelPan.title.Thrust = \u63A8\u529B -TCMotorSelPan.delayBox.None = Plugged (None) +TCMotorSelPan.delayBox.Plugged = Plugged (None) TCMotorSelPan.noDescription = No description available. diff --git a/core/resources/l10n/messages_nl.properties b/core/resources/l10n/messages_nl.properties index 0b152729d..a2de9a92c 100644 --- a/core/resources/l10n/messages_nl.properties +++ b/core/resources/l10n/messages_nl.properties @@ -1260,8 +1260,7 @@ TCMotorSelPan.MotorMountDimensions = Motorbevestiging afmetingen: TCMotorSelPan.lbl.Search = Zoek: TCMotorSelPan.lbl.Selectthrustcurve = Selecteer stuwkrachtcurve: TCMotorSelPan.lbl.Ejectionchargedelay = Vertraging schietlading: -TCMotorSelPan.equalsIgnoreCase.None = Geen -TCMotorSelPan.lbl.NumberofsecondsorNone = (Aantal seconden of \"Geen\") +TCMotorSelPan.lbl.Numberofseconds = (Aantal seconden of \"Geen\") TCMotorSelPan.lbl.Designation = Benaming TCMotorSelPan.lbl.Totalimpulse = Totale impuls: TCMotorSelPan.lbl.Avgthrust = Gemiddelde stuwkracht: @@ -1276,7 +1275,7 @@ TCMotorSelPan.lbl.Datapoints = Datapunten: TCMotorSelPan.lbl.Digest = Verteer: TCMotorSelPan.title.Thrustcurve = Stuwkrachtcurve: TCMotorSelPan.title.Thrust = Stuwkracht -TCMotorSelPan.delayBox.None = Geen +TCMotorSelPan.delayBox.Plugged = Geen TCMotorSelPan.noDescription = Geen beschrijving beschikbaar. TCMotorSelPan.btn.checkAll = Selecteer Alles TCMotorSelPan.btn.checkNone = Alles wissen diff --git a/core/resources/l10n/messages_pl.properties b/core/resources/l10n/messages_pl.properties index 9673f47e7..55577b105 100644 --- a/core/resources/l10n/messages_pl.properties +++ b/core/resources/l10n/messages_pl.properties @@ -943,8 +943,7 @@ ComponentInfo.EngineBlock = Blokada silnika unieruchamia silnik wewn\u01 TCMotorSelPan.lbl.Search = Szukaj: TCMotorSelPan.lbl.Selectthrustcurve = Wybierz krzyw\u0105 si\u0142y ci\u0105gu: TCMotorSelPan.lbl.Ejectionchargedelay = Op\u017Anienie odpalenia \u0142adunku odrzucaj\u0105cego: - TCMotorSelPan.equalsIgnoreCase.None = \u017Badne - TCMotorSelPan.lbl.NumberofsecondsorNone = (liczba sekund lub \"Brak\") + TCMotorSelPan.lbl.Numberofseconds = (liczba sekund lub \"Brak\") TCMotorSelPan.lbl.Designation = Oznaczenie: TCMotorSelPan.lbl.Totalimpulse = Ca\u0142kowity impuls: TCMotorSelPan.lbl.Avgthrust = \u015Arednia si\u0142a ci\u0105gu: @@ -956,7 +955,7 @@ ComponentInfo.EngineBlock = Blokada silnika unieruchamia silnik wewn\u01 TCMotorSelPan.lbl.Digest = Podsumowanie: TCMotorSelPan.title.Thrustcurve = Krzywa si\u0142y ci\u0105gu: TCMotorSelPan.title.Thrust = Ci\u0105g - TCMotorSelPan.delayBox.None = \u017Badne + TCMotorSelPan.delayBox.Plugged = \u017Badne ! PlotDialog diff --git a/core/resources/l10n/messages_pt.properties b/core/resources/l10n/messages_pt.properties index f7a86da1c..b9e7669c5 100644 --- a/core/resources/l10n/messages_pt.properties +++ b/core/resources/l10n/messages_pt.properties @@ -1083,8 +1083,7 @@ TCMotorSelPan.SHOW_DESCRIPTIONS.desc1 = Mostrar todos os motores TCMotorSelPan.SHOW_DESCRIPTIONS.desc2 = Mostrar motores com um di\u00e2metro menor do que a montagem do motor TCMotorSelPan.SHOW_DESCRIPTIONS.desc3 = Mostrar motores com um di\u00e2metro igual ao da montagem do motor TCMotorSelPan.checkbox.hideSimilar = Esconder as curvas de impulso muito semelhantes -TCMotorSelPan.delayBox.None = Nenhum -TCMotorSelPan.equalsIgnoreCase.None = Nenhum +TCMotorSelPan.delayBox.Plugged = Nenhum TCMotorSelPan.lbl.Avgthrust = Empuxo m\u00e9dio: TCMotorSelPan.lbl.Burntime = Tempo de queima: TCMotorSelPan.lbl.Datapoints = Pontos de dados: @@ -1094,7 +1093,7 @@ TCMotorSelPan.lbl.Emptymass = Massa em vazio: TCMotorSelPan.lbl.Launchmass = Massa do lan\u00e7amento: TCMotorSelPan.lbl.Maxthrust = Empuxo m\u00e1ximo: TCMotorSelPan.lbl.Motormountdia = Di\u00e2metro da montagem do motor: -TCMotorSelPan.lbl.NumberofsecondsorNone = (N\u00famero de segundos ou "Nenhum") +TCMotorSelPan.lbl.Numberofseconds = (N\u00famero de segundos ou "Nenhum") TCMotorSelPan.lbl.Search = Pesquisar: TCMotorSelPan.lbl.Selectthrustcurve = Selecione curva de empuxo: TCMotorSelPan.lbl.Selrocketmotor = Selecione motor do foguete: diff --git a/core/resources/l10n/messages_ru.properties b/core/resources/l10n/messages_ru.properties index 233e84419..4cc9d0b6b 100644 --- a/core/resources/l10n/messages_ru.properties +++ b/core/resources/l10n/messages_ru.properties @@ -1307,8 +1307,7 @@ TCMotorSelPan.MotorMountDimensions = \u0420\u0430\u0437\u043C\u0435\u0440\u044B TCMotorSelPan.lbl.Search = \u041F\u043E\u0438\u0441\u043A: TCMotorSelPan.lbl.Selectthrustcurve = \u0412\u044B\u0431\u043E\u0440 \u043F\u0440\u043E\u0444\u0438\u043B\u044F \u0442\u044F\u0433\u0438: TCMotorSelPan.lbl.Ejectionchargedelay = \u0417\u0430\u0434\u0435\u0440\u0436\u043A\u0430 \u0432\u044B\u0431\u0440\u043E\u0441\u0430 \u0437\u0430\u0440\u044F\u0434\u0430: -TCMotorSelPan.equalsIgnoreCase.None = \u041D\u0435\u0442 -TCMotorSelPan.lbl.NumberofsecondsorNone = (\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0438\u043B\u0438 "\u041D\u0435\u0442") +TCMotorSelPan.lbl.Numberofseconds = (\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0441\u0435\u043A\u0443\u043D\u0434 \u0438\u043B\u0438 "\u041D\u0435\u0442") TCMotorSelPan.lbl.Designation = \u041E\u0431\u043E\u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435: TCMotorSelPan.lbl.Totalimpulse = \u041E\u0431\u0449\u0438\u0439 \u0438\u043C\u043F\u0443\u043B\u044C\u0441: TCMotorSelPan.lbl.Avgthrust = \u0421\u0440\u0435\u0434\u043D\u044F\u044F \u0442\u044F\u0433\u0430: @@ -1323,7 +1322,7 @@ TCMotorSelPan.lbl.Datapoints = \u0422\u043E\u0447\u043A\u0438 \u0434\u0430\u043D TCMotorSelPan.lbl.Digest = \u0420\u0435\u0437\u044E\u043C\u0435: TCMotorSelPan.title.Thrustcurve = \u041F\u0440\u043E\u0444\u0438\u043B\u044C \u0442\u044F\u0433\u0438: TCMotorSelPan.title.Thrust = \u0422\u044F\u0433\u0430 -TCMotorSelPan.delayBox.None = \u041D\u0435\u0442 +TCMotorSelPan.delayBox.Plugged = \u041D\u0435\u0442 TCMotorSelPan.noDescription = \u041E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442. TCMotorSelPan.btn.checkAll = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435 TCMotorSelPan.btn.checkNone = \u041E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u0432\u044B\u0431\u043E\u0440 diff --git a/core/resources/l10n/messages_uk_UA.properties b/core/resources/l10n/messages_uk_UA.properties index af3f83a22..8a7e912d8 100644 --- a/core/resources/l10n/messages_uk_UA.properties +++ b/core/resources/l10n/messages_uk_UA.properties @@ -1102,8 +1102,7 @@ TCMotorSelPan.MotorMountDimensions = Motor mount dimensions: TCMotorSelPan.lbl.Search = Search: TCMotorSelPan.lbl.Selectthrustcurve = Select thrust curve: TCMotorSelPan.lbl.Ejectionchargedelay = Ejection charge delay: -TCMotorSelPan.equalsIgnoreCase.None = None -TCMotorSelPan.lbl.NumberofsecondsorNone = (Number of seconds or \"None\") +TCMotorSelPan.lbl.Numberofseconds = (Number of seconds or \"None\") TCMotorSelPan.lbl.Designation = Designation: TCMotorSelPan.lbl.Totalimpulse = Total impulse: TCMotorSelPan.lbl.Avgthrust = Avg. thrust: @@ -1115,7 +1114,7 @@ TCMotorSelPan.lbl.Datapoints = Data points: TCMotorSelPan.lbl.Digest = Digest: TCMotorSelPan.title.Thrustcurve = Thrust curve: TCMotorSelPan.title.Thrust = Thrust -TCMotorSelPan.delayBox.None = Plugged (None) +TCMotorSelPan.delayBox.Plugged = Plugged (None) TCMotorSelPan.noDescription = No description available. TCMotorSelPan.btn.checkAll = Select All TCMotorSelPan.btn.checkNone = Clear All diff --git a/core/resources/l10n/messages_zh_CN.properties b/core/resources/l10n/messages_zh_CN.properties index c2ff76d5e..b4bff94ed 100644 --- a/core/resources/l10n/messages_zh_CN.properties +++ b/core/resources/l10n/messages_zh_CN.properties @@ -1188,8 +1188,7 @@ TCMotorSelPan.checkbox.hideSimilar = \u9690\u85CF\u76F8\u4F3C\u7684\u63A8\u TCMotorSelPan.checkbox.hideUsed = \u9690\u85CF\u5DF2\u4F7F\u7528\u7684\u53D1\u52A8\u673A TCMotorSelPan.checkbox.limitdiameter = \u76F4\u5F84\u4E0D\u8D85\u8FC7\u5F53\u524D\u53D1\u52A8\u673A\u5EA7\u76F4\u5F84 TCMotorSelPan.checkbox.limitlength = \u957F\u5EA6\u4E0D\u8D85\u8FC7\u5F53\u524D\u53D1\u52A8\u673A\u5EA7\u957F\u5EA6 -TCMotorSelPan.delayBox.None = \u65E0 -TCMotorSelPan.equalsIgnoreCase.None = \u65E0 +TCMotorSelPan.delayBox.Plugged = \u65E0 TCMotorSelPan.lbl.Avgthrust = \u5E73\u5747\u63A8\u529B: TCMotorSelPan.lbl.Burntime = \u71C3\u70E7\u65F6\u95F4: TCMotorSelPan.lbl.Datapoints = \u6570\u636E\u70B9: @@ -1198,7 +1197,7 @@ TCMotorSelPan.lbl.Ejectionchargedelay = \u5F39\u5C04\u5EF6\u65F6: TCMotorSelPan.lbl.Emptymass = \u71C3\u5C3D\u8D28\u91CF: TCMotorSelPan.lbl.Launchmass = \u53D1\u5C04\u8D28\u91CF: TCMotorSelPan.lbl.Maxthrust = \u6700\u5927\u63A8\u529B: -TCMotorSelPan.lbl.NumberofsecondsorNone = (\u79D2\u6570\u6216"\u65E0") +TCMotorSelPan.lbl.Numberofseconds = (\u79D2\u6570\u6216"\u65E0") TCMotorSelPan.lbl.Search = \u641C\u7D22: TCMotorSelPan.lbl.Selectthrustcurve = \u9009\u62E9\u63A8\u529B\u66F2\u7EBF: TCMotorSelPan.lbl.Selrocketmotor = \u9009\u62E9\u706B\u7BAD\u53D1\u52A8\u673A: diff --git a/core/resources/pix/eventicons/event-exception.png b/core/resources/pix/eventicons/event-exception.png new file mode 100644 index 000000000..0cfd58596 Binary files /dev/null and b/core/resources/pix/eventicons/event-exception.png differ diff --git a/core/src/net/sf/openrocket/aerodynamics/Warning.java b/core/src/net/sf/openrocket/aerodynamics/Warning.java index 3c17c1e17..71946ca5b 100644 --- a/core/src/net/sf/openrocket/aerodynamics/Warning.java +++ b/core/src/net/sf/openrocket/aerodynamics/Warning.java @@ -382,6 +382,10 @@ public abstract class Warning { ////Jagged-edged fin predictions may be inaccurate. public static final Warning JAGGED_EDGED_FIN = new Other(trans.get("Warning.JAGGED_EDGED_FIN")); + /** A Warning that the fins have a zero area. */ + ////Fins with no area will not affect aerodynamics + public static final Warning ZERO_AREA_FIN = new Other(trans.get("Warning.ZERO_AREA_FIN")); + /** A Warning that simulation listeners have affected the simulation */ ////Listeners modified the flight simulation public static final Warning LISTENERS_AFFECTED = new Other(trans.get("Warning.LISTENERS_AFFECTED")); diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java index 53746d2e6..326c73a0d 100644 --- a/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java @@ -84,7 +84,9 @@ public class FinSetCalc extends RocketComponentCalc { public void calculateNonaxialForces(FlightConditions conditions, Transformation transform, AerodynamicForces forces, WarningSet warnings) { - if (span < 0.001) { + warnings.addAll(geometryWarnings); + + if (finArea < MathUtil.EPSILON) { forces.setCm(0); forces.setCN(0); forces.setCNa(0); @@ -96,12 +98,6 @@ public class FinSetCalc extends RocketComponentCalc { forces.setCyaw(0); return; } - - if ((bodyRadius > 0) && (thickness > bodyRadius / 2)){ - // Add warnings (radius/2 == diameter/4) - warnings.add(Warning.THICK_FIN); - } - warnings.addAll(geometryWarnings); //////// Calculate CNa. ///////// @@ -241,18 +237,27 @@ public class FinSetCalc extends RocketComponentCalc { Coordinate[] points = component.getFinPoints(); - // Check for jagged edges + // Check geometry 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); + geometryWarnings.add(Warning.JAGGED_EDGED_FIN, component.toString()); break; } if (points[i].y < points[i - 1].y - 0.001) { down = true; } } + + if (finArea < MathUtil.EPSILON) { + geometryWarnings.add(Warning.ZERO_AREA_FIN, component.toString()); + } + + if ((bodyRadius > 0) && (thickness > bodyRadius / 2)){ + // Add warnings (radius/2 == diameter/4) + geometryWarnings.add(Warning.THICK_FIN, component.toString()); + } // Calculate the chord lead and trail positions and length @@ -365,7 +370,6 @@ public class FinSetCalc extends RocketComponentCalc { macLead *= dy; area *= dy; rollSum *= dy; - macLength /= area; macSpan /= area; macLead /= area; @@ -614,6 +618,11 @@ public class FinSetCalc extends RocketComponentCalc { @Override public double calculateFrictionCD(FlightConditions conditions, double componentCf, WarningSet warnings) { + // a fin with 0 area contributes no drag + if (finArea < MathUtil.EPSILON) { + return 0.0; + } + double cd = componentCf * (1 + 2 * thickness / macLength) * 2 * finArea / conditions.getRefArea(); return cd; } @@ -622,6 +631,11 @@ public class FinSetCalc extends RocketComponentCalc { public double calculatePressureCD(FlightConditions conditions, double stagnationCD, double baseCD, WarningSet warnings) { + // a fin with 0 area contributes no drag + if (finArea < MathUtil.EPSILON) { + return 0.0; + } + double mach = conditions.getMach(); double cd = 0; diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java index a94bd76e5..26a279fbf 100644 --- a/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/RailButtonCalc.java @@ -58,44 +58,56 @@ public class RailButtonCalc extends RocketComponentCalc { final double notchArea = (button.getOuterDiameter() - button.getInnerDiameter()) * button.getInnerHeight(); final double refArea = outerArea - notchArea; - // accumulate Cd contribution from each rail button + // accumulate Cd contribution from each rail button. If velocity is 0 just set CDmul to a value previously + // competed for velocity MathUtil.EPSILON and skip the loop to avoid division by 0 double CDmul = 0.0; - for (int i = 0; i < button.getInstanceCount(); i++) { - - // compute boundary layer height at button location. I can't find a good reference for the - // formula, e.g. https://aerospaceengineeringblog.com/boundary-layers/ simply says it's the - // "scientific consensus". - double x = (button.toAbsolute(instanceOffsets[i]))[0].x; // location of button - double rex = calculateReynoldsNumber(x, conditions); // Reynolds number of button location - double del = 0.37 * x / Math.pow(rex, 0.2); // Boundary layer thickness - - // compute mean airspeed over button - // this assumes airspeed changes linearly through boundary layer - // and that all parts of the railbutton contribute equally to Cd, - // neither of which is true but both are plenty close enough for our purposes - - double mach; - if (buttonHt > del) { - // Case 1: button extends beyond boundary layer - // Mean velocity is 1/2 rocket velocity up to limit of boundary layer, - // full velocity after that - mach = (buttonHt - 0.5*del) * conditions.getMach()/buttonHt; - } else { - // Case 2: button is entirely within boundary layer - mach = MathUtil.map(buttonHt/2.0, 0, del, 0, conditions.getMach()); + if (conditions.getMach() > MathUtil.EPSILON) { + for (int i = 0; i < button.getInstanceCount(); i++) { + + // compute boundary layer height at button location. I can't find a good reference for the + // formula, e.g. https://aerospaceengineeringblog.com/boundary-layers/ simply says it's the + // "scientific consensus". + double x = (button.toAbsolute(instanceOffsets[i]))[0].x; // location of button + double rex = calculateReynoldsNumber(x, conditions); // Reynolds number of button location + double del = 0.37 * x / Math.pow(rex, 0.2); // Boundary layer thickness + + // compute mean airspeed over button + // this assumes airspeed changes linearly through boundary layer + // and that all parts of the railbutton contribute equally to Cd, + // neither of which is true but both are plenty close enough for our purposes + + double mach; + if (buttonHt > del) { + // Case 1: button extends beyond boundary layer + // Mean velocity is 1/2 rocket velocity up to limit of boundary layer, + // full velocity after that + mach = (buttonHt - 0.5*del) * conditions.getMach()/buttonHt; + } else { + // Case 2: button is entirely within boundary layer + mach = MathUtil.map(buttonHt/2.0, 0, del, 0, conditions.getMach()); + } + + // look up Cd as function of speed. It's pretty constant as a function of Reynolds + // number when slow, so we can just use a function of Mach number + double cd = MathUtil.interpolate(cdDomain, cdRange, mach); + + // Since later drag force calculations don't consider boundary layer, compute "effective Cd" + // based on rocket velocity + cd = cd * MathUtil.pow2(mach)/MathUtil.pow2(conditions.getMach()); + + // add to CDmul + CDmul += cd; + } - // look up Cd as function of speed. It's pretty constant as a function of Reynolds - // number when slow, so we can just use a function of Mach number - double cd = MathUtil.interpolate(cdDomain, cdRange, mach); - - // Since later drag force calculations don't consider boundary layer, compute "effective Cd" - // based on rocket velocity - cd = cd * MathUtil.pow2(mach)/MathUtil.pow2(conditions.getMach()); + // since we'll be multiplying by the instance count up in BarrowmanCalculator, + // we want to return the mean CD instead of the total + CDmul /= button.getInstanceCount(); - // add to CDmul - CDmul += cd; - } + } else { + // value at velocity of MathUtil.EPSILON + CDmul = 8.786395072609939E-4; + } return CDmul * stagnationCD * refArea / conditions.getRefArea(); } diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java index 9ade10f5e..068fc91c8 100644 --- a/core/src/net/sf/openrocket/document/Simulation.java +++ b/core/src/net/sf/openrocket/document/Simulation.java @@ -466,7 +466,7 @@ public class Simulation implements ChangeSource, Cloneable { /** * Return true if this simulation contains plottable flight data. * - * @return + * @return true if this simulation contains plottable flight data. */ public boolean hasSimulationData() { FlightData data = getSimulatedData(); @@ -478,6 +478,15 @@ public class Simulation implements ChangeSource, Cloneable { } return true; } + + /** + * Return true if this simulation contains summary flight data. + * @return true if this simulation contains summary flight data. + */ + public boolean hasSummaryData() { + FlightData data = getSimulatedData(); + return data != null; + } /** * Returns a copy of this simulation suitable for cut/copy/paste operations. diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java b/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java index 1afab2aab..2750285dc 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java @@ -61,7 +61,7 @@ public class OpenRocketLoader extends AbstractRocketLoader { } // If we saved data for a simulation before, we'll use that as our default option this time - boolean saveData = false; + // Also, updaet all the sims' modIDs to agree with flight config for (Simulation s : doc.getSimulations()) { s.syncModID(); // The config's modID can be out of sync with the simulation's after the whole loading process if (s.getStatus() == Simulation.Status.EXTERNAL || @@ -79,8 +79,6 @@ public class OpenRocketLoader extends AbstractRocketLoader { continue; doc.getDefaultStorageOptions().setSaveSimulationData(true); - break; - } doc.getDefaultStorageOptions().setExplicitlySet(false); diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java b/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java index 11f4af969..f24edfbc7 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/SingleSimulationHandler.java @@ -109,6 +109,13 @@ class SingleSimulationHandler extends AbstractElementHandler { public void endHandler(String element, HashMap 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; + } + SimulationOptions options; FlightConfigurationId idToSet= FlightConfigurationId.ERROR_FCID; if (conditionHandler != null) { @@ -123,20 +130,19 @@ class SingleSimulationHandler extends AbstractElementHandler { name = "Simulation"; // If the simulation was saved with flight data (which may just be a summary) - // mark it as loaded from the file else as not simulated. We're ignoring the - // simulation status attribute, since (1) it really isn't relevant now, and (2) - // sim summaries are getting marked as not simulated when they're saved + // mark it as loaded from the file else as not simulated. If outdated data was saved, + // it'll be marked as outdated (creating a new status for "loaded but outdated" seems + // excessive, and the fact that it's outdated is the more important) FlightData data; if (dataHandler == null) data = null; else data = dataHandler.getFlightData(); - Simulation.Status status; - if (data != null) { - status = Status.LOADED; - } else { + if (data == null) { status = Status.NOT_SIMULATED; + } else if (status != Status.OUTDATED) { + status = Status.LOADED; } Simulation simulation = new Simulation(doc, doc.getRocket(), status, name, diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 6c32cb151..6838c4a2b 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -86,13 +86,6 @@ public class BasicEventSimulationEngine implements SimulationEngine { throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noMotorsDefined")); } - // Can't calculate stability - if (currentStatus.getSimulationConditions().getAerodynamicCalculator() - .getCP(currentStatus.getConfiguration(), - new FlightConditions(currentStatus.getConfiguration()), - new WarningSet()).weight < MathUtil.EPSILON) - throw new SimulationException(trans.get("BasicEventSimulationEngine.error.cantCalculateStability")); - // Problems that let us simulate, but result is likely bad // No recovery device @@ -157,6 +150,8 @@ public class BasicEventSimulationEngine implements SimulationEngine { Coordinate originVelocity = currentStatus.getRocketVelocity(); try { + + checkGeometry(currentStatus); // Start the simulation while (handleEvents()) { @@ -284,6 +279,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { flightData.getWarningSet().addAll(currentStatus.getWarnings()); e.setFlightData(flightData); + e.setFlightDataBranch(currentStatus.getFlightData()); throw e; } @@ -495,7 +491,8 @@ public class BasicEventSimulationEngine implements SimulationEngine { SimulationStatus boosterStatus = new SimulationStatus(currentStatus); // Prepare the new simulation branch - boosterStatus.setFlightData(new FlightDataBranch(boosterStage.getName(), FlightDataType.TYPE_TIME)); + boosterStatus.setFlightData(new FlightDataBranch(boosterStage.getName(), currentStatus.getFlightData())); + boosterStatus.getFlightData().addEvent(event); // Mark the current status as having dropped the current stage and all stages below it currentStatus.getConfiguration().clearStagesBelow( stageNumber); @@ -504,6 +501,10 @@ public class BasicEventSimulationEngine implements SimulationEngine { boosterStatus.getConfiguration().clearStagesAbove(stageNumber); toSimulate.push(boosterStatus); + + // Make sure upper stages can still be simulated + checkGeometry(currentStatus); + log.info(String.format("==>> @ %g; from Branch: %s ---- Branching: %s ---- \n", currentStatus.getSimulationTime(), currentStatus.getFlightData().getBranchName(), boosterStatus.getFlightData().getBranchName())); @@ -665,8 +666,24 @@ public class BasicEventSimulationEngine implements SimulationEngine { return null; } } - - + + // we need to check geometry to make sure we can simulation the active + // stages in a simulation branch when the branch starts executing, and + // whenever a stage separation occurs + private void checkGeometry(SimulationStatus currentStatus) throws SimulationException { + + // Active stages have total length of 0. + if (currentStatus.getConfiguration().getLengthAerodynamic() < MathUtil.EPSILON) { + throw new SimulationException(trans.get("BasicEventSimulationEngine.error.activeLengthZero")); + } + + // Can't calculate stability + if (currentStatus.getSimulationConditions().getAerodynamicCalculator() + .getCP(currentStatus.getConfiguration(), + new FlightConditions(currentStatus.getConfiguration()), + new WarningSet()).weight < MathUtil.EPSILON) + throw new SimulationException(trans.get("BasicEventSimulationEngine.error.cantCalculateStability")); + } private void checkNaN() throws SimulationException { double d = 0; diff --git a/core/src/net/sf/openrocket/simulation/DefaultSimulationOptionFactory.java b/core/src/net/sf/openrocket/simulation/DefaultSimulationOptionFactory.java index c68cff0c2..31f2451d9 100644 --- a/core/src/net/sf/openrocket/simulation/DefaultSimulationOptionFactory.java +++ b/core/src/net/sf/openrocket/simulation/DefaultSimulationOptionFactory.java @@ -46,7 +46,7 @@ public class DefaultSimulationOptionFactory { defaults.setISAAtmosphere(prefs.getBoolean(SIMCONDITION_ATMOS_STD, defaults.isISAAtmosphere())); defaults.setLaunchTemperature(prefs.getDouble(SIMCONDITION_ATMOS_TEMP, defaults.getLaunchTemperature())); - defaults.setLaunchPressure(prefs.getDouble(SIMCONDITION_ATMOS_PRESSURE, defaults.getLaunchTemperature())); + defaults.setLaunchPressure(prefs.getDouble(SIMCONDITION_ATMOS_PRESSURE, defaults.getLaunchPressure())); defaults.setLaunchIntoWind(prefs.getBoolean(SIMCONDITION_ROD_INTO_WIND, defaults.getLaunchIntoWind())); defaults.setLaunchRodLength(prefs.getDouble(SIMCONDITION_ROD_LENGTH, defaults.getLaunchRodLength())); diff --git a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java index c7f71f7fc..77bc60f56 100644 --- a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java +++ b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java @@ -75,6 +75,26 @@ public class FlightDataBranch implements Monitorable { maxValues.put(t, Double.NaN); } } + + /** + * Make a flight data branch with one data point copied from its parent. Intended for use + * when creating a new branch upon stage separation, so the data at separation is present + * in both branches (and if the new branch has an immediate exception, it can be plotted) + */ + public FlightDataBranch(String branchName, FlightDataBranch parent) { + this.branchName = branchName; + + // need to have at least one type to set up values + values.put(FlightDataType.TYPE_TIME, new ArrayList()); + minValues.put(FlightDataType.TYPE_TIME, Double.NaN); + maxValues.put(FlightDataType.TYPE_TIME, Double.NaN); + + // copy all values into new FlightDataBranch + this.addPoint(); + for (FlightDataType t : parent.getTypes()) { + this.setValue(t, parent.getLast(t)); + } + } /** * Makes an 'empty' flight data branch which has no data but all built in data types are defined. diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java index e0e8ee8ec..720eadce0 100644 --- a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java @@ -1,10 +1,12 @@ package net.sf.openrocket.simulation.exception; import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; public class SimulationException extends Exception { private FlightData flightData = null; + private FlightDataBranch flightDataBranch = null; public SimulationException() { @@ -29,4 +31,13 @@ public class SimulationException extends Exception { public FlightData getFlightData() { return flightData; } + + public void setFlightDataBranch(FlightDataBranch f) { + flightDataBranch = f; + } + + public FlightDataBranch getFlightDataBranch() { + return flightDataBranch; + } + } diff --git a/core/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java index 08f52bea4..2a1c9397e 100644 --- a/core/src/net/sf/openrocket/util/TestRockets.java +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -1103,6 +1103,26 @@ public class TestRockets { return rocket; } + // Several simulations need the Falcon9Heavy, but with fins added to the + // core stage (without them, there is a simulation exception at stage separation + // This method is intended to add those fins to the F9H, but will in fact + // add them to the second stage of a rocket + public static void addCoreFins(Rocket rocket) { + final int bodyFinCount = 4; + final double bodyFinRootChord = 0.05; + final double bodyFinTipChord = bodyFinRootChord; + final double bodyFinHeight = 0.025; + final double bodyFinSweep = 0.0; + final AxialMethod bodyFinAxialMethod = AxialMethod.BOTTOM; + final double bodyFinAxialOffset = 0.0; + + final TrapezoidFinSet finSet = new TrapezoidFinSet(bodyFinCount, bodyFinRootChord, bodyFinTipChord, bodyFinSweep, bodyFinHeight); + finSet.setName("Body Tube FinSet"); + finSet.setAxialMethod(bodyFinAxialMethod); + + rocket.getChild(1).getChild(0).addChild(finSet); + } + // This is a simple four-fin rocket with large endplates on the // fins, for testing CG and CP calculations with fins on pods. // not a complete rocket (no motor mount nor recovery system) diff --git a/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java b/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java index ca4aaa3b4..3b1a66d20 100644 --- a/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java +++ b/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java @@ -20,11 +20,13 @@ import net.sf.openrocket.rocketcomponent.FlightConfiguration; import net.sf.openrocket.rocketcomponent.NoseCone; import net.sf.openrocket.rocketcomponent.ParallelStage; import net.sf.openrocket.rocketcomponent.PodSet; +import net.sf.openrocket.rocketcomponent.RailButton; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.TestRockets; public class BarrowmanCalculatorTest { @@ -496,5 +498,70 @@ public class BarrowmanCalculatorTest { testCP = testCalc.getCP(testConfig, testConditions, warnings).x; assertEquals("should be warning from podset airframe overlap", 1, warnings.size()); } + + /** + * Tests railbutton drag. Really is testing instancing more than actual drag calculations, and making + * sure we don't divide by 0 when not moving + */ + @Test + public void testRailButtonDrag() { + // minimal rocket with nothing on it but two railbuttons + final Rocket rocket = new Rocket(); + final AxialStage stage = new AxialStage(); + rocket.addChild(stage); + + // phantom tubes have no drag to confuse things + final BodyTube phantom = new BodyTube(); + phantom.setOuterRadius(0); + stage.addChild(phantom); + + // set up test environment + WarningSet warnings = new WarningSet(); + final FlightConfiguration config = rocket.getSelectedConfiguration(); + final FlightConditions conditions = new FlightConditions(config); + final BarrowmanCalculator calc = new BarrowmanCalculator(); + + // part 1: instancing + + // Put two individual railbuttons and get their CD + final RailButton button1 = new RailButton(); + button1.setInstanceCount(1); + button1.setAxialOffset(1.0); + phantom.addChild(button1); + + final RailButton button2 = new RailButton(); + button2.setInstanceCount(1); + button2.setAxialOffset(2.0); + phantom.addChild(button2); + + final AerodynamicForces individualForces = calc.getAerodynamicForces(config, conditions, warnings); + final double individualCD = individualForces.getCD(); + + // get rid of individual buttons and put in a railbutton set with two instances at same locations as original + // railbuttons + phantom.removeChild(button1); + phantom.removeChild(button2); + + final RailButton buttons = new RailButton(); + buttons.setInstanceCount(2); + buttons.setAxialOffset(1.0); + buttons.setInstanceSeparation(1.0); + + final AerodynamicForces pairForces = calc.getAerodynamicForces(config, conditions, warnings); + final double pairCD = pairForces.getCD(); + + assertEquals("two individual railbuttons should have same CD as a pair", individualCD, pairCD, EPSILON); + + // part 2: test at Mach 0 + conditions.setMach(MathUtil.EPSILON); + final AerodynamicForces epsForces = calc.getAerodynamicForces(config, conditions, warnings); + final double epsCD = epsForces.getCD(); + + conditions.setMach(0); + final AerodynamicForces zeroForces = calc.getAerodynamicForces(config, conditions, warnings); + final double zeroCD = zeroForces.getCD(); + assertEquals("drag at mach 0 should equal drag at mach MathUtil.EPSILON", epsCD, zeroCD, EPSILON); + } } + diff --git a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java index 52014e4f1..6c613ee32 100644 --- a/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java +++ b/core/test/net/sf/openrocket/file/openrocket/OpenRocketSaverTest.java @@ -22,6 +22,7 @@ import net.sf.openrocket.database.motor.MotorDatabase; import net.sf.openrocket.database.motor.ThrustCurveMotorSetDatabase; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.OpenRocketDocumentFactory; +import net.sf.openrocket.document.Simulation; import net.sf.openrocket.document.StorageOptions; import net.sf.openrocket.file.GeneralRocketLoader; import net.sf.openrocket.file.RocketLoadException; @@ -264,7 +265,64 @@ public class OpenRocketSaverTest { // TODO: fix estimateFileSize so that it's a lot more accurate } - + + /** + * Test sim status with/without sim data in file. + */ + @Test + public void TestSimStatus() { + Rocket rocket = TestRockets.makeEstesAlphaIII(); + OpenRocketDocument rocketDoc = OpenRocketDocumentFactory.createDocumentFromRocket(rocket); + + // Hook up some simulations. + // First sim will not have options set + Simulation sim1 = new Simulation(rocket); + rocketDoc.addSimulation(sim1); + + // Second sim has options, but hasn't been simulated + Simulation sim2 = new Simulation(rocket); + sim2.getOptions().setISAAtmosphere(true); + sim2.getOptions().setTimeStep(0.05); + sim2.setFlightConfigurationId(TestRockets.TEST_FCID_0); + rocketDoc.addSimulation(sim2); + + // Third sim has been executed + Simulation sim3 = new Simulation(rocket); + sim3.getOptions().setISAAtmosphere(true); + sim3.getOptions().setTimeStep(0.05); + sim3.setFlightConfigurationId(TestRockets.TEST_FCID_0); + try { + sim3.simulate(); + } catch (Exception e) { + fail(e.toString()); + } + rocketDoc.addSimulation(sim3); + + // Fourth sim has been executed, then configuration changed + Simulation sim4 = new Simulation(rocket); + sim4.getOptions().setISAAtmosphere(true); + sim4.getOptions().setTimeStep(0.05); + sim4.setFlightConfigurationId(TestRockets.TEST_FCID_0); + try { + sim4.simulate(); + } catch (Exception e) { + fail(e.toString()); + } + sim4.getOptions().setTimeStep(0.1); + rocketDoc.addSimulation(sim4); + + // save, then load document + StorageOptions options = new StorageOptions(); + options.setSaveSimulationData(true); + + File file = saveRocket(rocketDoc, options); + OpenRocketDocument rocketDocLoaded = loadRocket(file.getPath()); + + assertEquals(Simulation.Status.CANT_RUN, rocketDocLoaded.getSimulations().get(0).getStatus()); + assertEquals(Simulation.Status.NOT_SIMULATED, rocketDocLoaded.getSimulations().get(1).getStatus()); + assertEquals(Simulation.Status.LOADED, rocketDocLoaded.getSimulations().get(2).getStatus()); + assertEquals(Simulation.Status.OUTDATED, rocketDocLoaded.getSimulations().get(3).getStatus()); + } //////////////////////////////// // Tests for File Version 1.7 // @@ -276,7 +334,8 @@ public class OpenRocketSaverTest { assertEquals(108, getCalculatedFileVersion(rocketDoc)); } - + + //////////////////////////////// /* * Utility Functions */ diff --git a/core/test/net/sf/openrocket/simulation/DisableStageTest.java b/core/test/net/sf/openrocket/simulation/DisableStageTest.java index f92c634ea..42b0f77b7 100644 --- a/core/test/net/sf/openrocket/simulation/DisableStageTest.java +++ b/core/test/net/sf/openrocket/simulation/DisableStageTest.java @@ -148,9 +148,13 @@ public class DisableStageTest extends BaseTestCase { @Test public void testBooster1() { //// Test disabling the stage - Rocket rocketRemoved = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage removed - Rocket rocketDisabled = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage disabled + Rocket rocketRemoved = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage removed + TestRockets.addCoreFins(rocketRemoved); + + Rocket rocketDisabled = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage disabled + TestRockets.addCoreFins(rocketDisabled); + FlightConfigurationId fcid = new FlightConfigurationId(TestRockets.FALCON_9H_FCID_1); int stageNr = 2; // Stage 2 is the Parallel Booster Stage rocketRemoved.getChild(1).getChild(0).removeChild(0); // Remove the Parallel Booster Stage @@ -171,6 +175,8 @@ public class DisableStageTest extends BaseTestCase { //// Test re-enableing the stage. Rocket rocketOriginal = TestRockets.makeFalcon9Heavy(); + TestRockets.addCoreFins(rocketOriginal); + Simulation simOriginal = new Simulation(rocketOriginal); simOriginal.setFlightConfigurationId(fcid); simOriginal.getOptions().setISAAtmosphere(true); @@ -188,7 +194,10 @@ public class DisableStageTest extends BaseTestCase { public void testBooster2() { //// Test disabling the stage Rocket rocketRemoved = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage removed + TestRockets.addCoreFins(rocketRemoved); + Rocket rocketDisabled = TestRockets.makeFalcon9Heavy(); // Rocket with the last stage disabled + TestRockets.addCoreFins(rocketDisabled); FlightConfigurationId fid = new FlightConfigurationId(TestRockets.FALCON_9H_FCID_1); int stageNr = 1; // Stage 1 is the Parallel Booster Stage's parent stage @@ -225,6 +234,8 @@ public class DisableStageTest extends BaseTestCase { //// Test re-enableing the stage. Rocket rocketOriginal = TestRockets.makeFalcon9Heavy(); + TestRockets.addCoreFins(rocketOriginal); + Simulation simOriginal = new Simulation(rocketOriginal); simOriginal.setFlightConfigurationId(fid); simOriginal.getOptions().setISAAtmosphere(true); diff --git a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java index 74f573b98..ddb6993ca 100644 --- a/core/test/net/sf/openrocket/simulation/FlightEventsTest.java +++ b/core/test/net/sf/openrocket/simulation/FlightEventsTest.java @@ -85,6 +85,8 @@ public class FlightEventsTest extends BaseTestCase { @Test public void testMultiStage() throws SimulationException { final Rocket rocket = TestRockets.makeFalcon9Heavy(); + TestRockets.addCoreFins(rocket); + final Simulation sim = new Simulation(rocket); sim.getOptions().setISAAtmosphere(true); sim.getOptions().setTimeStep(0.05); @@ -98,32 +100,49 @@ public class FlightEventsTest extends BaseTestCase { final int branchCount = sim.getSimulatedData().getBranchCount(); assertEquals(" Multi-stage simulation invalid branch count", 3, branchCount); + final AxialStage coreStage = rocket.getStage(1); + final ParallelStage boosterStage = (ParallelStage) rocket.getStage(2); + final InnerTube boosterMotorTubes = (InnerTube) boosterStage.getChild(1).getChild(0); + final BodyTube coreBody = (BodyTube) coreStage.getChild(0); + + // events whose time is too variable to check are given a time of 1200 for (int b = 0; b < 3; b++) { - final FlightEvent.Type[] expectedEventTypes; - final double[] expectedEventTimes; + FlightEvent[] expectedEvents; final RocketComponent[] expectedSources; switch (b) { case 0: - expectedEventTypes = new FlightEvent.Type[]{FlightEvent.Type.LAUNCH, FlightEvent.Type.IGNITION, FlightEvent.Type.IGNITION, - FlightEvent.Type.LIFTOFF, FlightEvent.Type.LAUNCHROD, FlightEvent.Type.APOGEE, - FlightEvent.Type.BURNOUT, FlightEvent.Type.EJECTION_CHARGE, FlightEvent.Type.STAGE_SEPARATION, - FlightEvent.Type.BURNOUT, FlightEvent.Type.EJECTION_CHARGE, FlightEvent.Type.STAGE_SEPARATION, - FlightEvent.Type.TUMBLE, FlightEvent.Type.GROUND_HIT, FlightEvent.Type.SIMULATION_END}; - expectedEventTimes = new double[]{0.0, 0.0, 0.0, 0.1225, 0.125, 1.735, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0}; // Tumble and ground hit time are too variable, so don't include it - final AxialStage coreStage = rocket.getStage(1); - final ParallelStage boosterStage = (ParallelStage) rocket.getStage(2); - final InnerTube boosterMotorTubes = (InnerTube) boosterStage.getChild(1).getChild(0); - final BodyTube coreBody = (BodyTube) coreStage.getChild(0); - expectedSources = new RocketComponent[]{rocket, boosterMotorTubes, coreBody, null, null, rocket, - boosterMotorTubes, boosterStage, boosterStage, coreBody, coreStage, coreStage, - null, null, null}; + expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.LAUNCH, 0.0, rocket), + new FlightEvent(FlightEvent.Type.IGNITION, 0.0, boosterMotorTubes), + new FlightEvent(FlightEvent.Type.IGNITION, 0.0, coreBody), + new FlightEvent(FlightEvent.Type.LIFTOFF, 0.1225, null), + new FlightEvent(FlightEvent.Type.LAUNCHROD, 0.125, null), + new FlightEvent(FlightEvent.Type.APOGEE, 1.86, rocket), + new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, boosterMotorTubes), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, boosterStage), + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, boosterStage), + new FlightEvent(FlightEvent.Type.BURNOUT, 2.0, coreBody), + new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, 2.0, coreStage), + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, coreStage), + new FlightEvent(FlightEvent.Type.TUMBLE, 2.4127, null), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + }; break; case 1: + expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, coreStage), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + }; + break; case 2: - expectedEventTypes = new FlightEvent.Type[]{FlightEvent.Type.TUMBLE, FlightEvent.Type.GROUND_HIT, - FlightEvent.Type.SIMULATION_END}; - expectedEventTimes = new double[]{}; // Tumble and ground hit time are too variable, so don't include it - expectedSources = new RocketComponent[]{null, null, null}; + expectedEvents = new FlightEvent[] { + new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, 2.0, boosterStage), + new FlightEvent(FlightEvent.Type.TUMBLE, 3.551, null), + new FlightEvent(FlightEvent.Type.GROUND_HIT, 1200, null), + new FlightEvent(FlightEvent.Type.SIMULATION_END, 1200, null) + }; break; default: throw new IllegalStateException("Invalid branch number " + b); @@ -131,28 +150,28 @@ public class FlightEventsTest extends BaseTestCase { // Test event count final FlightDataBranch branch = sim.getSimulatedData().getBranch(b); - final List eventList = branch.getEvents(); - final List eventTypes = eventList.stream().map(FlightEvent::getType).collect(Collectors.toList()); - assertEquals(" Multi-stage simulation, branch " + b + " invalid number of events", expectedEventTypes.length, eventTypes.size()); + final FlightEvent[] events = (FlightEvent[]) branch.getEvents().toArray(new FlightEvent[0]); + for (int i = 0; i < events.length; i++) { + System.out.println("branch " + b + " index " + i + " event " + events[i]); + } + assertEquals(" Multi-stage simulation, branch " + b + " invalid number of events", expectedEvents.length, events.length); - // Test that all expected events are present, and in the right order - for (int i = 0; i < expectedEventTypes.length; i++) { - assertSame(" Flight type " + expectedEventTypes[i] + ", branch " + b + " not found in multi-stage simulation", - expectedEventTypes[i], eventTypes.get(i)); - } + // Test that all expected events are present, in the right order, at the right time, from the right sources + for (int i = 0; i < events.length; i++) { + final FlightEvent expected = expectedEvents[i]; + final FlightEvent actual = events[i]; + assertSame("Branch " + b + " FlightEvent " + i + " type " + expected.getType() + " not found; FlightEvent " + actual.getType() + " found instead", + expected.getType(), actual.getType()); - // Test that the event times are correct - for (int i = 0; i < expectedEventTimes.length; i++) { - assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong time", - expectedEventTimes[i], eventList.get(i).getTime(), EPSILON); - } + if (1200 != expected.getTime()) { + assertEquals("Branch " + b + " FlightEvent " + i + " type " + expected.getType() + " has wrong time", + expected.getTime(), actual.getTime(), EPSILON); + } - // Test that the event sources are correct - for (int i = 0; i < expectedSources.length; i++) { - assertEquals(" Flight type " + expectedEventTypes[i] + " has wrong source", - expectedSources[i], eventList.get(i).getSource()); + // Test that the event sources are correct + assertEquals("Branch " + b + " FlightEvent " + i + " type " + expected.getType() + " has wrong source", + expected.getSource(), actual.getSource()); } } } - } diff --git a/core/test/net/sf/openrocket/simulation/SimulationConditionsTest.java b/core/test/net/sf/openrocket/simulation/SimulationConditionsTest.java new file mode 100644 index 000000000..78c60539d --- /dev/null +++ b/core/test/net/sf/openrocket/simulation/SimulationConditionsTest.java @@ -0,0 +1,79 @@ +package net.sf.openrocket.simulation; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.util.Modules; +import net.sf.openrocket.ServicesForTesting; +import net.sf.openrocket.formatting.RocketDescriptor; +import net.sf.openrocket.formatting.RocketDescriptorImpl; +import net.sf.openrocket.l10n.DebugTranslator; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.plugin.PluginModule; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.MockPreferences; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.util.MathUtil; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class SimulationConditionsTest { + private final static double EPSILON = MathUtil.EPSILON; + + @BeforeClass + public static void setUp() throws Exception { + Module applicationModule = new PreferencesModule(); + Module debugTranslator = new AbstractModule() { + + @Override + protected void configure() { + bind(Translator.class).toInstance(new DebugTranslator(null)); + } + + }; + Module pluginModule = new PluginModule(); + Injector injector = Guice.createInjector(Modules.override(applicationModule).with(debugTranslator), pluginModule); + Application.setInjector(injector); + } + + @Test + public void testDefaultSimulationOptionFactory() { + Application.getInjector().injectMembers(this); + DefaultSimulationOptionFactory factory = Application.getInjector().getInstance(DefaultSimulationOptionFactory.class); + SimulationOptions options = factory.getDefault(); + assertNotNull(options); + + assertEquals(28.61, options.getLaunchLatitude(), EPSILON); + assertEquals(0.0, options.getLaunchAltitude(), EPSILON); + assertEquals(-80.60, options.getLaunchLongitude(), EPSILON); + assertTrue(options.isISAAtmosphere()); + assertEquals(288.15, options.getLaunchTemperature(), EPSILON); + assertEquals(101325, options.getLaunchPressure(), EPSILON); + assertEquals(1.0, options.getLaunchRodLength(), EPSILON); + assertEquals(Math.PI / 2, options.getLaunchRodDirection(), EPSILON); + assertEquals(0.0, options.getLaunchRodAngle(), EPSILON); + assertTrue(options.getLaunchIntoWind()); + assertEquals(Math.PI / 2, options.getWindDirection(), EPSILON); + assertEquals(0.1, options.getWindTurbulenceIntensity(), EPSILON); + assertEquals(2.0, options.getWindSpeedAverage(), EPSILON); + assertEquals(0.2, options.getWindSpeedDeviation(), EPSILON); + + assertEquals(0.05, options.getTimeStep(), EPSILON); + assertEquals(3 * Math.PI / 180, options.getMaximumStepAngle(), EPSILON); + + } + + private static class PreferencesModule extends AbstractModule { + @Override + protected void configure() { + bind(Preferences.class).to(MockPreferences.class); + bind(Translator.class).toProvider(ServicesForTesting.TranslatorProviderForTesting.class); + bind(RocketDescriptor.class).to(RocketDescriptorImpl.class); + } + } +} diff --git a/core/test/net/sf/openrocket/startup/MockPreferences.java b/core/test/net/sf/openrocket/startup/MockPreferences.java index eabfe18d9..8b9bb6685 100644 --- a/core/test/net/sf/openrocket/startup/MockPreferences.java +++ b/core/test/net/sf/openrocket/startup/MockPreferences.java @@ -3,11 +3,13 @@ package net.sf.openrocket.startup; import java.util.Set; import java.util.prefs.BackingStoreException; +import com.google.inject.Singleton; import net.sf.openrocket.material.Material; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.ComponentPreset.Type; import net.sf.openrocket.util.BugException; +@Singleton public class MockPreferences extends Preferences { private final String NODENAME = "OpenRocket-test-mock"; diff --git a/swing/resources/datafiles/examples/Airstart timing.ork b/swing/resources/datafiles/examples/Airstart timing.ork index 23f271285..667446242 100644 Binary files a/swing/resources/datafiles/examples/Airstart timing.ork and b/swing/resources/datafiles/examples/Airstart timing.ork differ diff --git a/swing/resources/datafiles/examples/Dual parachute deployment.ork b/swing/resources/datafiles/examples/Dual parachute deployment.ork index ba46c4944..237457789 100644 Binary files a/swing/resources/datafiles/examples/Dual parachute deployment.ork and b/swing/resources/datafiles/examples/Dual parachute deployment.ork differ diff --git a/swing/resources/datafiles/examples/Parallel booster staging.ork b/swing/resources/datafiles/examples/Parallel booster staging.ork index ac901c1c0..b7d3976d7 100644 Binary files a/swing/resources/datafiles/examples/Parallel booster staging.ork and b/swing/resources/datafiles/examples/Parallel booster staging.ork differ diff --git a/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java b/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java index 0906b22ed..7aeef0a25 100644 --- a/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java +++ b/swing/src/net/sf/openrocket/file/SimulationTableCSVExport.java @@ -87,9 +87,11 @@ public class SimulationTableCSVExport { * Generate the CSV data from the simulation table * @param fieldSep The field separator to use in the CSV file. * @param precision The number of decimal places to use in the CSV file. - * @return + * @param isExponentialNotation If true, use exponential notation for numbers. + * @param onlySelected If true, only export the selected rows in the table. + * @return The CSV data as one string block. */ - public String generateCSVDate(String fieldSep, int precision) { + public String generateCSVDate(String fieldSep, int precision, boolean isExponentialNotation, boolean onlySelected) { int modelColumnCount = simulationTableModel.getColumnCount(); int modelRowCount = simulationTableModel.getRowCount(); populateColumnNameToUnitsHashTable(); @@ -97,7 +99,7 @@ public class SimulationTableCSVExport { String CSVSimResultString; // Obtain the column titles for the first row of the CSV ArrayList rowColumnElement = new ArrayList<>(); - for (int j = 1; j curveSelectionBox; private final DefaultComboBoxModel curveSelectionModel; + private final JLabel ejectionChargeDelayLabel; private final JComboBox delayBox; private final JLabel nrOfMotorsLabel; @@ -172,17 +173,22 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec // Ejection charge delay: { - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay"))); + ejectionChargeDelayLabel = new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay")); + panel.add(ejectionChargeDelayLabel); delayBox = new JComboBox(); delayBox.setEditable(true); delayBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { - String sel = (String) delayBox.getSelectedItem(); - //// None - if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.equalsIgnoreCase.None"))) { + if (sel == null) { + log.debug("Selected charge delay is null"); + return; + } + //// Plugged (or None) + if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.delayBox.Plugged")) || + sel.equalsIgnoreCase(trans.get("TCMotorSelPan.delayBox.PluggedNone"))) { selectedDelay = Motor.PLUGGED_DELAY; } else { try { @@ -194,8 +200,8 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec } }); panel.add(delayBox, "growx,wrap"); - //// (Number of seconds or \"None\") - panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.NumberofsecondsorNone"), -3), "skip, wrap"); + //// (or type in desired delay in seconds) + panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.Numberofseconds"), -3), "skip, wrap"); setDelays(false); } @@ -507,11 +513,16 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec curveSelectionModel.removeAllElements(); curveSelectionBox.setEnabled(false); curveSelectionLabel.setEnabled(false); + ejectionChargeDelayLabel.setEnabled(false); + delayBox.setEnabled(false); motorInformationPanel.clearData(); table.clearSelection(); return; } + ejectionChargeDelayLabel.setEnabled(true); + delayBox.setEnabled(true); + // Check which thrust curves to display List motors = getFilteredCurves(); final int index = motors.indexOf(selectedMotor); @@ -562,7 +573,7 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec private void updateNrOfMotors() { if (table != null && nrOfMotorsLabel != null) { int rowCount = table.getRowCount(); - String motorCount = "None"; + String motorCount = trans.get("TCMotorSelPan.lbl.nrOfMotors.None"); if (rowCount > 0) { motorCount = String.valueOf(rowCount); } @@ -574,7 +585,6 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec private void scrollSelectionVisible() { if (selectedMotorSet != null) { int index = table.convertRowIndexToView(model.getIndex(selectedMotorSet)); - //System.out.println("index=" + index); 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); @@ -663,47 +673,45 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec */ private void setDelays(boolean reset) { if (selectedMotor == null) { - - //// None - delayBox.setModel(new DefaultComboBoxModel(new String[] { trans.get("TCMotorSelPan.delayBox.None") })); - delayBox.setSelectedIndex(0); - + //// Display nothing + delayBox.setModel(new DefaultComboBoxModel<>(new String[] {})); } else { - List delays = selectedMotorSet.getDelays(); - String[] delayStrings = new String[delays.size()]; + boolean containsPlugged = delays.contains(Motor.PLUGGED_DELAY); + int size = delays.size() + (containsPlugged ? 0 : 1); + String[] delayStrings = new String[size]; double currentDelay = selectedDelay; // Store current setting locally for (int i = 0; i < delays.size(); i++) { - //// None - delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), trans.get("TCMotorSelPan.delayBox.None")); + //// Plugged + delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), trans.get("TCMotorSelPan.delayBox.Plugged")); + } + // We always want the plugged option in the combobox, even if the motor doesn't have it + if (!containsPlugged) { + delayStrings[delayStrings.length - 1] = trans.get("TCMotorSelPan.delayBox.Plugged"); } delayBox.setModel(new DefaultComboBoxModel(delayStrings)); if (reset) { - // Find and set the closest value double closest = Double.NaN; - for (int i = 0; i < delays.size(); i++) { + for (Double delay : delays) { // if-condition to always become true for NaN - if (!(Math.abs(delays.get(i) - currentDelay) > Math.abs(closest - currentDelay))) { - closest = delays.get(i); + if (!(Math.abs(delay - currentDelay) > Math.abs(closest - currentDelay))) { + closest = delay; } } if (!Double.isNaN(closest)) { selectedDelay = closest; - //// None - delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(closest, trans.get("TCMotorSelPan.delayBox.None"))); + delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(closest, trans.get("TCMotorSelPan.delayBox.Plugged"))); } else { - delayBox.setSelectedItem("None"); + //// Plugged + delayBox.setSelectedItem(trans.get("TCMotorSelPan.delayBox.Plugged")); } } else { - selectedDelay = currentDelay; - //// None - delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, trans.get("TCMotorSelPan.delayBox.None"))); - + delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, trans.get("TCMotorSelPan.delayBox.Plugged"))); } } diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index 414344bb7..63b4de665 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -465,7 +465,7 @@ public class BasicFrame extends JFrame { fileMenu.add(item); // export sim table... - AbstractAction simTableExportAction = simulationPanel.getSimulationTableAsCSVExportAction(); + AbstractAction simTableExportAction = simulationPanel.getExportSimulationTableAsCSVAction(); JMenuItem exportSimTableToCSVMenuItem = new JMenuItem(simTableExportAction); fileMenu.add(exportSimTableToCSVMenuItem); diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index 5cc7ca8ce..78731f5e3 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -114,6 +114,7 @@ public class SimulationPanel extends JPanel { private final SimulationAction duplicateSimulationAction; private final SimulationAction deleteSimulationAction; private final SimulationAction simTableExportAction; + private final SimulationAction selectedSimsExportAction; private int[] previousSelection = null; private JMenuItem exportSimTableToCSVMenuItem; @@ -132,6 +133,7 @@ public class SimulationPanel extends JPanel { duplicateSimulationAction = new DuplicateSimulationAction(); deleteSimulationAction = new DeleteSimulationAction(); simTableExportAction = new ExportSimulationTableAsCSVAction(); + selectedSimsExportAction = new ExportSelectedSimulationsAsCSVAction(); //////// The simulation action buttons //////// @@ -190,7 +192,7 @@ public class SimulationPanel extends JPanel { pm.addSeparator(); pm.add(runSimulationAction); pm.add(plotSimulationAction); - pm.add(simTableExportAction); + pm.add(selectedSimsExportAction); // The normal left/right and tab/shift-tab key action traverses each cell/column of the table instead of going to the next row. TableRowTraversalPolicy.setTableRowTraversalPolicy(simulationTable); @@ -388,6 +390,80 @@ public class SimulationPanel extends JPanel { openDialog(false, sims); } + private void exportSimulationsToCSV(boolean onlySelected) { + Container tableParent = simulationTable.getParent(); + int rowCount = simulationTableModel.getRowCount(); + + // I'm pretty sure with the enablement/disablement of the menu item under the File dropdown, + // that this would no longer be needed because if there is no sim table yet, the context menu + // won't show up. But I'm going to leave this in just in case.... + if (rowCount <= 0) { + log.info("No simulation table rows to export"); + JOptionPane.showMessageDialog(tableParent, trans.get("simpanel.dlg.no.simulation.table.rows")); + return; + } + + JFileChooser fch = setUpSimExportCSVFileChooser(); + int selectionStatus = fch.showSaveDialog(tableParent); + if (selectionStatus != JFileChooser.APPROVE_OPTION) { + log.debug("User cancelled CSV export"); + return; + } + + // Fetch the info from the file chooser + File CSVFile = fch.getSelectedFile(); + CSVFile = FileHelper.forceExtension(CSVFile, "csv"); + if (!FileHelper.confirmWrite(CSVFile, SimulationPanel.this)) { + log.debug("User cancelled CSV export overwrite"); + return; + } + + String separator = ((CsvOptionPanel) fch.getAccessory()).getFieldSeparator(); + int precision = ((CsvOptionPanel) fch.getAccessory()).getDecimalPlaces(); + boolean isExponentialNotation = ((CsvOptionPanel) fch.getAccessory()).isExponentialNotation(); + ((CsvOptionPanel) fch.getAccessory()).storePreferences(); + + // Handle some special separator options from CsvOptionPanel + if (separator.equals(trans.get("CsvOptionPanel.separator.space"))) { + separator = " "; + } else if (separator.equals(trans.get("CsvOptionPanel.separator.tab"))) { + separator = "\t"; + } + + SimulationTableCSVExport exporter = new SimulationTableCSVExport(document, simulationTable, simulationTableModel); + exporter.export(CSVFile, separator, precision, isExponentialNotation, onlySelected); + } + + /** + * Create the file chooser to save the CSV file. + * @return The file chooser. + */ + private JFileChooser setUpSimExportCSVFileChooser() { + JFileChooser fch = new JFileChooser(); + fch.setDialogTitle(trans.get("simpanel.pop.exportToCSV.save.dialog.title")); + fch.setFileFilter(FileHelper.CSV_FILTER); + fch.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + fch.setAcceptAllFileFilterUsed(false); + + // Default output CSV to same name as the document's rocket name. + String fileName = document.getRocket().getName() + ".csv"; + fch.setSelectedFile(new File(fileName)); + + // Add CSV options to FileChooser + CsvOptionPanel CSVOptions = new CsvOptionPanel(SimulationTableCSVExport.class); + fch.setAccessory(CSVOptions); + + // TODO: update this dynamically instead of hard-coded values + // The macOS file chooser has an issue where it does not update its size when the accessory is added. + if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) { + Dimension currentSize = fch.getPreferredSize(); + Dimension newSize = new Dimension((int) (1.5 * currentSize.width), (int) (1.3 * currentSize.height)); + fch.setPreferredSize(newSize); + } + + return fch; + } + private Simulation[] getSelectedSimulations() { int[] selection = simulationTable.getSelectedRows(); if (selection.length == 0) { @@ -456,9 +532,9 @@ public class SimulationPanel extends JPanel { /** * Return the action for exporting the simulation table data to a CSV file. - * @return + * @return the action for exporting the simulation table data to a CSV file. */ - public AbstractAction getSimulationTableAsCSVExportAction() { + public AbstractAction getExportSimulationTableAsCSVAction() { return simTableExportAction; } @@ -473,6 +549,7 @@ public class SimulationPanel extends JPanel { plotSimulationAction.updateEnabledState(); duplicateSimulationAction.updateEnabledState(); simTableExportAction.updateEnabledState(); + selectedSimsExportAction.updateEnabledState(); } /// when the simulation tab is selected this run outdated simulated if appropriate. @@ -624,81 +701,19 @@ public class SimulationPanel extends JPanel { } } + /** + * Export the entire simulation table as a CSV file. + */ class ExportSimulationTableAsCSVAction extends SimulationAction { public ExportSimulationTableAsCSVAction() { - putValue(NAME, trans.get("simpanel.pop.exportToCSV")); + putValue(NAME, trans.get("simpanel.pop.exportSimTableToCSV")); putValue(SMALL_ICON, Icons.SIM_TABLE_EXPORT); } @Override public void actionPerformed(ActionEvent arg0) { - Container tableParent = simulationTable.getParent(); - int rowCount = simulationTableModel.getRowCount(); - - // I'm pretty sure with the enablement/disablement of the menu item under the File dropdown, - // that this would no longer be needed because if there is no sim table yet, the context menu - // won't show up. But I'm going to leave this in just in case.... - if (rowCount <= 0) { - log.info("No simulation table rows to export"); - JOptionPane.showMessageDialog(tableParent, trans.get("simpanel.dlg.no.simulation.table.rows")); - return; - } - - JFileChooser fch = this.setUpFileChooser(); - int selectionStatus = fch.showSaveDialog(tableParent); - if (selectionStatus != JFileChooser.APPROVE_OPTION) { - log.info("User cancelled CSV export"); - return; - } - - // Fetch the info from the file chooser - File CSVFile = fch.getSelectedFile(); - CSVFile = FileHelper.forceExtension(CSVFile, "csv"); - String separator = ((CsvOptionPanel) fch.getAccessory()).getFieldSeparator(); - int precision = ((CsvOptionPanel) fch.getAccessory()).getDecimalPlaces(); - ((CsvOptionPanel) fch.getAccessory()).storePreferences(); - - // Handle some special separator options from CsvOptionPanel - if (separator.equals(trans.get("CsvOptionPanel.separator.space"))) { - separator = " "; - } else if (separator.equals(trans.get("CsvOptionPanel.separator.tab"))) { - separator = "\t"; - } - - SimulationTableCSVExport exporter = new SimulationTableCSVExport(document, simulationTable, simulationTableModel); - exporter.export(CSVFile, separator, precision); - } - - /** - * Create the file chooser to save the CSV file. - * @return The file chooser. - */ - private JFileChooser setUpFileChooser() { - JFileChooser fch = new JFileChooser(); - fch.setDialogTitle(trans.get("simpanel.pop.exportToCSV.save.dialog.title")); - fch.setFileFilter(FileHelper.CSV_FILTER); - fch.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); - fch.setAcceptAllFileFilterUsed(false); - - // Default output CSV to same name as the document's rocket name. - String fileName = document.getRocket().getName() + ".csv"; - fch.setSelectedFile(new File(fileName)); - - // Add CSV options to FileChooser - CsvOptionPanel CSVOptions = new CsvOptionPanel(SimulationTableCSVExport.class); - fch.setAccessory(CSVOptions); - fch.revalidate(); - - // TODO: update this dynamically instead of hard-coded values - // The macOS file chooser has an issue where it does not update its size when the accessory is added. - if (SystemInfo.getPlatform() == SystemInfo.Platform.MAC_OS) { - Dimension currentSize = fch.getPreferredSize(); - Dimension newSize = new Dimension((int) (1.5 * currentSize.width), (int) (1.3 * currentSize.height)); - fch.setPreferredSize(newSize); - } - - return fch; + exportSimulationsToCSV(false); } @Override @@ -708,6 +723,29 @@ public class SimulationPanel extends JPanel { } + /** + * Export only the selected simulations as a CSV file. + */ + class ExportSelectedSimulationsAsCSVAction extends SimulationAction { + + public ExportSelectedSimulationsAsCSVAction() { + putValue(NAME, trans.get("simpanel.pop.exportSelectedSimsToCSV")); + putValue(SMALL_ICON, Icons.SIM_TABLE_EXPORT); + } + + @Override + public void actionPerformed(ActionEvent arg0) { + exportSimulationsToCSV(true); + } + + @Override + public void updateEnabledState() { + setEnabled(simulationTableModel != null && simulationTableModel.getRowCount() > 0 && + simulationTable.getSelectedRowCount() > 0); + } + + } + class PlotSimulationAction extends SimulationAction { public PlotSimulationAction() { putValue(NAME, trans.get("simpanel.pop.plot")); diff --git a/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java b/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java index 3443e9be5..6dd647b2b 100644 --- a/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java +++ b/swing/src/net/sf/openrocket/gui/plot/EventGraphics.java @@ -57,6 +57,7 @@ public class EventGraphics { "pix/eventicons/event-recovery-device-deployment.png"); loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png"); loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png"); + loadImage(FlightEvent.Type.EXCEPTION, "pix/eventicons/event-exception.png"); } private static void loadImage(FlightEvent.Type type, String file) { diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java index 63ac79c29..2ff3c7101 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java @@ -38,6 +38,7 @@ import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.MotorConfiguration; import net.sf.openrocket.rocketcomponent.FlightConfiguration; +import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.simulation.SimulationStatus; import net.sf.openrocket.simulation.customexpression.CustomExpression; @@ -433,12 +434,20 @@ public class SimulationRunDialog extends JDialog { null, simulation.getName(), JOptionPane.ERROR_MESSAGE); } else if (t instanceof SimulationException) { + String title = simulation.getName(); + FlightDataBranch dataBranch = ((SimulationException) t).getFlightDataBranch(); + String message; + if (dataBranch != null) { + message = trans.get("SimuRunDlg.msg.branchErrorOccurred") + "\"" + dataBranch.getBranchName() + "\""; + } else { + message = trans.get("SimuRunDlg.msg.errorOccurred"); + } DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, new Object[] { //// A error occurred during the simulation: - trans.get("SimuRunDlg.msg.errorOccurred"), t.getMessage() }, - null, simulation.getName(), JOptionPane.ERROR_MESSAGE); + message, t.getMessage() }, + null, simulation.getName(), JOptionPane.ERROR_MESSAGE); } else {