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