001/*
002 * $Id: McIdasFrameDisplay.java,v 1.17 2011/03/24 18:13:11 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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
031package edu.wisc.ssec.mcidasv.ui;
032
033import java.awt.Component;
034import java.awt.Dimension;
035import java.awt.Font;
036import java.awt.Image;
037import java.awt.Insets;
038import java.awt.MediaTracker;
039import java.awt.event.ActionEvent;
040import java.awt.event.ActionListener;
041import java.awt.event.ItemEvent;
042import java.awt.event.ItemListener;
043import java.awt.event.KeyAdapter;
044import java.awt.event.KeyEvent;
045import java.awt.event.KeyListener;
046import java.io.File;
047import java.io.FileOutputStream;
048import java.io.IOException;
049import java.util.ArrayList;
050import java.util.Hashtable;
051import java.util.List;
052import java.util.Vector;
053
054import javax.swing.AbstractButton;
055import javax.swing.BorderFactory;
056import javax.swing.Icon;
057import javax.swing.JButton;
058import javax.swing.JCheckBox;
059import javax.swing.JComboBox;
060import javax.swing.JComponent;
061import javax.swing.JLabel;
062import javax.swing.JPanel;
063import javax.swing.JRadioButton;
064import javax.swing.JSlider;
065import javax.swing.JTextField;
066import javax.swing.border.EtchedBorder;
067import javax.swing.event.ChangeEvent;
068import javax.swing.event.ChangeListener;
069
070import ucar.unidata.ui.AnimatedGifEncoder;
071import ucar.unidata.ui.ImageUtils;
072import ucar.unidata.ui.JpegImagesToMovie;
073import ucar.unidata.util.FileManager;
074import ucar.unidata.util.GuiUtils;
075import ucar.unidata.util.LogUtil;
076import ucar.unidata.util.Misc;
077import ucar.unidata.util.Resource;
078
079public 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}