// XXX Add sanity checks to new flight dialog package com.aerodynelabs.habtk.prediction; import java.awt.Container; import java.awt.Dimension; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Point2D; import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JTextField; import javax.swing.SpringLayout; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.aerodynelabs.habtk.atmosphere.AtmosphereProfile; import com.aerodynelabs.habtk.atmosphere.AtmosphereState; import com.aerodynelabs.habtk.atmosphere.GSDParser; import com.aerodynelabs.habtk.atmosphere.RUCGFS; import com.aerodynelabs.habtk.ui.DateTimePicker; import com.aerodynelabs.map.MapPath; import com.aerodynelabs.map.MapPoint; /** * A basic prediction for standard latex balloons. * Assumptions: * - Single wind definition * - Constant ascent rate * - Constant drag coefficient * - Balloon velocity = Wind velocity * * @author Ethan Harstad * */ public class LatexPredictor extends Predictor { private String balloonName = null; private long startTime; private double startLat, startLon, startAlt; private double groundLevel; private boolean isAscending = true; private double tStep = 15; private double payloadMass, balloonLift; private double parachuteArea, parachuteDrag; private double balloonMass, balloonDrag, burstRad; private MapPoint burst; private MapPoint landing; private static final double rhog = 0.1762; // kg/m^3 (Helium) private static final double rho = 1.276; // kg/m^3 (Air) private static final double R = 8.31432; private static final double MWGas = 0.004002602; private static final double MWAir = 0.0289644; private static final String balloons[] = {"Kaymont 200", "Kaymont 300", "Kaymont 350", "Kaymont 600", "Kaymont 800", "Kaymont 1000", "Kaymont 1200", "Kaymont 1500", "Kaymont 2000", "Kaymont 3000"}; private static final double balloonData[][] = { {0.2, 1.524, 0.25}, // Kaymont 200 {0.3, 1.981, 0.25}, // Kaymont 300 {0.35, 2.134, 0.25}, // Kaymont 350 {0.6, 3.048, 0.3}, // Kaymont 600 {0.8, 3.505, 0.3}, // Kaymont 800 {1.0, 3.962, 0.3}, // Kaymont 1000 {1.2, 4.267, 0.25}, // Kaymont 1200 {1.5, 4.724, 0.25}, // Kaymont 1500 {2.0, 5.334, 0.25}, // Kaymont 2000 {3.0, 6.553, 0.25}, // Kaymont 3000 }; /** * Dialog to chose prediction settings */ @SuppressWarnings("serial") class SetupDialog extends JDialog { boolean accepted = false; JTextField fStartLat, fStartLon, fStartAlt, fStartTime; JTextField fMass, fLift, fArea, fDrag; JComboBox<String> fBalloon; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm"); public SetupDialog() { setTitle("Setup Flight"); setModal(true); SpringLayout layout = new SpringLayout(); setLayout(layout); Container pane = getContentPane(); sdf.setTimeZone(TimeZone.getTimeZone("GMT")); JLabel lStartTime = new JLabel("Launch Time UTC:"); fStartTime = new JTextField(12); if(startTime > 0) { fStartTime.setText(sdf.format(new Date(startTime * 1000))); } else { fStartTime.setText(sdf.format(new Date())); } JButton bStartTime = new JButton("Calendar"); bStartTime.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { DateTimePicker picker; try { picker = new DateTimePicker(DateTimePicker.DATETIME, sdf.parse(fStartTime.getText())); } catch (ParseException e1) { picker = new DateTimePicker(DateTimePicker.DATETIME); } if(!picker.wasAccepted()) return; Date date = picker.getValue(); if(date != null) fStartTime.setText(sdf.format(date)); } }); layout.putConstraint(SpringLayout.WEST, lStartTime, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.NORTH, lStartTime, 6, SpringLayout.NORTH, pane); layout.putConstraint(SpringLayout.WEST, fStartTime, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.NORTH, fStartTime, 6, SpringLayout.SOUTH, lStartTime); layout.putConstraint(SpringLayout.WEST, bStartTime, 6, SpringLayout.EAST, fStartTime); layout.putConstraint(SpringLayout.NORTH, bStartTime, 0, SpringLayout.NORTH, fStartTime); layout.putConstraint(SpringLayout.EAST, bStartTime, -6, SpringLayout.EAST, getContentPane()); layout.putConstraint(SpringLayout.SOUTH, bStartTime, 0, SpringLayout.SOUTH, fStartTime); JLabel lStartLat = new JLabel("Start Latitude:"); fStartLat = new JTextField(10); if(startLat != 0.0d) { fStartLat.setText(Double.toString(startLat)); } else { fStartLat.setText("42.0000"); } JLabel lStartLon = new JLabel("Start Longitude:"); fStartLon = new JTextField(10); if(startLon != 0.0d) { fStartLon.setText(Double.toString(startLon)); } else { fStartLon.setText("-93.6350"); } layout.putConstraint(SpringLayout.NORTH, lStartLat, 6, SpringLayout.SOUTH, fStartTime); layout.putConstraint(SpringLayout.NORTH, lStartLon, 6, SpringLayout.SOUTH, fStartTime); layout.putConstraint(SpringLayout.WEST, lStartLat, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, lStartLon, 6, SpringLayout.WEST, fStartLon); layout.putConstraint(SpringLayout.EAST, lStartLon, -6, SpringLayout.EAST, pane); layout.putConstraint(SpringLayout.NORTH, fStartLat, 6, SpringLayout.SOUTH, lStartLat); layout.putConstraint(SpringLayout.NORTH, fStartLon, 6, SpringLayout.SOUTH, lStartLon); layout.putConstraint(SpringLayout.WEST, fStartLat, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fStartLon, 6, SpringLayout.EAST, fStartLat); layout.putConstraint(SpringLayout.EAST, pane, 6, SpringLayout.EAST, fStartLon); JLabel lStartAlt = new JLabel("Start Altitude (m):"); fStartAlt = new JTextField(10); if(startAlt > 0) { fStartAlt.setText(Double.toString(startAlt)); } else { fStartAlt.setText("297.5"); } layout.putConstraint(SpringLayout.NORTH, fStartAlt, 6, SpringLayout.SOUTH, fStartLat); layout.putConstraint(SpringLayout.BASELINE, lStartAlt, 0, SpringLayout.BASELINE, fStartAlt); layout.putConstraint(SpringLayout.WEST, lStartAlt, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fStartAlt, 6, SpringLayout.EAST, lStartAlt); layout.putConstraint(SpringLayout.EAST, fStartAlt, -6, SpringLayout.EAST, pane); JLabel lBalloon = new JLabel("Balloon:"); fBalloon = new JComboBox<>(balloons); if(balloonName != null) { fBalloon.setSelectedItem(balloonName); } layout.putConstraint(SpringLayout.NORTH, fBalloon, 6, SpringLayout.SOUTH, fStartAlt); layout.putConstraint(SpringLayout.BASELINE, lBalloon, 0, SpringLayout.BASELINE, fBalloon); layout.putConstraint(SpringLayout.WEST, lBalloon, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fBalloon, 6, SpringLayout.EAST, lBalloon); layout.putConstraint(SpringLayout.EAST, fBalloon, -6, SpringLayout.EAST, pane); JLabel lLift = new JLabel("Neck Lift (kg):"); fLift = new JTextField(); if(balloonLift > 0) fLift.setText(Double.toString(balloonLift)); layout.putConstraint(SpringLayout.NORTH, fLift, 6, SpringLayout.SOUTH, fBalloon); layout.putConstraint(SpringLayout.BASELINE, lLift, 0, SpringLayout.BASELINE, fLift); layout.putConstraint(SpringLayout.WEST, lLift, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fLift, 6, SpringLayout.EAST, lLift); layout.putConstraint(SpringLayout.EAST, fLift, -6, SpringLayout.EAST, pane); JLabel lMass = new JLabel("Payload Mass (kg):"); fMass = new JTextField(); if(payloadMass > 0) fMass.setText(Double.toString(payloadMass)); layout.putConstraint(SpringLayout.NORTH, fMass, 6, SpringLayout.SOUTH, fLift); layout.putConstraint(SpringLayout.BASELINE, lMass, 0, SpringLayout.BASELINE, fMass); layout.putConstraint(SpringLayout.WEST, lMass, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fMass, 6, SpringLayout.EAST, lMass); layout.putConstraint(SpringLayout.EAST, fMass, -6, SpringLayout.EAST, pane); JLabel lArea = new JLabel("Chute Area (m^2):"); fArea = new JTextField(); if(parachuteArea > 0) fArea.setText(Double.toString(parachuteArea)); layout.putConstraint(SpringLayout.NORTH, fArea, 6, SpringLayout.SOUTH, fMass); layout.putConstraint(SpringLayout.BASELINE, lArea, 0, SpringLayout.BASELINE, fArea); layout.putConstraint(SpringLayout.WEST, lArea, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fArea, 6, SpringLayout.EAST, lArea); layout.putConstraint(SpringLayout.EAST, fArea, -6, SpringLayout.EAST, pane); JLabel lDrag = new JLabel("Chute Cd:"); fDrag = new JTextField(); if(parachuteDrag > 0) fDrag.setText(Double.toString(parachuteDrag)); layout.putConstraint(SpringLayout.NORTH, fDrag, 6, SpringLayout.SOUTH, fArea); layout.putConstraint(SpringLayout.BASELINE, lDrag, 0, SpringLayout.BASELINE, fDrag); layout.putConstraint(SpringLayout.WEST, lDrag, 6, SpringLayout.WEST, pane); layout.putConstraint(SpringLayout.WEST, fDrag, 6, SpringLayout.EAST, lDrag); layout.putConstraint(SpringLayout.EAST, fDrag, -6, SpringLayout.EAST, pane); JButton cancel = new JButton("Cancel"); cancel.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { accepted = false; dispose(); } }); JButton ok = new JButton("Ok"); ok.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { startLat = Double.parseDouble(fStartLat.getText()); startLon = Double.parseDouble(fStartLon.getText()); startAlt = Double.parseDouble(fStartAlt.getText()); groundLevel = startAlt; startTime = sdf.parse(fStartTime.getText()).getTime() / 1000; payloadMass = Double.parseDouble(fMass.getText()); balloonLift = Double.parseDouble(fLift.getText()); parachuteArea = Double.parseDouble(fArea.getText()); parachuteDrag = Double.parseDouble(fDrag.getText()); balloonName = balloons[fBalloon.getSelectedIndex()]; double bDat[] = balloonData[fBalloon.getSelectedIndex()]; balloonMass = bDat[0]; burstRad = bDat[1]; balloonDrag = bDat[2]; } catch (Exception e1) { e1.printStackTrace(); accepted = false; } accepted = true; dispose(); } }); layout.putConstraint(SpringLayout.EAST, cancel, -6, SpringLayout.EAST, pane); layout.putConstraint(SpringLayout.EAST, ok, -6, SpringLayout.WEST, cancel); layout.putConstraint(SpringLayout.NORTH, cancel, 6, SpringLayout.SOUTH, fDrag); layout.putConstraint(SpringLayout.NORTH, ok, 6, SpringLayout.SOUTH, fDrag); layout.putConstraint(SpringLayout.SOUTH, cancel, -6, SpringLayout.SOUTH, pane); layout.putConstraint(SpringLayout.SOUTH, pane, 6, SpringLayout.SOUTH, ok); add(lStartTime); add(fStartTime); add(bStartTime); add(lStartLat); add(fStartLat); add(lStartLon); add(fStartLon); add(lStartAlt); add(fStartAlt); add(lBalloon); add(fBalloon); add(lMass); add(fMass); add(lLift); add(fLift); add(lDrag); add(fDrag); add(lArea); add(fArea); add(cancel); add(ok); pack(); Dimension screen = Toolkit.getDefaultToolkit().getScreenSize(); setLocation((screen.width - getWidth()) / 2, (screen.height - getHeight()) / 2); setVisible(true); } public boolean wasAccepted() { return accepted; } } /** * Write settings to XML document */ public void write(Document doc) { Element root = doc.createElement("balloonFlight"); doc.appendChild(root); Element predictor = doc.createElement("predictor"); predictor.appendChild(doc.createTextNode("Latex Predictor v1.0")); root.appendChild(predictor); Element startLat = doc.createElement("startLat"); startLat.appendChild(doc.createTextNode(Double.toString(this.startLat))); root.appendChild(startLat); Element startLon = doc.createElement("startLon"); startLon.appendChild(doc.createTextNode(Double.toString(this.startLon))); root.appendChild(startLon); Element startAlt = doc.createElement("startAlt"); startAlt.appendChild(doc.createTextNode(Double.toString(this.startAlt))); root.appendChild(startAlt); Element startTime = doc.createElement("startTime"); startTime.appendChild(doc.createTextNode(String.valueOf(this.startTime))); root.appendChild(startTime); Element balloon = doc.createElement("balloonName"); balloon.appendChild(doc.createTextNode(balloonName)); root.appendChild(balloon); Element lift = doc.createElement("balloonLift"); lift.appendChild(doc.createTextNode(Double.toString(this.balloonLift))); root.appendChild(lift); Element radius = doc.createElement("burstRadius"); radius.appendChild(doc.createTextNode(Double.toString(this.burstRad))); root.appendChild(radius); Element bMass = doc.createElement("balloonMass"); bMass.appendChild(doc.createTextNode(Double.toString(this.balloonMass))); root.appendChild(bMass); Element bDrag = doc.createElement("balloonDrag"); bDrag.appendChild(doc.createTextNode(Double.toString(this.balloonDrag))); root.appendChild(bDrag); Element mass = doc.createElement("payloadMass"); mass.appendChild(doc.createTextNode(Double.toString(this.payloadMass))); root.appendChild(mass); Element drag = doc.createElement("parachuteDrag"); drag.appendChild(doc.createTextNode(Double.toString(this.parachuteDrag))); root.appendChild(drag); Element area = doc.createElement("parachuteArea"); area.appendChild(doc.createTextNode(Double.toString(this.parachuteArea))); root.appendChild(area); } /** * Read settings from XML document */ public boolean read(Document doc) { try { Element root = doc.getDocumentElement(); startLat = Double.parseDouble(root.getElementsByTagName("startLat").item(0).getTextContent()); startLon = Double.parseDouble(root.getElementsByTagName("startLon").item(0).getTextContent()); startAlt = Double.parseDouble(root.getElementsByTagName("startAlt").item(0).getTextContent()); groundLevel = startAlt; startTime = Long.parseLong(root.getElementsByTagName("startTime").item(0).getTextContent()); balloonName = root.getElementsByTagName("balloonName").item(0).getTextContent(); balloonLift = Double.parseDouble(root.getElementsByTagName("balloonLift").item(0).getTextContent()); payloadMass = Double.parseDouble(root.getElementsByTagName("payloadMass").item(0).getTextContent()); parachuteDrag = Double.parseDouble(root.getElementsByTagName("parachuteDrag").item(0).getTextContent()); parachuteArea = Double.parseDouble(root.getElementsByTagName("parachuteArea").item(0).getTextContent()); burstRad = Double.parseDouble(root.getElementsByTagName("burstRadius").item(0).getTextContent()); balloonMass = Double.parseDouble(root.getElementsByTagName("balloonMass").item(0).getTextContent()); balloonDrag = Double.parseDouble(root.getElementsByTagName("balloonDrag").item(0).getTextContent()); } catch(Exception e) { e.printStackTrace(); return false; } return true; } /** * Default constructor */ public LatexPredictor() { } /** * Use given step size for integration. * @param step */ public LatexPredictor(double step) { tStep = step; } /** * Display a dialog to choose settings. */ public boolean setup() { SetupDialog dialog = new SetupDialog(); return dialog.wasAccepted(); } public String toString() { String ret = balloonName + ": " + balloonLift + "kg neck lift"; return ret; } /** * Run prediction from current start point to ground level. */ public MapPath runPrediction() { // Create output variable MapPath path = new MapPath(); // Get atmosphere profile File wind = new RUCGFS().getAtmosphere((int)startTime, startLat, startLon); AtmosphereProfile atmo = new GSDParser().parseAtmosphere(wind); if(atmo == null) return null; // Initial conditions boolean curAscending = isAscending; double cLat = startLat; double cLon = startLon; double cAlt = startAlt; double eTime = 0.0; double dX = 0.0; double dY = 0.0; double volume = (balloonLift + balloonMass) / (rho - rhog); double radius = Math.pow((3.0*volume)/(4*Math.PI), 1.0/3.0); double area = Math.PI*Math.pow(radius, 2.0); double ascentRate = Math.pow(((balloonLift - payloadMass) * 9.81) / (0.5 * rho * balloonDrag * area), 1.0/2.0); // Calculate ascent while(curAscending) { // Solve for motion AtmosphereState state = atmo.getAtAltitude(cAlt); double windX = state.getWindSpeed() * Math.sin(Math.toRadians(state.getWindDirection() + 180.0)); double windY = state.getWindSpeed() * Math.cos(Math.toRadians(state.getWindDirection() + 180.0)); dX += windX * tStep; dY += windY * tStep; cAlt += ascentRate * tStep; eTime += tStep; // Check for burst double cRhoG = (state.getPressure() * MWGas) / (R * (state.getTemperature() + 273.15)); double cRhoA = (state.getPressure() * MWAir) / (R * (state.getTemperature() + 273.15)); double cV = (balloonLift + balloonMass) / (cRhoA - cRhoG); double cR = Math.pow((3.0 * cV) / (4.0 * Math.PI), 1.0 / 3.0); if(cR >= burstRad) curAscending = false; // Convert to lat/lon double range = Math.pow(Math.pow(dX, 2.0) + Math.pow(dY, 2.0), 0.5); // double bearing = Math.atan(dX / dY); double bearing = Math.atan2(dX, dY); Point2D.Double cPos = directGeodesic(new Point2D.Double(startLon, startLat), bearing, range); cLat = cPos.y; cLon = cPos.x; // Store path.add(cLat, cLon, cAlt, startTime + Math.round(eTime)); } path.addMarker(new MapPoint(cLat, cLon, cAlt, startTime + Math.round(eTime), "Burst")); burst = new MapPoint(cLat, cLon, cAlt, startTime + Math.round(eTime), "Burst"); // Calculate descent while(cAlt > groundLevel) { AtmosphereState state = atmo.getAtAltitude(cAlt); double windX = state.getWindSpeed() * Math.sin(Math.toRadians(state.getWindDirection() + 180.0)); double windY = state.getWindSpeed() * Math.cos(Math.toRadians(state.getWindDirection() + 180.0)); dX += windX * tStep; dY += windY * tStep; double cRhoA = (state.getPressure() * MWAir) / (R * (state.getTemperature() + 273.15)); double descentRate = Math.sqrt((payloadMass * 9.81) / (0.5 * cRhoA * parachuteArea * parachuteDrag)); cAlt -= descentRate * tStep; eTime += tStep; // Convert to lat/lon double range = Math.pow(Math.pow(dX, 2.0) + Math.pow(dY, 2.0), 0.5); // double bearing = Math.atan(dX / dY); double bearing = Math.atan2(dX, dY); Point2D.Double cPos = directGeodesic(new Point2D.Double(startLon, startLat), bearing, range); cLat = cPos.y; cLon = cPos.x; // Store path.add(cLat, cLon, cAlt, startTime + Math.round(eTime)); } landing = new MapPoint(cLat, cLon, cAlt, startTime + Math.round(eTime), "Burst"); return path; } @Override public MapPoint getBurst() { return burst; } @Override public MapPoint getLanding() { return landing; } /** * Set if prediction begins with ascent. */ @Override public void setAscending(boolean ascending) { isAscending = ascending; } /** * Set termination altitude. */ @Override public void setGroundLevel(double level) { groundLevel = level; } /** * Set start point (including altitude and time */ @Override public void setStart(MapPoint start) { startLat = start.getLatitude(); startLon = start.getLongitude(); startTime = start.getTime(); startAlt = start.getAltitude(); } @Override public boolean equals(Object o) { if(o == this) return true; if(o == null) return false; if(!(o instanceof LatexPredictor)) return false; LatexPredictor obj = (LatexPredictor)o; if( startTime == obj.startTime && startLat == obj.startLat && startLon == obj.startLon && startAlt == obj.startAlt && groundLevel == obj.groundLevel && isAscending == obj.isAscending && payloadMass == obj.payloadMass && balloonLift == obj.balloonLift && parachuteArea == obj.parachuteArea && parachuteDrag == obj.parachuteDrag && balloonMass == obj.balloonMass && balloonDrag == obj.balloonDrag && burstRad == obj.burstRad ) return true; return false; } @Override public int hashCode() { int hash = 1; hash = hash * 31 + (new Long(startTime)).hashCode(); hash = hash * 31 + (new Double(startLat)).hashCode(); hash = hash * 31 + (new Double(startLon)).hashCode(); hash = hash * 31 + (new Double(startAlt)).hashCode(); hash = hash * 31 + (new Double(balloonMass)).hashCode(); hash = hash * 31 + (new Double(balloonLift)).hashCode(); return hash; } /** * Get start point. */ @Override public MapPoint getStart() { return new MapPoint(startLat, startLon, startAlt, startTime); } /** * Get balloon type. */ @Override public String getTypeName() { return balloonName; } /** * Get balloon neck lift. */ @Override public double getLift() { return balloonLift; } @Override public Predictor clone() { LatexPredictor clone = new LatexPredictor(); clone.balloonDrag = new Double(balloonDrag).doubleValue(); clone.balloonLift = new Double(balloonLift).doubleValue(); clone.balloonMass = new Double(balloonMass).doubleValue(); clone.balloonName = new String(balloonName); clone.burstRad = new Double(burstRad).doubleValue(); clone.groundLevel = new Double(groundLevel).doubleValue(); clone.isAscending = new Boolean(isAscending).booleanValue(); clone.parachuteArea = new Double(parachuteArea).doubleValue(); clone.parachuteDrag = new Double(parachuteDrag).doubleValue(); clone.payloadMass = new Double(payloadMass).doubleValue(); clone.startAlt = new Double(startAlt).doubleValue(); clone.startLat = new Double(startLat).doubleValue(); clone.startLon = new Double(startLon).doubleValue(); clone.startTime = new Long(startTime).longValue(); return clone; } /** * Set start time */ @Override public void setStartTime(long time) { startTime = time; } }