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