diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 0c13b37f3..fc47de6a3 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -470,6 +470,8 @@ SimulationExtension.javacode.name = Java code SimulationExtension.javacode.name.none = none SimulationExtension.javacode.desc = Add a custom SimulationListener to the simulation SimulationExtension.javacode.className = Fully-qualified Java class name: +SimulationExtension.javacode.classnotfound = Could not find class +SimulationExtension.javacode.couldnotinstantiate = Could not instantiate class %s.
Does it have a zero-argument, or @Inject constructor? SimulationExtension.scripting.name = {language} script SimulationExtension.scripting.desc = Extend OpenRocket simulations by custom scripts. @@ -552,6 +554,7 @@ SimuRunDlg.msg.errorOccurred = An error occurred during the simulation: BasicEventSimulationEngine.error.noMotorsDefined = No motors defined in the simulation. BasicEventSimulationEngine.error.earlyMotorBurnout = Motor burnout without liftoff. +BasicEventSimulationEngine.error.noConfiguredIgnition = No motors configured to ignite at liftoff BasicEventSimulationEngine.error.noIgnition = No motors ignited. BasicEventSimulationEngine.error.NaNResult = Simulation resulted in not-a-number (NaN) value, please report a bug. @@ -1028,6 +1031,8 @@ FreeformFinSetConfig.lbl.clickDrag = Click+drag: Add and move points FreeformFinSetConfig.lbl.ctrlClick = Ctrl+click: Delete point FreeformFinSetConfig.lbl.scaleFin = Scale Fin FreeformFinSetConfig.lbl.exportCSV = Export CSV +FreeformFinSetConfig.lbl.deletePoint = Delete point +FreeformFinSetConfig.lbl.insertPoint = Insert point !TubeFinSetConfig TubeFinSetCfg.lbl.Nbroffins = Number of fins: @@ -1827,7 +1832,7 @@ PlotConfiguration.Groundtrack = Ground track ! Warning Warning.LargeAOA.str1 = Large angle of attack encountered. Warning.LargeAOA.str2 = Large angle of attack encountered ( -Warning.DISCONTINUITY = Discontinuity in rocket body diameter. +Warning.DISCONTINUITY = Discontinuity in rocket body diameter Warning.THICK_FIN = Thick fins may not be modeled accurately. Warning.JAGGED_EDGED_FIN = Jagged-edged fin predictions may be inaccurate. Warning.LISTENERS_AFFECTED = Listeners modified the flight simulation @@ -1844,6 +1849,7 @@ Warning.ZERO_LENGTH_BODY = Zero length bodies may not result in accurate simulat Warning.ZERO_RADIUS_BODY = Zero length bodies may not result in accurate simulations. Warning.TUBE_SEPARATION = Space between tube fins may not result in accurate simulations. Warning.TUBE_OVERLAP = Overlapping tube fins may not result in accurate simulations. +Warning.EMPTY_BRANCH = Simulation branch contains no data Warning.SEPARATION_ORDER = Stages separated in an unreasonable order ! Scale dialog diff --git a/core/resources/l10n/messages_cs.properties b/core/resources/l10n/messages_cs.properties index 619f8419d..48cb441fd 100644 --- a/core/resources/l10n/messages_cs.properties +++ b/core/resources/l10n/messages_cs.properties @@ -1315,7 +1315,7 @@ PlotConfiguration.Simulationtime = Simulacn ! Warning Warning.LargeAOA.str1 = Velký úhel nábehu. Warning.LargeAOA.str2 = Velký úhel nábehu ( -Warning.DISCONTINUITY = Nespojitost v prumeru tela rakety. +Warning.DISCONTINUITY = Nespojitost v prumeru tela rakety Warning.THICK_FIN = Tlou\u0161tka stabilizátoru se nemu\u017Ee modelovat presne. Warning.JAGGED_EDGED_FIN = Zubaté hrany stabilizátoru mohou být vypocteny nepresne. Warning.LISTENERS_AFFECTED = Listeners modified the flight simulation diff --git a/core/resources/l10n/messages_es.properties b/core/resources/l10n/messages_es.properties index 48a1df482..92cf2afc9 100644 --- a/core/resources/l10n/messages_es.properties +++ b/core/resources/l10n/messages_es.properties @@ -1225,7 +1225,7 @@ TrapezoidFinSetCfg.tab.Generalproperties = Propiedades generales ! TubeCoupler TubeCoupler.TubeCoupler = Acoplador -Warning.DISCONTINUITY = Discontinuidad en el di\u00e1metro del fuselaje. +Warning.DISCONTINUITY = Discontinuidad en el di\u00e1metro del fuselaje Warning.FILE_INVALID_PARAMETER = Par\u00e1metro encontrado no v\u00e1lido, ignorado. Warning.JAGGED_EDGED_FIN = El perfil afilado de las aletas puede ser inexacto. Warning.LISTENERS_AFFECTED = Las Extensiones se ejecutaron con la simulaci\u00f3n del vuelo diff --git a/core/resources/l10n/messages_fr.properties b/core/resources/l10n/messages_fr.properties index 8eed74a8f..015842dc5 100644 --- a/core/resources/l10n/messages_fr.properties +++ b/core/resources/l10n/messages_fr.properties @@ -1219,7 +1219,7 @@ TrapezoidFinSetCfg.tab.Generalproperties = Propri\u00E9t\u00E9s g\u00E9n\u00E9ra ! TubeCoupler TubeCoupler.TubeCoupler = Coupleur de tube -Warning.DISCONTINUITY = Discontinuit\u00E9 dans le diam\u00E8tre du corps de la fus\u00E9e. +Warning.DISCONTINUITY = Discontinuit\u00E9 dans le diam\u00E8tre du corps de la fus\u00E9e Warning.FILE_INVALID_PARAMETER = Param\u00E8tre invalide rencontr\u00E9, ignorer. Warning.JAGGED_EDGED_FIN = Des ailerons aux bords irr\u00E9guliers ne seront pas mod\u00E9lis\u00E9s correctement. Warning.LISTENERS_AFFECTED = Les \u00E9couteurs ont modifi\u00E9 la simulation de vol diff --git a/core/resources/l10n/messages_it.properties b/core/resources/l10n/messages_it.properties index 897a2f29b..96f4a35c3 100644 --- a/core/resources/l10n/messages_it.properties +++ b/core/resources/l10n/messages_it.properties @@ -1379,7 +1379,7 @@ PlotConfiguration.Simulationtime = Intervallo temporale della simulazione e temp ! Warning Warning.LargeAOA.str1 = Incontrato grande angolo d'attacco. Warning.LargeAOA.str2 = Incontrato grande angolo d'attacco ( -Warning.DISCONTINUITY = Discontinuita' nel diametro del tubo del corpo. +Warning.DISCONTINUITY = Discontinuita' nel diametro del tubo del corpo Warning.THICK_FIN = Le pinne sottili potrebbero non essere modellate in modo accurato. Warning.JAGGED_EDGED_FIN = Jagged-edged fin Le predizioni per le pinne potrebbero non essere accurate. Warning.LISTENERS_AFFECTED = Gli osservatori possono modificare le condizioni di simulazione diff --git a/core/resources/l10n/messages_nl.properties b/core/resources/l10n/messages_nl.properties index 5d8668a29..c010211fc 100644 --- a/core/resources/l10n/messages_nl.properties +++ b/core/resources/l10n/messages_nl.properties @@ -1688,7 +1688,7 @@ PlotConfiguration.Groundtrack = Grondspoor ! Warning Warning.LargeAOA.str1 = Grote invalshoek aangetroffen. Warning.LargeAOA.str2 = Grote invalshoek aangetroffen ( -Warning.DISCONTINUITY = DiscontinuĂŻteit in raketromp diameter. +Warning.DISCONTINUITY = DiscontinuĂŻteit in raketromp diameter Warning.THICK_FIN = Dikke vinnen worden mogelijk niet nauwkeurig gemodelleerd. Warning.JAGGED_EDGED_FIN = De voorspellingen van gekartelde vinnen kunnen onnauwkeurig zijn. Warning.LISTENERS_AFFECTED = Luisteraars veranderden de vluchtsimulatie diff --git a/core/resources/l10n/messages_pl.properties b/core/resources/l10n/messages_pl.properties index 3b5bd6b0e..f65d3dda8 100644 --- a/core/resources/l10n/messages_pl.properties +++ b/core/resources/l10n/messages_pl.properties @@ -1319,7 +1319,7 @@ update.dlg.latestVersion = Korzystasz z najnowszej wersji OpenRocket: %s. ! Warning Warning.LargeAOA.str1 = Wyst\u0105pi\u0142 du\u017Cy k\u0105t natarcia. Warning.LargeAOA.str2 = Wyst\u0105pi\u0142 du\u017Cy k\u0105t natarcia ( - Warning.DISCONTINUITY = Nieci\u0105g\u0142o\u015B\u0107 \u015Brednicy rakiety. + Warning.DISCONTINUITY = Nieci\u0105g\u0142o\u015B\u0107 \u015Brednicy rakiety Warning.THICK_FIN = Grube stateczniki mog\u0105 nie by\u0107 modelowane dok\u0142adnie. Warning.JAGGED_EDGED_FIN = Stateczniki o nieregularnych kraw\u0119dziach mog\u0105 zmniejszy\u0107 dok\u0142adno\u015B\u0107 prognoz. Warning.LISTENERS_AFFECTED = Detektory zmodyfikowa\u0142y symulacj\u0119 lotu diff --git a/core/resources/l10n/messages_pt.properties b/core/resources/l10n/messages_pt.properties index af3120d63..a4a3d1931 100644 --- a/core/resources/l10n/messages_pt.properties +++ b/core/resources/l10n/messages_pt.properties @@ -1182,7 +1182,7 @@ TrapezoidFinSetCfg.tab.Generalproperties = Propriedades gerais # TubeCoupler TubeCoupler.TubeCoupler = Acoplador de tubo -Warning.DISCONTINUITY = Descontinuidade no di\u00e2metro do corpo do foguete. +Warning.DISCONTINUITY = Descontinuidade no di\u00e2metro do corpo do foguete Warning.FILE_INVALID_PARAMETER = Par\u00e2metro inv\u00e1lido encontrado, ignorando. Warning.JAGGED_EDGED_FIN = Previs\u00f5es com aletas de bordo irregular podem ser imprecisos. Warning.LISTENERS_AFFECTED = Observador modificou a simula\u00e7\u00e3o de voo diff --git a/core/resources/l10n/messages_uk_UA.properties b/core/resources/l10n/messages_uk_UA.properties index 4dd2ea040..169171641 100644 --- a/core/resources/l10n/messages_uk_UA.properties +++ b/core/resources/l10n/messages_uk_UA.properties @@ -1535,7 +1535,7 @@ PlotConfiguration.Simulationtime = Simulation time step and computation time ! Warning Warning.LargeAOA.str1 = Large angle of attack encountered. Warning.LargeAOA.str2 = Large angle of attack encountered ( -Warning.DISCONTINUITY = Discontinuity in rocket body diameter. +Warning.DISCONTINUITY = Discontinuity in rocket body diameter Warning.THICK_FIN = Thick fins may not be modeled accurately. Warning.JAGGED_EDGED_FIN = Jagged-edged fin predictions may be inaccurate. Warning.LISTENERS_AFFECTED = Listeners modified the flight simulation diff --git a/core/resources/l10n/messages_zh_CN.properties b/core/resources/l10n/messages_zh_CN.properties index 03f619d04..2d3eed4ab 100644 --- a/core/resources/l10n/messages_zh_CN.properties +++ b/core/resources/l10n/messages_zh_CN.properties @@ -1299,7 +1299,7 @@ TubeFinSetCfg.lbl.Outerdiam = \u5916\u76F4\u5F84: TubeFinSetCfg.lbl.Thickness = \u539A\u5EA6: TubeFinSetCfg.lbl.ttip.Finrotation = \u7A33\u5B9A\u7FFC\u7EC4\u5408\u91CC\u7B2C\u4E00\u7247\u7684\u89D2\u5EA6 -Warning.DISCONTINUITY = \u7BAD\u4F53\u76F4\u5F84\u4E0D\u8FDE\u7EED. +Warning.DISCONTINUITY = \u7BAD\u4F53\u76F4\u5F84\u4E0D\u8FDE\u7EED Warning.FILE_INVALID_PARAMETER = \u65E0\u6548\u53C2\u6570, \u5FFD\u7565. Warning.JAGGED_EDGED_FIN = \u952F\u9F7F\u7FFC\u9884\u6D4B\u53EF\u80FD\u4E0D\u51C6\u786E. Warning.LISTENERS_AFFECTED = \u76D1\u542C\u5668\u4FEE\u6539\u4E86\u98DE\u884C\u4EFF\u771F diff --git a/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java b/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java index fbdac87e4..9889a4987 100644 --- a/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java +++ b/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java @@ -67,6 +67,9 @@ public interface AerodynamicCalculator extends Monitorable { * @return a new, independent instance of this aerodynamic calculator type */ public AerodynamicCalculator newInstance(); - - public boolean isContinuous(FlightConfiguration configuration, final Rocket rkt); + + /** + * Test component assembly for continuity (esp. diameter), and post any needed warnings + */ + public void testIsContinuous(FlightConfiguration configuration, final RocketComponent component, WarningSet warnings); } diff --git a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java index b85074405..ac75e95a4 100644 --- a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java +++ b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java @@ -251,10 +251,8 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { if (calcMap == null) buildCalcMap(configuration); - - if (!isContinuous(configuration, configuration.getRocket())){ - warnings.add( Warning.DIAMETER_DISCONTINUITY); - } + + testIsContinuous(configuration, configuration.getRocket(), warnings); final InstanceMap imap = configuration.getActiveInstances(); @@ -276,13 +274,9 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { return assemblyForces; } - + @Override - public boolean isContinuous(FlightConfiguration configuration, final Rocket rkt){ - return testIsContinuous(configuration, rkt); - } - - private boolean testIsContinuous(FlightConfiguration configuration, final RocketComponent treeRoot ){ + public void testIsContinuous(FlightConfiguration configuration, final RocketComponent treeRoot, WarningSet warnings ){ Queue queue = new LinkedList<>(); for (RocketComponent child : treeRoot.getChildren()) { // Ignore inactive stages @@ -292,9 +286,8 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { queue.add(child); } - boolean isContinuous = true; SymmetricComponent prevComp = null; - while((isContinuous)&&( null != queue.peek())){ + while(null != queue.peek()) { RocketComponent comp = queue.poll(); if( comp instanceof SymmetricComponent ){ for (RocketComponent child : comp.getChildren()) { @@ -313,7 +306,7 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { // Check for radius discontinuity if ( !MathUtil.equals(sym.getForeRadius(), prevComp.getAftRadius())) { - isContinuous = false; + warnings.add( Warning.DIAMETER_DISCONTINUITY, sym + ", " + prevComp); } // double x = component.toAbsolute(Coordinate.NUL)[0].x; @@ -327,11 +320,10 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { prevComp = sym; }else if( comp instanceof ComponentAssembly ){ - isContinuous &= testIsContinuous(configuration, comp); + testIsContinuous(configuration, comp, warnings); } } - return isContinuous; } diff --git a/core/src/net/sf/openrocket/aerodynamics/Warning.java b/core/src/net/sf/openrocket/aerodynamics/Warning.java index 803073948..ae7c94e35 100644 --- a/core/src/net/sf/openrocket/aerodynamics/Warning.java +++ b/core/src/net/sf/openrocket/aerodynamics/Warning.java @@ -18,7 +18,6 @@ public abstract class Warning { return new Warning.Other(text); } - /** * Return true if the other warning should replace * this warning. The method should return true if the other @@ -398,4 +397,6 @@ public abstract class Warning { public static final Warning TUBE_OVERLAP = new Other(trans.get("Warning.TUBE_OVERLAP")); public static final Warning SEPARATION_ORDER = new Other(trans.get("Warning.SEPARATION_ORDER")); + + public static final Warning EMPTY_BRANCH = new Other(trans.get("Warning.EMPTY_BRANCH")); } diff --git a/core/src/net/sf/openrocket/aerodynamics/WarningSet.java b/core/src/net/sf/openrocket/aerodynamics/WarningSet.java index 4873976f3..6f709e83b 100644 --- a/core/src/net/sf/openrocket/aerodynamics/WarningSet.java +++ b/core/src/net/sf/openrocket/aerodynamics/WarningSet.java @@ -67,7 +67,17 @@ public class WarningSet extends AbstractSet implements Cloneable, Monit mutable.check(); return add(Warning.fromString(s)); } - + + /** + * Add a Warning of the specified type with the specified discriminator to the + * set. + * @param w the warning + * @param d the extra discriminator + * + */ + public boolean add (Warning w, String d) { + return this.add(w.toString() + ": " + d); + } @Override public Iterator iterator() { diff --git a/core/src/net/sf/openrocket/file/openrocket/importt/ComponentPresetSetter.java b/core/src/net/sf/openrocket/file/openrocket/importt/ComponentPresetSetter.java index 8a10be1d7..b90940629 100644 --- a/core/src/net/sf/openrocket/file/openrocket/importt/ComponentPresetSetter.java +++ b/core/src/net/sf/openrocket/file/openrocket/importt/ComponentPresetSetter.java @@ -68,7 +68,10 @@ class ComponentPresetSetter implements Setter { if (digest != null && !matchingPreset.getDigest().equals(digest)) { warnings.add(Warning.fromString("ComponentPreset for component " + c.getName() + " has wrong digest")); } - + + // The preset loader can override the component name, so first store it and then apply it again + String componentName = c.getName(); setMethod.invoke(c, matchingPreset); + c.setName(componentName); } } diff --git a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java index a36ed0a93..45e0b2a1d 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java @@ -33,7 +33,7 @@ public abstract class FinSet extends ExternalComponent implements AxialPositiona * Maximum number of root points in the root geometry. */ private static final int MAX_ROOT_DIVISIONS = 100; - private static final int MAX_ROOT_DIVISIONS_LOW_RES = 15; + private static final int MAX_ROOT_DIVISIONS_LOW_RES = MAX_ROOT_DIVISIONS / 5; public void setOverrideMass() { } diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 339c0704b..63e3f1d7b 100644 --- a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -1,6 +1,7 @@ package net.sf.openrocket.simulation; import java.util.ArrayDeque; +import java.util.Collection; import java.util.Deque; import org.slf4j.Logger; @@ -8,6 +9,7 @@ import org.slf4j.LoggerFactory; import net.sf.openrocket.aerodynamics.Warning; import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.IgnitionEvent; import net.sf.openrocket.motor.MotorConfiguration; import net.sf.openrocket.motor.MotorConfigurationId; import net.sf.openrocket.rocketcomponent.AxialStage; @@ -68,11 +70,25 @@ public class BasicEventSimulationEngine implements SimulationEngine { FlightConfiguration origConfig = simulationConditions.getRocket().getFlightConfiguration(this.fcid); FlightConfiguration simulationConfig = origConfig.clone(); simulationConfig.copyStages(origConfig); // Clone the stage activation configuration - if ( ! simulationConfig.hasMotors() ) { - throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noMotorsDefined")); - } currentStatus = new SimulationStatus(simulationConfig, simulationConditions); + + // Sanity checks on design and configuration + + // Problems that keep us from simulating at all + + // No motors in configuration + if (!simulationConfig.hasMotors() ) { + throw new MotorIgnitionException(trans.get("BasicEventSimulationEngine.error.noMotorsDefined")); + } + + // Problems that let us simulate, but result is likely bad + + // No recovery device + if (!simulationConfig.hasRecoveryDevice()) { + currentStatus.getWarnings().add(Warning.NO_RECOVERY_DEVICE); + } + currentStatus.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket())); { // main simulation branch @@ -97,6 +113,12 @@ public class BasicEventSimulationEngine implements SimulationEngine { dataBranch.getBranchName(), currentStatus.getSimulationTime(), dataBranch.getLast(FlightDataType.TYPE_TIME))); + + + // Did the branch generate any data? + if (dataBranch.getLength() == 0) { + flightData.getWarningSet().add(Warning.EMPTY_BRANCH, dataBranch.getBranchName()); + } }while( ! toSimulate.isEmpty()); SimulationListenerHelper.fireEndSimulation(currentStatus, null); @@ -108,7 +130,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { return flightData; } - private FlightDataBranch simulateLoop() { + private FlightDataBranch simulateLoop() throws SimulationException { // Initialize the simulation. We'll use the flight stepper unless we're already on the ground if (currentStatus.isLanded()) @@ -240,9 +262,11 @@ public class BasicEventSimulationEngine implements SimulationEngine { } catch (SimulationException e) { SimulationListenerHelper.fireEndSimulation(currentStatus, e); + // Add FlightEvent for Abort. currentStatus.getFlightData().addEvent(new FlightEvent(FlightEvent.Type.EXCEPTION, currentStatus.getSimulationTime(), currentStatus.getConfiguration().getRocket(), e.getLocalizedMessage())); - currentStatus.getWarnings().add(e.getLocalizedMessage()); + + throw e; } return currentStatus.getFlightData(); @@ -341,11 +365,6 @@ public class BasicEventSimulationEngine implements SimulationEngine { event.getTime() + Math.max(0.001, deployConfig.getDeployDelay()), c)); } } - - // Add a warning if there is no recovery device defined. - if (!currentStatus.getConfiguration().hasRecoveryDevice()) { - currentStatus.getWarnings().add(Warning.NO_RECOVERY_DEVICE); - } // Handle event log.trace("Handling event " + event); diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java index 3602ec175..8a1acfbfa 100644 --- a/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java +++ b/core/src/net/sf/openrocket/simulation/extension/impl/JavaCode.java @@ -1,5 +1,6 @@ package net.sf.openrocket.simulation.extension.impl; +import com.google.inject.ConfigurationException; import net.sf.openrocket.simulation.SimulationConditions; import net.sf.openrocket.simulation.exception.SimulationException; import net.sf.openrocket.simulation.extension.AbstractSimulationExtension; @@ -23,11 +24,15 @@ public class JavaCode extends AbstractSimulationExtension { if (!SimulationListener.class.isAssignableFrom(clazz)) { throw new SimulationException("Class " + className + " does not implement SimulationListener"); } - SimulationListener listener = (SimulationListener) injector.getInstance(clazz); - conditions.getSimulationListenerList().add(listener); + try { + SimulationListener listener = (SimulationListener) injector.getInstance(clazz); + conditions.getSimulationListenerList().add(listener); + } catch (ConfigurationException e) { + throw new SimulationException(String.format(trans.get("SimulationExtension.javacode.couldnotinstantiate"), className), e); + } } } catch (ClassNotFoundException e) { - throw new SimulationException("Could not find class " + className); + throw new SimulationException(trans.get("SimulationExtension.javacode.classnotfound") + " " + className); } } diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java index e179d2da9..a76461696 100644 --- a/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java +++ b/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java @@ -22,7 +22,11 @@ public class StopSimulationListener extends AbstractSimulationListener { private long startTime = -1; private long time = -1; - + + public StopSimulationListener() { + this(0, 0); + } + public StopSimulationListener(double t, int n) { stopTime = t; stopStep = n; diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java index 796fc6d22..024fc3509 100644 --- a/core/src/net/sf/openrocket/unit/UnitGroup.java +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -18,6 +18,7 @@ import java.util.regex.Pattern; import net.sf.openrocket.rocketcomponent.FlightConfiguration; import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.util.StringUtil; /** @@ -670,8 +671,8 @@ public class UnitGroup { if (!matcher.matches()) { throw new NumberFormatException("string did not match required pattern"); } - - double value = Double.parseDouble(matcher.group(1)); + + double value = StringUtil.convertToDouble(matcher.group(1)); String unit = matcher.group(2).trim(); if (unit.equals("")) { diff --git a/core/src/net/sf/openrocket/util/StringUtil.java b/core/src/net/sf/openrocket/util/StringUtil.java index e5c3bb2ee..27be8ac44 100644 --- a/core/src/net/sf/openrocket/util/StringUtil.java +++ b/core/src/net/sf/openrocket/util/StringUtil.java @@ -19,5 +19,24 @@ public class StringUtil { } return "".equals(s.trim()); } + + /** + * Converts a string to a double, but with a more robust locale handling. + * Some systems use a comma as a decimal separator, some a dot. This method + * should work for both cases + * @param input string to convert + * @return double converted from string + * @throws NumberFormatException if the string cannot be parsed. + */ + public static double convertToDouble(String input) { + input = input.replace(',', '.'); + int decimalSeparator = input.lastIndexOf('.'); + + if (decimalSeparator > -1) { + input = input.substring(0, decimalSeparator).replace(".", "") + input.substring(decimalSeparator); + } + + return Double.parseDouble(input); + } } diff --git a/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java b/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java index 90d8044c0..d113cf003 100644 --- a/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java +++ b/core/test/net/sf/openrocket/aerodynamics/BarrowmanCalculatorTest.java @@ -288,8 +288,10 @@ public class BarrowmanCalculatorTest { Rocket rocket = TestRockets.makeEstesAlphaIII(); AerodynamicCalculator calc = new BarrowmanCalculator(); FlightConfiguration configuration = rocket.getSelectedConfiguration(); + WarningSet warnings = new WarningSet(); - assertTrue("Estes Alpha III should be continuous: ", calc.isContinuous(configuration, rocket)); + calc.testIsContinuous(configuration, rocket, warnings); + assertTrue("Estes Alpha III should be continuous: ", warnings.isEmpty()); } @Test @@ -297,8 +299,10 @@ public class BarrowmanCalculatorTest { Rocket rocket = TestRockets.makeFalcon9Heavy(); AerodynamicCalculator calc = new BarrowmanCalculator(); FlightConfiguration configuration = rocket.getSelectedConfiguration(); + WarningSet warnings = new WarningSet(); - assertTrue("F9H should be continuous: ", calc.isContinuous(configuration, rocket)); + calc.testIsContinuous(configuration, rocket, warnings); + assertTrue("F9H should be continuous: ", warnings.isEmpty()); } @Test @@ -306,6 +310,7 @@ public class BarrowmanCalculatorTest { Rocket rocket = TestRockets.makeEstesAlphaIII(); AerodynamicCalculator calc = new BarrowmanCalculator(); FlightConfiguration configuration = rocket.getSelectedConfiguration(); + WarningSet warnings = new WarningSet(); NoseCone nose = (NoseCone)rocket.getChild(0).getChild(0); BodyTube body = (BodyTube)rocket.getChild(0).getChild(1); @@ -313,8 +318,9 @@ public class BarrowmanCalculatorTest { nose.setAftRadius(0.015); body.setOuterRadius( 0.012 ); body.setName( body.getName()+" << discontinuous"); - - assertFalse(" Estes Alpha III has an undetected discontinuity:", calc.isContinuous(configuration, rocket)); + + calc.testIsContinuous(configuration, rocket, warnings); + assertFalse(" Estes Alpha III has an undetected discontinuity:", warnings.isEmpty()); } @Test @@ -322,6 +328,7 @@ public class BarrowmanCalculatorTest { Rocket rocket = TestRockets.makeFalcon9Heavy(); AerodynamicCalculator calc = new BarrowmanCalculator(); FlightConfiguration configuration = rocket.getSelectedConfiguration(); + WarningSet warnings = new WarningSet(); final AxialStage coreStage = (AxialStage)rocket.getChild(1); final ParallelStage booster = (ParallelStage)coreStage.getChild(0).getChild(0); @@ -333,7 +340,8 @@ public class BarrowmanCalculatorTest { body.setOuterRadius( 0.012 ); body.setName( body.getName()+" << discontinuous"); - assertFalse(" Missed discontinuity in Falcon 9 Heavy:", calc.isContinuous(configuration, rocket)); + calc.testIsContinuous(configuration, rocket, warnings); + assertFalse(" Missed discontinuity in Falcon 9 Heavy:" , warnings.isEmpty()); } @Test diff --git a/core/test/net/sf/openrocket/util/StringUtilTest.java b/core/test/net/sf/openrocket/util/StringUtilTest.java index 2d89772e5..7b1543d4c 100644 --- a/core/test/net/sf/openrocket/util/StringUtilTest.java +++ b/core/test/net/sf/openrocket/util/StringUtilTest.java @@ -4,6 +4,7 @@ import org.junit.Test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; /** * A class that tests @@ -22,4 +23,25 @@ public class StringUtilTest { assertFalse(StringUtil.isEmpty("A")); assertFalse(StringUtil.isEmpty(" . ")); } + + @Test + public void testConvertToDouble() { + assertEquals(0.2, StringUtil.convertToDouble(".2"), MathUtil.EPSILON); + assertEquals(0.2, StringUtil.convertToDouble(",2"), MathUtil.EPSILON); + assertEquals(1, StringUtil.convertToDouble("1,"), MathUtil.EPSILON); + assertEquals(2, StringUtil.convertToDouble("2."), MathUtil.EPSILON); + assertEquals(1, StringUtil.convertToDouble("1"), MathUtil.EPSILON); + assertEquals(1.52, StringUtil.convertToDouble("1.52"), MathUtil.EPSILON); + assertEquals(1.52, StringUtil.convertToDouble("1,52"), MathUtil.EPSILON); + assertEquals(1.5, StringUtil.convertToDouble("1.500"), MathUtil.EPSILON); + assertEquals(1.5, StringUtil.convertToDouble("1,500"), MathUtil.EPSILON); + assertEquals(1500.61, StringUtil.convertToDouble("1.500,61"), MathUtil.EPSILON); + assertEquals(1500.61, StringUtil.convertToDouble("1,500.61"), MathUtil.EPSILON); + assertEquals(1500.2, StringUtil.convertToDouble("1,500,200"), MathUtil.EPSILON); + assertEquals(1500.2, StringUtil.convertToDouble("1.500.200"), MathUtil.EPSILON); + assertEquals(1500200.23, StringUtil.convertToDouble("1500200.23"), MathUtil.EPSILON); + assertEquals(1500200.23, StringUtil.convertToDouble("1500200,23"), MathUtil.EPSILON); + assertEquals(1500200.23, StringUtil.convertToDouble("1,500,200.23"), MathUtil.EPSILON); + assertEquals(1500200.23, StringUtil.convertToDouble("1.500.200,23"), MathUtil.EPSILON); + } } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java index f3777b3b0..66ba5710c 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.EventObject; import java.util.List; +import javax.swing.AbstractAction; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; @@ -26,16 +27,21 @@ import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JSpinner; +import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import net.sf.openrocket.gui.adaptors.CustomFocusTraversalPolicy; +import net.sf.openrocket.gui.util.Icons; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -77,6 +83,7 @@ public class FreeformFinSetConfig extends FinSetConfig { private JTable table = null; private FinPointTableModel tableModel = null; + private JPopupMenu pm; private int dragIndex = -1; private Point dragPoint = null; @@ -84,6 +91,9 @@ public class FreeformFinSetConfig extends FinSetConfig { private FinPointFigure figure = null; private ScaleScrollPane figurePane = null; private ScaleSelector selector; + + private FinPointAction insertFinPointAction; + private FinPointAction deleteFinPointAction; public FreeformFinSetConfig(OpenRocketDocument d, RocketComponent component, JDialog parent) { super(d, component, parent); @@ -246,11 +256,31 @@ public class FreeformFinSetConfig extends FinSetConfig { } table.addMouseListener(new MouseAdapter() { @Override - public void mouseClicked(MouseEvent ev) { + public void mouseClicked(MouseEvent e) { + int row = table.rowAtPoint(e.getPoint()); + + // Context menu on right-click + if (e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() == 1) { + // Select new row + if (!table.isRowSelected(row)) { + if (row >= 0 && row < table.getRowCount()) { + table.setRowSelectionInterval(row, row); + } else { + return; + } + } + + doPopup(e); + } + } + + }); + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { figure.setSelectedIndex(table.getSelectedRow()); figure.updateFigure(); } - }); JScrollPane tablePane = new JScrollPane(table); @@ -264,6 +294,21 @@ public class FreeformFinSetConfig extends FinSetConfig { dialog.dispose(); } }); + + // Context menu for table + insertFinPointAction = new InsertPointAction(); + deleteFinPointAction = new DeletePointAction(); + pm = new JPopupMenu(); + pm.add(insertFinPointAction); + pm.add(deleteFinPointAction); + + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + updateActionStates(); + } + }); + // panel.add(new JLabel("Coordinates:"), "aligny bottom, alignx 50%"); // panel.add(new JLabel(" View:"), "wrap, aligny bottom"); @@ -303,10 +348,12 @@ public class FreeformFinSetConfig extends FinSetConfig { panel.setLayout(new MigLayout("fill, gap 5!","", "[nogrid, fill, sizegroup display, growprio 200]5![sizegroup text, growprio 5]5![sizegroup buttons, align top, growprio 5]0!")); // first row: main display - panel.add(tablePane, "width 100lp:100lp:, growy"); + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, tablePane, figurePane); + splitPane.setResizeWeight(0.1); + splitPane.setBorder(null); + panel.add(splitPane, "width 300lp:500lp:, gap unrel, grow, height 100lp:250lp:, wrap"); order.add(table); - panel.add(figurePane, "width 200lp:400lp:, gap unrel, grow, height 100lp:250lp:, wrap"); - + // row of text directly below figure panel.add(new StyledLabel(trans.get("lbl.doubleClick1")+" "+trans.get("FreeformFinSetConfig.lbl.doubleClick2"), -2), "spanx 3"); panel.add(new StyledLabel(trans.get("FreeformFinSetConfig.lbl.clickDrag"), -2), "spanx 3"); @@ -414,6 +461,51 @@ public class FreeformFinSetConfig extends FinSetConfig { figurePane.revalidate(); } } + + /** + * Insert a new fin point between the currently selected point and the next point. + * The coordinates of the new point will be the average of the two points. + */ + private void insertPoint() { + int currentPointIdx = table.getSelectedRow(); + if (currentPointIdx == -1 || currentPointIdx >= table.getRowCount() - 1) { + return; + } + final FreeformFinSet finSet = (FreeformFinSet) component; + Coordinate currentPoint = finSet.getFinPoints()[currentPointIdx]; + Coordinate nextPoint = finSet.getFinPoints()[currentPointIdx + 1]; + Point2D.Double toAdd = new Point2D.Double((currentPoint.x + nextPoint.x) / 2, (currentPoint.y + nextPoint.y) / 2); + finSet.addPoint(currentPointIdx + 1, toAdd); + } + + /** + * Delete the currently selected fin point. + */ + private void deletePoint() { + int currentPointIdx = table.getSelectedRow(); + if (currentPointIdx == -1) { + return; + } + final FreeformFinSet finSet = (FreeformFinSet) component; + try { + finSet.removePoint(currentPointIdx); + } catch (IllegalFinPointException ex) { + throw new RuntimeException(ex); + } + } + + private void doPopup(MouseEvent e) { + pm.show(e.getComponent(), e.getX(), e.getY()); + } + + private void updateActionStates() { + if (insertFinPointAction == null) { // If one of the actions is null, the rest will be too + return; + } + + insertFinPointAction.updateEnabledState(); + deleteFinPointAction.updateEnabledState(); + } private class FinPointScrollPane extends ScaleScrollPane { @@ -693,4 +785,46 @@ public class FreeformFinSetConfig extends FinSetConfig { } } } + + private abstract static class FinPointAction extends AbstractAction { + private static final long serialVersionUID = 1L; + + public abstract void updateEnabledState(); + } + + private class InsertPointAction extends FinPointAction { + public InsertPointAction() { + putValue(NAME, trans.get("FreeformFinSetConfig.lbl.insertPoint")); + this.putValue(SMALL_ICON, Icons.FILE_NEW); + } + + @Override + public void actionPerformed(ActionEvent e) { + insertPoint(); + } + + @Override + public void updateEnabledState() { + // You can't add to the last fin point + setEnabled(table.getSelectedRow() < table.getRowCount() - 1); + } + } + + private class DeletePointAction extends FinPointAction { + public DeletePointAction() { + putValue(NAME, trans.get("FreeformFinSetConfig.lbl.deletePoint")); + this.putValue(SMALL_ICON, Icons.EDIT_DELETE); + } + + @Override + public void actionPerformed(ActionEvent e) { + deletePoint(); + } + + @Override + public void updateEnabledState() { + // You can't delete the first or last fin point + setEnabled(table.getSelectedRow() > 0 && table.getSelectedRow() < table.getRowCount() - 1); + } + } } diff --git a/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java b/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java index c4b1e2c4a..a8098964e 100644 --- a/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java +++ b/swing/src/net/sf/openrocket/gui/figure3d/geometry/FinRenderer.java @@ -17,7 +17,7 @@ import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.gui.figure3d.geometry.Geometry.Surface; public class FinRenderer { - private GLUtessellator tobj = GLU.gluNewTess(); + private GLUtessellator tess = GLU.gluNewTess(); public void renderFinSet(final GL2 gl, FinSet finSet, Surface which) { @@ -50,7 +50,7 @@ public class FinRenderer { GLUtessellatorCallback cb = new GLUtessellatorCallbackAdapter() { @Override public void vertex(Object vertexData) { - double d[] = (double[]) vertexData; + double[] d = (double[]) vertexData; gl.glTexCoord2d(d[0], d[1]); gl.glVertex3dv(d, 0); } @@ -64,69 +64,82 @@ public class FinRenderer { public void end() { gl.glEnd(); } + + @Override + public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) { + double[] vertex = new double[3]; + vertex[0] = coords[0]; + vertex[1] = coords[1]; + vertex[2] = coords[2]; + outData[0] = vertex; + } }; - GLU.gluTessCallback(tobj, GLU.GLU_TESS_VERTEX, cb); - GLU.gluTessCallback(tobj, GLU.GLU_TESS_BEGIN, cb); - GLU.gluTessCallback(tobj, GLU.GLU_TESS_END, cb); - + GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX, cb); + GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN, cb); + GLU.gluTessCallback(tess, GLU.GLU_TESS_END, cb); + GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE, cb); + // fin side: +z if (finSet.getSpan() > 0 && finSet.getLength() > 0 && which == Surface.INSIDE) { // Right side - GLU.gluTessBeginPolygon(tobj, null); - GLU.gluTessBeginContour(tobj); + GLU.gluTessBeginPolygon(tess, null); + GLU.gluTessBeginContour(tess); gl.glNormal3f(0, 0, 1); for (int i = finPoints.length - 1; i >= 0; i--) { Coordinate c = finPoints[i]; double[] p = new double[]{c.x, c.y + finSet.getBodyRadius(), c.z + finSet.getThickness() / 2.0}; - GLU.gluTessVertex(tobj, p, 0, p); + GLU.gluTessVertex(tess, p, 0, p); } - GLU.gluTessEndContour(tobj); - GLU.gluTessEndPolygon(tobj); + GLU.gluTessEndContour(tess); + GLU.gluTessEndPolygon(tess); } // tab side: +z if (finSet.getTabHeight() > 0 && finSet.getTabLength() > 0 && which == Surface.INSIDE) { // Right side - GLU.gluTessBeginPolygon(tobj, null); - GLU.gluTessBeginContour(tobj); + GLU.gluTessBeginPolygon(tess, null); + GLU.gluTessBeginContour(tess); gl.glNormal3f(0, 0, 1); for (int i = tabPoints.length - 1; i >= 0; i--) { Coordinate c = tabPoints[i]; double[] p = new double[]{c.x, c.y + finSet.getBodyRadius(), c.z + finSet.getThickness() / 2.0}; - GLU.gluTessVertex(tobj, p, 0, p); + GLU.gluTessVertex(tess, p, 0, p); } - GLU.gluTessEndContour(tobj); - GLU.gluTessEndPolygon(tobj); + GLU.gluTessEndContour(tess); + GLU.gluTessEndPolygon(tess); } // fin side: -z if (finSet.getSpan() > 0 && finSet.getLength() > 0 && which == Surface.OUTSIDE) { // Left side - GLU.gluTessBeginPolygon(tobj, null); - GLU.gluTessBeginContour(tobj); + GLU.gluTessBeginPolygon(tess, null); + GLU.gluTessBeginContour(tess); gl.glNormal3f(0, 0, -1); for (Coordinate c : finPoints) { double[] p = new double[]{c.x, c.y + finSet.getBodyRadius(), c.z - finSet.getThickness() / 2.0}; - GLU.gluTessVertex(tobj, p, 0, p); + GLU.gluTessVertex(tess, p, 0, p); } - GLU.gluTessEndContour(tobj); - GLU.gluTessEndPolygon(tobj); + GLU.gluTessEndContour(tess); + GLU.gluTessEndPolygon(tess); } // tab side: -z if (finSet.getTabHeight() > 0 && finSet.getTabLength() > 0 && which == Surface.OUTSIDE) { // Left side - GLU.gluTessBeginPolygon(tobj, null); - GLU.gluTessBeginContour(tobj); + GLU.gluTessBeginPolygon(tess, null); + GLU.gluTessBeginContour(tess); gl.glNormal3f(0, 0, -1); for (Coordinate c : tabPoints) { double[] p = new double[]{c.x, c.y + finSet.getBodyRadius(), c.z - finSet.getThickness() / 2.0}; - GLU.gluTessVertex(tobj, p, 0, p); + GLU.gluTessVertex(tess, p, 0, p); } - GLU.gluTessEndContour(tobj); - GLU.gluTessEndPolygon(tobj); + GLU.gluTessEndContour(tess); + GLU.gluTessEndPolygon(tess); } + + // delete tessellator after processing + GLU.gluDeleteTess(tess); // Fin strip around the edge if (finSet.getSpan() > 0 && finSet.getLength() > 0 && which == Surface.EDGES) { diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index de604e585..8134f1fb7 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -1,6 +1,8 @@ package net.sf.openrocket.gui.main; -import java.awt.*; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; @@ -20,7 +22,25 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; -import javax.swing.*; +import javax.swing.Action; +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +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.JPopupMenu; +import javax.swing.JSpinner; +import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import javax.swing.event.ChangeEvent; import javax.swing.tree.DefaultTreeSelectionModel; @@ -259,11 +279,13 @@ public class BasicFrame extends JFrame { if( componentSelectionModel.isSelectionEmpty() ){ final Rocket rocket = document.getRocket(); if( rocket != null ) { - final AxialStage topStage = (AxialStage) rocket.getChild(0); + final RocketComponent topStage = rocket.getChild(0); if (topStage != null) { final TreePath selectionPath = new TreePath(topStage); componentSelectionModel.setSelectionPath(selectionPath); tree.setSelectionRow(1); + // Don't select children components at startup (so override the default behavior with this new selection) + rocketpanel.getFigure().setSelection(new RocketComponent[] { topStage }); log.debug("... Setting Initial Selection: " + tree.getSelectionPath() ); } } diff --git a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java index 9795207e7..dcb300261 100644 --- a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java @@ -69,36 +69,30 @@ public class DesignPanel extends JSplitPane { im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, SHORTCUT_KEY), null); im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, SHORTCUT_KEY), null); - // Visually select all child components of a stage/rocket/podset when it is selected + // Highlight all child components of a stage/rocket/podset when it is selected tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { - if (tree == null || tree.getSelectionPaths() == null || tree.getSelectionPaths().length == 0 - || parent.getRocketPanel() == null) return; - - // Get all the components that need to be selected = currently selected components + children of stages/boosters/podsets - List children = new ArrayList<>(Arrays.asList(parent.getRocketPanel().getFigure().getSelection())); - for (TreePath p : tree.getSelectionPaths()) { - if (p != null) { - RocketComponent c = (RocketComponent) p.getLastPathComponent(); - if (c instanceof AxialStage || c instanceof Rocket || c instanceof PodSet) { - Iterator iter = c.iterator(false); - while (iter.hasNext()) { - RocketComponent child = iter.next(); - children.add(child); - } - } - } - } - - // Select all the child components - if (parent.getRocketPanel().getFigure() != null && parent.getRocketPanel().getFigure3d() != null) { - parent.getRocketPanel().getFigure().setSelection(children.toArray(new RocketComponent[0])); - parent.getRocketPanel().getFigure3d().setSelection(children.toArray(new RocketComponent[0])); - } + highlightAssemblyChildren(tree, parent); } }); + // Add a mouse listener for when the sustainer is selected at startup, to ensure that its children are highlighted. + // This is necessary because we force the children to not be highlighted when the tree is first created, and + // re-clicking the sustainer would not fire a change event in the tree (which normally highlights the children). + MouseAdapter mouseAdapter = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (tree.getSelectionPath() != null && + tree.getSelectionPath().getLastPathComponent() == document.getRocket().getChild(0)) { + highlightAssemblyChildren(tree, parent); + } + // Delete the listener again. We only need it at start-up, i.e. when the first click is registered. + tree.removeMouseListener(this); + } + }; + tree.addMouseListener(mouseAdapter); + // Double-click opens config dialog MouseListener ml = new MouseAdapter() { @Override @@ -234,6 +228,37 @@ public class DesignPanel extends JSplitPane { this.setRightComponent(panel); } + /** + * Highlight all child components of a stage/rocket/podset when it is selected + * @param tree the tree in which the component selection took place + * @param parent the parent frame to highlight the components in + */ + private static void highlightAssemblyChildren(ComponentTree tree, BasicFrame parent) { + if (tree == null || tree.getSelectionPaths() == null || tree.getSelectionPaths().length == 0 + || parent.getRocketPanel() == null) return; + + // Get all the components that need to be selected = currently selected components + children of stages/boosters/podsets + List children = new ArrayList<>(Arrays.asList(parent.getRocketPanel().getFigure().getSelection())); + for (TreePath p : tree.getSelectionPaths()) { + if (p != null) { + RocketComponent c = (RocketComponent) p.getLastPathComponent(); + if (c instanceof AxialStage || c instanceof Rocket || c instanceof PodSet) { + Iterator iter = c.iterator(false); + while (iter.hasNext()) { + RocketComponent child = iter.next(); + children.add(child); + } + } + } + } + + // Select all the child components + if (parent.getRocketPanel().getFigure() != null && parent.getRocketPanel().getFigure3d() != null) { + parent.getRocketPanel().getFigure().setSelection(children.toArray(new RocketComponent[0])); + parent.getRocketPanel().getFigure3d().setSelection(children.toArray(new RocketComponent[0])); + } + } + /** * Focus on the component tree. */ diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java index 04c657986..abc649ddd 100644 --- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -208,13 +208,10 @@ public class SimulationPanel extends JPanel { } // Show context menu else if (e.getButton() == MouseEvent.BUTTON3 && e.getClickCount() == 1) { - // Get the row that the right-click action happened on - int r = simulationTable.rowAtPoint(e.getPoint()); - // Select new row - if (!simulationTable.isRowSelected(r)) { - if (r >= 0 && r < simulationTable.getRowCount()) { - simulationTable.setRowSelectionInterval(r, r); + if (!simulationTable.isRowSelected(row)) { + if (row >= 0 && row < simulationTable.getRowCount()) { + simulationTable.setRowSelectionInterval(row, row); } else { return; } diff --git a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java index e80cd4735..1714fbd6f 100644 --- a/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java +++ b/swing/src/net/sf/openrocket/gui/plot/SimulationPlot.java @@ -25,7 +25,6 @@ import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.LinearInterpolator; import net.sf.openrocket.utils.DecimalFormatter; diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java index 567495c0c..5a61fd46e 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -5,7 +5,6 @@ import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.Point; -import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; @@ -16,7 +15,6 @@ import java.util.EventListener; import java.util.EventObject; import java.util.List; import java.util.LinkedList; -import java.util.TimerTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java index 7955bdae3..287182c92 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java @@ -209,12 +209,10 @@ public class SimulationEditDialog extends JDialog { @Override public void actionPerformed(ActionEvent e) { copyChangesToAllSims(); - SimulationRunDialog.runSimulations(parentWindow, SimulationEditDialog.this.document, simulationList); - refreshView(); - if (allowsPlotMode()) { + SimulationRunDialog dialog = SimulationRunDialog.runSimulations(parentWindow, SimulationEditDialog.this.document, simulationList); + if (allowsPlotMode() && dialog.isAllSimulationsSuccessful()) { + refreshView(); setPlotMode(); - } else { - setVisible(false); } } }); diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java index b0ca3a4cc..8ff5dd1f1 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationOptionsPanel.java @@ -237,6 +237,7 @@ class SimulationOptionsPanel extends JPanel { SwingSimulationExtensionConfigurator configurator = findConfigurator(e); if (configurator != null) { configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + updateCurrentExtensions(); } } }); @@ -281,6 +282,7 @@ class SimulationOptionsPanel extends JPanel { SwingSimulationExtensionConfigurator configurator = findConfigurator(e); if (configurator != null) { configurator.configure(e, simulation, SwingUtilities.windowForComponent(SimulationOptionsPanel.this)); + updateCurrentExtensions(); } } }); diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java index 44416bf24..63ac79c29 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationRunDialog.java @@ -23,6 +23,8 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; +import net.sf.openrocket.document.events.DocumentChangeEvent; +import net.sf.openrocket.document.events.SimulationChangeEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -207,9 +209,12 @@ public class SimulationRunDialog extends JDialog { * the parent Window of the dialog to use. * @param simulations * the simulations to run. + * @return the simulation run dialog instance. */ - public static void runSimulations(Window parent, OpenRocketDocument document, Simulation... simulations) { - new SimulationRunDialog(parent, document, simulations).setVisible(true); + public static SimulationRunDialog runSimulations(Window parent, OpenRocketDocument document, Simulation... simulations) { + SimulationRunDialog dialog = new SimulationRunDialog(parent, document, simulations); + dialog.setVisible(true); + return dialog; } private void updateProgress() { @@ -257,6 +262,18 @@ public class SimulationRunDialog extends JDialog { + u.toStringUnit(simulationMaxVelocity[index]) + ")"); } + /** + * Returns true if all the simulations ran successfully. Returns false if the simulations encountered + * an exception, or were cancelled. + */ + public boolean isAllSimulationsSuccessful() { + for (SimulationWorker w : simulationWorkers) { + if (w.getThrowable() != null) + return false; + } + return true; + } + /** * A SwingWorker that performs a flight simulation. It periodically updates * the simulation statuses of the parent class and calls updateProgress(). @@ -273,6 +290,7 @@ public class SimulationRunDialog extends JDialog { private volatile double apogeeAltitude; private final CustomExpressionSimulationListener exprListener; + private final OpenRocketDocument document; /* * Keep track of current phase ("stage") of simulation @@ -287,7 +305,8 @@ public class SimulationRunDialog extends JDialog { public InteractiveSimulationWorker(OpenRocketDocument doc, Simulation sim, int index) { super(sim); - List exprs = doc.getCustomExpressions(); + this.document = doc; + List exprs = document.getCustomExpressions(); exprListener = new CustomExpressionSimulationListener(exprs); this.index = index; @@ -389,6 +408,7 @@ public class SimulationRunDialog extends JDialog { log.debug("Simulation done"); setSimulationProgress(1.0); updateProgress(); + document.fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); } /** diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationWorker.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationWorker.java index 201b4f22e..1fb6b682b 100644 --- a/swing/src/net/sf/openrocket/gui/simulation/SimulationWorker.java +++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationWorker.java @@ -70,6 +70,17 @@ public abstract class SimulationWorker extends SwingWorker { - + private JavaCode extension; + private JTextField classNameField; + private StyledLabel errorMsg; + + private static final Translator trans = Application.getTranslator(); + public JavaCodeConfigurator() { super(JavaCode.class); } @Override protected JComponent getConfigurationComponent(final JavaCode extension, Simulation simulation, JPanel panel) { + this.extension = extension; panel.add(new JLabel(trans.get("SimulationExtension.javacode.desc")), "wrap para"); panel.add(new JLabel(trans.get("SimulationExtension.javacode.className")), "wrap rel"); - final JTextField textField = new JTextField(extension.getClassName()); - textField.getDocument().addDocumentListener(new DocumentListener() { + classNameField = new JTextField(extension.getClassName()); + panel.add(classNameField, "growx, wrap"); + this.errorMsg = new StyledLabel(); + errorMsg.setFontColor(Color.DARK_RED.toAWTColor()); + errorMsg.setVisible(false); + panel.add(errorMsg, "growx, wrap"); + + classNameField.getDocument().addDocumentListener(new DocumentListener() { public void changedUpdate(DocumentEvent e) { update(); } @@ -37,11 +53,41 @@ public class JavaCodeConfigurator extends AbstractSwingSimulationExtensionConfig } public void update() { - extension.setClassName(textField.getText()); + updateErrorMsg(); } }); - panel.add(textField, "growx"); + updateErrorMsg(); + return panel; } + + private void updateErrorMsg() { + if (this.errorMsg == null) { + return; + } + // Display error message if the class name is invalid + String text = classNameField.getText().trim(); + try { + Class.forName(text); + errorMsg.setVisible(false); + } catch (ClassNotFoundException e) { + // Don't display an error message for an empty field + if (text.length() == 0) { + errorMsg.setVisible(false); + return; + } + errorMsg.setText(trans.get("SimulationExtension.javacode.classnotfound")); + errorMsg.setVisible(true); + } + } + + @Override + protected void close() { + if (this.extension != null && this.classNameField != null) { + this.extension.setClassName(this.classNameField.getText().trim()); + + } + super.close(); + } }