package net.sf.openrocket.gui.configdialog;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.gui.SpinnerEditor;
import net.sf.openrocket.gui.adaptors.DoubleModel;
import net.sf.openrocket.gui.adaptors.EnumModel;
import net.sf.openrocket.gui.adaptors.MaterialModel;
import net.sf.openrocket.gui.components.BasicSlider;
import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.components.StyledLabel.Style;
import net.sf.openrocket.gui.components.UnitSelector;
import net.sf.openrocket.l10n.Translator;
import net.sf.openrocket.logging.Markers;
import net.sf.openrocket.material.Material;
import net.sf.openrocket.rocketcomponent.CenteringRing;
import net.sf.openrocket.rocketcomponent.Coaxial;
import net.sf.openrocket.rocketcomponent.FinSet;
import net.sf.openrocket.rocketcomponent.FreeformFinSet;
import net.sf.openrocket.rocketcomponent.InnerTube;
import net.sf.openrocket.rocketcomponent.RocketComponent;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.unit.UnitGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class FinSetConfig extends RocketComponentConfig {
private static final Logger log = LoggerFactory.getLogger(FinSetConfig.class);
private static final Translator trans = Application.getTranslator();
private JButton split = null;
public FinSetConfig(OpenRocketDocument d, RocketComponent component) {
super(d, component);
//// Fin tabs and Through-the-wall fin tabs
tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(),
trans.get("FinSetConfig.tab.Through-the-wall"), 0);
}
protected void addFinSetButtons() {
JButton convert = null;
//// Convert buttons
if (!(component instanceof FreeformFinSet)) {
//// Convert to freeform
convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform"));
//// Convert this fin set into a freeform fin set
convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip"));
convert.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
log.info(Markers.USER_MARKER, "Converting " + component.getComponentName() + " into freeform fin set");
// Do change in future for overall safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
//// Convert fin set
document.addUndoPosition(trans.get("FinSetConfig.Convertfinset"));
RocketComponent freeform =
FreeformFinSet.convertFinSet((FinSet) component);
ComponentConfigDialog.showDialog(freeform);
}
});
ComponentConfigDialog.hideDialog();
}
});
}
//// Split fins
split = new JButton(trans.get("FinSetConfig.but.Splitfins"));
//// Split the fin set into separate fins
split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip"));
split.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
log.info(Markers.USER_MARKER, "Splitting " + component.getComponentName() + " into separate fins, fin count=" +
((FinSet) component).getFinCount());
// Do change in future for overall safety
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
RocketComponent parent = component.getParent();
int index = parent.getChildPosition(component);
int count = ((FinSet) component).getFinCount();
double base = ((FinSet) component).getBaseRotation();
if (count <= 1)
return;
document.addUndoPosition("Split fin set");
parent.removeChild(index);
for (int i = 0; i < count; i++) {
FinSet copy = (FinSet) component.copy();
copy.setFinCount(1);
copy.setBaseRotation(base + i * 2 * Math.PI / count);
copy.setName(copy.getName() + " #" + (i + 1));
parent.addChild(copy, index + i);
}
}
});
ComponentConfigDialog.hideDialog();
}
});
split.setEnabled(((FinSet) component).getFinCount() > 1);
if (convert == null)
addButtons(split);
else
addButtons(split, convert);
}
public JPanel finTabPanel() {
JPanel panel = new JPanel(
new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%",
"[150lp::][65lp::][30lp::][200lp::]", ""));
// JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel",
// "[40lp][80lp::][30lp::][100lp::]",""));
//// Through-the-wall fin tabs:
panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD),
"spanx, wrap 30lp");
JLabel label;
DoubleModel length;
DoubleModel length2;
DoubleModel length_2;
JSpinner spin;
JButton autoCalc;
length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0);
length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0);
length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0);
register(length);
register(length2);
register(length_2);
//// Tab length
//// Tab length:
label = new JLabel(trans.get("FinSetConfig.lbl.Tablength"));
//// The length of the fin tab.
label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength"));
panel.add(label, "gapleft para, gapright 40lp, growx 1");
final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0);
spin = new JSpinner(mtl.getSpinnerModel());
spin.setEditor(new SpinnerEditor(spin));
panel.add(spin, "growx 1");
panel.add(new UnitSelector(mtl), "growx 1");
panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)),
"w 100lp, growx 5, wrap");
//// Tab length
//// Tab height:
label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight"));
//// The spanwise height of the fin tab.
label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight"));
panel.add(label, "gapleft para");
final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0);
spin = new JSpinner(mth.getSpinnerModel());
spin.setEditor(new SpinnerEditor(spin));
panel.add(spin, "growx");
panel.add(new UnitSelector(mth), "growx");
panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)),
"w 100lp, growx 5, wrap");
//// Tab position:
label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition"));
//// The position of the fin tab.
label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition"));
panel.add(label, "gapleft para");
final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH);
spin = new JSpinner(mts.getSpinnerModel());
spin.setEditor(new SpinnerEditor(spin));
panel.add(spin, "growx");
panel.add(new UnitSelector(mts), "growx");
panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap");
//// relative to
label = new JLabel(trans.get("FinSetConfig.lbl.relativeto"));
panel.add(label, "right, gapright unrel");
final EnumModel<FinSet.TabRelativePosition> em =
new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition");
panel.add(new JComboBox(em), "spanx 3, growx, wrap para");
// Calculate fin tab height, length, and position
autoCalc = new JButton(trans.get("FinSetConfig.but.AutoCalc"));
autoCalc.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
log.info(Markers.USER_MARKER, "Computing " + component.getComponentName() + " tab height.");
RocketComponent parent = component.getParent();
if (parent instanceof Coaxial) {
try {
document.startUndo("Compute fin tabs");
List<CenteringRing> rings = new ArrayList<CenteringRing>();
//Do deep recursive iteration
Iterator<RocketComponent> iter = parent.iterator(false);
while (iter.hasNext()) {
RocketComponent rocketComponent = iter.next();
if (rocketComponent instanceof InnerTube) {
InnerTube it = (InnerTube) rocketComponent;
if (it.isMotorMount()) {
double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius();
//Set fin tab depth
if (depth >= 0.0d) {
mth.setValue(depth);
mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
}
}
} else if (rocketComponent instanceof CenteringRing) {
rings.add((CenteringRing) rocketComponent);
}
}
//Figure out position and length of the fin tab
if (!rings.isEmpty()) {
FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem();
em.setSelectedItem(FinSet.TabRelativePosition.FRONT);
double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP, parent),
component.getLength(), mts, parent);
mtl.setValue(len);
//Be nice to the user and set the tab relative position enum back the way they had it.
em.setSelectedItem(temp);
}
} finally {
document.stopUndo();
}
}
}
});
panel.add(autoCalc, "skip 1, spanx");
return panel;
}
/**
* Scenarios:
* <p/>
* 1. All rings ahead of start of fin.
* 2. First ring ahead of start of fin. Second ring ahead of end of fin.
* 3. First ring ahead of start of fin. Second ring behind end of fin.
* 4. First ring equal or behind start of fin. Second ring ahead of, or equal to, end of fin.
* 5. First ring equal or behind start of fin. Second ring behind end of fin.
* 6. All rings behind end of fin.
*
* @param rings an unordered list of centering rings attached to the parent of the fin set
* @param finPositionFromTop the position from the top of the parent of the start of the fin set root
* @param finLength the length of the root chord
* @param mts the model for the tab shift (position); the model's value is modified as a result of this method call
* @param relativeTo the parent component of the finset
*
* @return the length of the fin tab
*/
private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts,
final RocketComponent relativeTo) {
List<SortableRing> positionsFromTop = new ArrayList<SortableRing>();
//Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here.
SortableRing top = null;
SortableRing bottom = null;
if (rings != null) {
//Sort rings from top of parent to bottom
Collections.sort(rings, new Comparator<CenteringRing>() {
@Override
public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) {
return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo) -
centeringRing1.asPositionValue(RocketComponent.Position.TOP, relativeTo)));
}
});
for (int i = 0; i < rings.size(); i++) {
CenteringRing centeringRing = rings.get(i);
//Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring.
if (!positionsFromTop.isEmpty() &&
positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >=
centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo)) {
SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1);
adjacent.merge(centeringRing, relativeTo);
} else {
positionsFromTop.add(new SortableRing(centeringRing, relativeTo));
}
}
for (int i = 0; i < positionsFromTop.size(); i++) {
SortableRing sortableRing = positionsFromTop.get(i);
if (top == null) {
top = sortableRing;
} else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) {
top = sortableRing;
bottom = null;
} else if (top.bottomSidePositionFromTop() <= finPositionFromTop) {
if (bottom == null) {
//If the current ring is in the upper half of the root chord, make it the top ring
if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) {
top = sortableRing;
} else {
bottom = sortableRing;
}
}
//Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring,
//and the current ring the bottom
else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) {
top = bottom;
bottom = sortableRing;
}
} else {
if (bottom == null) {
bottom = sortableRing;
}
}
}
}
double resultFinTabLength = 0d;
// Edge case where there are no centering rings or for some odd reason top and bottom are identical.
if (top == null || top == bottom) {
mts.setValue(0);
resultFinTabLength = finLength;
} else if (bottom == null) {
// If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then
// set the position of the fin tab starting at the bottom side of the top ring.
if (top.bottomSidePositionFromTop() >= finPositionFromTop) {
mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
resultFinTabLength = (finPositionFromTop + finLength - top.bottomSidePositionFromTop());
} else {
mts.setValue(0);
double diffLen = top.positionFromTop() - finPositionFromTop;
if (diffLen < 0) {
// Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire
// root chord.
resultFinTabLength = finLength;
}
else {
// Otherwise there is one ring within the span. Return the length from the start of the fin to the top
// side of the ring.
resultFinTabLength = diffLen;
}
}
}
// If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the
// fin tab align with the start of the root chord.
else if (top.bottomSidePositionFromTop() < finPositionFromTop) {
mts.setValue(0);
double lenToBottomRing = bottom.positionFromTop - finPositionFromTop;
// If the bottom ring lies farther back (down) than the trailing edge of the fin, then the tab should
// only be as long as the fin.
if (lenToBottomRing > finLength) {
resultFinTabLength = finLength;
}
else {
resultFinTabLength = lenToBottomRing;
}
} else {
mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop);
// The bottom ring is beyond the trailing edge of the fin.
if (bottom.positionFromTop() > finLength + finPositionFromTop) {
resultFinTabLength = (finLength + finPositionFromTop - top.bottomSidePositionFromTop());
}
// The rings are within the span of the root chord. Place the tab between them.
else {
resultFinTabLength = (bottom.positionFromTop() - top.bottomSidePositionFromTop());
}
}
if (resultFinTabLength < 0) {
resultFinTabLength = 0d;
}
return resultFinTabLength;
}
@Override
public void updateFields() {
super.updateFields();
if (split != null)
split.setEnabled(((FinSet) component).getFinCount() > 1);
}
/**
* A container class to store pertinent info about centering rings. This is used in the computation to figure
* out tab length and position.
*/
static class SortableRing {
/**
* The length of the ring (more commonly called the thickness).
*/
private double thickness;
/**
* The position of the ring from the top of the parent.
*/
private double positionFromTop;
/**
* Constructor.
*
* @param r the source centering ring
*/
SortableRing(CenteringRing r, RocketComponent relativeTo) {
thickness = r.getLength();
positionFromTop = r.asPositionValue(RocketComponent.Position.TOP, relativeTo);
}
/**
* Merge an adjacent ring.
*
* @param adjacent the adjacent ring
*/
public void merge(CenteringRing adjacent, RocketComponent relativeTo) {
double v = adjacent.asPositionValue(RocketComponent.Position.TOP, relativeTo);
if (positionFromTop < v) {
thickness = (v + adjacent.getLength()) - positionFromTop;
} else {
double tmp = positionFromTop + thickness;
positionFromTop = v;
thickness = tmp - v;
}
}
/**
* Compute the position of the bottom edge of the ring, relative to the top of the parent.
*
* @return the distance from the top of the parent to the bottom edge of the ring
*/
public double bottomSidePositionFromTop() {
return positionFromTop + thickness;
}
/**
* Compute the position of the top edge of the ring, relative to the top of the parent.
*
* @return the distance from the top of the parent to the top edge of the ring
*/
public double positionFromTop() {
return positionFromTop;
}
}
protected JPanel filletMaterialPanel(){
JPanel filletPanel=new JPanel(new MigLayout("", "[][65lp::][30lp::]"));
String tip = trans.get("FinsetCfg.ttip.Finfillets1") +
trans.get("FinsetCfg.ttip.Finfillets2") +
trans.get("FinsetCfg.ttip.Finfillets3");
filletPanel.setBorder(BorderFactory.createTitledBorder("Root Fillets"));
filletPanel.add(new JLabel(trans.get("FinSetCfg.lbl.Filletradius")));
DoubleModel m = new DoubleModel(component, "FilletRadius", UnitGroup.UNITS_LENGTH, 0);
JSpinner spin = new JSpinner(m.getSpinnerModel());
spin.setEditor(new SpinnerEditor(spin));
spin.setToolTipText(tip);
filletPanel.add(spin, "growx, w 40");
UnitSelector us = new UnitSelector(m);
filletPanel.add(us, "growx");
us.setToolTipText(tip);
BasicSlider bs =new BasicSlider(m.getSliderModel(0, 10));
filletPanel.add(bs, "w 100lp, wrap para");
bs.setToolTipText(tip);
JLabel label = new JLabel(trans.get("FinSetCfg.lbl.Finfilletmaterial"));
label.setToolTipText(tip);
//// The component material affects the weight of the component.
label.setToolTipText(trans.get("RocketCompCfg.lbl.ttip.componentmaterialaffects"));
filletPanel.add(label, "spanx 4, wrap rel");
JComboBox combo = new JComboBox(new MaterialModel(filletPanel, component, Material.Type.BULK, "FilletMaterial"));
//// The component material affects the weight of the component.
combo.setToolTipText(trans.get("RocketCompCfg.combo.ttip.componentmaterialaffects"));
filletPanel.add(combo, "spanx 4, growx, wrap paragraph");
filletPanel.setToolTipText(tip);
return filletPanel;
}
}