001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2003, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.gui.swing.image;
018:
019: // J2SE dependencies
020: import java.util.Locale;
021: import java.util.ResourceBundle;
022: import javax.swing.JLabel;
023: import javax.swing.JPanel;
024: import javax.swing.JTable;
025: import javax.swing.JTabbedPane;
026: import javax.swing.JScrollPane;
027: import javax.swing.table.AbstractTableModel;
028: import java.beans.PropertyChangeListener;
029: import java.beans.PropertyChangeEvent;
030: import java.awt.GridBagConstraints;
031: import java.awt.GridBagLayout;
032: import java.awt.BorderLayout;
033: import java.awt.EventQueue;
034: import java.awt.Dimension;
035: import java.lang.reflect.Array;
036:
037: // Image and JAI
038: import java.awt.Image;
039: import java.awt.image.DataBuffer;
040: import java.awt.image.ColorModel;
041: import java.awt.image.SampleModel;
042: import java.awt.image.RenderedImage;
043: import java.awt.image.IndexColorModel;
044: import java.awt.image.renderable.RenderableImage;
045: import javax.media.jai.OperationNode;
046: import javax.media.jai.PropertySource;
047: import javax.media.jai.PropertyChangeEmitter;
048: import javax.media.jai.RegistryElementDescriptor;
049: import javax.media.jai.OperationDescriptor;
050:
051: // Geotools dependencies
052: import org.geotools.resources.Utilities;
053: import org.geotools.resources.i18n.Vocabulary;
054: import org.geotools.resources.i18n.VocabularyKeys;
055:
056: /**
057: * A panel showing image properties. An image can actually be any instance of
058: * {@link PropertySource}, {@link RenderedImage} or {@link RenderableImage} interfaces.
059: * The method {@link PropertySource#getProperty} will be invoked only when a property
060: * is first required, in order to avoid the computation of deferred properties before
061: * needed. If the source implements also the {@link PropertyChangeEmitter} interface,
062: * then this widget will register a listener for property changes. The changes can be
063: * emitted from any thread, which may or may not be the <cite>Swing</cite> thread.
064: * <p>
065: * If the image is an instance of {@link RenderedImage}, then this panel will also show
066: * informations about the {@linkplain ColorModel color model}, {@linkplain SampleModel
067: * sample model}, image size, tile size, etc.
068: *
069: * @since 2.3
070: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/image/ImageProperties.java $
071: * @version $Id: ImageProperties.java 24084 2007-01-27 19:18:46Z desruisseaux $
072: * @author Martin Desruisseaux
073: *
074: * @see org.geotools.gui.swing.ParameterEditor
075: * @see OperationTreeBrowser
076: */
077: public class ImageProperties extends JPanel {
078: /**
079: * The operation name, or the image class name if the image is not an instance of
080: * {@link OperationNode}.
081: */
082: private final JLabel operationName = new JLabel(" ");
083:
084: /**
085: * The operation description.
086: */
087: private final JLabel operationDescription = new JLabel(" ");
088:
089: /**
090: * The operation vendor and version.
091: */
092: private final JLabel operationVersion = new JLabel(" ");
093:
094: /**
095: * The text area for image size.
096: */
097: private final JLabel imageSize = new JLabel();
098:
099: /**
100: * The text area for tile size.
101: */
102: private final JLabel tileSize = new JLabel();
103:
104: /**
105: * The text area for sample type (e.g. "8 bits unsigned integer".
106: */
107: private final JLabel dataType = new JLabel();
108:
109: /**
110: * The text area for the sample model.
111: */
112: private final JLabel sampleModel = new JLabel();
113:
114: /**
115: * The text area for the color model.
116: */
117: private final JLabel colorModel = new JLabel();
118:
119: /**
120: * The color bar for {@link IndexColorModel}.
121: */
122: private final ColorRamp colorRamp = new ColorRamp();
123:
124: /**
125: * The table model for image's properties.
126: */
127: private final Table properties;
128:
129: /**
130: * The table for sample values.
131: */
132: private final ImageSampleValues samples;
133:
134: /**
135: * The viewer for an image quick look.
136: */
137: private final ImagePane viewer;
138:
139: /**
140: * Create a new instance of {@code ImageProperties} with no image.
141: * One of {@link #setImage(PropertySource) setImage(...)} methods must
142: * be invoked in order to set the property source.
143: */
144: public ImageProperties() {
145: super (new BorderLayout());
146: final Vocabulary resources = Vocabulary
147: .getResources(getLocale());
148: final JTabbedPane tabs = new JTabbedPane();
149: final GridBagConstraints c = new GridBagConstraints();
150: /*
151: * Build the informations tab.
152: */
153: if (true) {
154: final JPanel panel = new JPanel(new GridBagLayout());
155: c.anchor = c.WEST;
156: c.fill = c.HORIZONTAL;
157: c.insets.left = 9;
158: c.gridx = 0;
159: c.gridwidth = 2;
160: c.weightx = 1;
161: c.gridy = 0;
162: panel.add(operationName, c);
163: c.gridy++;
164: panel.add(operationDescription, c);
165: c.insets.bottom = 15;
166: c.gridy++;
167: panel.add(operationVersion, c);
168:
169: final int ytop = c.gridy;
170: c.gridwidth = 1;
171: c.weightx = 0;
172: c.insets.bottom = 0;
173: c.gridy++;
174: panel
175: .add(
176: getLabel(VocabularyKeys.IMAGE_SIZE,
177: resources), c);
178: c.gridy++;
179: panel
180: .add(
181: getLabel(VocabularyKeys.TILES_SIZE,
182: resources), c);
183: c.gridy++;
184: panel.add(getLabel(VocabularyKeys.DATA_TYPE, resources), c);
185: c.gridy++;
186: panel.add(getLabel(VocabularyKeys.SAMPLE_MODEL, resources),
187: c);
188: c.gridy++;
189: panel.add(getLabel(VocabularyKeys.COLOR_MODEL, resources),
190: c);
191: c.gridy++;
192: panel.add(getLabel(VocabularyKeys.COLORS, resources), c);
193:
194: c.gridx = 1;
195: c.gridy = ytop;
196: c.weightx = 1;
197: c.gridy++;
198: panel.add(imageSize, c);
199: c.gridy++;
200: panel.add(tileSize, c);
201: c.gridy++;
202: panel.add(dataType, c);
203: c.gridy++;
204: panel.add(sampleModel, c);
205: c.gridy++;
206: panel.add(colorModel, c);
207: c.gridy++;
208: c.anchor = c.CENTER;
209: c.insets.right = 6;
210:
211: panel.add(colorRamp, c);
212: tabs.addTab(resources
213: .getString(VocabularyKeys.INFORMATIONS), panel);
214: }
215: /*
216: * Build the image's properties tab.
217: */
218: if (true) {
219: properties = new Table(resources);
220: final JTable table = new JTable(properties);
221: tabs.addTab(resources.getString(VocabularyKeys.PROPERTIES),
222: new JScrollPane(table));
223: }
224: /*
225: * Build the image sample value tab.
226: */
227: if (true) {
228: samples = new ImageSampleValues();
229: tabs.addTab(resources.getString(VocabularyKeys.PIXELS),
230: samples);
231: }
232: /*
233: * Build the image preview tab.
234: */
235: if (true) {
236: viewer = new ImagePane();
237: viewer.setPaintingWhileAdjusting(true);
238: tabs.addTab(resources.getString(VocabularyKeys.PREVIEW),
239: viewer.createScrollPane());
240: }
241: add(tabs, BorderLayout.CENTER);
242: setPreferredSize(new Dimension(400, 250));
243: }
244:
245: /**
246: * Returns the localized label for the given key.
247: */
248: private static JLabel getLabel(final int key,
249: final Vocabulary resources) {
250: return new JLabel(resources.getLabel(key));
251: }
252:
253: /**
254: * Create a new instance of {@code ImageProperties} for the specified
255: * rendered image.
256: *
257: * @param image The image, or {@code null} if none.
258: */
259: public ImageProperties(final RenderedImage image) {
260: this ();
261: if (image != null) {
262: setImage(image);
263: }
264: }
265:
266: /**
267: * Set the operation name, description and version for the given image. If the image is
268: * an instance of {@link OperationNode}, then a description of the operation will be fetch
269: * from its resources bundle.
270: *
271: * @param image The image, or {@code null} if none.
272: */
273: private void setDescription(final Object image) {
274: String name = " ";
275: String description = " ";
276: String version = " ";
277: final Locale locale = getLocale();
278: final Vocabulary resources = Vocabulary.getResources(locale);
279: if (image instanceof OperationNode) {
280: final String mode;
281: final RegistryElementDescriptor descriptor;
282: final OperationNode operation = (OperationNode) image;
283: name = operation.getOperationName();
284: mode = operation.getRegistryModeName();
285: descriptor = operation.getRegistry().getDescriptor(mode,
286: name);
287: if (descriptor instanceof OperationDescriptor) {
288: final ResourceBundle bundle;
289: bundle = ((OperationDescriptor) descriptor)
290: .getResourceBundle(locale);
291: name = bundle.getString("LocalName");
292: description = bundle.getString("Description");
293: version = resources.getString(
294: VocabularyKeys.VERSION_$1, bundle
295: .getString("Version"))
296: + ", " + bundle.getString("Vendor");
297: name = resources.getString(VocabularyKeys.OPERATION_$1,
298: name);
299: }
300: } else if (image != null) {
301: name = Utilities.getShortClassName(image);
302: name = resources.getString(VocabularyKeys.IMAGE_CLASS_$1,
303: name);
304: }
305: operationName.setText(name);
306: operationDescription.setText(description);
307: operationVersion.setText(version);
308: }
309:
310: /**
311: * Set all text fields to {@code null}. This method do not set the {@link #properties}
312: * table; this is left to the caller.
313: */
314: private void clear() {
315: imageSize.setText(null);
316: tileSize.setText(null);
317: dataType.setText(null);
318: sampleModel.setText(null);
319: colorModel.setText(null);
320: colorRamp.setColors((IndexColorModel) null);
321: }
322:
323: /**
324: * Set the {@linkplain PropertySource property source} for this widget. If the source is a
325: * {@linkplain RenderedImage rendered} or a {@linkplain RenderableImage renderable} image,
326: * then the widget will be set as if the most specific flavor of {@code setImage(...)}
327: * was invoked.
328: *
329: * @param image The image, or {@code null} if none.
330: */
331: public void setImage(final PropertySource image) {
332: if (image instanceof RenderedImage) {
333: setImage((RenderedImage) image);
334: return;
335: }
336: if (image instanceof RenderableImage) {
337: setImage((RenderableImage) image);
338: return;
339: }
340: clear();
341: setDescription(image);
342: properties.setSource(image);
343: viewer.setImage((RenderedImage) null);
344: samples.setImage((RenderedImage) null);
345: }
346:
347: /**
348: * Set the specified {@linkplain RenderableImage renderable image} as the properties source.
349: *
350: * @param image The image, or {@code null} if none.
351: */
352: public void setImage(final RenderableImage image) {
353: clear();
354: if (image != null) {
355: final Vocabulary resources = Vocabulary
356: .getResources(getLocale());
357: imageSize.setText(resources.getString(
358: VocabularyKeys.SIZE_$2,
359: new Float(image.getWidth()), new Float(image
360: .getHeight())));
361: }
362: setDescription(image);
363: properties.setSource(image);
364: viewer.setImage(image);
365: samples.setImage((RenderedImage) null);
366: }
367:
368: /**
369: * Set the specified {@linkplain RenderedImage rendered image} as the properties source.
370: *
371: * @param image The image, or {@code null} if none.
372: */
373: public void setImage(final RenderedImage image) {
374: if (image == null) {
375: clear();
376: } else {
377: final Vocabulary resources = Vocabulary
378: .getResources(getLocale());
379: final ColorModel cm = image.getColorModel();
380: final SampleModel sm = image.getSampleModel();
381: imageSize.setText(resources.getString(
382: VocabularyKeys.IMAGE_SIZE_$3, new Integer(image
383: .getWidth()),
384: new Integer(image.getHeight()), new Integer(sm
385: .getNumBands())));
386: tileSize.setText(resources.getString(
387: VocabularyKeys.TILE_SIZE_$4, new Integer(image
388: .getNumXTiles()), new Integer(image
389: .getNumYTiles()), new Integer(image
390: .getTileWidth()), new Integer(image
391: .getTileHeight())));
392: dataType.setText(getDataType(sm.getDataType(), cm,
393: resources));
394: sampleModel.setText(formatClassName(sm, resources));
395: colorModel.setText(formatClassName(cm, resources));
396: if (cm instanceof IndexColorModel) {
397: colorRamp.setColors((IndexColorModel) cm);
398: } else {
399: colorRamp.setColors((IndexColorModel) null);
400: }
401: }
402: setDescription(image);
403: properties.setSource(image);
404: viewer.setImage(image);
405: samples.setImage(image);
406: }
407:
408: /**
409: * Returns a string representation for the given data type.
410: *
411: * @param type The data type (one of {@link DataBuffer} constants).
412: * @param cm The color model, for computing the pixel size in bits.
413: * @param resources The resources to use for formatting the type.
414: * @return The data type as a localized string.
415: */
416: private static String getDataType(final int type,
417: final ColorModel cm, final Vocabulary resources) {
418: final int key;
419: switch (type) {
420: case DataBuffer.TYPE_BYTE: // Fall through
421: case DataBuffer.TYPE_USHORT:
422: key = VocabularyKeys.UNSIGNED_INTEGER_$2;
423: break;
424: case DataBuffer.TYPE_SHORT: // Fall through
425: case DataBuffer.TYPE_INT:
426: key = VocabularyKeys.SIGNED_INTEGER_$1;
427: break;
428: case DataBuffer.TYPE_FLOAT: // Fall through
429: case DataBuffer.TYPE_DOUBLE:
430: key = VocabularyKeys.REAL_NUMBER_$1;
431: break;
432: case DataBuffer.TYPE_UNDEFINED: // Fall through
433: default:
434: return resources.getString(VocabularyKeys.UNDEFINED);
435: }
436: final Integer typeSize = new Integer(DataBuffer
437: .getDataTypeSize(type));
438: final Integer pixelSize = (cm != null) ? new Integer(cm
439: .getPixelSize()) : typeSize;
440: return resources.getString(key, typeSize, pixelSize);
441: }
442:
443: /**
444: * Split a class name into a more human readeable sentence
445: * (e.g. "PixelInterleavedSampleModel" into "Pixel interleaved sample model").
446: *
447: * @param object The object to format.
448: * @param resources The resources to use for formatting localized text.
449: * @return The object class name.
450: */
451: private static String formatClassName(final Object object,
452: final Vocabulary resources) {
453: if (object == null) {
454: return resources.getString(VocabularyKeys.UNDEFINED);
455: }
456: final String name = Utilities.getShortClassName(object);
457: final int length = name.length();
458: final StringBuffer buffer = new StringBuffer(length + 8);
459: int last = 0;
460: for (int i = 1; i <= length; i++) {
461: if (i == length
462: || (Character.isUpperCase(name.charAt(i)) && Character
463: .isLowerCase(name.charAt(i - 1)))) {
464: final int pos = buffer.length();
465: buffer.append(name.substring(last, i));
466: buffer.append(' ');
467: if (pos != 0 && last < length - 1
468: && Character.isLowerCase(name.charAt(last + 1))) {
469: buffer.setCharAt(pos, Character.toLowerCase(buffer
470: .charAt(pos)));
471: }
472: last = i;
473: }
474: }
475: if (object instanceof IndexColorModel) {
476: final IndexColorModel cm = (IndexColorModel) object;
477: buffer.append(" (");
478: buffer.append(resources.getString(
479: VocabularyKeys.COLOR_COUNT_$1, new Integer(cm
480: .getMapSize())));
481: buffer.append(')');
482: }
483: return buffer.toString().trim();
484: }
485:
486: /**
487: * The table model for image's properties. The image can actually be any of
488: * {@link PropertySource}, {@link RenderedImage} or {@link RenderableImage}
489: * interface. The method {@link PropertySource#getProperty} will be invoked
490: * only when a property is first required, in order to avoid the computation
491: * of deferred properties before needed. If the source implements also the
492: * {@link PropertyChangeEmitter} interface, then this table will be registered
493: * as a listener for property changes. The changes can be emitted from any thread,
494: * which may or may not be the <cite>Swing</cite> thread.
495: *
496: * @version $Id: ImageProperties.java 24084 2007-01-27 19:18:46Z desruisseaux $
497: * @author Martin Desruisseaux
498: *
499: * @todo Check for {@code WritablePropertySource} and make cells editable accordingly.
500: */
501: private static final class Table extends AbstractTableModel
502: implements PropertyChangeListener {
503: /**
504: * The resources for formatting localized strings.
505: */
506: private final Vocabulary resources;
507:
508: /**
509: * The property sources. Usually (but not always) the same object than
510: * {@link #changeEmitter}. May be {@code null} if no source has been set.
511: */
512: private PropertySource source;
513:
514: /**
515: * The property change emitter, or {@code null} if none. Usually (but not always)
516: * the same object than {@link #source}.
517: */
518: private PropertyChangeEmitter changeEmitter;
519:
520: /**
521: * The properties names, or {@code null} if none.
522: */
523: private String[] names;
524:
525: /**
526: * Constructs a default table with no properties source. The method {@link #setSource}
527: * must be invoked after the construction in order to display some image's properties.
528: *
529: * @param resources The resources for formatting localized strings.
530: */
531: public Table(final Vocabulary resources) {
532: this .resources = resources;
533: }
534:
535: /**
536: * Wrap the specified {@link RenderedImage} into a {@link PropertySource}.
537: */
538: private static PropertySource wrap(final RenderedImage image) {
539: return new PropertySource() {
540: public String[] getPropertyNames() {
541: return image.getPropertyNames();
542: }
543:
544: public String[] getPropertyNames(final String prefix) {
545: // TODO: Not the real answer, but this method
546: // is not needed by this Table implementation.
547: return getPropertyNames();
548: }
549:
550: public Class getPropertyClass(final String name) {
551: return null;
552: }
553:
554: public Object getProperty(final String name) {
555: return image.getProperty(name);
556: }
557: };
558: }
559:
560: /**
561: * Wrap the specified {@link RenderableImage} into a {@link PropertySource}.
562: */
563: private static PropertySource wrap(final RenderableImage image) {
564: return new PropertySource() {
565: public String[] getPropertyNames() {
566: return image.getPropertyNames();
567: }
568:
569: public String[] getPropertyNames(final String prefix) {
570: // TODO: Not the real answer, but this method
571: // is not needed by this Table implementation.
572: return getPropertyNames();
573: }
574:
575: public Class getPropertyClass(final String name) {
576: return null;
577: }
578:
579: public Object getProperty(final String name) {
580: return image.getProperty(name);
581: }
582: };
583: }
584:
585: /**
586: * Set the source as a {@link PropertySource}, a {@link RenderedImage} or a
587: * {@link RenderableImage}. If the source implements the {@link PropertyChangeEmitter}
588: * interface, then this table will be registered as a listener for property changes.
589: * The changes can be emitted from any thread (may or may not be the Swing thread).
590: *
591: * @param image The properties source, or {@code null} for removing any source.
592: */
593: public void setSource(final Object image) {
594: if (image == source) {
595: return;
596: }
597: if (changeEmitter != null) {
598: changeEmitter.removePropertyChangeListener(this );
599: changeEmitter = null;
600: }
601: if (image instanceof PropertySource) {
602: source = (PropertySource) image;
603: } else if (image instanceof RenderedImage) {
604: source = wrap((RenderedImage) image);
605: } else if (image instanceof RenderableImage) {
606: source = wrap((RenderableImage) image);
607: } else {
608: source = null;
609: }
610: names = (source != null) ? source.getPropertyNames() : null;
611: if (image instanceof PropertyChangeEmitter) {
612: changeEmitter = (PropertyChangeEmitter) image;
613: changeEmitter.addPropertyChangeListener(this );
614: }
615: fireTableDataChanged();
616: }
617:
618: /**
619: * Returns the number of rows, which is equals to the number of properties.
620: */
621: public int getRowCount() {
622: return (names != null) ? names.length : 0;
623: }
624:
625: /**
626: * Returns the number of columns, which is 2 (the property name and its value).
627: */
628: public int getColumnCount() {
629: return 2;
630: }
631:
632: /**
633: * Returns the column name for the given index.
634: */
635: public String getColumnName(final int column) {
636: final int key;
637: switch (column) {
638: case 0:
639: key = VocabularyKeys.NAME;
640: break;
641: case 1:
642: key = VocabularyKeys.VALUE;
643: break;
644: default:
645: throw new IndexOutOfBoundsException(String
646: .valueOf(column));
647: }
648: return resources.getString(key);
649: }
650:
651: /**
652: * Returns the most specific superclass for all the cell values in the column.
653: */
654: public Class getColumnClass(final int column) {
655: switch (column) {
656: case 0:
657: return String.class;
658: case 1:
659: return Object.class;
660: default:
661: throw new IndexOutOfBoundsException(String
662: .valueOf(column));
663: }
664: }
665:
666: /**
667: * Returns the property for the given cell.
668: *
669: * @param row The row index.
670: * @param column The column index.
671: * @return The cell value at the given index.
672: * @throws IndexOutOfBoundsException if the row or the column is out of bounds.
673: */
674: public Object getValueAt(int row, int column)
675: throws IndexOutOfBoundsException {
676: final String name = names[row];
677: switch (column) {
678: case 0: {
679: return name;
680: }
681: case 1: {
682: Object value = source.getProperty(name);
683: if (value == Image.UndefinedProperty) {
684: value = resources
685: .getString(VocabularyKeys.UNDEFINED);
686: }
687: return expandArray(value);
688: }
689: default: {
690: throw new IndexOutOfBoundsException(String
691: .valueOf(column));
692: }
693: }
694: }
695:
696: /**
697: * If the specified object is an array, enumerate the array components.
698: * Otherwise, returns the object unchanged. This method is sligtly different
699: * than {@link java.util.Arrays#toString(Object[])} in that it expands inner
700: * array components recursively.
701: */
702: private static Object expandArray(final Object array) {
703: if (array != null && array.getClass().isArray()) {
704: final StringBuffer buffer = new StringBuffer();
705: buffer.append('{');
706: final int length = Array.getLength(array);
707: for (int i = 0; i < length; i++) {
708: if (i != 0) {
709: buffer.append(", ");
710: }
711: buffer.append(expandArray(Array.get(array, i)));
712: }
713: buffer.append('}');
714: return buffer.toString();
715: }
716: return array;
717: }
718:
719: /**
720: * Invoked when a property changed. This method find the row for the modified
721: * property and fire a table change event.
722: *
723: * @param The property change event.
724: */
725: public void propertyChange(final PropertyChangeEvent event) {
726: /*
727: * Make sure that we are running in the Swing thread.
728: */
729: if (!EventQueue.isDispatchThread()) {
730: EventQueue.invokeLater(new Runnable() {
731: public void run() {
732: propertyChange(event);
733: }
734: });
735: return;
736: }
737: /*
738: * Find the rows for the modified property, and fire a "table updated' event.
739: */
740: final String name = event.getPropertyName();
741: int first = getRowCount(); // Past the last row.
742: int last = -1; // Before the first row.
743: if (name == null) {
744: last = first - 1;
745: first = 0;
746: } else {
747: for (int i = first; --i >= 0;) {
748: if (names[i].equalsIgnoreCase(name)) {
749: first = i;
750: if (last < 0) {
751: last = i;
752: }
753: }
754: }
755: }
756: if (first <= last) {
757: fireTableRowsUpdated(first, last);
758: }
759: }
760: }
761: }
|