0001: /*
0002: * GeoTools - OpenSource mapping toolkit
0003: * http://geotools.org
0004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
0005: * (C) 2002, Institut de Recherche pour le Développement
0006: *
0007: * This library is free software; you can redistribute it and/or
0008: * modify it under the terms of the GNU Lesser General Public
0009: * License as published by the Free Software Foundation; either
0010: * version 2.1 of the License, or (at your option) any later version.
0011: *
0012: * This library is distributed in the hope that it will be useful,
0013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
0015: * Lesser General Public License for more details.
0016: */
0017: package org.geotools.gui.swing.image;
0018:
0019: // JAI dependencies
0020: import javax.media.jai.KernelJAI;
0021: import javax.media.jai.operator.ConvolveDescriptor; // For Javadoc
0022:
0023: // Graphical user interface
0024: import java.awt.Component;
0025: import java.awt.Dimension;
0026: import java.awt.GridBagLayout;
0027: import java.awt.GridBagConstraints;
0028: import java.awt.IllegalComponentStateException;
0029:
0030: // Graphical user interface (Swing)
0031: import javax.swing.JFrame;
0032: import javax.swing.JPanel;
0033: import javax.swing.JLabel;
0034: import javax.swing.JTable;
0035: import javax.swing.JSpinner;
0036: import javax.swing.JComboBox;
0037: import javax.swing.JComponent;
0038: import javax.swing.JScrollPane;
0039: import javax.swing.BorderFactory;
0040: import javax.swing.ComboBoxModel;
0041: import javax.swing.table.TableModel;
0042: import javax.swing.table.AbstractTableModel;
0043:
0044: // Events
0045: import java.awt.event.ItemEvent;
0046: import java.awt.event.ItemListener;
0047: import javax.swing.event.ChangeEvent;
0048: import javax.swing.event.ListDataEvent;
0049: import javax.swing.event.ChangeListener;
0050: import javax.swing.event.ListDataListener;
0051:
0052: // Utilities
0053: import java.util.Map;
0054: import java.util.TreeMap;
0055: import java.util.HashMap;
0056: import java.util.LinkedHashMap;
0057: import java.util.Comparator;
0058: import java.util.Iterator;
0059: import java.util.Arrays;
0060: import java.util.Locale;
0061:
0062: // Geotools dependencies
0063: import org.geotools.resources.XArray;
0064: import org.geotools.resources.Utilities;
0065: import org.geotools.resources.i18n.Vocabulary;
0066: import org.geotools.resources.i18n.VocabularyKeys;
0067: import org.geotools.resources.SwingUtilities;
0068:
0069: /**
0070: * A widget for selecting and/or editing a {@link KernelJAI} object. Kernels are used for
0071: * {@linkplain ConvolveDescriptor image convolutions}. {@code KernelEditor} widgets are
0072: * initially empty, but a set of default kernels can be added with {@link #addDefaultKernels}
0073: * including (but not limited to)
0074: *
0075: * {@linkplain KernelJAI#ERROR_FILTER_FLOYD_STEINBERG Floyd & Steinberg (1975)},
0076: * {@linkplain KernelJAI#ERROR_FILTER_JARVIS Jarvis, Judice & Ninke (1976)} and
0077: * {@linkplain KernelJAI#ERROR_FILTER_STUCKI Stucki (1981)}.
0078: *
0079: * Each kernel can belong to an optional category. Example of categories includes
0080: * "Error filters" and "Gradient masks".
0081: *
0082: * <p> </p>
0083: * <p align="center"><img src="doc-files/KernelEditor.png"></p>
0084: * <p> </p>
0085: *
0086: * @since 2.3
0087: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/image/KernelEditor.java $
0088: * @version $Id: KernelEditor.java 20883 2006-08-07 13:48:09Z jgarnett $
0089: * @author Martin Desruisseaux
0090: *
0091: * @see GradientKernelEditor
0092: * @see ConvolveDescriptor
0093: * @see org.geotools.coverage.processing.operation.GradientMagnitude
0094: */
0095: public class KernelEditor extends JComponent {
0096: /**
0097: * The matrix coefficient as a table.
0098: */
0099: private final Model model = new Model();
0100:
0101: /**
0102: * The list of available filter's categories.
0103: */
0104: private final JComboBox categorySelector = new JComboBox();
0105:
0106: /**
0107: * The list of available kernels.
0108: */
0109: private final JComboBox kernelSelector = new JComboBox(model);
0110:
0111: /**
0112: * The matrix width.
0113: */
0114: private final JSpinner widthSelector = new JSpinner();
0115:
0116: /**
0117: * The matrix height.
0118: */
0119: private final JSpinner heightSelector = new JSpinner();
0120:
0121: /**
0122: * Constructs a new kernel editor. No kernel will be initially shown. The method
0123: * {@link #setKernel} must be invoked, or the user must performs a selection in
0124: * a combo box, in order to make a kernel visible.
0125: */
0126: public KernelEditor() {
0127: setLayout(new GridBagLayout());
0128: setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
0129: final Vocabulary resources = Vocabulary
0130: .getResources(getDefaultLocale());
0131:
0132: categorySelector.addItem(resources
0133: .getString(VocabularyKeys.ALL)); // Must be first category
0134: categorySelector.addItemListener(model);
0135: widthSelector.addChangeListener(model);
0136: heightSelector.addChangeListener(model);
0137:
0138: final JTable matrixView = new JTable(model);
0139: matrixView.setTableHeader(null);
0140: matrixView.setRowSelectionAllowed(false);
0141: matrixView.setColumnSelectionAllowed(false);
0142:
0143: final GridBagConstraints c = new GridBagConstraints();
0144: final JPanel predefinedKernels = new JPanel(new GridBagLayout());
0145:
0146: ////////////////////////////////////////////////////
0147: //// ////
0148: //// Put combo box for predefined kernels ////
0149: //// ////
0150: ////////////////////////////////////////////////////
0151: c.gridx = 0;
0152: c.fill = c.HORIZONTAL;
0153: c.gridy = 2;
0154: predefinedKernels.add(new JLabel(resources
0155: .getLabel(VocabularyKeys.CATEGORY), JLabel.RIGHT), c);
0156: c.gridy = 3;
0157: predefinedKernels.add(new JLabel(resources
0158: .getLabel(VocabularyKeys.KERNEL), JLabel.RIGHT), c);
0159:
0160: c.gridx = 1;
0161: c.weightx = 1;
0162: c.insets.left = 0;
0163: c.gridy = 2;
0164: predefinedKernels.add(categorySelector, c);
0165: c.gridy = 3;
0166: predefinedKernels.add(kernelSelector, c);
0167:
0168: c.gridx = 0;
0169: c.gridy = 2;
0170: c.gridwidth = c.REMAINDER;
0171: add(predefinedKernels, c);
0172: predefinedKernels.setBorder(BorderFactory.createCompoundBorder(
0173: BorderFactory.createTitledBorder(resources
0174: .getString(VocabularyKeys.PREDEFINED_KERNELS)),
0175: BorderFactory.createEmptyBorder(/*top*/3,/*left*/9,/*bottom*/
0176: 6,/*right*/6)));
0177:
0178: //////////////////////////////////////////////
0179: //// ////
0180: //// Put spinners for kernel's size ////
0181: //// ////
0182: //////////////////////////////////////////////
0183: c.weightx = 0;
0184: c.gridwidth = 1;
0185: c.insets.bottom = 3;
0186: c.gridy = 0;
0187: add(new JLabel(resources.getLabel(VocabularyKeys.SIZE),
0188: JLabel.RIGHT), c);
0189: c.gridx = 2;
0190: add(new JLabel(' '
0191: + resources.getString(VocabularyKeys.LINES)
0192: .toLowerCase() + " \u00D7 ", JLabel.CENTER), c);
0193: c.gridx = 4;
0194: add(new JLabel(' ' + resources
0195: .getString(VocabularyKeys.COLUMNS).toLowerCase(),
0196: JLabel.LEFT), c);
0197:
0198: c.weightx = 1;
0199: c.gridx = 1;
0200: add(heightSelector, c);
0201: c.gridx = 3;
0202: add(widthSelector, c);
0203:
0204: /////////////////////////////////////////////////
0205: //// ////
0206: //// Put table for kernel coefficients ////
0207: //// ////
0208: /////////////////////////////////////////////////
0209: c.gridx = 0;
0210: c.gridwidth = c.REMAINDER;
0211: c.fill = c.BOTH;
0212: c.insets.bottom = 9;
0213: c.gridy = 1;
0214: c.weighty = 1;
0215: c.insets.top = 6;
0216: add(new JScrollPane(matrixView), c);
0217: setPreferredSize(new Dimension(300, 220));
0218: }
0219:
0220: /**
0221: * Returns the resources for the widget locale.
0222: */
0223: final Vocabulary getResources() {
0224: Locale locale;
0225: try {
0226: locale = getLocale();
0227: } catch (IllegalComponentStateException exception) {
0228: locale = getDefaultLocale();
0229: }
0230: return Vocabulary.getResources(locale);
0231: }
0232:
0233: /**
0234: * Add a set of predefined kernels. Default kernels includes (but is not limited to)
0235: *
0236: * {@linkplain KernelJAI#ERROR_FILTER_FLOYD_STEINBERG Floyd & Steinberg (1975)},
0237: * {@linkplain KernelJAI#ERROR_FILTER_JARVIS Jarvis, Judice & Ninke (1976)} and
0238: * {@linkplain KernelJAI#ERROR_FILTER_STUCKI Stucki (1981)}.
0239: */
0240: public void addDefaultKernels() {
0241: final Vocabulary resources = getResources();
0242: final String ERROR_FILTERS = resources
0243: .getString(VocabularyKeys.ERROR_FILTERS);
0244: final String GRADIENT_MASKS = resources
0245: .getString(VocabularyKeys.GRADIENT_MASKS);
0246: addKernel(ERROR_FILTERS, "Floyd & Steinberg (1975)",
0247: KernelJAI.ERROR_FILTER_FLOYD_STEINBERG);
0248: addKernel(ERROR_FILTERS, "Jarvis, Judice & Ninke (1976)",
0249: KernelJAI.ERROR_FILTER_JARVIS);
0250: addKernel(ERROR_FILTERS, "Stucki (1981)",
0251: KernelJAI.ERROR_FILTER_STUCKI);
0252: /*
0253: * NOTE: Horizontal and vertical sobel masks seems to have been swapped in KernelJAI.
0254: * See for example J.J. Simpson (1990) in Remote sensing environment, 33:17-33.
0255: * See also some tests in a speadsheet.
0256: * Is it an error in JAI 1.1.2 or a misunderstanding of mine?
0257: */
0258: addKernel(GRADIENT_MASKS, "Sobel horizontal",
0259: KernelJAI.GRADIENT_MASK_SOBEL_VERTICAL);
0260: addKernel(GRADIENT_MASKS, "Sobel vertical",
0261: KernelJAI.GRADIENT_MASK_SOBEL_HORIZONTAL);
0262:
0263: addKernel("Sharp 1", new float[] { 0.0f, -1.0f, 0.0f, -1.0f,
0264: 5.0f, -1.0f, 0.0f, -1.0f, 0.0f });
0265:
0266: addKernel("Sharp 2", new float[] { -1.0f, -1.0f, -1.0f, -1.0f,
0267: 9.0f, -1.0f, -1.0f, -1.0f, -1.0f });
0268:
0269: addKernel("Sharp 3", new float[] { 1.0f, -2.0f, 1.0f, -2.0f,
0270: 5.0f, -2.0f, 1.0f, -2.0f, 1.0f });
0271:
0272: addKernel("Sharp 4", new float[] { -1.0f, 1.0f, -1.0f, 1.0f,
0273: 1.0f, 1.0f, -1.0f, 1.0f, -1.0f });
0274:
0275: addKernel("Laplace 1", new float[] { 0.0f, -1.0f, 0.0f, -1.0f,
0276: 4.0f, -1.0f, 0.0f, -1.0f, 0.0f });
0277:
0278: addKernel("Laplace 2", new float[] { -1.0f, -1.0f, -1.0f,
0279: -1.0f, 8.0f, -1.0f, -1.0f, -1.0f, -1.0f });
0280:
0281: addKernel("Box", new float[] { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
0282: 1.0f, 1.0f, 1.0f, 1.0f });
0283:
0284: addKernel("Low pass", new float[] { 1.0f, 2.0f, 1.0f, 2.0f,
0285: 4.0f, 2.0f, 1.0f, 2.0f, 1.0f });
0286:
0287: if (model.getRowCount() * model.getColumnCount() == 0) {
0288: setKernel("Box");
0289: }
0290: }
0291:
0292: /**
0293: * Adds a 3x3 kernel to the list of available kernels.
0294: */
0295: private void addKernel(final String name, final float[] data) {
0296: double sum = 0;
0297: for (int i = 0; i < data.length; i++) {
0298: sum += data[i];
0299: }
0300: if (sum != 0) {
0301: for (int i = 0; i < data.length; i++) {
0302: data[i] /= sum;
0303: }
0304: }
0305: addKernel(null, name, new KernelJAI(3, 3, data));
0306: }
0307:
0308: /**
0309: * Adds a kernel to the list of available kernels. The widget list kernels in the same order
0310: * they were added, unless {@link #sortKernelNames} has been invoked. Each kernel can belong
0311: * to an optional category. Example of categories includes "Error filters" and "Gradient masks".
0312: *
0313: * @param category The kernel's category name, or {@code null} if none.
0314: * @param name The kernel name. Kernels will be displayed in alphabetic order.
0315: * @param kernel The kernel. If an other kernel was registered with the same
0316: * name, the previous kernel will be discarted.
0317: */
0318: public void addKernel(String category, final String name,
0319: final KernelJAI kernel) {
0320: if (category == null) {
0321: category = Vocabulary.getResources(getLocale()).getString(
0322: VocabularyKeys.OTHERS);
0323: }
0324: model.addKernel(category, name, kernel);
0325: }
0326:
0327: /**
0328: * Adds a new category if not already present.
0329: */
0330: private void addCategory(final String category) {
0331: final ComboBoxModel categories = categorySelector.getModel();
0332: for (int i = categories.getSize(); --i >= 0;) {
0333: if (category.equals(categories.getElementAt(i))) {
0334: return;
0335: }
0336: }
0337: categorySelector.addItem(category);
0338: }
0339:
0340: /**
0341: * Removes a kernel. If the kernel was the only one in
0342: * its category, the category is removed as well.
0343: */
0344: public void removeKernel(final KernelJAI kernel) {
0345: model.removeKernel(kernel);
0346: }
0347:
0348: /**
0349: * Removes a kernel by its name. If the kernel was the only
0350: * one in its category, the category is removed as well.
0351: */
0352: public void removeKernel(final String kernel) {
0353: removeKernel(model.getKernel(kernel));
0354: }
0355:
0356: /**
0357: * Removes all kernels and categories.
0358: */
0359: public void removeAllKernels() {
0360: model.removeAllKernels();
0361: }
0362:
0363: /**
0364: * Set the kernel. The table size will be set to the specified kernel size, add all
0365: * coefficients will be copied in the table. If the specified kernel matches one of
0366: * the kernel registered with the {@link #addKernel addKernel} method, then the kernel
0367: * name and category will be updated according.
0368: *
0369: * @param kernel The new kernel.
0370: */
0371: public void setKernel(final KernelJAI kernel) {
0372: model.setKernel(kernel);
0373: model.findKernelName();
0374: }
0375:
0376: /**
0377: * Set the kernel by its name. It must be one of the name registered with {@link #addKernel}.
0378: * If {@code name} is not found, then nothing is done.
0379: *
0380: * @param name The name of the kernel to select.
0381: */
0382: public void setKernel(final String name) {
0383: kernelSelector.setSelectedItem(name);
0384: kernelSelector.repaint();
0385: }
0386:
0387: /**
0388: * Set the size of the current kernel.
0389: *
0390: * @param width The number of rows.
0391: * @param height The number of columns.
0392: */
0393: public void setKernelSize(final int width, final int height) {
0394: model.setKernelSize(height, width); // Inverse argument order.
0395: model.findKernelName();
0396: }
0397:
0398: /**
0399: * Returns the currently edited kernel.
0400: *
0401: * @return The edited kernel.
0402: */
0403: public KernelJAI getKernel() {
0404: return model.getKernel();
0405: }
0406:
0407: /**
0408: * Returns the category for the current kernel. This is the {@code category} argument
0409: * given to <code>{@linkplain #addKernel addKernel}(category, name, kernel)</code>, where
0410: * {@code kernel} is the {@linkplain #getKernel current kernel}.
0411: *
0412: * @return The category for the current kernel, or {@code null} if none.
0413: */
0414: public String getKernelCategory() {
0415: // Category at index 0 is "all", which need a special handling.
0416: return categorySelector.getSelectedIndex() <= 0 ? null
0417: : (String) categorySelector.getSelectedItem();
0418: }
0419:
0420: /**
0421: * Sort all kernel names according the specified comparator.
0422: *
0423: * @param comparator The comparator, or {@code null} for the natural ordering.
0424: */
0425: public void sortKernelNames(final Comparator comparator) {
0426: model.sortKernelNames(comparator);
0427: }
0428:
0429: /**
0430: * Returns an array of kernel names in the current category.
0431: * Changes in the returned array will not affect the {@code KernelEditor} state.
0432: *
0433: * @return The name of all kernels in the current category.
0434: */
0435: public String[] getKernelNames() {
0436: return (String[]) model.getKernelNames().clone();
0437: }
0438:
0439: /**
0440: * Returns the list of predefined kernels in the current category. The content of
0441: * this list will changes every time a kernel is {@linkplain #addKernel added} or
0442: * {@linkplain #removeKernel(KernelJAI) removed} and every time the user selects a
0443: * new category. The selected item can change at any time as well, according user action.
0444: */
0445: public ComboBoxModel getKernelListModel() {
0446: return model;
0447: }
0448:
0449: /**
0450: * Returns the table model containing the current kernel coefficients. The content of this
0451: * table will changes every time the user select a new predefined kernel, or when the user
0452: * edit cell values.
0453: */
0454: public TableModel getKernelTableModel() {
0455: return model;
0456: }
0457:
0458: /**
0459: * The table and list model to use. The list model contains a list of
0460: * predefined kernels. The table model contains coefficients for the
0461: * currently selected kernel. This object is also a listener for various
0462: * events (like changing the size of the table).
0463: *
0464: * @version $Id: KernelEditor.java 20883 2006-08-07 13:48:09Z jgarnett $
0465: * @author Martin Desruisseaux
0466: */
0467: private final class Model extends AbstractTableModel implements
0468: ComboBoxModel, ChangeListener, ItemListener {
0469: /**
0470: * Dictionnary of kernels by their name.
0471: * Keys are {@link String} and values are {@link KernelJAI}.
0472: */
0473: private final Map kernels = new HashMap();
0474:
0475: /**
0476: * List of categories by kernel's name.
0477: * Keys and values are both {@link String}.
0478: */
0479: private final Map categories = new LinkedHashMap();
0480:
0481: /**
0482: * {@code true} if the keys into {@link #categories}
0483: * are sorted according their <em>natural</em> ordering.
0484: */
0485: private boolean sorted;
0486:
0487: /**
0488: * List of kernel names in alphabetical order.
0489: * This list is constructed only when first needed.
0490: */
0491: private String[] names;
0492:
0493: /**
0494: * Name of the current kernel, or {@code null}
0495: * if the user is editing a custom kernel.
0496: */
0497: private String name;
0498:
0499: /**
0500: * Array of elements for the current kernel.
0501: */
0502: private float[][] elements = new float[0][];
0503:
0504: /**
0505: * Returns the number of kernels in the list.
0506: * Used by the combox box of kernel names.
0507: */
0508: public int getSize() {
0509: return getKernelNames().length;
0510: }
0511:
0512: /**
0513: * Returns the number of rows in the kernel.
0514: * Used by the table of kernel values.
0515: */
0516: public int getRowCount() {
0517: return elements.length;
0518: }
0519:
0520: /**
0521: * Returns the number of columns in the model.
0522: * Used by the table of kernel values.
0523: */
0524: public int getColumnCount() {
0525: return (elements.length != 0) ? elements[0].length : 0;
0526: }
0527:
0528: /**
0529: * Returns {@code true} regardless of row and column index
0530: * Used by the table of kernel values.
0531: */
0532: public boolean isCellEditable(final int rowIndex,
0533: final int columnIndex) {
0534: return true;
0535: }
0536:
0537: /**
0538: * Returns {@code Float.class} regardless of column index
0539: * Used by the table of kernel values.
0540: */
0541: public Class getColumnClass(final int columnIndex) {
0542: return Float.class;
0543: }
0544:
0545: /**
0546: * Returns the value for the cell at {@code columnIndex} and {@code rowIndex}.
0547: * This method is automatically invoked in order to paint the kernel as a table.
0548: */
0549: public Object getValueAt(final int rowIndex,
0550: final int columnIndex) {
0551: return new Float(elements[rowIndex][columnIndex]);
0552: }
0553:
0554: /**
0555: * Set the value for the cell at {@code columnIndex} and {@code rowIndex}.
0556: * This method is automatically invoked when the user edited one of kernel values.
0557: */
0558: public void setValueAt(final Object value, final int rowIndex,
0559: final int columnIndex) {
0560: elements[rowIndex][columnIndex] = (value != null) ? ((Number) value)
0561: .floatValue()
0562: : 0;
0563: fireTableCellUpdated(rowIndex, columnIndex);
0564: findKernelName();
0565: }
0566:
0567: /**
0568: * Returns the kernel at the specified index.
0569: * Used by the combox box of kernel names.
0570: */
0571: public Object getElementAt(final int index) {
0572: return getKernelNames()[index];
0573: }
0574:
0575: /**
0576: * Returns the selected kernel name (never {@code null}).
0577: * Used by the combox box of kernel names.
0578: */
0579: public Object getSelectedItem() {
0580: return (name != null) ? name
0581: : getString(VocabularyKeys.PERSONALIZED);
0582: }
0583:
0584: /**
0585: * Set the selected kernel by its name (never {@code null}).
0586: * Used by the combox box of kernel names.
0587: */
0588: public void setSelectedItem(final Object item) {
0589: final String newName = item.toString();
0590: if (!newName.equals(name)) {
0591: // 'kernel' may be null if 'item' is the "Personalized" kernel name.
0592: final KernelJAI kernel = (KernelJAI) kernels
0593: .get(newName);
0594: if (kernel != null) {
0595: setKernel(kernel);
0596: }
0597: categorySelector.setSelectedItem(categories
0598: .get(newName));
0599: this .name = newName;
0600: }
0601: }
0602:
0603: /**
0604: * Returns the current kernel.
0605: *
0606: * @see KernelEditor#getKernel
0607: */
0608: public KernelJAI getKernel() {
0609: final int height = elements.length;
0610: final int width = height != 0 ? elements[0].length : 0;
0611: final float[] data = new float[width * height];
0612: int c = 0;
0613: for (int j = 0; j < height; j++) {
0614: for (int i = 0; i < width; i++) {
0615: data[c++] = elements[j][i];
0616: }
0617: }
0618: return new KernelJAI(width, height, data);
0619: }
0620:
0621: /**
0622: * Set the kernel. The table size will be set to the specified kernel size, add all
0623: * coefficients will be copied in the table. If the specified kernel matches one of
0624: * the kernel registered with the {@link #addKernel addKernel} method, then the kernel
0625: * name and category will be updated according.
0626: *
0627: * @see KernelEditor#setKernel
0628: */
0629: public void setKernel(final KernelJAI kernel) {
0630: final int rowCount = kernel.getHeight();
0631: final int colCount = kernel.getWidth();
0632: setKernelSize(rowCount, colCount);
0633: for (int j = 0; j < rowCount; j++) {
0634: for (int i = 0; i < colCount; i++) {
0635: elements[j][i] = kernel.getElement(i, j);
0636: }
0637: }
0638: fireTableDataChanged();
0639: }
0640:
0641: /**
0642: * Set the size of the current kernel.
0643: *
0644: * @param width The number of rows.
0645: * @param height The number of columns.
0646: *
0647: * @see KernelEditor#setKernelSize
0648: */
0649: public void setKernelSize(final int rowCount, final int colCount) {
0650: final int oldRowCount = elements.length;
0651: final int oldColCount = oldRowCount != 0 ? elements[0].length
0652: : 0;
0653: if (rowCount != oldRowCount || colCount != oldColCount) {
0654: elements = (float[][]) XArray
0655: .resize(elements, rowCount);
0656: for (int i = 0; i < elements.length; i++) {
0657: if (elements[i] == null) {
0658: elements[i] = new float[colCount];
0659: } else {
0660: elements[i] = XArray.resize(elements[i],
0661: colCount);
0662: }
0663: }
0664: if (colCount != oldColCount) {
0665: fireTableStructureChanged();
0666: } else if (rowCount > oldRowCount) {
0667: fireTableRowsInserted(oldRowCount, rowCount - 1);
0668: } else if (rowCount < oldRowCount) {
0669: fireTableRowsDeleted(rowCount, oldRowCount - 1);
0670: }
0671: widthSelector.setValue(new Integer(colCount));
0672: heightSelector.setValue(new Integer(rowCount));
0673: }
0674: }
0675:
0676: /**
0677: * Returns the index of the specified kernel name in the specified category,
0678: * or -1 if it was not found. This method is invoked by {@link #addKernel}
0679: * and {@link #removeKernel} in order to determine the range of index values
0680: * to give to {@link ListDataEvent}.
0681: *
0682: * @param category The kernel category. Only kernels in this category will
0683: * be taken in account. This argument is usually provided by
0684: * {@link KernelEditor#getKernelCategory}. {@code null}
0685: * is a special value taking all categories in account.
0686: * @param toSearch The name of the kernel to search.
0687: */
0688: private int indexOf(final String category, final String toSearch) {
0689: int index = 0;
0690: for (final Iterator it = categories.entrySet().iterator(); it
0691: .hasNext();) {
0692: final Map.Entry entry = (Map.Entry) it.next();
0693: final String name = (String) entry.getKey();
0694: if (category == null
0695: || category.equals(entry.getValue())) {
0696: // A kernel of the required category has been found.
0697: if (toSearch.equals(name)) {
0698: // Found the kernel we are looking for.
0699: assert !sorted
0700: || Arrays.binarySearch(
0701: getKernelNames(), toSearch) == index;
0702: return index;
0703: }
0704: // Right category, but wrong kernel.
0705: index++;
0706: }
0707: if (sorted && name.compareTo(toSearch) >= 0) {
0708: // Since kernel names are sorted, there is no
0709: // need to continue the iteration past this point.
0710: break;
0711: }
0712: }
0713: assert !sorted
0714: || Arrays.binarySearch(getKernelNames(), toSearch) < 0;
0715: return -1;
0716: }
0717:
0718: /**
0719: * Adds a kernel to the list of available kernels. Each kernel can belong to an optional
0720: * category. Example of categories includes "Error filters" and "Gradient masks".
0721: *
0722: * @param category The kernel's category name, which <strong>must not</strong> be
0723: * {@code null}. The public method in {@link KernelEditor} is responsible
0724: * for substituing a string (usually "Others" or "Personalized") in place of the
0725: * null value.
0726: * @param name The kernel name. Kernels will be displayed in alphabetic order.
0727: * @param kernel The kernel. If an other kernel was registered with the same
0728: * name, the previous kernel will be discarted.
0729: *
0730: * @see KernelEditor#addKernel
0731: */
0732: public void addKernel(final String category, final String name,
0733: final KernelJAI kernel) {
0734: sorted = false;
0735: if (!category.equals(categories.put(name, category))) {
0736: // The category doesn't already exists.
0737: addCategory(category);
0738: }
0739: if (kernels.put(name, kernel) != null) {
0740: // The new kernel replace an existing one.
0741: findKernelName();
0742: } else {
0743: // The kernel must be added to existing ones.
0744: final String cc = getKernelCategory();
0745: if (cc == null || category.equals(cc)) {
0746: names = null; // Must be before 'indexOf'
0747: final int index = indexOf(cc, name);
0748: assert index >= 0 : name;
0749: fireListChanged(ListDataEvent.INTERVAL_ADDED,
0750: index, index);
0751: }
0752: }
0753: assert kernels.size() == categories.size();
0754: }
0755:
0756: /**
0757: * Removes a kernel. If the kernel was the only one in
0758: * its category, the category is removed as well.
0759: *
0760: * @see KernelEditor#removeKernel
0761: */
0762: public void removeKernel(final KernelJAI kernel) {
0763: final String cc = getKernelCategory();
0764: for (final Iterator it = kernels.entrySet().iterator(); it
0765: .hasNext();) {
0766: final Map.Entry entry = (Map.Entry) it.next();
0767: if (kernel.equals(entry.getValue())) {
0768: // Found the kernel to remove.
0769: final String name = (String) entry.getKey();
0770: final int index = indexOf(cc, name); // Must be before any remove.
0771: final String category = (String) categories
0772: .remove(name);
0773: if (!categories.values().contains(category)) {
0774: // No other kernel in this category.
0775: categorySelector.removeItem(category);
0776: }
0777: it.remove();
0778: if (index >= 0) {
0779: names = null; // Must be after 'it.remove'
0780: fireListChanged(ListDataEvent.INTERVAL_REMOVED,
0781: index, index);
0782: }
0783: }
0784: }
0785: assert kernels.size() == categories.size();
0786: }
0787:
0788: /**
0789: * Removes all kernels and categories.
0790: *
0791: * @see KernelEditor#removeAllKernels
0792: */
0793: public void removeAllKernels() {
0794: final int size = kernels.size();
0795: kernels.clear();
0796: categories.clear();
0797: names = null;
0798: fireListChanged(ListDataEvent.INTERVAL_REMOVED, 0, size - 1);
0799: categorySelector.removeAllItems();
0800: categorySelector.addItem(getResources().getString(
0801: VocabularyKeys.ALL));
0802: }
0803:
0804: /**
0805: * Returns a kernel by its name.
0806: *
0807: * @param name The kernel name.
0808: * @return The kernel, or {@code null} if there is no kernel for the specified name.
0809: */
0810: public KernelJAI getKernel(final String name) {
0811: return (KernelJAI) kernels.get(name);
0812: }
0813:
0814: /**
0815: * Returns the array of kernel names. <strong>This method
0816: * returns the array by reference; do not modify!</strong>.
0817: *
0818: * @see KernelEditor#getKernelNames
0819: */
0820: public String[] getKernelNames() {
0821: if (names == null) {
0822: int count = 0;
0823: names = new String[kernels.size() + 1];
0824: final String category = getKernelCategory();
0825: for (final Iterator it = categories.entrySet()
0826: .iterator(); it.hasNext();) {
0827: final Map.Entry entry = (Map.Entry) it.next();
0828: if (category == null
0829: || category.equals(entry.getValue())) {
0830: names[count++] = (String) entry.getKey();
0831: }
0832: }
0833: names[count++] = getString(VocabularyKeys.PERSONALIZED);
0834: names = (String[]) XArray.resize(names, count);
0835: }
0836: return names;
0837: }
0838:
0839: /**
0840: * Find the name for the current kernel. If such a name is
0841: * found, it will be given to the combo-box. Otherwise,
0842: * nothing is done.
0843: */
0844: protected void findKernelName() {
0845: String newName = null; // "Personalized"
0846: final int rowCount = elements.length;
0847: final int colCount = rowCount != 0 ? elements[0].length : 0;
0848: iter: for (final Iterator it = kernels.entrySet()
0849: .iterator(); it.hasNext();) {
0850: final Map.Entry entry = (Map.Entry) it.next();
0851: final KernelJAI kernel = (KernelJAI) entry.getValue();
0852: if (rowCount == kernel.getHeight()
0853: && colCount == kernel.getWidth()) {
0854: for (int j = 0; j < rowCount; j++) {
0855: for (int i = 0; i < colCount; i++) {
0856: if (elements[j][i] != kernel.getElement(i,
0857: j)) {
0858: continue iter;
0859: }
0860: }
0861: }
0862: newName = (String) entry.getKey();
0863: }
0864: }
0865: if (newName == null) {
0866: newName = getString(VocabularyKeys.PERSONALIZED);
0867: }
0868: if (!newName.equals(name)) {
0869: // Set the name now in order to avoid that
0870: // setSelectedItem invokes setKernel again.
0871: this .name = newName;
0872: categorySelector.setSelectedItem(categories
0873: .get(newName));
0874: kernelSelector.setSelectedItem(newName);
0875: kernelSelector.repaint(); // JComboBox doesn't seems to repaint by itself.
0876: }
0877: }
0878:
0879: /**
0880: * Sorts all kernel names according the specified comparator.
0881: *
0882: * @param comparator The comparator, or {@code null} for the natural ordering.
0883: *
0884: * @see KernelEditor#sortKernelNames
0885: */
0886: public void sortKernelNames(final Comparator comparator) {
0887: final Map sorted = new TreeMap(comparator);
0888: sorted.putAll(categories);
0889: categories.clear();
0890: categories.putAll(sorted);
0891: names = null;
0892: this .sorted = (comparator == null);
0893: fireListChanged(ListDataEvent.CONTENTS_CHANGED, 0,
0894: categories.size() - 1);
0895: }
0896:
0897: /**
0898: * Invoked when a {@link JSpinner} has changed its state.
0899: * This method reset the matrix size according the new
0900: * spinner value.
0901: */
0902: public void stateChanged(final ChangeEvent event) {
0903: final int rowCount = ((Number) heightSelector.getValue())
0904: .intValue();
0905: final int colCount = ((Number) widthSelector.getValue())
0906: .intValue();
0907: setKernelSize(rowCount, colCount);
0908: findKernelName();
0909: }
0910:
0911: /**
0912: * Invoked when the user selected a new kernel category.
0913: * The kernel list must be cleared and reconstructed.
0914: */
0915: public void itemStateChanged(final ItemEvent event) {
0916: if (event.getStateChange() == ItemEvent.SELECTED) {
0917: names = null;
0918: fireListChanged(ListDataEvent.CONTENTS_CHANGED, 0,
0919: categories.size());
0920: }
0921: }
0922:
0923: /**
0924: * Convenience method returning a string for the specified resource keys.
0925: */
0926: private String getString(final int key) {
0927: return getResources().getString(key);
0928: }
0929:
0930: /**
0931: * Adds a listener to the list that's notified
0932: * each time a change to the data model occurs.
0933: */
0934: public void addListDataListener(final ListDataListener listener) {
0935: listenerList.add(ListDataListener.class, listener);
0936: }
0937:
0938: /**
0939: * Removes a listener from the list that's notified
0940: * each time a change to the data model occurs.
0941: */
0942: public void removeListDataListener(
0943: final ListDataListener listener) {
0944: listenerList.remove(ListDataListener.class, listener);
0945: }
0946:
0947: /**
0948: * Invoked after one or more kernels are added to the model.
0949: *
0950: * @param type Must be one of {@link ListDataEvent#CONTENTS_CHANGED},
0951: * {@link ListDataEvent#INTERVAL_ADDED} or {@link ListDataEvent#INTERVAL_REMOVED}.
0952: * @param index0 Lower index, inclusive.
0953: * @param index1 Upper index, <strong>inclusive</strong>.
0954: */
0955: private void fireListChanged(final int type, final int index0,
0956: final int index1) {
0957: ListDataEvent event = null;
0958: final Object[] listeners = listenerList.getListenerList();
0959: for (int i = listeners.length; (i -= 2) >= 0;) {
0960: if (listeners[i] == ListDataListener.class) {
0961: if (event == null) {
0962: event = new ListDataEvent(this , type, index0,
0963: index1);
0964: }
0965: final ListDataListener listener = (ListDataListener) listeners[i + 1];
0966: switch (type) {
0967: case ListDataEvent.CONTENTS_CHANGED:
0968: listener.contentsChanged(event);
0969: break;
0970: case ListDataEvent.INTERVAL_ADDED:
0971: listener.intervalAdded(event);
0972: break;
0973: case ListDataEvent.INTERVAL_REMOVED:
0974: listener.intervalRemoved(event);
0975: break;
0976: }
0977: }
0978: }
0979: }
0980: }
0981:
0982: /**
0983: * Shows a dialog box requesting input from the user. If {@code owner} is contained into a
0984: * {@link javax.swing.JDesktopPane}, the dialog box will appears as an internal frame. This
0985: * method can be invoked from any thread (may or may not be the <cite>Swing</cite> thread).
0986: *
0987: * @param owner The parent component for the dialog box, or {@code null} if there is no parent.
0988: * @param title The dialog box title.
0989: * @return {@code true} if user pressed the "Ok" button, or {@code false} otherwise
0990: * (e.g. pressing "Cancel" or closing the dialog box from the title bar).
0991: */
0992: public boolean showDialog(final Component owner, final String title) {
0993: return SwingUtilities.showOptionDialog(owner, this , title);
0994: }
0995:
0996: /**
0997: * Show the dialog box. This method is provided only as an easy
0998: * way to test the dialog appearance from the command line.
0999: */
1000: public static void main(final String[] args) {
1001: final KernelEditor editor = new KernelEditor();
1002: editor.addDefaultKernels();
1003: editor.showDialog(null, Utilities.getShortClassName(editor));
1004: }
1005: }
|