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 }