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 }