SafetyMutex and rocket optimization updates
[debian/openrocket] / src / net / sf / openrocket / gui / scalefigure / RocketPanel.java
1 package net.sf.openrocket.gui.scalefigure;
2
3
4 import java.awt.Dimension;
5 import java.awt.Font;
6 import java.awt.Point;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.InputEvent;
9 import java.awt.event.MouseEvent;
10 import java.util.ArrayList;
11 import java.util.Collection;
12 import java.util.List;
13 import java.util.concurrent.Executor;
14 import java.util.concurrent.Executors;
15 import java.util.concurrent.ThreadFactory;
16
17 import javax.swing.AbstractAction;
18 import javax.swing.Action;
19 import javax.swing.JComboBox;
20 import javax.swing.JLabel;
21 import javax.swing.JPanel;
22 import javax.swing.JSlider;
23 import javax.swing.JToggleButton;
24 import javax.swing.JViewport;
25 import javax.swing.SwingUtilities;
26 import javax.swing.event.ChangeEvent;
27 import javax.swing.event.ChangeListener;
28 import javax.swing.event.TreeSelectionEvent;
29 import javax.swing.event.TreeSelectionListener;
30 import javax.swing.tree.TreePath;
31 import javax.swing.tree.TreeSelectionModel;
32
33 import net.miginfocom.swing.MigLayout;
34 import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
35 import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
36 import net.sf.openrocket.aerodynamics.FlightConditions;
37 import net.sf.openrocket.aerodynamics.WarningSet;
38 import net.sf.openrocket.document.OpenRocketDocument;
39 import net.sf.openrocket.document.Simulation;
40 import net.sf.openrocket.gui.adaptors.DoubleModel;
41 import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
42 import net.sf.openrocket.gui.components.BasicSlider;
43 import net.sf.openrocket.gui.components.StageSelector;
44 import net.sf.openrocket.gui.components.UnitSelector;
45 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
46 import net.sf.openrocket.gui.figureelements.CGCaret;
47 import net.sf.openrocket.gui.figureelements.CPCaret;
48 import net.sf.openrocket.gui.figureelements.Caret;
49 import net.sf.openrocket.gui.figureelements.RocketInfo;
50 import net.sf.openrocket.gui.main.SimulationWorker;
51 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
52 import net.sf.openrocket.masscalc.BasicMassCalculator;
53 import net.sf.openrocket.masscalc.MassCalculator;
54 import net.sf.openrocket.masscalc.MassCalculator.MassCalcType;
55 import net.sf.openrocket.rocketcomponent.Configuration;
56 import net.sf.openrocket.rocketcomponent.Rocket;
57 import net.sf.openrocket.rocketcomponent.RocketComponent;
58 import net.sf.openrocket.rocketcomponent.SymmetricComponent;
59 import net.sf.openrocket.simulation.FlightData;
60 import net.sf.openrocket.simulation.listeners.SimulationListener;
61 import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener;
62 import net.sf.openrocket.simulation.listeners.system.InterruptListener;
63 import net.sf.openrocket.unit.UnitGroup;
64 import net.sf.openrocket.util.ChangeSource;
65 import net.sf.openrocket.util.Chars;
66 import net.sf.openrocket.util.Coordinate;
67 import net.sf.openrocket.util.MathUtil;
68 import net.sf.openrocket.util.Prefs;
69
70 /**
71  * A JPanel that contains a RocketFigure and buttons to manipulate the figure. 
72  * 
73  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
74  */
75 public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
76         
77         private final RocketFigure figure;
78         private final ScaleScrollPane scrollPane;
79         
80         private JLabel infoMessage;
81         
82         private TreeSelectionModel selectionModel = null;
83         
84
85         /* Calculation of CP and CG */
86         private AerodynamicCalculator aerodynamicCalculator;
87         private MassCalculator massCalculator;
88         
89
90         private final OpenRocketDocument document;
91         private final Configuration configuration;
92         
93         private Caret extraCP = null;
94         private Caret extraCG = null;
95         private RocketInfo extraText = null;
96         
97
98         private double cpAOA = Double.NaN;
99         private double cpTheta = Double.NaN;
100         private double cpMach = Double.NaN;
101         private double cpRoll = Double.NaN;
102         
103         // The functional ID of the rocket that was simulated
104         private int flightDataFunctionalID = -1;
105         private String flightDataMotorID = null;
106         
107
108         private SimulationWorker backgroundSimulationWorker = null;
109         
110
111         private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
112         
113
114         /**
115          * The executor service used for running the background simulations.
116          * This uses a fixed-sized thread pool for all background simulations
117          * with all threads in daemon mode and with minimum priority.
118          */
119         private static final Executor backgroundSimulationExecutor;
120         static {
121                 backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
122                                 new ThreadFactory() {
123                                         private ThreadFactory factory = Executors.defaultThreadFactory();
124                                         
125                                         @Override
126                                         public Thread newThread(Runnable r) {
127                                                 Thread t = factory.newThread(r);
128                                                 t.setDaemon(true);
129                                                 t.setPriority(Thread.MIN_PRIORITY);
130                                                 return t;
131                                         }
132                                 });
133         }
134         
135         
136         public RocketPanel(OpenRocketDocument document) {
137                 
138                 this.document = document;
139                 configuration = document.getDefaultConfiguration();
140                 
141                 // TODO: FUTURE: calculator selection
142                 aerodynamicCalculator = new BarrowmanCalculator();
143                 massCalculator = new BasicMassCalculator();
144                 
145                 // Create figure and custom scroll pane
146                 figure = new RocketFigure(configuration);
147                 
148                 scrollPane = new ScaleScrollPane(figure) {
149                         @Override
150                         public void mouseClicked(MouseEvent event) {
151                                 handleMouseClick(event);
152                         }
153                 };
154                 scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
155                 scrollPane.setFitting(true);
156                 
157                 createPanel();
158                 
159                 configuration.addChangeListener(new ChangeListener() {
160                         @Override
161                         public void stateChanged(ChangeEvent e) {
162                                 System.out.println("Configuration changed, calling updateFigure");
163                                 updateExtras();
164                                 figure.updateFigure();
165                         }
166                 });
167         }
168         
169         
170         /**
171          * Creates the layout and components of the panel.
172          */
173         private void createPanel() {
174                 setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]"));
175                 
176                 setPreferredSize(new Dimension(800, 300));
177                 
178
179                 //// Create toolbar
180                 
181                 // Side/back buttons
182                 FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
183                 action.putValue(Action.NAME, "Side view");
184                 action.putValue(Action.SHORT_DESCRIPTION, "Side view");
185                 JToggleButton toggle = new JToggleButton(action);
186                 add(toggle, "spanx, split");
187                 
188                 action = new FigureTypeAction(RocketFigure.TYPE_BACK);
189                 action.putValue(Action.NAME, "Back view");
190                 action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
191                 toggle = new JToggleButton(action);
192                 add(toggle, "gap rel");
193                 
194
195                 // Zoom level selector
196                 ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
197                 add(scaleSelector);
198                 
199
200
201                 // Stage selector
202                 StageSelector stageSelector = new StageSelector(configuration);
203                 add(stageSelector, "");
204                 
205
206
207                 // Motor configuration selector
208                 
209                 JLabel label = new JLabel("Motor configuration:");
210                 label.setHorizontalAlignment(JLabel.RIGHT);
211                 add(label, "growx, right");
212                 add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap");
213                 
214
215
216
217
218                 // Create slider and scroll pane
219                 
220                 DoubleModel theta = new DoubleModel(figure, "Rotation",
221                                 UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
222                 UnitSelector us = new UnitSelector(theta, true);
223                 us.setHorizontalAlignment(JLabel.CENTER);
224                 add(us, "alignx 50%, growx");
225                 
226                 // Add the rocket figure
227                 add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap");
228                 
229
230                 // Add rotation slider
231                 // Minimum size to fit "360deg"
232                 JLabel l = new JLabel("360" + Chars.DEGREE);
233                 Dimension d = l.getPreferredSize();
234                 
235                 add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true),
236                                 "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy");
237                 
238
239                 infoMessage = new JLabel("<html>" +
240                                 "Click to select &nbsp;&nbsp; " +
241                                 "Shift+click to select other &nbsp;&nbsp; " +
242                                 "Double-click to edit &nbsp;&nbsp; " +
243                                 "Click+drag to move");
244                 infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
245                 add(infoMessage, "skip, span, gapleft 25, wrap");
246                 
247                 addExtras();
248         }
249         
250         
251
252         public RocketFigure getFigure() {
253                 return figure;
254         }
255         
256         public AerodynamicCalculator getAerodynamicCalculator() {
257                 return aerodynamicCalculator;
258         }
259         
260         public Configuration getConfiguration() {
261                 return configuration;
262         }
263         
264         public void setSelectionModel(TreeSelectionModel m) {
265                 if (selectionModel != null) {
266                         selectionModel.removeTreeSelectionListener(this);
267                 }
268                 selectionModel = m;
269                 selectionModel.addTreeSelectionListener(this);
270                 valueChanged((TreeSelectionEvent) null); // updates FigureParameters
271         }
272         
273         
274
275         /**
276          * Return the angle of attack used in CP calculation.  NaN signifies the default value
277          * of zero.
278          * @return   the angle of attack used, or NaN.
279          */
280         public double getCPAOA() {
281                 return cpAOA;
282         }
283         
284         /**
285          * Set the angle of attack to be used in CP calculation.  A value of NaN signifies that
286          * the default AOA (zero) should be used.
287          * @param aoa   the angle of attack to use, or NaN
288          */
289         public void setCPAOA(double aoa) {
290                 if (MathUtil.equals(aoa, cpAOA) ||
291                                 (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
292                         return;
293                 cpAOA = aoa;
294                 updateExtras();
295                 figure.updateFigure();
296                 fireChangeEvent();
297         }
298         
299         public double getCPTheta() {
300                 return cpTheta;
301         }
302         
303         public void setCPTheta(double theta) {
304                 if (MathUtil.equals(theta, cpTheta) ||
305                                 (Double.isNaN(theta) && Double.isNaN(cpTheta)))
306                         return;
307                 cpTheta = theta;
308                 if (!Double.isNaN(theta))
309                         figure.setRotation(theta);
310                 updateExtras();
311                 figure.updateFigure();
312                 fireChangeEvent();
313         }
314         
315         public double getCPMach() {
316                 return cpMach;
317         }
318         
319         public void setCPMach(double mach) {
320                 if (MathUtil.equals(mach, cpMach) ||
321                                 (Double.isNaN(mach) && Double.isNaN(cpMach)))
322                         return;
323                 cpMach = mach;
324                 updateExtras();
325                 figure.updateFigure();
326                 fireChangeEvent();
327         }
328         
329         public double getCPRoll() {
330                 return cpRoll;
331         }
332         
333         public void setCPRoll(double roll) {
334                 if (MathUtil.equals(roll, cpRoll) ||
335                                 (Double.isNaN(roll) && Double.isNaN(cpRoll)))
336                         return;
337                 cpRoll = roll;
338                 updateExtras();
339                 figure.updateFigure();
340                 fireChangeEvent();
341         }
342         
343         
344
345         @Override
346         public void addChangeListener(ChangeListener listener) {
347                 listeners.add(0, listener);
348         }
349         
350         @Override
351         public void removeChangeListener(ChangeListener listener) {
352                 listeners.remove(listener);
353         }
354         
355         protected void fireChangeEvent() {
356                 ChangeEvent e = new ChangeEvent(this);
357                 ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
358                 for (ChangeListener l : list) {
359                         l.stateChanged(e);
360                 }
361         }
362         
363         
364
365
366         /**
367          * Handle clicking on figure shapes.  The functioning is the following:
368          * 
369          * Get the components clicked.
370          * If no component is clicked, do nothing.
371          * If the currently selected component is in the set, keep it, 
372          * unless the selector specified is pressed.  If it is pressed, cycle to 
373          * the next component. Otherwise select the first component in the list. 
374          */
375         public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
376         
377         private void handleMouseClick(MouseEvent event) {
378                 if (event.getButton() != MouseEvent.BUTTON1)
379                         return;
380                 Point p0 = event.getPoint();
381                 Point p1 = scrollPane.getViewport().getViewPosition();
382                 int x = p0.x + p1.x;
383                 int y = p0.y + p1.y;
384                 
385                 RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
386                 
387                 // If no component is clicked, do nothing
388                 if (clicked.length == 0)
389                         return;
390                 
391                 // Check whether the currently selected component is in the clicked components.
392                 TreePath path = selectionModel.getSelectionPath();
393                 if (path != null) {
394                         RocketComponent current = (RocketComponent) path.getLastPathComponent();
395                         path = null;
396                         for (int i = 0; i < clicked.length; i++) {
397                                 if (clicked[i] == current) {
398                                         if (event.isShiftDown() && (event.getClickCount() == 1)) {
399                                                 path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]);
400                                         } else {
401                                                 path = ComponentTreeModel.makeTreePath(clicked[i]);
402                                         }
403                                         break;
404                                 }
405                         }
406                 }
407                 
408                 // Currently selected component not clicked
409                 if (path == null) {
410                         if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) {
411                                 path = ComponentTreeModel.makeTreePath(clicked[1]);
412                         } else {
413                                 path = ComponentTreeModel.makeTreePath(clicked[0]);
414                         }
415                 }
416                 
417                 // Set selection and check for double-click
418                 selectionModel.setSelectionPath(path);
419                 if (event.getClickCount() == 2) {
420                         RocketComponent component = (RocketComponent) path.getLastPathComponent();
421                         
422                         ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
423                                         document, component);
424                 }
425         }
426         
427         
428
429
430         /**
431          * Updates the extra data included in the figure.  Currently this includes
432          * the CP and CG carets.
433          */
434         private WarningSet warnings = new WarningSet();
435         
436         private void updateExtras() {
437                 Coordinate cp, cg;
438                 double cpx, cgx;
439                 
440                 // TODO: MEDIUM: User-definable conditions
441                 FlightConditions conditions = new FlightConditions(configuration);
442                 warnings.clear();
443                 
444                 if (!Double.isNaN(cpMach)) {
445                         conditions.setMach(cpMach);
446                         extraText.setMach(cpMach);
447                 } else {
448                         conditions.setMach(Prefs.getDefaultMach());
449                         extraText.setMach(Prefs.getDefaultMach());
450                 }
451                 
452                 if (!Double.isNaN(cpAOA)) {
453                         conditions.setAOA(cpAOA);
454                 } else {
455                         conditions.setAOA(0);
456                 }
457                 extraText.setAOA(cpAOA);
458                 
459                 if (!Double.isNaN(cpRoll)) {
460                         conditions.setRollRate(cpRoll);
461                 } else {
462                         conditions.setRollRate(0);
463                 }
464                 
465                 if (!Double.isNaN(cpTheta)) {
466                         conditions.setTheta(cpTheta);
467                         cp = aerodynamicCalculator.getCP(configuration, conditions, warnings);
468                 } else {
469                         cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings);
470                 }
471                 extraText.setTheta(cpTheta);
472                 
473
474                 cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS);
475                 //              System.out.println("CG computed as "+cg+ " CP as "+cp);
476                 
477                 if (cp.weight > 0.000001)
478                         cpx = cp.x;
479                 else
480                         cpx = Double.NaN;
481                 
482                 if (cg.weight > 0.000001)
483                         cgx = cg.x;
484                 else
485                         cgx = Double.NaN;
486                 
487                 // Length bound is assumed to be tight
488                 double length = 0, diameter = 0;
489                 Collection<Coordinate> bounds = configuration.getBounds();
490                 if (!bounds.isEmpty()) {
491                         double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
492                         for (Coordinate c : bounds) {
493                                 if (c.x < minX)
494                                         minX = c.x;
495                                 if (c.x > maxX)
496                                         maxX = c.x;
497                         }
498                         length = maxX - minX;
499                 }
500                 
501                 for (RocketComponent c : configuration) {
502                         if (c instanceof SymmetricComponent) {
503                                 double d1 = ((SymmetricComponent) c).getForeRadius() * 2;
504                                 double d2 = ((SymmetricComponent) c).getAftRadius() * 2;
505                                 diameter = MathUtil.max(diameter, d1, d2);
506                         }
507                 }
508                 
509                 extraText.setCG(cgx);
510                 extraText.setCP(cpx);
511                 extraText.setLength(length);
512                 extraText.setDiameter(diameter);
513                 extraText.setMass(cg.weight);
514                 extraText.setWarnings(warnings);
515                 
516
517                 if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
518                         
519                         // TODO: LOW: Y-coordinate and rotation
520                         extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
521                         extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
522                         
523                 } else {
524                         
525                         extraCP.setPosition(Double.NaN, Double.NaN);
526                         extraCG.setPosition(Double.NaN, Double.NaN);
527                         
528                 }
529                 
530
531                 ////////  Flight simulation in background
532                 
533                 // Check whether to compute or not
534                 if (!Prefs.computeFlightInBackground()) {
535                         extraText.setFlightData(null);
536                         extraText.setCalculatingData(false);
537                         stopBackgroundSimulation();
538                         return;
539                 }
540                 
541                 // Check whether data is already up to date
542                 if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
543                                 flightDataMotorID == configuration.getMotorConfigurationID()) {
544                         return;
545                 }
546                 
547                 flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
548                 flightDataMotorID = configuration.getMotorConfigurationID();
549                 
550                 // Stop previous computation (if any)
551                 stopBackgroundSimulation();
552                 
553                 // Check that configuration has motors
554                 if (!configuration.hasMotors()) {
555                         extraText.setFlightData(FlightData.NaN_DATA);
556                         extraText.setCalculatingData(false);
557                         return;
558                 }
559                 
560                 // Start calculation process
561                 extraText.setCalculatingData(true);
562                 
563                 Rocket duplicate = (Rocket) configuration.getRocket().copy();
564                 Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
565                 simulation.getConditions().setMotorConfigurationID(
566                                 configuration.getMotorConfigurationID());
567                 
568                 backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
569                 backgroundSimulationExecutor.execute(backgroundSimulationWorker);
570         }
571         
572         /**
573          * Cancels the current background simulation worker, if any.
574          */
575         private void stopBackgroundSimulation() {
576                 if (backgroundSimulationWorker != null) {
577                         backgroundSimulationWorker.cancel(true);
578                         backgroundSimulationWorker = null;
579                 }
580         }
581         
582         
583         /**
584          * A SimulationWorker that simulates the rocket flight in the background and
585          * sets the results to the extra text when finished.  The worker can be cancelled
586          * if necessary.
587          */
588         private class BackgroundSimulationWorker extends SimulationWorker {
589                 
590                 public BackgroundSimulationWorker(Simulation sim) {
591                         super(sim);
592                 }
593                 
594                 @Override
595                 protected FlightData doInBackground() {
596                         
597                         // Pause a little while to allow faster UI reaction
598                         try {
599                                 Thread.sleep(300);
600                         } catch (InterruptedException ignore) {
601                         }
602                         if (isCancelled() || backgroundSimulationWorker != this)
603                                 return null;
604                         
605                         return super.doInBackground();
606                 }
607                 
608                 @Override
609                 protected void simulationDone() {
610                         // Do nothing if cancelled
611                         if (isCancelled() || backgroundSimulationWorker != this)
612                                 return;
613                         
614                         backgroundSimulationWorker = null;
615                         extraText.setFlightData(simulation.getSimulatedData());
616                         extraText.setCalculatingData(false);
617                         figure.repaint();
618                 }
619                 
620                 @Override
621                 protected SimulationListener[] getExtraListeners() {
622                         return new SimulationListener[] {
623                                         InterruptListener.INSTANCE,
624                                         ApogeeEndListener.INSTANCE };
625                 }
626                 
627                 @Override
628                 protected void simulationInterrupted(Throwable t) {
629                         // Do nothing on cancel, set N/A data otherwise
630                         if (isCancelled() || backgroundSimulationWorker != this) // Double-check
631                                 return;
632                         
633                         backgroundSimulationWorker = null;
634                         extraText.setFlightData(FlightData.NaN_DATA);
635                         extraText.setCalculatingData(false);
636                         figure.repaint();
637                 }
638         }
639         
640         
641
642         /**
643          * Adds the extra data to the figure.  Currently this includes the CP and CG carets.
644          */
645         private void addExtras() {
646                 figure.clearRelativeExtra();
647                 extraCG = new CGCaret(0, 0);
648                 extraCP = new CPCaret(0, 0);
649                 extraText = new RocketInfo(configuration);
650                 updateExtras();
651                 figure.addRelativeExtra(extraCP);
652                 figure.addRelativeExtra(extraCG);
653                 figure.addAbsoluteExtra(extraText);
654         }
655         
656         
657         /**
658          * Updates the selection in the FigureParameters and repaints the figure.  
659          * Ignores the event itself.
660          */
661         @Override
662         public void valueChanged(TreeSelectionEvent e) {
663                 TreePath[] paths = selectionModel.getSelectionPaths();
664                 if (paths == null) {
665                         figure.setSelection(null);
666                         return;
667                 }
668                 
669                 RocketComponent[] components = new RocketComponent[paths.length];
670                 for (int i = 0; i < paths.length; i++)
671                         components[i] = (RocketComponent) paths[i].getLastPathComponent();
672                 figure.setSelection(components);
673         }
674         
675         
676
677         /**
678          * An <code>Action</code> that shows whether the figure type is the type
679          * given in the constructor.
680          * 
681          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
682          */
683         private class FigureTypeAction extends AbstractAction implements ChangeListener {
684                 private final int type;
685                 
686                 public FigureTypeAction(int type) {
687                         this.type = type;
688                         stateChanged(null);
689                         figure.addChangeListener(this);
690                 }
691                 
692                 @Override
693                 public void actionPerformed(ActionEvent e) {
694                         boolean state = (Boolean) getValue(Action.SELECTED_KEY);
695                         if (state == true) {
696                                 // This view has been selected
697                                 figure.setType(type);
698                                 updateExtras();
699                         }
700                         stateChanged(null);
701                 }
702                 
703                 @Override
704                 public void stateChanged(ChangeEvent e) {
705                         putValue(Action.SELECTED_KEY, figure.getType() == type);
706                 }
707         }
708         
709 }