001    /*
002     * $Id: McIdasFrameDisplay.java,v 1.18 2012/02/19 17:35:51 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv.ui;
032    
033    import java.awt.Component;
034    import java.awt.Dimension;
035    import java.awt.Font;
036    import java.awt.Image;
037    import java.awt.Insets;
038    import java.awt.MediaTracker;
039    import java.awt.event.ActionEvent;
040    import java.awt.event.ActionListener;
041    import java.awt.event.ItemEvent;
042    import java.awt.event.ItemListener;
043    import java.awt.event.KeyAdapter;
044    import java.awt.event.KeyEvent;
045    import java.awt.event.KeyListener;
046    import java.io.File;
047    import java.io.FileOutputStream;
048    import java.io.IOException;
049    import java.util.ArrayList;
050    import java.util.Hashtable;
051    import java.util.List;
052    import java.util.Vector;
053    
054    import javax.swing.AbstractButton;
055    import javax.swing.BorderFactory;
056    import javax.swing.Icon;
057    import javax.swing.JButton;
058    import javax.swing.JCheckBox;
059    import javax.swing.JComboBox;
060    import javax.swing.JComponent;
061    import javax.swing.JLabel;
062    import javax.swing.JPanel;
063    import javax.swing.JRadioButton;
064    import javax.swing.JSlider;
065    import javax.swing.JTextField;
066    import javax.swing.border.EtchedBorder;
067    import javax.swing.event.ChangeEvent;
068    import javax.swing.event.ChangeListener;
069    
070    import ucar.unidata.ui.AnimatedGifEncoder;
071    import ucar.unidata.ui.ImageUtils;
072    import ucar.unidata.ui.JpegImagesToMovie;
073    import ucar.unidata.util.FileManager;
074    import ucar.unidata.util.GuiUtils;
075    import ucar.unidata.util.LogUtil;
076    import ucar.unidata.util.Misc;
077    import ucar.unidata.util.Resource;
078    
079    public class McIdasFrameDisplay extends JPanel implements ActionListener {
080            
081        /** Do we show the big icon */
082        public static boolean bigIcon = false;
083        
084        /** The start/stop button */
085        AbstractButton startStopBtn;
086        
087        /** stop icon */
088        private static Icon stopIcon;
089    
090        /** start icon */
091        private static Icon startIcon;
092            
093        /** Flag for changing the INDEX */
094        public static final String CMD_INDEX = "CMD_INDEX";
095    
096        /** property for setting the widget to the first frame */
097        public static final String CMD_BEGINNING = "CMD_BEGINNING";
098    
099        /** property for setting the widget to the loop in reverse */
100        public static final String CMD_BACKWARD = "CMD_BACKWARD";
101    
102        /** property for setting the widget to the start or stop */
103        public static final String CMD_STARTSTOP = "CMD_STARTSTOP";
104    
105        /** property for setting the widget to the loop forward */
106        public static final String CMD_FORWARD = "CMD_FORWARD";
107    
108        /** property for setting the widget to the last frame */
109        public static final String CMD_END = "CMD_END";
110        
111        /** hi res button */
112        private static JRadioButton hiBtn;
113    
114        /** medium res button */
115        private static JRadioButton medBtn;
116    
117        /** low res button */
118        private static JRadioButton lowBtn;
119        
120        /** display rate field */
121        private JTextField displayRateFld;
122    
123            private Integer frameNumber = 1;
124            private Integer frameIndex = 0;
125            private List frameNumbers;
126            private Hashtable images;
127            private Image theImage;
128            private JPanelImage pi;
129            private JComboBox indicator;
130            private Dimension d;
131            
132        private Thread loopThread;
133        private boolean isLooping = false;
134        private int loopDwell = 500;
135        
136        private boolean antiAlias = false;
137            
138        public McIdasFrameDisplay(List frameNumbers) {
139            this(frameNumbers, new Dimension(640, 480));
140        }
141        
142            public McIdasFrameDisplay(List frameNumbers, Dimension d) {
143                    if (frameNumbers.size()<1) return;
144                    this.frameIndex = 0;
145                    this.frameNumbers = frameNumbers;
146                    this.frameNumber = (Integer)frameNumbers.get(this.frameIndex);
147                    this.images = new Hashtable(frameNumbers.size());
148                    this.d = d;
149                    this.pi = new JPanelImage();
150                    this.pi.setFocusable(true);
151                    this.pi.setSize(this.d);
152                    this.pi.setPreferredSize(this.d);
153                    this.pi.setMinimumSize(this.d);
154                    this.pi.setMaximumSize(this.d);
155                    
156                    String[] frameNames = new String[frameNumbers.size()];
157                    for (int i=0; i<frameNumbers.size(); i++) {
158                            frameNames[i] = "Frame " + (Integer)frameNumbers.get(i);
159                    }
160                    indicator = new JComboBox(frameNames);
161            indicator.setFont(new Font("Dialog", Font.PLAIN, 9));
162            indicator.setLightWeightPopupEnabled(false);
163            indicator.setVisible(true);
164            indicator.addActionListener(new ActionListener() {
165                public void actionPerformed(ActionEvent e) {
166                    showIndexNumber(indicator.getSelectedIndex());
167                }
168            });
169            
170    /*
171                    // Create the File menu
172            JMenuBar menuBar = new JMenuBar();
173            JMenu fileMenu = new JMenu("File");
174            menuBar.add(fileMenu);
175                    fileMenu.add(GuiUtils.makeMenuItem("Print...", this,
176                    "doPrintImage", null, true));
177            fileMenu.add(GuiUtils.makeMenuItem("Save image...", this,
178                    "doSaveImageInThread"));
179            fileMenu.add(GuiUtils.makeMenuItem("Save movie...", this,
180                    "doSaveMovieInThread"));
181            
182                    setTitle(title);
183                    setJMenuBar(menuBar);
184    */
185            
186            JComponent controls = GuiUtils.hgrid(
187                            GuiUtils.left(doMakeAntiAlias()), GuiUtils.right(doMakeVCR()));
188            add(GuiUtils.vbox(controls, pi));
189                    
190            }
191            
192            /**
193             * Make the UI for anti-aliasing controls
194             * 
195             * @return  UI as a Component
196             */
197            private Component doMakeAntiAlias() {
198            JCheckBox newBox = new JCheckBox("Smooth images", antiAlias);
199            newBox.setToolTipText("Set to use anti-aliasing to smooth images when resizing to fit frame display");
200            newBox.addItemListener(new ItemListener() {
201                    public void itemStateChanged(ItemEvent e) {
202                            JCheckBox myself = (JCheckBox)e.getItemSelectable();
203                            antiAlias = myself.isSelected();
204                            paintFrame();
205                    }
206            });
207            return newBox;
208            }
209            
210        /**
211         * Make the UI for VCR controls.
212         *
213         * @return  UI as a Component
214         */
215        private JComponent doMakeVCR() {
216            KeyListener listener = new KeyAdapter() {
217                public void keyPressed(KeyEvent e) {
218                    char c    = e.getKeyChar();
219                                    if (e.isAltDown()) {
220                                            if (c == (char)'a') showFrameNext();
221                                            else if (c == (char)'b') showFramePrevious();
222                                            else if (c == (char)'l') toggleLoop(true);
223                                    }
224                }
225            };
226            List buttonList = new ArrayList();
227            indicator.addKeyListener(listener);
228            buttonList.add(GuiUtils.inset(indicator, new Insets(0, 0, 0, 2)));
229            String[][] buttonInfo = {
230                { "Go to first frame", CMD_BEGINNING, getIcon("Rewind") },
231                { "One frame back", CMD_BACKWARD, getIcon("StepBack") },
232                { "Run/Stop", CMD_STARTSTOP, getIcon("Play") },
233                { "One frame forward", CMD_FORWARD, getIcon("StepForward") },
234                { "Go to last frame", CMD_END, getIcon("FastForward") }
235            };
236    
237            for (int i = 0; i < buttonInfo.length; i++) {
238                JButton btn = GuiUtils.getImageButton(buttonInfo[i][2], getClass(), 2, 2);
239                btn.setToolTipText(buttonInfo[i][0]);
240                btn.setActionCommand(buttonInfo[i][1]);
241                btn.addActionListener(this);
242                btn.addKeyListener(listener);
243                btn.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));
244                buttonList.add(btn);
245                if (i == 2) {
246                    startStopBtn = btn;
247                }
248            }
249    
250            JComponent sbtn = makeSlider();
251            sbtn.addKeyListener(listener);
252            buttonList.add(sbtn);
253    
254            JComponent contents = GuiUtils.hflow(buttonList, 1, 0);
255    
256            updateRunButton();
257            return contents;
258        }
259            
260        /**
261         * Get the correct icon name based on whether we are in big icon mode
262         *
263         * @param name base name
264         *
265         * @return Full path to icon
266         */
267        private String getIcon(String name) {
268            return "/auxdata/ui/icons/" + name + (bigIcon
269                    ? "24"
270                    : "16") + ".gif";
271        }
272            
273        /**
274         * Public by implementing ActionListener.
275         *
276         * @param e  ActionEvent to check
277         */
278        public void actionPerformed(ActionEvent e) {
279            actionPerformed(e.getActionCommand());
280        }
281        
282        /**
283         * Handle the action
284         *
285         * @param cmd The action
286         */
287        private void actionPerformed(String cmd) {
288            if (cmd.equals(CMD_STARTSTOP)) {
289                    toggleLoop(false);
290            } else if (cmd.equals(CMD_FORWARD)) {
291                showFrameNext();
292            } else if (cmd.equals(CMD_BACKWARD)) {
293                showFramePrevious();
294            } else if (cmd.equals(CMD_BEGINNING)) {
295                showFrameFirst();
296            } else if (cmd.equals(CMD_END)) {
297                showFrameLast();
298            }
299        }
300        
301        /**
302         * Update the icon in the run button
303         */
304        private void updateRunButton() {
305            if (stopIcon == null) {
306                stopIcon  = Resource.getIcon(getIcon("Pause"), true);
307                startIcon = Resource.getIcon(getIcon("Play"), true);
308            }
309            if (startStopBtn != null) {
310                    if (isLooping) {
311                    startStopBtn.setIcon(stopIcon);
312                    startStopBtn.setToolTipText("Stop animation");
313                } else {
314                    startStopBtn.setIcon(startIcon);
315                    startStopBtn.setToolTipText("Start animation");
316                }
317            }
318        }
319            
320            public void setFrameImage(int inFrame, Image inImage) {
321                    images.put("Frame " + inFrame, inImage);
322            }
323            
324            private int getIndexPrevious() {
325                    int thisIndex = frameIndex.intValue();
326                    if (thisIndex > 0)
327                            thisIndex--;
328                    else
329                            thisIndex = frameNumbers.size() - 1;
330                    return thisIndex;
331            }
332            
333            private int getIndexNext() {
334                    int thisIndex = frameIndex.intValue();
335                    if (thisIndex < frameNumbers.size() - 1)
336                            thisIndex++;
337                    else
338                            thisIndex = 0;
339                    return thisIndex;
340            }
341            
342            public void showFramePrevious() {
343                    showIndexNumber(getIndexPrevious());
344            }
345            
346            public void showFrameNext() {
347                    showIndexNumber(getIndexNext());
348            }
349            
350            public void showFrameFirst() {
351                    showIndexNumber(0);
352            }
353            
354            public void showFrameLast() {
355                    showIndexNumber(frameNumbers.size() - 1);
356            }
357            
358            public void toggleLoop(boolean goFirst) {
359                    if (isLooping) stopLoop(goFirst);
360                    else startLoop(goFirst);
361            }
362            
363            public void startLoop(boolean goFirst) {
364    //              if (goFirst) showFrameFirst();
365            loopThread = new Thread(new Runnable() {
366                public void run() {
367                    runLoop();
368                }
369            });
370            loopThread.start();
371            isLooping = true;
372            updateRunButton();
373            }
374            
375            public void stopLoop(boolean goFirst) {
376                    loopThread = null;
377                    isLooping = false;
378                    if (goFirst) showFrameFirst();
379                    updateRunButton();
380            }
381            
382        private void runLoop() {
383            try {
384                Thread myThread = Thread.currentThread();
385                while (myThread == loopThread) {
386                    long sleepTime = (long)loopDwell;
387                    showFrameNext();
388                    //Make sure we're sleeping for a minimum of 100ms
389                    if (sleepTime < 100) {
390                        sleepTime = 100;
391                    }
392                    Misc.sleep(sleepTime);
393                }
394            } catch (Exception e) {
395                LogUtil.logException("Loop animation: ", e);
396            }
397        }
398            
399            private void showIndexNumber(int inIndex) {
400                    if (inIndex < 0 || inIndex >= frameNumbers.size()) return;
401                    frameIndex = (Integer)inIndex;
402                    frameNumber = (Integer)frameNumbers.get(inIndex);
403                    indicator.setSelectedIndex(frameIndex);
404                    paintFrame();
405            }
406            
407            public void showFrameNumber(int inFrame) {
408                    int inIndex = -1;
409                    for (int i=0; i<frameNumbers.size(); i++) {
410                            Integer frameInt = (Integer)frameNumbers.get(i);
411                            if (frameInt.intValue() == inFrame) {
412                                    inIndex = (Integer)i;
413                                    break;
414                            }
415                    }
416                    if (inIndex >= 0)
417                            showIndexNumber(inIndex);
418                    else
419                            System.err.println("showFrameNumber: " + inFrame + " is not a valid frame");
420            }
421            
422            public int getFrameNumber() {
423                    return frameNumber.intValue();
424            }
425            
426            private void paintFrame() {
427                    theImage = (Image)images.get("Frame " + frameNumber);
428                    if (theImage == null) {
429                            System.err.println("paintFrame: Got a null image for frame " + frameNumber);
430                            return;
431                    }
432                    
433                    MediaTracker mediaTracker = new MediaTracker(this);
434                    mediaTracker.addImage(theImage, frameNumber);
435                    try {
436                            mediaTracker.waitForID(frameNumber);
437                    } catch (InterruptedException ie) {
438                            System.err.println("MediaTracker exception: " + ie);
439                    }
440    
441                    this.pi.setImage(theImage);
442                    this.pi.repaint();
443            }
444                    
445        /**
446         * Make the value slider
447         *
448         * @return The slider button
449         */
450        private JComponent makeSlider() {
451            ChangeListener listener = new ChangeListener() {
452                public void stateChanged(ChangeEvent e) {
453                    JSlider slide = (JSlider) e.getSource();
454                    if (slide.getValueIsAdjusting()) {
455                        //                      return;
456                    }
457                    loopDwell = slide.getValue() * 100;
458                }
459            };
460            JComponent[] comps = GuiUtils.makeSliderPopup(1, 50, loopDwell / 100, listener);
461            comps[0].setToolTipText("Change dwell rate");
462            return comps[0];
463        }
464    
465        /**
466         * Print the image
467         */
468    /*
469        public void doPrintImage() {
470            try {
471                toFront();
472                PrinterJob printJob = PrinterJob.getPrinterJob();
473                printJob.setPrintable(
474                    ((DisplayImpl) getMaster().getDisplay()).getPrintable());
475                if ( !printJob.printDialog()) {
476                    return;
477                }
478                printJob.print();
479            } catch (Exception exc) {
480                logException("There was an error printing the image", exc);
481            }
482        }
483    */
484        
485        /**
486         * User has requested saving display as an image. Prompt
487         * for a filename and save the image to it.
488         */
489        public void doSaveImageInThread() {
490            Misc.run(this, "doSaveImage");
491        }
492        
493        /**
494         * Save the image
495         */
496        public void doSaveImage() {
497    
498            SecurityManager backup = System.getSecurityManager();
499            System.setSecurityManager(null);
500            try {
501                if (hiBtn == null) {
502                    hiBtn  = new JRadioButton("High", true);
503                    medBtn = new JRadioButton("Medium", false);
504                    lowBtn = new JRadioButton("Low", false);
505                    GuiUtils.buttonGroup(hiBtn, medBtn).add(lowBtn);
506                }
507                JPanel qualityPanel = GuiUtils.vbox(new JLabel("Quality:"),
508                                          hiBtn, medBtn, lowBtn);
509    
510                JComponent accessory = GuiUtils.vbox(Misc.newList(qualityPanel));
511    
512                List filters = Misc.newList(FileManager.FILTER_IMAGE);
513    
514                String filename = FileManager.getWriteFile(filters,
515                                      FileManager.SUFFIX_JPG,
516                                      GuiUtils.top(GuiUtils.inset(accessory, 5)));
517    
518                if (filename != null) {
519                    if (filename.endsWith(".pdf")) {
520                        ImageUtils.writePDF(
521                            new FileOutputStream(filename), this.pi);
522                        System.setSecurityManager(backup);
523                        return;
524                    }
525                    float quality = 1.0f;
526                    if (medBtn.isSelected()) {
527                        quality = 0.6f;
528                    } else if (lowBtn.isSelected()) {
529                        quality = 0.2f;
530                    }
531                    ImageUtils.writeImageToFile(theImage, filename, quality);
532                }
533            } catch (Exception e) {
534                    System.err.println("doSaveImage exception: " + e);
535            }
536            // for webstart
537            System.setSecurityManager(backup);
538    
539        }
540        
541        /**
542         * User has requested saving display as a movie. Prompt
543         * for a filename and save the images to it.
544         */
545        public void doSaveMovieInThread() {
546            Misc.run(this, "doSaveMovie");
547        }
548        
549        /**
550         * Save the movie
551         */
552        public void doSaveMovie() {
553    
554            try {
555                    Dimension size = new Dimension();
556                    List theImages = new ArrayList(frameNumbers.size());
557                    for (int i=0; i<frameNumbers.size(); i++) {
558                            Integer frameInt = (Integer)frameNumbers.get(i);
559                            theImages.add((Image)images.get("Frame " + frameInt));
560                            if (size == null) {
561                            int width = theImage.getWidth(null);
562                            int height = theImage.getHeight(null);
563                            size = new Dimension(width, height);
564                            }
565                    }
566                    
567                    //TODO: theImages should actually be a list of filenames that we have already saved
568                    
569                if (displayRateFld == null) {
570                    displayRateFld = new JTextField("2", 3);
571                }
572                if (hiBtn == null) {
573                    hiBtn  = new JRadioButton("High", true);
574                    medBtn = new JRadioButton("Medium", false);
575                    lowBtn = new JRadioButton("Low", false);
576                    GuiUtils.buttonGroup(hiBtn, medBtn).add(lowBtn);
577                }
578                JPanel qualityPanel = GuiUtils.vbox(new JLabel("Quality:"),
579                                          hiBtn, medBtn, lowBtn);
580                JPanel ratePanel = GuiUtils.vbox(new JLabel("Frames per second:"),
581                                          displayRateFld);
582    
583                JComponent accessory = GuiUtils.vbox(Misc.newList(qualityPanel,
584                            new JLabel(" "), ratePanel));
585                
586                List filters = Misc.newList(FileManager.FILTER_MOV,
587                        FileManager.FILTER_AVI, FileManager.FILTER_ANIMATEDGIF);
588    
589                String filename = FileManager.getWriteFile(filters,
590                                      FileManager.SUFFIX_MOV,
591                                      GuiUtils.top(GuiUtils.inset(accessory, 5)));
592                
593                    double displayRate =
594                    (new Double(displayRateFld.getText())).doubleValue();
595    
596                if (filename.toLowerCase().endsWith(".gif")) {
597                    double rate = 1.0 / displayRate;
598                    AnimatedGifEncoder.createGif(filename, theImages,
599                            AnimatedGifEncoder.REPEAT_FOREVER,
600                            (int) (rate * 1000));
601                } else if (filename.toLowerCase().endsWith(".avi")) {
602                    ImageUtils.writeAvi(theImages, displayRate,
603                                        new File(filename));
604                } else {
605                    SecurityManager backup = System.getSecurityManager();
606                    System.setSecurityManager(null);
607                    JpegImagesToMovie.createMovie(filename, size.width,
608                            size.height, (int) displayRate,
609                            new Vector(theImages));
610                    System.setSecurityManager(backup);
611                }
612            } catch (NumberFormatException nfe) {
613                LogUtil.userErrorMessage("Bad number format");
614                return;
615            } catch (IOException ioe) {
616                LogUtil.userErrorMessage("Error writing movie: " + ioe);
617                return;
618            }
619    
620        }
621      
622    }