From 3cf305a033d347380020d857e82ff1ddd6a4ef41 Mon Sep 17 00:00:00 2001 From: plaa Date: Sun, 18 Sep 2011 18:38:34 +0000 Subject: [PATCH] menu icons, window sizing, compass direction selector git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@173 180e2498-e6e9-4542-8430-84ac67f01cd8 --- ChangeLog | 8 + l10n/messages.properties | 21 +- pix/icons/copyright.txt | 5 +- pix/icons/help-about.png | Bin 0 -> 3632 bytes pix/icons/help-bug.png | Bin 0 -> 3692 bytes pix/icons/help-license.png | Bin 0 -> 3247 bytes pix/icons/help-log.png | Bin 0 -> 3424 bytes .../file/openrocket/OpenRocketLoader.java | 2 +- .../openrocket/gui/components/FlatButton.java | 69 ++++++ .../components/compass/CompassPointer.java | 218 +++++++++++++++++ .../gui/components/compass/CompassRose.java | 221 ++++++++++++++++++ .../compass/CompassSelectionButton.java | 190 +++++++++++++++ .../components/compass/CompassSelector.java | 97 ++++++++ .../gui/components/compass/Tester.java | 66 ++++++ .../configdialog/ComponentConfigDialog.java | 23 +- .../optimization/OptimizationPlotDialog.java | 1 + .../sf/openrocket/gui/main/BasicFrame.java | 34 ++- .../gui/plot/SimulationPlotDialog.java | 1 + .../models/gravity/WGSGravityModel.java | 10 +- .../simulation/RK4SimulationStepper.java | 9 +- .../simulation/SimulationOptions.java | 1 + src/net/sf/openrocket/util/GUIUtil.java | 58 +++++ .../util/GeodeticComputationStrategy.java | 78 ++++--- src/net/sf/openrocket/util/Icons.java | 5 + src/net/sf/openrocket/util/Prefs.java | 13 +- .../sf/openrocket/util/WorldCoordinate.java | 18 +- .../util/GeodeticComputationStrategyTest.java | 105 ++++++++- 27 files changed, 1159 insertions(+), 94 deletions(-) create mode 100644 pix/icons/help-about.png create mode 100644 pix/icons/help-bug.png create mode 100644 pix/icons/help-license.png create mode 100644 pix/icons/help-log.png create mode 100644 src/net/sf/openrocket/gui/components/FlatButton.java create mode 100644 src/net/sf/openrocket/gui/components/compass/CompassPointer.java create mode 100644 src/net/sf/openrocket/gui/components/compass/CompassRose.java create mode 100644 src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java create mode 100644 src/net/sf/openrocket/gui/components/compass/CompassSelector.java create mode 100644 src/net/sf/openrocket/gui/components/compass/Tester.java diff --git a/ChangeLog b/ChangeLog index a6a34dd0..24d7bd2e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2011-09-18 Sampo Niskanen + + * Remember window/dialog sizes and/or positions + +2011-09-13 Sampo Niskanen + + * Added icons to help menu + 2011-08-28 Richard Graham * Patch for geodetic computations + coriolis effect diff --git a/l10n/messages.properties b/l10n/messages.properties index 7fbef931..f58be3d5 100644 --- a/l10n/messages.properties +++ b/l10n/messages.properties @@ -351,8 +351,8 @@ simedtdlg.IntensityDesc.High = High simedtdlg.IntensityDesc.Veryhigh = Very high simedtdlg.IntensityDesc.Extreme = Extreme -GeodeticComputationStrategy.none.name = None -GeodeticComputationStrategy.none.desc = Perform no geodetic computations. +GeodeticComputationStrategy.flat.name = Flat Earth +GeodeticComputationStrategy.flat.desc = Perform computations with a flat Earth approximation. Sufficient for low-altitude flights. GeodeticComputationStrategy.spherical.name = Spherical approximation GeodeticComputationStrategy.spherical.desc = Perform geodetic computations assuming a spherical Earth.
This is sufficiently accurate for almost all purposes. GeodeticComputationStrategy.wgs84.name = WGS84 ellipsoid @@ -1501,3 +1501,20 @@ LandingDistanceParameter.name = Landing distance TotalFlightTimeParameter.name = Total flight time DeploymentVelocityParameter.name = Velocity at parachute deployment + +! Compass directions drawn on a compass rose. +CompassRose.lbl.north = N +CompassRose.lbl.east = E +CompassRose.lbl.south = S +CompassRose.lbl.west = W + +! Compass directions with subdirections. These might not be localized even if the directions on the compass rose are. +CompassSelectionButton.lbl.N = N +CompassSelectionButton.lbl.NE = NE +CompassSelectionButton.lbl.E = E +CompassSelectionButton.lbl.SE = SE +CompassSelectionButton.lbl.S = S +CompassSelectionButton.lbl.SW = SW +CompassSelectionButton.lbl.W = W +CompassSelectionButton.lbl.NW = NW + diff --git a/pix/icons/copyright.txt b/pix/icons/copyright.txt index 81429b31..6583a503 100644 --- a/pix/icons/copyright.txt +++ b/pix/icons/copyright.txt @@ -30,4 +30,7 @@ delete.png preferences.png zoom-in.png zoom-out.png - +help-license.png +help-log.png +help-about.png +help-bug.png diff --git a/pix/icons/help-about.png b/pix/icons/help-about.png new file mode 100644 index 0000000000000000000000000000000000000000..e0fad561901197400298c08aa18bdde65441a9c7 GIT binary patch literal 3632 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 KMNUMnLSTYHEZI~5 literal 0 HcmV?d00001 diff --git a/pix/icons/help-license.png b/pix/icons/help-license.png new file mode 100644 index 0000000000000000000000000000000000000000..eec0392f06a5eb295a6e52aa48002abdaf4f5df6 GIT binary patch literal 3247 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 literal 0 HcmV?d00001 diff --git a/pix/icons/help-log.png b/pix/icons/help-log.png new file mode 100644 index 0000000000000000000000000000000000000000..087a84b355df12b3165b01f56429a435c8b18892 GIT binary patch literal 3424 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 + */ +public class FlatButton extends JButton { + + public FlatButton() { + super(); + initialize(); + } + + public FlatButton(Icon icon) { + super(icon); + initialize(); + } + + public FlatButton(String text) { + super(text); + initialize(); + } + + public FlatButton(Action a) { + super(a); + initialize(); + } + + public FlatButton(String text, Icon icon) { + super(text, icon); + initialize(); + } + + + private void initialize() { + this.addMouseListener(new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + flatten(); + } + + @Override + public void mouseEntered(MouseEvent e) { + raise(); + } + }); + flatten(); + } + + + private void flatten() { + this.setContentAreaFilled(false); + this.setBorderPainted(false); + } + + private void raise() { + this.setContentAreaFilled(true); + this.setBorderPainted(true); + } + +} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassPointer.java b/src/net/sf/openrocket/gui/components/compass/CompassPointer.java new file mode 100644 index 00000000..37fdad42 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/compass/CompassPointer.java @@ -0,0 +1,218 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.sf.openrocket.gui.Resettable; +import net.sf.openrocket.gui.adaptors.DoubleModel; + +/** + * A component that draws a pointer onto a compass rose. + * + * @author Sampo Niskanen + */ +public class CompassPointer extends CompassRose implements Resettable { + + private static final Color PRIMARY_POINTER_COLOR = new Color(1.0f, 0.2f, 0.2f); + private static final Color SECONDARY_POINTER_COLOR = new Color(0.2f, 0.2f, 0.2f, 0.2f); + + private final DoubleModel model; + private final ChangeListener listener; + + protected int width = -1; + protected int mid = -1; + + private DoubleModel secondaryModel; + + private float pointerLength = 0.95f; + private float pointerWidth = 0.1f; + private float pointerArrowWidth = 0.2f; + private boolean pointerArrow = true; + + + + public CompassPointer(DoubleModel model) { + super(); + this.model = model; + listener = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + CompassPointer.this.repaint(); + } + }; + model.addChangeListener(listener); + } + + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + Graphics2D g2 = (Graphics2D) g; + + + Dimension dimension = this.getSize(); + + width = Math.min(dimension.width, dimension.height); + mid = width / 2; + width = (int) (getScaler() * width); + + + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + + if (secondaryModel != null) { + drawArrow(secondaryModel.getValue(), SECONDARY_POINTER_COLOR, g2); + } + drawArrow(model.getValue(), PRIMARY_POINTER_COLOR, g2); + + + } + + + private void drawArrow(double angle, Color color, Graphics2D g2) { + + int pLength = (int) (width * pointerLength / 2); + int pWidth = (int) (width * pointerWidth / 2); + int pArrowWidth = (int) (width * pointerArrowWidth / 2); + + int[] x = new int[8]; + int[] y = new int[8]; + + g2.setColor(color); + + + double sin = Math.sin(angle); + double cos = Math.cos(angle); + + int n = 0; + + // Top part + x[n] = 0; + y[n] = -pLength; + n++; + if (pointerArrow) { + x[n] = -pArrowWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + x[n] = -pWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + } + + // Bottom part + x[n] = -pWidth; + y[n] = pLength; + n++; + x[n] = 0; + y[n] = pLength - pWidth; + n++; + x[n] = pWidth; + y[n] = pLength; + n++; + + // Top part + if (pointerArrow) { + x[n] = pWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + x[n] = pArrowWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + } + + // Rotate and shift + for (int i = 0; i < n; i++) { + double x2, y2; + x2 = cos * x[i] - sin * y[i]; + y2 = sin * x[i] + cos * y[i]; + + x[i] = (int) (x2 + mid); + y[i] = (int) (y2 + mid); + } + + g2.fillPolygon(x, y, n); + + g2.setColor(color.darker()); + g2.drawPolygon(x, y, n); + + } + + + public boolean isPointerArrow() { + return pointerArrow; + } + + + public void setPointerArrow(boolean useArrow) { + this.pointerArrow = useArrow; + repaint(); + } + + + public float getPointerLength() { + return pointerLength; + } + + + public void setPointerLength(float pointerLength) { + this.pointerLength = pointerLength; + repaint(); + } + + + public float getPointerWidth() { + return pointerWidth; + } + + + public void setPointerWidth(float pointerWidth) { + this.pointerWidth = pointerWidth; + repaint(); + } + + + public float getPointerArrowWidth() { + return pointerArrowWidth; + } + + + public void setPointerArrowWidth(float pointerArrowWidth) { + this.pointerArrowWidth = pointerArrowWidth; + repaint(); + } + + + + public DoubleModel getSecondaryModel() { + return secondaryModel; + } + + + public void setSecondaryModel(DoubleModel secondaryModel) { + if (this.secondaryModel != null) { + this.secondaryModel.removeChangeListener(listener); + } + this.secondaryModel = secondaryModel; + if (this.secondaryModel != null) { + this.secondaryModel.addChangeListener(listener); + } + } + + + @Override + public void resetModel() { + model.removeChangeListener(listener); + setSecondaryModel(null); + } + + + +} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassRose.java b/src/net/sf/openrocket/gui/components/compass/CompassRose.java new file mode 100644 index 00000000..49f1d059 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/compass/CompassRose.java @@ -0,0 +1,221 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.font.GlyphVector; +import java.awt.geom.Rectangle2D; + +import javax.swing.JComponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +/** + * A component that draws a compass rose. This class has no other functionality, but superclasses + * may add functionality to it. + * + * @author Sampo Niskanen + */ +public class CompassRose extends JComponent { + private static final Translator trans = Application.getTranslator(); + + + private static final Color MAIN_COLOR = new Color(0.4f, 0.4f, 1.0f); + private static final float MAIN_LENGTH = 0.95f; + private static final float MAIN_WIDTH = 0.15f; + + private static final int CIRCLE_BORDER = 2; + private static final Color CIRCLE_HIGHLIGHT = new Color(1.0f, 1.0f, 1.0f, 0.7f); + private static final Color CIRCLE_SHADE = new Color(0.0f, 0.0f, 0.0f, 0.2f); + + private static final Color MARKER_COLOR = Color.BLACK; + + + private double scaler; + + private double markerRadius; + private Font markerFont; + + + /** + * Construct a compass rose with the default settings. + */ + public CompassRose() { + this(0.8, 1.1, Font.decode("Serif-PLAIN-16")); + } + + + /** + * Construct a compass rose with the specified settings. + * + * @param scaler The scaler of the rose. The bordering circle will we this portion of the component dimensions. + * @param markerRadius The radius for the marker positions (N/E/S/W), or NaN for no markers. A value greater than one + * will position the markers outside of the bordering circle. + * @param markerFont The font used for the markers. + */ + public CompassRose(double scaler, double markerRadius, Font markerFont) { + this.scaler = scaler; + this.markerRadius = markerRadius; + this.markerFont = markerFont; + } + + + + @Override + public void paintComponent(Graphics g) { + + Graphics2D g2 = (Graphics2D) g; + + int[] x = new int[3]; + int[] y = new int[3]; + Dimension dimension = this.getSize(); + + int width = Math.min(dimension.width, dimension.height); + int mid = width / 2; + width = (int) (scaler * width); + + int mainLength = (int) (width * MAIN_LENGTH / 2); + int mainWidth = (int) (width * MAIN_WIDTH / 2); + + + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2.setColor(MAIN_COLOR); + + // North + x[0] = mid; + y[0] = mid; + x[1] = mid; + y[1] = mid - mainLength; + x[2] = mid - mainWidth; + y[2] = mid - mainWidth; + g2.fillPolygon(x, y, 3); + + x[2] = mid + mainWidth; + g2.drawPolygon(x, y, 3); + + // East + x[0] = mid; + y[0] = mid; + x[1] = mid + mainLength; + y[1] = mid; + x[2] = mid + mainWidth; + y[2] = mid - mainWidth; + g2.fillPolygon(x, y, 3); + + y[2] = mid + mainWidth; + g2.drawPolygon(x, y, 3); + + // South + x[0] = mid; + y[0] = mid; + x[1] = mid; + y[1] = mid + mainLength; + x[2] = mid + mainWidth; + y[2] = mid + mainWidth; + g2.fillPolygon(x, y, 3); + + x[2] = mid - mainWidth; + g2.drawPolygon(x, y, 3); + + // West + x[0] = mid; + y[0] = mid; + x[1] = mid - mainLength; + y[1] = mid; + x[2] = mid - mainWidth; + y[2] = mid + mainWidth; + g2.fillPolygon(x, y, 3); + + y[2] = mid - mainWidth; + g2.drawPolygon(x, y, 3); + + + // Border circle + g2.setColor(CIRCLE_SHADE); + g2.drawArc(mid - width / 2 + CIRCLE_BORDER, mid - width / 2 + CIRCLE_BORDER, + width - 2 * CIRCLE_BORDER, width - 2 * CIRCLE_BORDER, 45, 180); + g2.setColor(CIRCLE_HIGHLIGHT); + g2.drawArc(mid - width / 2 + CIRCLE_BORDER, mid - width / 2 + CIRCLE_BORDER, + width - 2 * CIRCLE_BORDER, width - 2 * CIRCLE_BORDER, 180 + 45, 180); + + + // Draw direction markers + if (!Double.isNaN(markerRadius) && markerFont != null) { + + int pos = (int) (width * markerRadius / 2); + + g2.setColor(MARKER_COLOR); + drawMarker(g2, mid, mid - pos, trans.get("lbl.north")); + drawMarker(g2, mid + pos, mid, trans.get("lbl.east")); + drawMarker(g2, mid, mid + pos, trans.get("lbl.south")); + drawMarker(g2, mid - pos, mid, trans.get("lbl.west")); + + } + + } + + + + private void drawMarker(Graphics2D g2, float x, float y, String str) { + GlyphVector gv = markerFont.createGlyphVector(g2.getFontRenderContext(), str); + Rectangle2D rect = gv.getVisualBounds(); + + x -= rect.getWidth() / 2; + y += rect.getHeight() / 2; + + g2.drawGlyphVector(gv, x, y); + + } + + + + + + public double getScaler() { + return scaler; + } + + + public void setScaler(double scaler) { + this.scaler = scaler; + repaint(); + } + + + public double getMarkerRadius() { + return markerRadius; + } + + + public void setMarkerRadius(double markerRadius) { + this.markerRadius = markerRadius; + repaint(); + } + + + public Font getMarkerFont() { + return markerFont; + } + + + public void setMarkerFont(Font markerFont) { + this.markerFont = markerFont; + repaint(); + } + + @Override + public Dimension getPreferredSize() { + Dimension dim = super.getPreferredSize(); + int min = Math.min(dim.width, dim.height); + dim.setSize(min, min); + return dim; + } + + +} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java b/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java new file mode 100644 index 00000000..c8b0fee2 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java @@ -0,0 +1,190 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSpinner; +import javax.swing.border.BevelBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.Resettable; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.FlatButton; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.MathUtil; + + +/** + * A button that displays a current compass direction and opens a popup to edit + * the value when clicked. + * + * @author Sampo Niskanen + */ +public class CompassSelectionButton extends FlatButton implements Resettable { + + private static final Translator trans = Application.getTranslator(); + + private static final int POPUP_COMPASS_SIZE = 200; + private static final double SECTOR = 45; + + private static int minWidth = -1; + + + private final DoubleModel model; + private final ChangeListener listener; + + private JPopupMenu popup; + + + public CompassSelectionButton(final DoubleModel model) { + this.model = model; + + JPanel panel = new JPanel(new MigLayout("fill, ins 0")); + panel.setOpaque(false); + + CompassPointer pointer = new CompassPointer(model); + pointer.setPreferredSize(new Dimension(24, 24)); + pointer.setMarkerFont(null); + pointer.setPointerArrow(false); + pointer.setPointerWidth(0.45f); + pointer.setScaler(1.0f); + panel.add(pointer, "gapright rel"); + + + final JLabel label = new JLabel(); + label.setText(getLabel(model.getValue())); + panel.add(label); + + listener = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + label.setText(getLabel(model.getValue())); + } + }; + model.addChangeListener(listener); + + + if (minWidth < 0) { + calculateMinWidth(); + label.setMinimumSize(new Dimension(minWidth, 0)); + } + + + this.add(panel); + + this.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + openPopup(); + } + }); + } + + + + + private String getLabel(double value) { + String str; + + value = MathUtil.reduce360(value); + value = Math.toDegrees(value); + str = "" + Math.round(value) + Chars.DEGREE + " ("; + + if (value <= 0.5 * SECTOR || value >= 7.5 * SECTOR) { + str += trans.get("lbl.N"); + } else if (value <= 1.5 * SECTOR) { + str += trans.get("lbl.NE"); + } else if (value <= 2.5 * SECTOR) { + str += trans.get("lbl.E"); + } else if (value <= 3.5 * SECTOR) { + str += trans.get("lbl.SE"); + } else if (value <= 4.5 * SECTOR) { + str += trans.get("lbl.S"); + } else if (value <= 5.5 * SECTOR) { + str += trans.get("lbl.SW"); + } else if (value <= 6.5 * SECTOR) { + str += trans.get("lbl.W"); + } else { + str += trans.get("lbl.NW"); + } + + str += ")"; + return str; + } + + + private void openPopup() { + if (popup == null) { + popup = new JPopupMenu(); + + + final JPanel panel = new JPanel(new MigLayout("fill")); + + final CompassPointer rose = new CompassSelector(model); + rose.setPreferredSize(new Dimension(POPUP_COMPASS_SIZE, POPUP_COMPASS_SIZE)); + panel.add(rose, "spany, gapright unrel"); + + panel.add(new JPanel(), "growy, wrap"); + + JSpinner spin = new JSpinner(model.getSpinnerModel()); + panel.add(spin, "wmin 50lp, growx, gapright 0, aligny bottom"); + + panel.add(new JLabel("" + Chars.DEGREE), "wrap para"); + + JButton close = new JButton("OK"); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + popup.setVisible(false); + } + }); + panel.add(close, "span 2, growx, wrap"); + + panel.add(new JPanel(), "growy, wrap"); + + popup.add(panel); + popup.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); + } + + popup.pack(); + + Dimension popupSize = popup.getPreferredSize(); + Dimension buttonSize = this.getSize(); + + int posX = buttonSize.width / 2 - popupSize.width / 2; + int posY = buttonSize.height / 2 - popupSize.height / 2; + popup.show(this, posX, posY); + } + + private void calculateMinWidth() { + JLabel label = new JLabel(); + int max = 0; + for (double deg = 0; deg < 360; deg += 0.99999999999) { + label.setText(getLabel(Math.toRadians(deg))); + int w = label.getPreferredSize().width; + if (w > max) { + max = w; + } + } + minWidth = max + 1; + } + + + + + @Override + public void resetModel() { + model.removeChangeListener(listener); + } + +} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassSelector.java b/src/net/sf/openrocket/gui/components/compass/CompassSelector.java new file mode 100644 index 00000000..deed9cf7 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/compass/CompassSelector.java @@ -0,0 +1,97 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.util.MathUtil; + +/** + * Component that allows selecting a compass direction on a CompassSelector. + * + * @author Sampo Niskanen + */ +public class CompassSelector extends CompassPointer { + + private final DoubleModel model; + + public CompassSelector(DoubleModel model) { + super(model); + this.model = model; + + MouseAdapter mouse = new MouseAdapter() { + private boolean dragging = false; + + @Override + public void mousePressed(MouseEvent e) { + if (!isWithinCircle(e)) + return; + if (e.getButton() != MouseEvent.BUTTON1) + return; + dragging = true; + clicked(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() != MouseEvent.BUTTON1) + return; + dragging = false; + } + + + @Override + public void mouseDragged(MouseEvent e) { + if (!dragging) + return; + clicked(e); + } + }; + this.addMouseListener(mouse); + this.addMouseMotionListener(mouse); + + } + + private boolean isWithinCircle(MouseEvent e) { + if (mid < 0 || width < 0) { + return false; + } + + int x = e.getX() - mid; + int y = e.getY() - mid; + + double distance = Math.hypot(x, y); + return distance < width / 2; + } + + private void clicked(MouseEvent e) { + + if (mid < 0 || width < 0) { + return; + } + + int x = e.getX() - mid; + int y = e.getY() - mid; + + double distance = Math.hypot(x, y); + + double theta = Math.atan2(y, x); + theta = MathUtil.reduce360(theta + Math.PI / 2); + + // Round the value appropriately + theta = Math.toDegrees(theta); + + if (distance > 50) { + theta = Math.round(theta); + } else if (distance > 10) { + theta = 5 * Math.round(theta / 5); + } else { + // Do nothing if too close to center + return; + } + theta = Math.toRadians(theta); + + model.setValue(theta); + } + +} diff --git a/src/net/sf/openrocket/gui/components/compass/Tester.java b/src/net/sf/openrocket/gui/components/compass/Tester.java new file mode 100644 index 00000000..eda95f56 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/compass/Tester.java @@ -0,0 +1,66 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Dimension; +import java.lang.reflect.InvocationTargetException; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.l10n.ResourceBundleTranslator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.GUIUtil; + +public class Tester { + + + public static void main(String[] args) throws InterruptedException, InvocationTargetException { + + Application.setBaseTranslator(new ResourceBundleTranslator("l10n.messages")); + + GUIUtil.setBestLAF(); + + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + JFrame frame = new JFrame(); + + JPanel panel = new JPanel(new MigLayout("fill")); + DoubleModel model = new DoubleModel(Math.toRadians(45), UnitGroup.UNITS_ANGLE); + DoubleModel second = new DoubleModel(Math.toRadians(30), UnitGroup.UNITS_ANGLE); + + + CompassPointer rose = new CompassSelector(model); + rose.setPreferredSize(new Dimension(300, 300)); + rose.setSecondaryModel(second); + panel.add(rose); + + rose = new CompassPointer(model); + rose.setPreferredSize(new Dimension(24, 24)); + panel.add(rose); + rose.setMarkerFont(null); + rose.setPointerArrow(false); + rose.setPointerWidth(0.45f); + rose.setScaler(1.0f); + + JSpinner spin = new JSpinner(model.getSpinnerModel()); + spin.setPreferredSize(new Dimension(50, 20)); + panel.add(spin, "wrap para"); + + + CompassSelectionButton button = new CompassSelectionButton(model); + panel.add(button); + + + frame.add(panel); + frame.pack(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + } + }); + } +} diff --git a/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java b/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java index a04c65b6..0f9f1159 100644 --- a/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java +++ b/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java @@ -1,10 +1,7 @@ package net.sf.openrocket.gui.configdialog; -import java.awt.Point; import java.awt.Window; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; @@ -18,7 +15,6 @@ import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.GUIUtil; -import net.sf.openrocket.util.Prefs; import net.sf.openrocket.util.Reflection; /** @@ -44,7 +40,7 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis private final Window parent; private static final Translator trans = Application.getTranslator(); - + private ComponentConfigDialog(Window parent, OpenRocketDocument document, RocketComponent component) { super(parent); @@ -53,22 +49,7 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis setComponent(document, component); GUIUtil.setDisposableDialogOptions(this, null); - - // Set window position according to preferences, and set prefs when moving - Point position = Prefs.getWindowPosition(this.getClass()); - if (position != null) { - this.setLocationByPlatform(false); - this.setLocation(position); - } - - this.addComponentListener(new ComponentAdapter() { - @Override - public void componentMoved(ComponentEvent e) { - Prefs.setWindowPosition(ComponentConfigDialog.this.getClass(), - ComponentConfigDialog.this.getLocation()); - } - }); - + GUIUtil.rememberWindowPosition(this); } diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java index 04885a82..c90c31ab 100644 --- a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java +++ b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java @@ -113,6 +113,7 @@ public class OptimizationPlotDialog extends JDialog { this.add(panel); GUIUtil.setDisposableDialogOptions(this, close); + GUIUtil.rememberWindowSize(this); } diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java index 5fb99fb2..5f2d92e0 100644 --- a/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -6,8 +6,6 @@ import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; @@ -233,19 +231,16 @@ public class BasicFrame extends JFrame { setTitle(); this.pack(); - Dimension size = Prefs.getWindowSize(this.getClass()); - if (size == null) { - size = Toolkit.getDefaultToolkit().getScreenSize(); - size.width = size.width * 9 / 10; - size.height = size.height * 9 / 10; - } + + // Set initial window size + Dimension size = Toolkit.getDefaultToolkit().getScreenSize(); + size.width = size.width * 9 / 10; + size.height = size.height * 9 / 10; this.setSize(size); - this.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize()); - } - }); + + // Remember changed size + GUIUtil.rememberWindowSize(this); + this.setLocationByPlatform(true); GUIUtil.setWindowIcons(this); @@ -259,8 +254,8 @@ public class BasicFrame extends JFrame { closeAction(); } }); - frames.add(this); + frames.add(this); log.debug("BasicFrame instantiation complete"); } @@ -673,14 +668,13 @@ public class BasicFrame extends JFrame { menu = new JMenu(trans.get("main.menu.help")); menu.setMnemonic(KeyEvent.VK_H); - //// Information about OpenRocket menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.desc")); menubar.add(menu); //// License item = new JMenuItem(trans.get("main.menu.help.license"), KeyEvent.VK_L); - //// OpenRocket license information + item.setIcon(Icons.HELP_LICENSE); item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.license.desc")); item.addActionListener(new ActionListener() { @Override @@ -695,7 +689,7 @@ public class BasicFrame extends JFrame { //// Bug report item = new JMenuItem(trans.get("main.menu.help.bugReport"), KeyEvent.VK_B); - //// Information about reporting bugs in OpenRocket + item.setIcon(Icons.HELP_BUG_REPORT); item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.bugReport.desc")); item.addActionListener(new ActionListener() { @Override @@ -708,7 +702,7 @@ public class BasicFrame extends JFrame { //// Debug log item = new JMenuItem(trans.get("main.menu.help.debugLog")); - //// View the OpenRocket debug log + item.setIcon(Icons.HELP_DEBUG_LOG); item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.debugLog.desc")); item.addActionListener(new ActionListener() { @Override @@ -723,7 +717,7 @@ public class BasicFrame extends JFrame { //// About item = new JMenuItem(trans.get("main.menu.help.about"), KeyEvent.VK_A); - //// About OpenRocket + item.setIcon(Icons.HELP_ABOUT); item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.about.desc")); item.addActionListener(new ActionListener() { @Override diff --git a/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java b/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java index c68617b7..e1e1b2d7 100644 --- a/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java +++ b/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java @@ -461,6 +461,7 @@ public class SimulationPlotDialog extends JDialog { this.pack(); GUIUtil.setDisposableDialogOptions(this, button); + GUIUtil.rememberWindowSize(this); } private String getLabel(FlightDataType type, Unit unit) { diff --git a/src/net/sf/openrocket/models/gravity/WGSGravityModel.java b/src/net/sf/openrocket/models/gravity/WGSGravityModel.java index 9339c0eb..253d4a41 100644 --- a/src/net/sf/openrocket/models/gravity/WGSGravityModel.java +++ b/src/net/sf/openrocket/models/gravity/WGSGravityModel.java @@ -10,13 +10,10 @@ import net.sf.openrocket.util.WorldCoordinate; */ public class WGSGravityModel implements GravityModel { + // Cache the previously computed value private WorldCoordinate lastWorldCoordinate; private double lastg; - - private static int hit = 0; - private static int miss = 0; - @Override public double getGravity(WorldCoordinate wc) { @@ -25,12 +22,7 @@ public class WGSGravityModel implements GravityModel { if (wc != this.lastWorldCoordinate) { this.lastg = calcGravity(wc); this.lastWorldCoordinate = wc; - - miss++; - } else { - hit++; } - System.out.println("GRAVITY MODEL: hit=" + hit + " miss=" + miss); return this.lastg; diff --git a/src/net/sf/openrocket/simulation/RK4SimulationStepper.java b/src/net/sf/openrocket/simulation/RK4SimulationStepper.java index 4aa9494c..eea60277 100644 --- a/src/net/sf/openrocket/simulation/RK4SimulationStepper.java +++ b/src/net/sf/openrocket/simulation/RK4SimulationStepper.java @@ -266,6 +266,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { if (status.getRocketVelocity().length2() > 1e18 || status.getRocketPosition().length2() > 1e18 || status.getRocketRotationVelocity().length2() > 1e18) { + + // FIXME: Make error message better, recommend shortening time step + throw new SimulationCalculationException("Simulation values exceeded limits"); } } @@ -547,9 +550,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { data.setValue(FlightDataType.TYPE_POSITION_X, status.getRocketPosition().x); data.setValue(FlightDataType.TYPE_POSITION_Y, status.getRocketPosition().y); - if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.NONE) { - data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); - data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); + data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); + data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); + if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, store.coriolisAcceleration.length()); } diff --git a/src/net/sf/openrocket/simulation/SimulationOptions.java b/src/net/sf/openrocket/simulation/SimulationOptions.java index 0c7f103a..e6e8f821 100644 --- a/src/net/sf/openrocket/simulation/SimulationOptions.java +++ b/src/net/sf/openrocket/simulation/SimulationOptions.java @@ -62,6 +62,7 @@ public class SimulationOptions implements ChangeSource, Cloneable { private double windAverage = 2.0; private double windTurbulence = 0.1; + /* * SimulationOptions maintains the launch site parameters as separate double values, * and converts them into a WorldCoordinate when converting to SimulationConditions. diff --git a/src/net/sf/openrocket/util/GUIUtil.java b/src/net/sf/openrocket/util/GUIUtil.java index d8aec3b0..e74dbc19 100644 --- a/src/net/sf/openrocket/util/GUIUtil.java +++ b/src/net/sf/openrocket/util/GUIUtil.java @@ -2,6 +2,7 @@ package net.sf.openrocket.util; import java.awt.Component; import java.awt.Container; +import java.awt.Dimension; import java.awt.Font; import java.awt.Image; import java.awt.KeyboardFocusManager; @@ -9,6 +10,8 @@ import java.awt.Point; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.FocusListener; import java.awt.event.KeyEvent; @@ -39,6 +42,7 @@ import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; +import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JRootPane; import javax.swing.JSlider; @@ -267,6 +271,60 @@ public class GUIUtil { } + + /** + * Automatically remember the size of a window. This stores the window size in the user + * preferences when resizing/maximizing the window and sets the state on the first call. + */ + public static void rememberWindowSize(final Window window) { + window.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + log.debug("Storing size of " + window.getClass().getName() + ": " + window.getSize()); + Prefs.setWindowSize(window.getClass(), window.getSize()); + if (window instanceof JFrame) { + if ((((JFrame) window).getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) { + log.debug("Storing maximized state of " + window.getClass().getName()); + Prefs.setWindowMaximized(window.getClass()); + } + } + } + }); + + if (Prefs.isWindowMaximized(window.getClass())) { + if (window instanceof JFrame) { + ((JFrame) window).setExtendedState(JFrame.MAXIMIZED_BOTH); + } + } else { + Dimension dim = Prefs.getWindowSize(window.getClass()); + if (dim != null) { + window.setSize(dim); + } + } + } + + + /** + * Automatically remember the position of a window. The position is stored in the user preferences + * every time the window is moved and set from there when first calling this method. + */ + public static void rememberWindowPosition(final Window window) { + window.addComponentListener(new ComponentAdapter() { + @Override + public void componentMoved(ComponentEvent e) { + Prefs.setWindowPosition(window.getClass(), window.getLocation()); + } + }); + + // Set window position according to preferences, and set prefs when moving + Point position = Prefs.getWindowPosition(window.getClass()); + if (position != null) { + window.setLocationByPlatform(false); + window.setLocation(position); + } + } + + /** * Changes the style of the font of the specified border. * diff --git a/src/net/sf/openrocket/util/GeodeticComputationStrategy.java b/src/net/sf/openrocket/util/GeodeticComputationStrategy.java index 8e7d2f29..6f1fb53a 100644 --- a/src/net/sf/openrocket/util/GeodeticComputationStrategy.java +++ b/src/net/sf/openrocket/util/GeodeticComputationStrategy.java @@ -5,22 +5,41 @@ import net.sf.openrocket.startup.Application; /** * A strategy that performs computations on WorldCoordinates. + *

+ * The directions of the coordinate is: + * positive X = EAST + * positive Y = NORTH + * positive Z = UPWARDS */ public enum GeodeticComputationStrategy { /** - * Perform no geodetic computations. The addCoordinate method does nothing and - * getCoriolisAcceleration returns Coordinate.NUL. + * Perform computations using a flat Earth approximation. addCoordinate computes the + * location using a direct meters-per-degree scaling and getCoriolisAcceleration always + * returns NUL. */ - NONE { + FLAT { + private static final double METERS_PER_DEGREE_LATITUDE = 111325; // "standard figure" + private static final double METERS_PER_DEGREE_LONGITUDE_EQUATOR = 111050; + + @Override - public WorldCoordinate addCoordinate(WorldCoordinate latlon, Coordinate delta) { - return latlon; + public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { + + double metersPerDegreeLongitude = METERS_PER_DEGREE_LONGITUDE_EQUATOR * Math.cos(location.getLatitudeRad()); + // Limit to 1 meter per degree near poles + metersPerDegreeLongitude = MathUtil.max(metersPerDegreeLongitude, 1); + + double newLat = location.getLatitudeDeg() + delta.y / METERS_PER_DEGREE_LATITUDE; + double newLon = location.getLongitudeDeg() + delta.x / metersPerDegreeLongitude; + double newAlt = location.getAltitude() + delta.z; + + return new WorldCoordinate(newLat, newLon, newAlt); } @Override - public Coordinate getCoriolisAcceleration(WorldCoordinate latlon, Coordinate velocity) { + public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { return Coordinate.NUL; } }, @@ -37,9 +56,13 @@ public enum GeodeticComputationStrategy { // bearing (in radians, clockwise from north); // d/R is the angular distance (in radians), where d is the distance traveled and R is the earth’s radius double d = MathUtil.hypot(delta.x, delta.y); - double bearing = Math.atan(delta.x / delta.y); - if (delta.y < 0) - bearing = bearing + Math.PI; + + // Check for zero movement before computing bearing + if (MathUtil.equals(d, 0)) { + return new WorldCoordinate(location.getLatitudeDeg(), location.getLongitudeDeg(), newAlt); + } + + double bearing = Math.atan2(delta.x, delta.y); // Calculate the new lat and lon double newLat, newLon; @@ -51,20 +74,17 @@ public enum GeodeticComputationStrategy { newLat = Math.asin(sinLat * cosDR + cosLat * sinDR * Math.cos(bearing)); newLon = location.getLongitudeRad() + Math.atan2(Math.sin(bearing) * sinDR * cosLat, cosDR - sinLat * Math.sin(newLat)); - if (Double.isNaN(newLat)) { - newLat = location.getLatitudeRad(); - } - - if (Double.isNaN(newLon)) { - newLon = location.getLongitudeRad(); + if (Double.isNaN(newLat) || Double.isNaN(newLon)) { + throw new BugException("addCoordinate resulted in NaN location: location=" + location + " delta=" + delta + + " newLat=" + newLat + " newLon=" + newLon); } return new WorldCoordinate(Math.toDegrees(newLat), Math.toDegrees(newLon), newAlt); } @Override - public Coordinate getCoriolisAcceleration(WorldCoordinate latlon, Coordinate velocity) { - return computeCoriolisAcceleration(latlon, velocity); + public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { + return computeCoriolisAcceleration(location, velocity); } }, @@ -81,6 +101,12 @@ public enum GeodeticComputationStrategy { // bearing (in radians, clockwise from north); // d/R is the angular distance (in radians), where d is the distance traveled and R is the earth’s radius double d = MathUtil.hypot(delta.x, delta.y); + + // Check for zero movement before computing bearing + if (MathUtil.equals(d, 0)) { + return new WorldCoordinate(location.getLatitudeDeg(), location.getLongitudeDeg(), newAlt); + } + double bearing = Math.atan(delta.x / delta.y); if (delta.y < 0) bearing = bearing + Math.PI; @@ -91,19 +117,17 @@ public enum GeodeticComputationStrategy { newLat = ret[0]; newLon = ret[1]; - if (Double.isNaN(newLat)) { - newLat = location.getLatitudeRad(); + if (Double.isNaN(newLat) || Double.isNaN(newLon)) { + throw new BugException("addCoordinate resulted in NaN location: location=" + location + " delta=" + delta + + " newLat=" + newLat + " newLon=" + newLon); } - if (Double.isNaN(newLon)) { - newLon = location.getLongitudeRad(); - } - return new WorldCoordinate(newLat, newLon, newAlt); + return new WorldCoordinate(Math.toDegrees(newLat), Math.toDegrees(newLon), newAlt); } @Override - public Coordinate getCoriolisAcceleration(WorldCoordinate latlon, Coordinate velocity) { - return computeCoriolisAcceleration(latlon, velocity); + public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { + return computeCoriolisAcceleration(location, velocity); } }; @@ -136,13 +160,13 @@ public enum GeodeticComputationStrategy { /** * Add a cartesian movement coordinate to a WorldCoordinate. */ - public abstract WorldCoordinate addCoordinate(WorldCoordinate latlon, Coordinate delta); + public abstract WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta); /** * Compute the coriolis acceleration at a specified WorldCoordinate and velocity. */ - public abstract Coordinate getCoriolisAcceleration(WorldCoordinate latlon, Coordinate velocity); + public abstract Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity); diff --git a/src/net/sf/openrocket/util/Icons.java b/src/net/sf/openrocket/util/Icons.java index 7639bcdc..e5f41317 100644 --- a/src/net/sf/openrocket/util/Icons.java +++ b/src/net/sf/openrocket/util/Icons.java @@ -62,6 +62,11 @@ public class Icons { public static final Icon EDIT_DELETE = loadImageIcon("pix/icons/edit-delete.png", "Delete"); public static final Icon EDIT_SCALE = loadImageIcon("pix/icons/edit-scale.png", "Scale"); + public static final Icon HELP_ABOUT = loadImageIcon("pix/icons/help-about.png", "About"); + public static final Icon HELP_BUG_REPORT = loadImageIcon("pix/icons/help-bug.png", "Bug report"); + public static final Icon HELP_DEBUG_LOG = loadImageIcon("pix/icons/help-log.png", "Debug log"); + public static final Icon HELP_LICENSE = loadImageIcon("pix/icons/help-license.png", "License"); + public static final Icon ZOOM_IN = loadImageIcon("pix/icons/zoom-in.png", "Zoom in"); public static final Icon ZOOM_OUT = loadImageIcon("pix/icons/zoom-out.png", "Zoom out"); diff --git a/src/net/sf/openrocket/util/Prefs.java b/src/net/sf/openrocket/util/Prefs.java index 2d59f776..dfe05a1d 100644 --- a/src/net/sf/openrocket/util/Prefs.java +++ b/src/net/sf/openrocket/util/Prefs.java @@ -38,8 +38,8 @@ import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.RK4SimulationStepper; +import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; @@ -715,11 +715,22 @@ public class Prefs { return new Dimension(x, y); } + + public static boolean isWindowMaximized(Class c) { + String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null); + return "max".equals(pref); + } + public static void setWindowSize(Class c, Dimension d) { PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height); storeVersion(); } + public static void setWindowMaximized(Class c) { + PREFNODE.node("windows").put("size." + c.getCanonicalName(), "max"); + storeVersion(); + } + //// Printing diff --git a/src/net/sf/openrocket/util/WorldCoordinate.java b/src/net/sf/openrocket/util/WorldCoordinate.java index b3a047fd..762b0295 100644 --- a/src/net/sf/openrocket/util/WorldCoordinate.java +++ b/src/net/sf/openrocket/util/WorldCoordinate.java @@ -15,9 +15,10 @@ public class WorldCoordinate { /** * Constructs a new WorldCoordinate + * * @param lat latitude in degrees north. From -90 to 90, values outside are clamped. * @param lon longitude in degrees east. From -180 to 180, values outside are reduced to the range. - * @param alt altitude in m. Unbounded. + * @param alt altitude in meters. Unbounded. */ public WorldCoordinate(double lat, double lon, double alt) { this.lat = MathUtil.clamp(Math.toRadians(lat), -Math.PI / 2, Math.PI / 2); @@ -26,33 +27,35 @@ public class WorldCoordinate { } - + /** + * Returns the altitude. + */ public double getAltitude() { return this.alt; } - /* + /** * Returns Longitude in radians */ public double getLongitudeRad() { return this.lon; } - /* + /** * Returns Longitude in degrees */ public double getLongitudeDeg() { return Math.toDegrees(this.lon); } - /* + /** * Returns latitude in radians */ public double getLatitudeRad() { return this.lat; } - /* + /** * Returns latitude in degrees */ public double getLatitudeDeg() { @@ -60,9 +63,10 @@ public class WorldCoordinate { } + @Override public String toString() { - return "WorldCoordinate[lat=" + lat + ", lon=" + lon + ", alt=" + alt + "]"; + return "WorldCoordinate[lat=" + getLatitudeDeg() + ", lon=" + getLongitudeDeg() + ", alt=" + getAltitude() + "]"; } diff --git a/test/net/sf/openrocket/util/GeodeticComputationStrategyTest.java b/test/net/sf/openrocket/util/GeodeticComputationStrategyTest.java index 7adf21c8..72800d01 100644 --- a/test/net/sf/openrocket/util/GeodeticComputationStrategyTest.java +++ b/test/net/sf/openrocket/util/GeodeticComputationStrategyTest.java @@ -7,7 +7,7 @@ import org.junit.Test; public class GeodeticComputationStrategyTest { @Test - public void testAddCoordinate() { + public void testSpericalAddCoordinate() { double arcmin = (1.0 / 60.0); double arcsec = (1.0 / (60.0 * 60.0)); @@ -36,8 +36,109 @@ public class GeodeticComputationStrategyTest { assertEquals(1000.0, wc.getAltitude(), 0.0); } + + @Test + public void testAddCoordinates() { + + double min = 1 / 60.0; + double sec = 1 / 3600.0; + + + // Test zero movement + testAddCoordinate(50.0, 20.0, 0, 123, 50.0, 20.0, false); + + + /* + * These example values have been computed using the calculator at + * http://www.movable-type.co.uk/scripts/latlong.html + */ + + // Long distance NE over England, crosses Greenwich meridian + // 50 03N 005 42W to 58 38N 003 04E is 1109km at 027 16'07" + testAddCoordinate(50 + 3 * min, -5 - 42 * min, 1109000, 27 + 16 * min + 7 * sec, 58 + 38 * min, 3 + 4 * min, false); + + // SW over Brazil + // -10N -60E to -11N -61E is 155.9km at 224 25'34" + testAddCoordinate(-10, -60, 155900, 224 + 25 * min + 34 * sec, -11, -61, true); + + // NW over the 180 meridian + // 63N -179E to 63 01N 179E is 100.9km at 271 56'34" + testAddCoordinate(63, -179, 100900, 271 + 56 * min + 34 * sec, 63 + 1 * min, 179, true); + + // NE near the north pole + // 89 50N 0E to 89 45N 175E is 46.29 km at 003 00'01" + testAddCoordinate(89 + 50 * min, 0, 46290, 3 + 0 * min + 1 * sec, 89 + 45 * min, 175, false); + + // S directly over south pole + // -89 50N 12E to -89 45N 192E is 46.33km at 180 00'00" + testAddCoordinate(-89 - 50 * min, 12, 46330, 180, -89 - 45 * min, -168, false); + + } + + private void testAddCoordinate(double initialLatitude, double initialLongitude, double distance, double bearing, + double finalLatitude, double finalLongitude, boolean testFlat) { + + double tolerance; + + bearing = Math.toRadians(bearing); + + // positive X is EAST, positive Y is NORTH + double deltaX = distance * Math.sin(bearing); + double deltaY = distance * Math.cos(bearing); + + Coordinate coord = new Coordinate(deltaX, deltaY, 1000.0); + WorldCoordinate wc = new WorldCoordinate(initialLatitude, initialLongitude, 0.0); + + // Test SPHERICAL + tolerance = 0.0015 * distance / 111325; + System.out.println("\nSpherical tolerance: " + tolerance); + WorldCoordinate result = GeodeticComputationStrategy.SPHERICAL.addCoordinate(wc, coord); + + System.out.println("Difference Lat: " + Math.abs(finalLatitude - result.getLatitudeDeg())); + System.out.println("Difference Lon: " + Math.abs(finalLongitude - result.getLongitudeDeg())); + assertEquals(finalLatitude, result.getLatitudeDeg(), tolerance); + assertEquals(finalLongitude, result.getLongitudeDeg(), tolerance); + assertEquals(1000.0, result.getAltitude(), 0.0); + + + // Test WGS84 + /* + * TODO: Since the example values are computed using a spherical earth approximation, + * the WGS84 method will have significantly larger errors. The tolerance should be + * increased correspondingly. + */ + //tolerance = ... + System.out.println("\nWGS84 tolerance: " + tolerance); + result = GeodeticComputationStrategy.WGS84.addCoordinate(result, coord); + + System.out.println("Difference Lat: " + Math.abs(finalLatitude - result.getLatitudeDeg())); + System.out.println("Difference Lon: " + Math.abs(finalLongitude - result.getLongitudeDeg())); + // FIXME: Re-enable these when they function + // assertEquals(finalLatitude, result.getLatitudeDeg(), tolerance); + // assertEquals(finalLongitude, result.getLongitudeDeg(), tolerance); + // assertEquals(1000.0, result.getAltitude(), 0.0); + + + // Test FLAT + if (testFlat) { + tolerance = 0.02 * distance / 111325; + System.out.println("\nFlat tolerance: " + tolerance); + result = GeodeticComputationStrategy.FLAT.addCoordinate(wc, coord); + + System.out.println("Difference Lat: " + Math.abs(finalLatitude - result.getLatitudeDeg())); + System.out.println("Difference Lon: " + Math.abs(finalLongitude - result.getLongitudeDeg())); + assertEquals(finalLatitude, result.getLatitudeDeg(), tolerance); + assertEquals(finalLongitude, result.getLongitudeDeg(), tolerance); + assertEquals(1000.0, result.getAltitude(), 0.0); + + } + + } + + + @Test - public void testGetCoriolisAcceleration1() { + public void testSpericalGetCoriolisAcceleration() { // For positive latitude and rotational velocity, a movement due east results in an acceleration due south Coordinate velocity = new Coordinate(-1000, 0, 0); -- 2.30.2