001    /*
002    Copyright (c) 1996-2011, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.clApp.atHome;
031    
032    import java.awt.BorderLayout;
033    import java.awt.GridBagConstraints;
034    import java.awt.GridBagLayout;
035    import java.awt.Insets;
036    import java.awt.event.ActionEvent;
037    import java.awt.event.ActionListener;
038    import java.awt.event.MouseAdapter;
039    import java.awt.event.MouseEvent;
040    import java.awt.event.WindowAdapter;
041    import java.awt.event.WindowEvent;
042    import java.util.Arrays;
043    
044    import javax.jnlp.SingleInstanceListener;
045    import javax.swing.AbstractButton;
046    import javax.swing.Action;
047    import javax.swing.BorderFactory;
048    import javax.swing.JComponent;
049    import javax.swing.JFrame;
050    import javax.swing.JLabel;
051    import javax.swing.JMenu;
052    import javax.swing.JMenuBar;
053    import javax.swing.JOptionPane;
054    import javax.swing.JPanel;
055    import javax.swing.JProgressBar;
056    import javax.swing.JSlider;
057    import javax.swing.Timer;
058    import javax.swing.UIManager;
059    
060    import org.hd.d.pg2k.ai.scorer.MiniScorerCacheImpl;
061    import org.hd.d.pg2k.ai.scorer.ScoreAndConf;
062    import org.hd.d.pg2k.svrCore.Rnd;
063    import org.hd.d.pg2k.svrCore.SimpleLoggerIF;
064    import org.hd.d.pg2k.svrCore.TextUtils;
065    import org.hd.d.pg2k.svrCore.Tuple;
066    
067    
068    /**Main (UI) class of JWS-based exhibit uploader.
069     * Runs as a Swing App using JNLP resource-access facilities
070     * such as persistence and file read/write.
071     * <p>
072     * This class/file contains as little non-UI code as is reasonably practical,
073     * so that if we change the UI details then other classes should be unaffected.
074     * <p>
075     * We don't really want/need this class to be Serializable,
076     * but this class inherits Serializable from JFrame.
077     */
078    public final class AHJWSMain extends JFrame implements ActionListener
079        {
080        /**Central logger instance for uploader; never null.
081         * This instance may log to the status bar and elsewhere.
082         */
083        private final SimpleLoggerIF logger = new SimpleLoggerIF()
084            {
085            public final void log(final String message)
086                {
087                // Log to the Java console.
088                System.out.println(message);
089    
090                // Schedule a job for the event-dispatching thread:
091                // logging this message in the status bar.
092                // We hope that these events do not get re-ordered too often.
093                javax.swing.SwingUtilities.invokeLater(new Runnable(){
094                    public final void run()
095                        { status.setText(message); }
096                    });
097                }
098            };
099    
100        /**Our companion "business-logic" class; never null. */
101        private final AHStandaloneMain logic;
102    
103        /**The action performed by the "About" menu entry. */
104        private final AboutAction aboutAction = new AboutAction();
105        /**The action performed by the "Exit" menu entry. */
106        private final ExitAction exitAction = new ExitAction();
107    
108        /**Status bar; never null. */
109        private final JLabel status = createStatusBar();
110    
111        /**Single listener instance. */
112        private final SISListener sisListener = new SISListener();
113    
114        /**Handles Mouse over messages on toolbar buttons and menu items; never null. */
115        private final MouseHandler mouseHandler = new MouseHandler(status);
116    
117        /**Title shown for application. */
118        private static final String APPLICATION_WINDOW_TITLE = "Gallery 'At Home' Worker";
119    
120    
121        /**Create an instance of the Worker app main window.
122         * Designed to be called by main().
123         */
124        private AHJWSMain()
125            {
126            super(APPLICATION_WINDOW_TITLE);
127    
128            // Set up user actions.
129            initActions();
130    
131            // Create the logic/worker.
132            logic = new AHStandaloneMain(logger);
133    
134            // ASAP, try to avoid multiple instances being started.
135            if(logic.sis != null)
136                { logic.sis.addSingleInstanceListener(sisListener); }
137    
138            // Create the contents of this frame...
139            setJMenuBar(createMenu());
140            final Tuple.Pair<JComponent, ActionListener> mainPane = createMainPane();
141            pollAL = mainPane.second;
142            assert(mainPane != null);
143            assert(mainPane.first != null);
144            assert(mainPane.second != null);
145            getContentPane().add(mainPane.first, BorderLayout.CENTER);
146            getContentPane().add(status, BorderLayout.SOUTH);
147    
148            // Arrange to exit the VM if the window is closed...
149            addWindowListener(new WindowAdapter()
150                {
151                @Override
152                public final void windowClosing(final WindowEvent evt)
153                    {
154                    // Shut down gracefully (and exit()) if allowed to,
155                    // else attempt to veto with an exception.
156                    try { shutdown(); }
157                    catch(final UnsupportedOperationException e) { }
158                    }
159                });
160            }
161    
162        /**Perform any activity required to shut down cleanly, eg save state, then exit.
163         * This should try to avoid taking a long time.
164         * <p>
165         * This may throw an UnsupportedOperationException to try to veto
166         * an exit that the user changes their mind about,
167         * eg because we have work in progress!
168         *
169         * @throws UnsupportedOperationException  if the user vetoes the shut-down
170         */
171        private void shutdown()
172            throws UnsupportedOperationException
173            {
174            // Do any shutdown required by the non-GUI components...
175            try { logic.shutdown(); }
176            catch(final Throwable t) { /* Absorb: we're trying to quit ASAP... */ }
177    
178            // Now allow other instances to be started...
179            if(logic.sis != null)
180                { logic.sis.removeSingleInstanceListener(sisListener); }
181    
182            // Now allow a gracefull exit!
183            try { System.exit(0); }
184            catch(final Throwable t) { /* Absorb: we're trying to quit ASAP... */ }
185            }
186    
187        /**This method should be called before creating the UI to create all the Actions. */
188        private void initActions()
189            {
190    //        actions.clear();
191            registerAction(aboutAction);
192            registerAction(exitAction);
193            }
194    
195        private void registerAction(final JLFAbstractAction action)
196            {
197            action.addActionListener(this);
198    //        actions.addElement(action);
199            }
200    
201        /**Create the application menu bar. */
202        private JMenuBar createMenu()
203            {
204            final JMenuBar menuBar = new JMenuBar();
205    
206            // Build the File menu
207            final JMenu fileMenu = new JMenu("File");
208            fileMenu.setMnemonic('F');
209            fileMenu.add(exitAction).addMouseListener(mouseHandler);
210    
211            // Build the help menu
212            final JMenu helpMenu = new JMenu("Help");
213            helpMenu.setMnemonic('H');
214            helpMenu.add(aboutAction).addMouseListener(mouseHandler);
215    
216            menuBar.add(fileMenu);
217            menuBar.add(helpMenu);
218    
219            return menuBar;
220            }
221    
222        /**Create the (main) tabbed pane component and a lister for UI polling. */
223        private Tuple.Pair<JComponent, ActionListener> createMainPane()
224            {
225            // Have a lone simple status panel for now.
226            return(createStatusPanel());
227            }
228    
229        /**Make the login/status component; never null.
230         * Also creates a listener suitable to poll regularly
231         * from the Swing thread to do async UI updates.
232         */
233        private Tuple.Pair<JComponent, ActionListener> createStatusPanel()
234            {
235            final JPanel pane = new JPanel(new GridBagLayout());
236            final GridBagConstraints c = new GridBagConstraints();
237    
238            // Wrap insets round everything.
239            c.insets = makeStandardInsets();
240    
241            // Heading top (left).
242            c.gridy = 0;
243            c.gridx = 0;
244            c.gridwidth = GridBagConstraints.REMAINDER; // Span all columns.
245            final JLabel heading = new JLabel("<html><strong>STATUS</strong></html>");
246            pane.add(heading, c);
247            c.gridwidth = 1; // Back to default.
248    
249            // New/best Scorers reported home.
250            c.gridx = 0;
251            ++c.gridy;
252            final JLabel genLabel = new JLabel("Generation:");
253            pane.add(genLabel, c);
254            ++c.gridx;
255            final JLabel gen = new JLabel("????"); // Pad to roughly appropriate size.
256            pane.add(gen, c);
257            genLabel.setLabelFor(gen);
258    
259            // New/best Scorers reported home.
260            c.gridx = 0;
261            ++c.gridy;
262            final JLabel scReportedLabel = new JLabel("New/best Scorers found/reported:");
263            pane.add(scReportedLabel, c);
264            ++c.gridx;
265            final JLabel scReported = new JLabel("??????"); // Pad to roughly appropriate size.
266            pane.add(scReported, c);
267            scReportedLabel.setLabelFor(scReported);
268    
269            // Rating of last/best Scorer found as % of best possible...
270            c.gridx = 0;
271            ++c.gridy;
272            final JLabel scLastPCLabel = new JLabel("Last reported Scorer's rating:");
273            pane.add(scLastPCLabel, c);
274            ++c.gridx;
275            final JProgressBar scLastPC = new JProgressBar(0, 100);
276            scLastPC.setStringPainted(true); // Show a string.
277            scLastPC.setString("-"); // Hide % string.
278            scLastPC.setIndeterminate(false);
279            pane.add(scLastPC, c);
280            scLastPCLabel.setLabelFor(scLastPC);
281    
282            // Current population size.
283            c.gridx = 0;
284            ++c.gridy;
285            final JLabel popSizeLabel = new JLabel("Population size:");
286            pane.add(popSizeLabel, c);
287            ++c.gridx;
288            final JProgressBar popSize = new JProgressBar(0, MiniScorerCacheImpl.MAX_MINI_SCORER_SCORES_RETAINED);
289            popSize.setStringPainted(true); // Show a string.
290            popSize.setString("0"); // Hide % string.
291            popSize.setIndeterminate(false);
292            pane.add(popSize, c);
293            popSizeLabel.setLabelFor(popSize);
294    
295            // Time remaining for this generation.
296            c.gridx = 0;
297            ++c.gridy;
298            final JLabel gTRLabel = new JLabel("Generation time remaining:");
299            pane.add(gTRLabel, c);
300            ++c.gridx;
301            final JProgressBar gTR = new JProgressBar(0, AHStandaloneMain.MIN_GENERATION_MS);
302            gTR.setStringPainted(true); // Show a string.
303            gTR.setString("-"); // Hide % string.
304            gTR.setIndeterminate(false);
305            pane.add(gTR, c);
306            gTRLabel.setLabelFor(gTR);
307    
308            // Calibration set size...
309            c.gridx = 0;
310            ++c.gridy;
311            final JLabel calibSizeLabel = new JLabel("Calibration set size:");
312            pane.add(calibSizeLabel, c);
313            ++c.gridx;
314            final JLabel calibSize = new JLabel("???"); // Pad to roughly appropriate size.
315            pane.add(calibSize, c);
316            calibSizeLabel.setLabelFor(calibSize);
317    
318            // Memory usage and total available...
319            c.gridx = 0;
320            ++c.gridy;
321            final JLabel memoryUsageLabel = new JLabel("Memory use/total:");
322            pane.add(memoryUsageLabel, c);
323            ++c.gridx;
324            final JLabel memoryUsage = new JLabel("???MB / ???MB"); // Pad to roughly appropriate size.
325            pane.add(memoryUsage, c);
326            memoryUsageLabel.setLabelFor(memoryUsage);
327    
328            // Target CPU load...
329            c.gridx = 0;
330            ++c.gridy;
331            final JLabel pcCPULabel = new JLabel("CPU target load %:");
332            pane.add(pcCPULabel, c);
333            ++c.gridx;
334            // We allow the bounds to go down to 0 for simplicity.
335            final JSlider pcCPU = new JSlider(0, 100, AHStandaloneMain.DEFAULT_CPU_PERCENT);
336            pcCPU.setMajorTickSpacing(20);
337            pcCPU.setMinorTickSpacing(5);
338            pcCPU.setPaintLabels(true);
339            pcCPU.setPaintTicks(true);
340            pane.add(pcCPU, c);
341            pcCPULabel.setLabelFor(pcCPU);
342    
343            // Create a listener to handle async UI-safe updates.
344            final ActionListener al = new ActionListener(){
345                public void actionPerformed(final ActionEvent evt)
346                    {
347    //                try
348    //                    {
349    //                    }
350    //                catch(final Exception e)
351    //                    {
352    //                    e.printStackTrace();
353    ////                    serverConnectionStatus.setText("unexpected exception: " + e.getMessage() + ((e.getStackTrace().length > 0) ? (" @ " + e.getStackTrace()[0]) : ""));
354    //                    }
355    
356                    // Update population stats.
357                    gen.setText(Integer.toString(logic.getGeneration()));
358                    final int populationSize = logic.getPopulationSize();
359                    popSize.setValue(populationSize);
360                    popSize.setString(Integer.toString(populationSize));
361                    scReported.setText(Integer.toString(logic.getScorersReported()));
362                    final int pcRating = (int) ((ScoreAndConf.computeScorerGoodness(logic.getLastSAC())*100L)/
363                                            (ScoreAndConf.MAX*ScoreAndConf.MAX));
364                    scLastPC.setValue(pcRating);
365                    scLastPC.setString(Integer.toString(pcRating) + '%');
366                    final int genTimeRemaining = logic.getGenTimeRemaining();
367                    gTR.setValue(genTimeRemaining);
368                    // Show remaining time in minutes...
369                    final int minsRemaining = ((genTimeRemaining + 30000) / 60000);
370                    gTR.setString(Integer.toString(minsRemaining) + 'm');
371    
372                    // Update calibration set stats...
373                    final int calibSetSize = logic.getCalibrationSetSize();
374                    calibSize.setText(String.valueOf(calibSetSize));
375    
376                    // Update memory stats.
377                    final Runtime r = Runtime.getRuntime();
378                    memoryUsage.setText(TextUtils.sizeAsText(r.totalMemory() - r.freeMemory(), true) + " / " +
379                                        TextUtils.sizeAsText(r.totalMemory(), true));
380    
381                    // Update and track CPU usage.
382                    // We set the target to the user-supplied value,
383                    // but reflect any constraint on the value back to the UI.
384                    logic.setTargetPercentCPU(pcCPU.getValue());
385                    pcCPU.setValue(logic.getTargetPercentCPU());
386                    }
387                };
388    
389            return(new Tuple.Pair<JComponent, ActionListener>(pane, al));
390            }
391    
392    
393        /**Make the standard insets to wrap round most components.
394         * Note that the returned Insets() object is mutable.
395         */
396        private static Insets makeStandardInsets()
397            { return(new Insets(2, 2, 2, 2)); }
398    
399        /**Creates and initialises a status bar. */
400        private JLabel createStatusBar()
401            {
402            final JLabel sbar = new JLabel("Ready");
403            sbar.setBorder(BorderFactory.createEtchedBorder());
404            return(sbar);
405            }
406    
407        /**This method acts as the Action handler delegate for all the actions. */
408        public void actionPerformed(final ActionEvent evt)
409            {
410            final String command = evt.getActionCommand();
411    
412            // Compare the action command to the known actions,
413            // most-frequent first for efficiency.
414            if(command.equals(aboutAction.getActionCommand()))
415                {
416                // The "About" action was invoked...
417                JOptionPane.showMessageDialog(this, aboutAction.getLongDescription(), aboutAction.getShortDescription(), JOptionPane.INFORMATION_MESSAGE);
418                }
419            else if(command.equals(exitAction.getActionCommand()))
420                {
421                // The "Exit" action was invoked...
422                // Shut down unless vetoed...
423                try { shutdown(); }
424                catch(final UnsupportedOperationException e) { }
425                }
426            else
427                {
428                logger.log("Unexpected command: " + command);
429                }
430            }
431    
432        /**This adapter is constructed to handle mouse-over component events. */
433        private static final class MouseHandler extends MouseAdapter
434            {
435            private JLabel label;
436            private String oldMsg;
437    
438            /**Adaptor constructor.
439             * @param label the JLabel which will recieve value of the
440             *              Action.LONG_DESCRIPTION key
441             */
442            public MouseHandler(final JLabel label)
443                {
444                setLabel(label);
445                oldMsg = label.getText();
446                }
447    
448            public void setLabel(final JLabel label)
449                {
450                this.label = label;
451                }
452    
453            @Override
454            public void mouseEntered(final MouseEvent evt)
455                {
456                if(evt.getSource() instanceof AbstractButton)
457                    {
458                    final AbstractButton button = (AbstractButton) evt.getSource();
459                    final Action action = button.getAction(); // getAction is new in JDK 1.3
460                    if(action != null)
461                        {
462                        oldMsg = label.getText();
463                        final String message = (String) action.getValue(Action.LONG_DESCRIPTION);
464                        label.setText(message);
465                        }
466                    }
467                }
468    
469            @Override
470            public void mouseExited(final MouseEvent evt)
471                {
472                label.setText(oldMsg);
473                }
474            }
475    
476    
477    
478    
479        /**ActionListener invoked by pollUI; never null. */
480        private final ActionListener pollAL;
481    
482        /**Poll UI (in Swing thread, ie Swing-safe). */
483        private void pollUI(final ActionEvent e)
484            {
485            pollAL.actionPerformed(e);
486            }
487    
488    
489        /**Listener class used to veto attempts to start another app instance.
490         * A (modal) dialog[ue] is shown instead.
491         */
492        private static final class SISListener implements SingleInstanceListener
493            {
494            public final void newActivation(final String[] params)
495                {
496                System.err.println("Attempted to launch another instance, args: " + Arrays.asList(params));
497    
498                // Schedule a job for the event-dispatching thread:
499                // creating and showing a blocking dialogue.
500                javax.swing.SwingUtilities.invokeLater(new Runnable(){
501                    public final void run()
502                        {
503                        JOptionPane.showMessageDialog(null, "Gallery AH Worker already running...", "Already running", JOptionPane.ERROR_MESSAGE);
504                        }
505                    });
506                }
507            }
508    
509        /**Main method invoked from JWS/JNLP.
510         * We don't use any arguments passed to us.
511         */
512        public static void main(final String[] args)
513            {
514            // Try to set a local look-and-feel...
515            try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
516            catch(final Exception e) { }
517    
518            // Schedule a job for the event-dispatching thread:
519            // creating and showing this application's GUI.
520            // Also start a daemon thread to drive async non-GUI activity.
521            javax.swing.SwingUtilities.invokeLater(new Runnable(){
522                public final void run()
523                    {
524                    final AHJWSMain mainFrame = new AHJWSMain();
525                    mainFrame.pack();
526                    mainFrame.setVisible(true);
527    
528    //                // After displaying the UI,
529    //                // but before allowing much else to happen,
530    //                // reload any persisted data
531    //                // and start any background processing...
532    //                mainFrame.logic.startup();
533    
534                    // Start a (daemon) poller thread for UI async activity.
535                    // This is called from a Swing timerUI, so is Swing-safe.
536                    // This is created and started AFTER the UI object construction
537                    // is complete to avoid any unpleasant races.
538                    // We call this every 100ms or so in order to feel responsive.
539                    final Timer timerUI = new javax.swing.Timer(70 + Rnd.fastRnd.nextInt(30),
540                                                                (new ActionListener(){
541                        /**Poll the UI logic. */
542                        public void actionPerformed(final ActionEvent e)
543                            { mainFrame.pollUI(e); }
544                        }));
545                    // Delay first poll a little to allow system to start up...
546    //                timerUI.setInitialDelay(301);
547                    timerUI.start();
548    
549                    // Run the worker, cast adrift in its own thread.
550                    (new Thread("worker main thread") {
551                        @Override public void run() { mainFrame.logic.doWork(); }
552                        }).start();
553                    }
554                });
555            }
556    
557        /**Unique Serialisation class ID generated by http://random&#46;hd&#46;org/. */
558        private static final long serialVersionUID = -8895213711913005041L;
559        }