- Implemented a DampingMoment simulation listener example
authorrichardgraham <richardgraham@180e2498-e6e9-4542-8430-84ac67f01cd8>
Wed, 12 Sep 2012 07:44:13 +0000 (07:44 +0000)
committerrichardgraham <richardgraham@180e2498-e6e9-4542-8430-84ac67f01cd8>
Wed, 12 Sep 2012 07:44:13 +0000 (07:44 +0000)
- Added ability for simulation listeners to reserve their own data types. To support this:
  -- All data types can now be found from just the OpenRocketDocument (reduces some code duplication also).
  -- Custom expressions rebuilt after loading from file in case they use a listeners reserved type
- Fixed (possibly unrelated) issue where datatypes would be deleted and re-made each step if a customexpression used a range or index subexpression

git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@1021 180e2498-e6e9-4542-8430-84ac67f01cd8

12 files changed:
core/src/net/sf/openrocket/document/OpenRocketDocument.java
core/src/net/sf/openrocket/file/openrocket/importt/OpenRocketLoader.java
core/src/net/sf/openrocket/simulation/FlightDataType.java
core/src/net/sf/openrocket/simulation/customexpression/CustomExpression.java
core/src/net/sf/openrocket/simulation/customexpression/CustomExpressionSimulationListener.java
core/src/net/sf/openrocket/simulation/customexpression/IndexExpression.java
core/src/net/sf/openrocket/simulation/customexpression/RangeExpression.java
core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java
core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java
core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java
core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java
core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java [new file with mode: 0644]

index 06422181417b3090d8e2b51e2043461f5614ab7a..12535220b75bcfecc4f983b98b101763d85f6b71 100644 (file)
@@ -1,8 +1,12 @@
 package net.sf.openrocket.document;
 
 import java.io.File;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import net.sf.openrocket.document.events.DocumentChangeEvent;
 import net.sf.openrocket.document.events.DocumentChangeListener;
@@ -13,7 +17,10 @@ import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
 import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
 import net.sf.openrocket.rocketcomponent.Configuration;
 import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.customexpression.CustomExpression;
+import net.sf.openrocket.simulation.exception.SimulationListenerException;
+import net.sf.openrocket.simulation.listeners.SimulationListener;
 import net.sf.openrocket.startup.Application;
 import net.sf.openrocket.util.ArrayList;
 
@@ -121,6 +128,43 @@ public class OpenRocketDocument implements ComponentChangeListener {
                return customExpressions;
        }
        
+       /*
+        * Returns a set of all the flight data types defined or available in any way in the rocket document
+        */
+       public Set<FlightDataType> getFlightDataTypes(){
+               Set<FlightDataType> allTypes = new LinkedHashSet<FlightDataType>();
+               
+               // built in
+               Collections.addAll(allTypes, FlightDataType.ALL_TYPES);
+               
+               // custom expressions
+               for (CustomExpression exp : customExpressions){
+                       allTypes.add(exp.getType());
+               }
+               
+               // simulation listeners
+               for (Simulation sim : simulations){
+                       for (String className : sim.getSimulationListeners()) {
+                               SimulationListener l = null;
+                               try {
+                                       Class<?> c = Class.forName(className);
+                                       l = (SimulationListener) c.newInstance();
+                                       
+                                       Collections.addAll(allTypes, l.getFlightDataTypes());
+                                       //System.out.println("This document has expression datatype from "+l.getName());
+                               } catch (Exception e) {
+                                       log.error("Could not instantiate listener: " + className);
+                               }
+                       }                       
+               }
+               
+               // imported data
+               /// not implemented yet
+               
+               
+               return allTypes;
+       }
+       
 
        public Rocket getRocket() {
                return rocket;
index f4c615a1b1e68bc26c1373f88779d5df0e7f9b8c..4efdf9dbd5f962b15ce2bccbec136884c2796d63 100644 (file)
@@ -704,8 +704,6 @@ class OpenRocketContentHandler extends AbstractElementHandler {
        }
 }
 
-
-
 class DatatypeHandler extends AbstractElementHandler {
        private final DocumentLoadingContext context;
        private final OpenRocketContentHandler contentHandler;
@@ -1288,10 +1286,14 @@ class SimulationsHandler extends AbstractElementHandler {
        public void closeElement(String element, HashMap<String, String> attributes,
                        String content, WarningSet warnings) throws SAXException {
                attributes.remove("status");
+               
+               //Finished loading. Rebuilding custom expressions in case something has changed such as listener variable come available.
+               for (CustomExpression exp : doc.getCustomExpressions()){
+                       exp.setExpression(exp.getExpressionString());
+               }
+               
                super.closeElement(element, attributes, content, warnings);
        }
-       
-       
 }
 
 class SingleSimulationHandler extends AbstractElementHandler {
index 251d85d6c9b2b6e47a5fc8b82905660f55d5b13e..adb4b51bc88eae89612fce7ed2df8b03833b89b5 100644 (file)
@@ -269,8 +269,8 @@ public class FlightDataType implements Comparable<FlightDataType> {
                if (type != null) {
                        // found it from symbol
                        
-                       // if name was not give (empty string), can use the one we found name
-                       if ( s.equals("") || s == null ){
+                       // if name was not given (empty string), can use the one we found
+                       if ( s == null || s.isEmpty()){
                                s = type.getName();
                        }
                        if ( u == null ){
@@ -279,14 +279,19 @@ public class FlightDataType implements Comparable<FlightDataType> {
                        
                        // if something has changed, then we need to remove the old one
                        // otherwise, just return what we found
-                       if ( !u.equals(type.getUnitGroup()) ||
-                                !s.equals(type.getName())
-                               )
+                       if ( !u.equals(type.getUnitGroup()) )
                           {
                                oldPriority = type.priority;
-                               
                                EXISTING_TYPES.remove(type);
-                               log.info("Something changed with the type "+type.getName()+", removed old version.");
+                               log.info("Unitgroup of type "+type.getName() + 
+                                                ", has changed from "+type.getUnitGroup().toString() + 
+                                                " to "+u.toString() +
+                                                ". Removing old version.");
+                       }
+                       else if (!s.equals(type.getName())) {
+                               oldPriority = type.priority;
+                               EXISTING_TYPES.remove(type);
+                               log.info("Name of type "+type.getName()+", has changed to "+s+". Removing old version.");
                        }
                        else{
                                return type;
index 47a5dff1fcb98ed3eef8261910006e6d15e009cb..7102b55be9f24e3091e3c4a43c64c657d9b864de 100644 (file)
@@ -34,11 +34,12 @@ public class CustomExpression implements Cloneable{
        private List<CustomExpression> subExpressions = new ArrayList<CustomExpression>();
        
        public CustomExpression(OpenRocketDocument doc){
+               this.doc = doc;
+               
                setName("");
                setSymbol("");
                setUnit("");
                setExpression("");
-               this.doc = doc;
        }
        
        public CustomExpression(OpenRocketDocument doc, 
@@ -171,6 +172,7 @@ public class CustomExpression implements Cloneable{
        // get a list of all the names of all the available variables
        protected ArrayList<String> getAllNames(){
                ArrayList<String> names = new ArrayList<String>();
+               /*
                for (FlightDataType type : FlightDataType.ALL_TYPES)
                        names.add(type.getName());
 
@@ -181,12 +183,22 @@ public class CustomExpression implements Cloneable{
                                        names.add(exp.getName());
                        }
                }
+               */
+               for (FlightDataType type : doc.getFlightDataTypes()){
+                       String symb = type.getName();
+                       if (name == null) continue;
+                       
+                       if (!name.equals( this.getName() )){
+                               names.add(symb);
+                       }
+               }
                return names;
        }
        
        // get a list of all the symbols of the available variables ignoring this one
        protected ArrayList<String> getAllSymbols(){
                ArrayList<String> symbols = new ArrayList<String>();
+               /*
                for (FlightDataType type : FlightDataType.ALL_TYPES)
                        symbols.add(type.getSymbol());
                
@@ -196,6 +208,14 @@ public class CustomExpression implements Cloneable{
                                        symbols.add(exp.getSymbol());
                        }
                }
+               */
+               for (FlightDataType type : doc.getFlightDataTypes()){
+                       String symb = type.getSymbol();                 
+                       if (!symb.equals( this.getSymbol() )){
+                               symbols.add(symb);
+                       }
+               }
+               
                return symbols;
        }
        
@@ -305,6 +325,7 @@ public class CustomExpression implements Cloneable{
                
                //// Define the available variables as empty
                // The built in data types
+               /*
                for (FlightDataType type : FlightDataType.ALL_TYPES){
                        builder.withVariable(new Variable(type.getSymbol()));
                }
@@ -312,12 +333,16 @@ public class CustomExpression implements Cloneable{
                for (String symb : getAllSymbols()){
                        builder.withVariable(new Variable(symb));
                }
+               */
+               for (FlightDataType type : doc.getFlightDataTypes()){
+                       builder.withVariable(new Variable(type.getSymbol()));
+               }
                
                // Try to build
                try {
                        builder.build();
                } catch (Exception e) {
-                       log.user("Custom expression invalid : " + e.toString());
+                       log.user("Custom expression " + this.toString() + " invalid : " + e.toString());
                        return false;
                }
                
@@ -341,14 +366,14 @@ public class CustomExpression implements Cloneable{
         * Builds a specified expression, log any errors and returns null in case of error.
         */
        protected Calculable buildExpression(ExpressionBuilder b){
-               Calculable calc;
+               Calculable calc = null;
                try {
                        calc = b.build();
                } catch (UnknownFunctionException e1) {
-                       log.user("Unknown function. Could not build custom expression "+name);
+                       log.user("Unknown function. Could not build custom expression "+this.toString());
                        return null;
                } catch (UnparsableExpressionException e1) {
-                       log.user("Unparsable expression. Could not build custom expression "+name+". "+e1.getMessage());
+                       log.user("Unparsable expression. Could not build custom expression "+this.toString()+". "+e1.getMessage());
                        return null;
                }
                
@@ -396,10 +421,13 @@ public class CustomExpression implements Cloneable{
         */
        public FlightDataType getType(){
                
+               
                UnitGroup ug = UnitGroup.SIUNITS.get(unit); 
                if ( ug == null ){
+                       log.debug("SI unit not found for "+unit+" in expression "+toString()+". Making a new fixed unit.");
                        ug = new FixedUnitGroup(unit);
                }
+               //UnitGroup ug = new FixedUnitGroup(unit);
                
                FlightDataType type = FlightDataType.getType(name, symbol, ug);
                
@@ -442,7 +470,7 @@ public class CustomExpression implements Cloneable{
        
        @Override
        public String toString(){
-               return "Custom expression : "+this.name.toString()+ " " + this.expression.toString();
+               return "[Expression name="+this.name.toString()+ " expression=" + this.expression+" unit="+this.unit+"]";
        }
        
        @Override
index d1ce6e97348bcad4861f4085116d61a9d555f7f6..243f7b1553f8976a023333fdf95d0c02a351020b 100644 (file)
@@ -3,13 +3,16 @@ package net.sf.openrocket.simulation.customexpression;
 import java.util.ArrayList;
 import java.util.List;
 
+import net.sf.openrocket.logging.LogHelper;
 import net.sf.openrocket.simulation.FlightDataBranch;
 import net.sf.openrocket.simulation.SimulationStatus;
 import net.sf.openrocket.simulation.exception.SimulationException;
 import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.startup.Application;
 
 public class CustomExpressionSimulationListener extends        AbstractSimulationListener {
 
+       private static final LogHelper log = Application.getLogger();
        private final List<CustomExpression> expressions;
        
        public CustomExpressionSimulationListener(List<CustomExpression> expressions) {
@@ -25,10 +28,17 @@ public class CustomExpressionSimulationListener extends     AbstractSimulationListen
                // Calculate values for custom expressions
                FlightDataBranch data = status.getFlightData();
                for (CustomExpression expression : expressions ) {
-                       data.setValue(expression.getType(), expression.evaluateDouble(status));
+                       double value = expression.evaluateDouble(status);
+                       //log.debug("Setting value of custom expression "+expression.toString()+" = "+value);
+                       data.setValue(expression.getType(), value);
                }
                
 
        }
 
+       @Override
+       public boolean isSystemListener(){
+               return true;
+       }
+       
 }
index ab7e1572cb32c379e9118d773a3ec4b6d9d8886b..6fb084de4154f18ab2b66436acf8334c497bf137 100644 (file)
@@ -23,7 +23,6 @@ public class IndexExpression extends CustomExpression {
                setExpression(indexText);
                this.setName("");
                this.setSymbol(typeText);
-               
        }
        
        @Override
@@ -35,7 +34,10 @@ public class IndexExpression extends CustomExpression {
                }
                
                // From the given datatype, get the time and function values and make an interpolator
-               FlightDataType type = getType();
+
+               //Note: must get in a way that flight data system will figure out units. Otherwise there will be a type conflict when we get the new data.
+               FlightDataType type = FlightDataType.getType(null, getSymbol(), null);  
+                               
                List<Double> data = status.getFlightData().get(type);
                List<Double> time = status.getFlightData().get(FlightDataType.TYPE_TIME);
                LinearInterpolator interp = new LinearInterpolator(time, data); 
index 83b5e6c506dce15c80891c087c3c005cdef1eff3..1667b61e9a1695fee2ea4df06410836f20539bdb 100644 (file)
@@ -40,7 +40,6 @@ public class RangeExpression extends CustomExpression {
                this.expression = variableType+startTime+endTime; // this is used just for generating the hash
                
                log.info("New range expression, "+startTime + " to "+endTime);
-               
        }
        
        /*
@@ -73,7 +72,9 @@ public class RangeExpression extends CustomExpression {
                }               
                
                // From the given datatype, get the time and function values and make an interpolator
-               FlightDataType type = getType();
+
+               //Note: must get in a way that flight data system will figure out units. Otherwise there will be a type conflict when we get the new data.
+               FlightDataType type = FlightDataType.getType(null, getSymbol(), null);
                
                List<Double> data = status.getFlightData().get(type);
                List<Double> time = status.getFlightData().get(FlightDataType.TYPE_TIME);
index a3a3b19279626a47161bac3f023faa9fba62617f..3f156f38ee392c9e9dcf793ed624fc517affa4a3 100644 (file)
@@ -1,5 +1,7 @@
 package net.sf.openrocket.simulation.listeners;
 
+import java.util.List;
+
 import net.sf.openrocket.aerodynamics.AerodynamicForces;
 import net.sf.openrocket.aerodynamics.FlightConditions;
 import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
@@ -8,6 +10,7 @@ import net.sf.openrocket.motor.MotorInstance;
 import net.sf.openrocket.rocketcomponent.MotorMount;
 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
 import net.sf.openrocket.simulation.AccelerationData;
+import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.FlightEvent;
 import net.sf.openrocket.simulation.MassData;
 import net.sf.openrocket.simulation.SimulationStatus;
@@ -67,7 +70,13 @@ public class AbstractSimulationListener implements SimulationListener, Simulatio
                return false;
        }
        
-       
+       /**
+        * Return an array of any flight data types this listener creates.
+        */
+       @Override
+       public FlightDataType[] getFlightDataTypes(){
+               return new FlightDataType[] {};
+       }
        
        
        ////  SimulationEventListener  ////
index be46c50f8e747fcb33823f618103e648401919d9..3089d053938707430b2adc58c9ea16751cf99b2e 100644 (file)
@@ -1,9 +1,12 @@
 package net.sf.openrocket.simulation.listeners;
 
+import java.util.List;
+
 import net.sf.openrocket.aerodynamics.AerodynamicForces;
 import net.sf.openrocket.aerodynamics.FlightConditions;
 import net.sf.openrocket.models.atmosphere.AtmosphericConditions;
 import net.sf.openrocket.simulation.AccelerationData;
+import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.MassData;
 import net.sf.openrocket.simulation.SimulationStatus;
 import net.sf.openrocket.simulation.exception.SimulationException;
@@ -64,4 +67,5 @@ public interface SimulationComputationListener extends SimulationListener {
        
        public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException;
 
+       public FlightDataType[] getFlightDataTypes();
 }
index 6cbb87f3ba23d91cfca35965fe06dd77678ef4cf..8ac0e11a73877a0b0b67ae9af61b67a5e2ce8665 100644 (file)
@@ -1,9 +1,12 @@
 package net.sf.openrocket.simulation.listeners;
 
+import java.util.List;
+
 import net.sf.openrocket.motor.MotorId;
 import net.sf.openrocket.motor.MotorInstance;
 import net.sf.openrocket.rocketcomponent.MotorMount;
 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.FlightEvent;
 import net.sf.openrocket.simulation.SimulationStatus;
 import net.sf.openrocket.simulation.exception.SimulationException;
@@ -56,6 +59,10 @@ public interface SimulationEventListener {
         */
        public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice)
                        throws SimulationException;
+
+
+
+       public FlightDataType[] getFlightDataTypes();
        
 
 }
index d5663ccbdc05441112f4bbcfed37627c23d0f4c9..322db8e714d96a60ab83bfd65ab8e5c6bf1b0486 100644 (file)
@@ -1,5 +1,8 @@
 package net.sf.openrocket.simulation.listeners;
 
+import java.util.List;
+
+import net.sf.openrocket.simulation.FlightDataType;
 import net.sf.openrocket.simulation.SimulationStatus;
 import net.sf.openrocket.simulation.exception.SimulationException;
 
@@ -76,4 +79,11 @@ public interface SimulationListener {
         */
        public boolean isSystemListener();
        
+       
+       /**
+        * Return a list of any flight data types this listener creates.
+        */
+       public FlightDataType[] getFlightDataTypes();
+       
+       
 }
diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java b/core/src/net/sf/openrocket/simulation/listeners/example/DampingMoment.java
new file mode 100644 (file)
index 0000000..b9d2e46
--- /dev/null
@@ -0,0 +1,129 @@
+package net.sf.openrocket.simulation.listeners.example;
+
+import java.util.List;
+import java.util.Map;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.motor.MotorId;
+import net.sf.openrocket.motor.MotorInstance;
+import net.sf.openrocket.motor.MotorInstanceConfiguration;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightDataType;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ArrayList;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.PolyInterpolator;
+
+public class DampingMoment extends AbstractSimulationListener {
+       
+       private static final FlightDataType type = FlightDataType.getType("Damping moment coefficient", "Cdm", UnitGroup.UNITS_COEFFICIENT);
+       private static final FlightDataType[] typeList = {type};
+       
+       public String getName(){
+               return "Damping moment listener";
+       }
+       
+       /**
+        * Return a list of any flight data types this listener creates.
+        */
+       public FlightDataType[] getFlightDataTypes(){
+               return typeList;
+       }
+       
+       @Override
+       public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException {
+               
+               // Save it as a flightdatatype
+               
+               //status.getFlightData().setValue(type, aerodynamicPart + propulsivePart);
+               status.getFlightData().setValue(type, calculate(status, flightConditions));
+
+               return flightConditions;
+       }
+       
+       private double calculate(SimulationStatus status, FlightConditions flightConditions){
+               
+               // Work out the propulsive/jet damping part of the moment.
+               
+               // dm/dt = (thrust - ma)/v
+               FlightDataBranch data = status.getFlightData();
+               
+               List<Double> mpAll = data.get(FlightDataType.TYPE_PROPELLANT_MASS);
+               List<Double> time = data.get(FlightDataType.TYPE_TIME);
+               if (mpAll == null || time == null){
+                       return Double.NaN;
+               }
+               
+               int len = mpAll.size();
+               
+               // This isn't as accurate as I would like
+               double mdot=Double.NaN;
+               if (len > 2){
+                       // Using polynomial interpolator for derivative. Doesn't help much
+                       //double[] x = { time.get(len-5), time.get(len-4), time.get(len-3), time.get(len-2), time.get(len-1) };
+                       //double[] y = { mpAll.get(len-5), mpAll.get(len-4), mpAll.get(len-3), mpAll.get(len-2), mpAll.get(len-1) };
+                       //PolyInterpolator interp = new PolyInterpolator(x);
+                       //double[] coeff = interp.interpolator(y);
+                       //double dt = .01;
+                       //mdot = (interp.eval(x[4], coeff) - interp.eval(x[4]-dt, coeff))/dt; 
+                                               
+                       mdot = (mpAll.get(len-1) - mpAll.get(len-2)) / (time.get(len-1) - time.get(len-2));
+               }
+               
+               double cg = data.getLast(FlightDataType.TYPE_CG_LOCATION);
+               
+               // find the maximum distance from nose to nozzle. 
+               double nozzleDistance = 0;
+               for (MotorId id: status.getMotorConfiguration().getMotorIDs()){
+                       MotorInstanceConfiguration config = status.getMotorConfiguration();
+                       Coordinate position = config.getMotorPosition(id);
+                       
+                       double x = position.x + config.getMotorInstance(id).getParentMotor().getLength();
+                       if (x > nozzleDistance){
+                               nozzleDistance = x;
+                       }                       
+               }
+               
+               // now can get the propulsive part
+               double propulsivePart = mdot * Math.pow(nozzleDistance - cg, 2);
+               
+               // Work out the aerodynamic part of the moment.
+               double aerodynamicPart = 0;
+               
+               AerodynamicCalculator aerocalc = status.getSimulationConditions().getAerodynamicCalculator();
+               
+               // Must go through each component ...
+               Map<RocketComponent, AerodynamicForces> forces = aerocalc.getForceAnalysis(status.getConfiguration(), flightConditions, null);
+               for (Map.Entry<RocketComponent, AerodynamicForces> entry : forces.entrySet()){
+                       
+                       RocketComponent comp = entry.getKey();
+                       
+                       if (!comp.isAerodynamic()) continue;
+                       
+                       //System.out.println(comp.toString());
+                       
+                       double CNa = entry.getValue().getCNa(); //?
+                       double Cp = entry.getValue().getCP().length();
+                       double z = comp.getPositionValue(); //?
+                       
+                       aerodynamicPart += CNa*Math.pow(z-Cp, 2);
+               }
+               
+               double v = flightConditions.getVelocity();
+               double rho = flightConditions.getAtmosphericConditions().getDensity();
+               double ar = flightConditions.getRefArea();
+               
+               aerodynamicPart = aerodynamicPart * .5 * rho * v * ar;
+               
+               return aerodynamicPart + propulsivePart;
+       
+       }
+       
+}