/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2002, Institut de Recherche pour le Développement
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.gui.swing.image;

// JAI dependencies
import javax.media.jai.KernelJAI;
import javax.media.jai.operator.ConvolveDescriptor; // For Javadoc

// Graphical user interface
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.IllegalComponentStateException;

// Graphical user interface (Swing)
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.JSpinner;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.BorderFactory;
import javax.swing.ComboBoxModel;
import javax.swing.table.TableModel;
import javax.swing.table.AbstractTableModel;

// Events
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListDataListener;

// Utilities
import java.util.Map;
import java.util.TreeMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Arrays;
import java.util.Locale;

// Geotools dependencies
import org.geotools.resources.XArray;
import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.SwingUtilities;


/**
 * A widget for selecting and/or editing a {@link KernelJAI} object. Kernels are used for
 * {@linkplain ConvolveDescriptor image convolutions}. {@code KernelEditor} widgets are
 * initially empty, but a set of default kernels can be added with {@link #addDefaultKernels}
 * including (but not limited to)
 *
 * {@linkplain KernelJAI#ERROR_FILTER_FLOYD_STEINBERG Floyd & Steinberg (1975)},
 * {@linkplain KernelJAI#ERROR_FILTER_JARVIS Jarvis, Judice & Ninke (1976)} and
 * {@linkplain KernelJAI#ERROR_FILTER_STUCKI Stucki (1981)}.
 *
 * Each kernel can belong to an optional category. Example of categories includes
 * "Error filters" and "Gradient masks".
 *
 * <p>&nbsp;</p>
 * <p align="center"><img src="doc-files/KernelEditor.png"></p>
 * <p>&nbsp;</p>
 *
 * @since 2.3
 * @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 $
 * @version $Id: KernelEditor.java 20883 2006-08-07 13:48:09Z jgarnett $
 * @author Martin Desruisseaux
 *
 * @see GradientKernelEditor
 * @see ConvolveDescriptor
 * @see org.geotools.coverage.processing.operation.GradientMagnitude
 */
public class KernelEditor extends JComponent {
    /**
     * The matrix coefficient as a table.
     */
    private final Model model = new Model();

    /**
     * The list of available filter's categories.
     */
    private final JComboBox categorySelector = new JComboBox();

    /**
     * The list of available kernels.
     */
    private final JComboBox kernelSelector = new JComboBox(model);

    /**
     * The matrix width.
     */
    private final JSpinner widthSelector = new JSpinner();

    /**
     * The matrix height.
     */
    private final JSpinner heightSelector = new JSpinner();

    /**
     * Constructs a new kernel editor. No kernel will be initially shown. The method
     * {@link #setKernel} must be invoked, or the user must performs a selection in
     * a combo box, in order to make a kernel visible.
     */
    public KernelEditor() {
        setLayout(new GridBagLayout());
        setBorder(BorderFactory.createEmptyBorder(6,6,6,6));
        final Vocabulary resources = Vocabulary.getResources(getDefaultLocale());

        categorySelector.addItem(resources.getString(VocabularyKeys.ALL)); // Must be first category
        categorySelector.addItemListener(model);
        widthSelector.   addChangeListener(model);
        heightSelector.  addChangeListener(model);

        final JTable matrixView = new JTable(model);
        matrixView.setTableHeader(null);
        matrixView.setRowSelectionAllowed(false);
        matrixView.setColumnSelectionAllowed(false);

        final GridBagConstraints c = new GridBagConstraints();
        final JPanel predefinedKernels = new JPanel(new GridBagLayout());

        ////////////////////////////////////////////////////
        ////                                            ////
        ////    Put combo box for predefined kernels    ////
        ////                                            ////
        ////////////////////////////////////////////////////
        c.gridx=0; c.fill=c.HORIZONTAL;
        c.gridy=2; predefinedKernels.add(new JLabel(resources.getLabel(VocabularyKeys.CATEGORY), JLabel.RIGHT ), c);
        c.gridy=3; predefinedKernels.add(new JLabel(resources.getLabel(VocabularyKeys.KERNEL),   JLabel.RIGHT ), c);

        c.gridx=1; c.weightx=1; c.insets.left=0;
        c.gridy=2; predefinedKernels.add(categorySelector, c);
        c.gridy=3; predefinedKernels.add(kernelSelector,   c);

        c.gridx=0; c.gridy=2; c.gridwidth=c.REMAINDER; add(predefinedKernels, c);
        predefinedKernels.setBorder(
            BorderFactory.createCompoundBorder(
            BorderFactory.createTitledBorder(resources.getString(VocabularyKeys.PREDEFINED_KERNELS)),
            BorderFactory.createEmptyBorder(/*top*/3,/*left*/9,/*bottom*/6,/*right*/6)));


        //////////////////////////////////////////////
        ////                                      ////
        ////    Put spinners for kernel's size    ////
        ////                                      ////
        //////////////////////////////////////////////
        c.weightx=0; c.gridwidth=1; c.insets.bottom=3;
        c.gridy=0; add(new JLabel(    resources.getLabel(VocabularyKeys.SIZE), JLabel.RIGHT), c);
        c.gridx=2; add(new JLabel(' '+resources.getString(VocabularyKeys.LINES).toLowerCase()+" \u00D7 ", JLabel.CENTER), c);
        c.gridx=4; add(new JLabel(' '+resources.getString(VocabularyKeys.COLUMNS).toLowerCase(), JLabel.LEFT),  c);

        c.weightx=1;
        c.gridx=1; add(heightSelector, c);
        c.gridx=3; add(widthSelector,  c);


        /////////////////////////////////////////////////
        ////                                         ////
        ////    Put table for kernel coefficients    ////
        ////                                         ////
        /////////////////////////////////////////////////
        c.gridx=0; c.gridwidth=c.REMAINDER; c.fill=c.BOTH; c.insets.bottom=9;
        c.gridy=1; c.weighty=1; c.insets.top=6; add(new JScrollPane(matrixView), c);
        setPreferredSize(new Dimension(300,220));
    }

    /**
     * Returns the resources for the widget locale.
     */
    final Vocabulary getResources() {
        Locale locale;
        try {
            locale = getLocale();
        } catch (IllegalComponentStateException exception) {
            locale = getDefaultLocale();
        }
        return Vocabulary.getResources(locale);
    }

    /**
     * Add a set of predefined kernels. Default kernels includes (but is not limited to)
     *
     * {@linkplain KernelJAI#ERROR_FILTER_FLOYD_STEINBERG Floyd & Steinberg (1975)},
     * {@linkplain KernelJAI#ERROR_FILTER_JARVIS Jarvis, Judice & Ninke (1976)} and
     * {@linkplain KernelJAI#ERROR_FILTER_STUCKI Stucki (1981)}.
     */
    public void addDefaultKernels() {
        final Vocabulary resources  = getResources();
        final String ERROR_FILTERS  = resources.getString(VocabularyKeys.ERROR_FILTERS);
        final String GRADIENT_MASKS = resources.getString(VocabularyKeys.GRADIENT_MASKS);
        addKernel(ERROR_FILTERS,  "Floyd & Steinberg (1975)",      KernelJAI.ERROR_FILTER_FLOYD_STEINBERG);
        addKernel(ERROR_FILTERS,  "Jarvis, Judice & Ninke (1976)", KernelJAI.ERROR_FILTER_JARVIS);
        addKernel(ERROR_FILTERS,  "Stucki (1981)",                 KernelJAI.ERROR_FILTER_STUCKI);
        /*
         * NOTE: Horizontal and vertical sobel masks seems to have been swapped in KernelJAI.
         *       See for example J.J. Simpson (1990) in Remote sensing environment, 33:17-33.
         *       See also some tests in a speadsheet.
         *       Is it an error in JAI 1.1.2 or a misunderstanding of mine?
         */
        addKernel(GRADIENT_MASKS, "Sobel horizontal", KernelJAI.GRADIENT_MASK_SOBEL_VERTICAL);
        addKernel(GRADIENT_MASKS, "Sobel vertical",   KernelJAI.GRADIENT_MASK_SOBEL_HORIZONTAL);

        addKernel("Sharp 1",   new float[] { 0.0f, -1.0f,  0.0f,
                                            -1.0f,  5.0f, -1.0f,
                                             0.0f, -1.0f,  0.0f});

        addKernel("Sharp 2",   new float[] {-1.0f, -1.0f, -1.0f,
                                            -1.0f,  9.0f, -1.0f,
                                            -1.0f, -1.0f, -1.0f});

        addKernel("Sharp 3",   new float[] { 1.0f, -2.0f,  1.0f,
                                            -2.0f,  5.0f, -2.0f,
                                             1.0f, -2.0f,  1.0f});

        addKernel("Sharp 4"  , new float[] {-1.0f,  1.0f, -1.0f,
                                             1.0f,  1.0f,  1.0f,
                                            -1.0f,  1.0f, -1.0f});

        addKernel("Laplace 1", new float[] { 0.0f, -1.0f,  0.0f,
                                            -1.0f,  4.0f, -1.0f,
                                             0.0f, -1.0f,  0.0f});

        addKernel("Laplace 2", new float[] {-1.0f, -1.0f, -1.0f,
                                            -1.0f,  8.0f, -1.0f,
                                            -1.0f, -1.0f, -1.0f});

        addKernel("Box",       new float[] { 1.0f,  1.0f,  1.0f,
                                             1.0f,  1.0f,  1.0f,
                                             1.0f,  1.0f,  1.0f});

        addKernel("Low pass",  new float[] { 1.0f,  2.0f,  1.0f,
                                             2.0f,  4.0f,  2.0f,
                                             1.0f,  2.0f,  1.0f});

        if (model.getRowCount()*model.getColumnCount() == 0) {
            setKernel("Box");
        }
    }

    /**
     * Adds a 3x3 kernel to the list of available kernels.
     */
    private void addKernel(final String name, final float[] data) {
        double sum = 0;
        for (int i=0; i<data.length; i++) {
            sum += data[i];
        }
        if (sum != 0) {
            for (int i=0; i<data.length; i++) {
                data[i] /= sum;
            }
        }
        addKernel(null, name, new KernelJAI(3,3,data));
    }

    /**
     * Adds a kernel to the list of available kernels. The widget list kernels in the same order
     * they were added, unless {@link #sortKernelNames} has been invoked. Each kernel can belong
     * to an optional category. Example of categories includes "Error filters" and "Gradient masks".
     *
     * @param category The kernel's category name, or {@code null} if none.
     * @param name The kernel name. Kernels will be displayed in alphabetic order.
     * @param kernel The kernel. If an other kernel was registered with the same
     *        name, the previous kernel will be discarted.
     */
    public void addKernel(String category, final String name, final KernelJAI kernel) {
        if (category == null) {
            category = Vocabulary.getResources(getLocale()).getString(VocabularyKeys.OTHERS);
        }
        model.addKernel(category, name, kernel);
    }

    /**
     * Adds a new category if not already present.
     */
    private void addCategory(final String category) {
        final ComboBoxModel categories = categorySelector.getModel();
        for (int i=categories.getSize(); --i>=0;) {
            if (category.equals(categories.getElementAt(i))) {
                return;
            }
        }
        categorySelector.addItem(category);
    }

    /**
     * Removes a kernel. If the kernel was the only one in
     * its category, the category is removed as well.
     */
    public void removeKernel(final KernelJAI kernel) {
        model.removeKernel(kernel);
    }

    /**
     * Removes a kernel by its name. If the kernel was the only
     * one in its category, the category is removed as well.
     */
    public void removeKernel(final String kernel) {
        removeKernel(model.getKernel(kernel));
    }

    /**
     * Removes all kernels and categories.
     */
    public void removeAllKernels() {
        model.removeAllKernels();
    }

    /**
     * Set the kernel. The table size will be set to the specified kernel size, add all
     * coefficients will be copied in the table. If the specified kernel matches one of
     * the kernel registered with the {@link #addKernel addKernel} method, then the kernel
     * name and category will be updated according.
     *
     * @param kernel The new kernel.
     */
    public void setKernel(final KernelJAI kernel) {
        model.setKernel(kernel);
        model.findKernelName();
    }

    /**
     * Set the kernel by its name. It must be one of the name registered with {@link #addKernel}.
     * If {@code name} is not found, then nothing is done.
     *
     * @param name The name of the kernel to select.
     */
    public void setKernel(final String name) {
        kernelSelector.setSelectedItem(name);
        kernelSelector.repaint();
    }

    /**
     * Set the size of the current kernel.
     *
     * @param width  The number of rows.
     * @param height The number of columns.
     */
    public void setKernelSize(final int width, final int height) {
        model.setKernelSize(height, width); // Inverse argument order.
        model.findKernelName();
    }

    /**
     * Returns the currently edited kernel.
     *
     * @return The edited kernel.
     */
    public KernelJAI getKernel() {
        return model.getKernel();
    }

    /**
     * Returns the category for the current kernel. This is the {@code category} argument
     * given to <code>{@linkplain #addKernel addKernel}(category, name, kernel)</code>, where
     * {@code kernel} is the {@linkplain #getKernel current kernel}.
     *
     * @return The category for the current kernel, or {@code null} if none.
     */
    public String getKernelCategory() {
        // Category at index 0 is "all", which need a special handling.
        return categorySelector.getSelectedIndex()<=0 ? null :
                  (String) categorySelector.getSelectedItem();
    }

    /**
     * Sort all kernel names according the specified comparator.
     *
     * @param comparator The comparator, or {@code null} for the natural ordering.
     */
    public void sortKernelNames(final Comparator comparator) {
        model.sortKernelNames(comparator);
    }

    /**
     * Returns an array of kernel names in the current category.
     * Changes in the returned array will not affect the {@code KernelEditor} state.
     *
     * @return The name of all kernels in the current category.
     */
    public String[] getKernelNames() {
        return (String[]) model.getKernelNames().clone();
    }

    /**
     * Returns the list of predefined kernels in the current category. The content of
     * this list will changes every time a kernel is {@linkplain #addKernel added} or
     * {@linkplain #removeKernel(KernelJAI) removed} and every time the user selects a
     * new category. The selected item can change at any time as well, according user action.
     */
    public ComboBoxModel getKernelListModel() {
        return model;
    }

    /**
     * Returns the table model containing the current kernel coefficients. The content of this
     * table will changes every time the user select a new predefined kernel, or when the user
     * edit cell values.
     */
    public TableModel getKernelTableModel() {
        return model;
    }

    /**
     * The table and list model to use. The list model contains a list of
     * predefined kernels. The table model contains coefficients for the
     * currently selected kernel. This object is also a listener for various
     * events (like changing the size of the table).
     *
     * @version $Id: KernelEditor.java 20883 2006-08-07 13:48:09Z jgarnett $
     * @author Martin Desruisseaux
     */
    private final class Model extends AbstractTableModel implements ComboBoxModel,
                                                     ChangeListener, ItemListener
    {
        /**
         * Dictionnary of kernels by their name.
         * Keys are {@link String} and values are {@link KernelJAI}.
         */
        private final Map kernels = new HashMap();

        /**
         * List of categories by kernel's name.
         * Keys and values are both {@link String}.
         */
        private final Map categories = new LinkedHashMap();

        /**
         * {@code true} if the keys into {@link #categories}
         * are sorted according their <em>natural</em> ordering.
         */
        private boolean sorted;

        /**
         * List of kernel names in alphabetical order.
         * This list is constructed only when first needed.
         */
        private String[] names;

        /**
         * Name of the current kernel, or {@code null}
         * if the user is editing a custom kernel.
         */
        private String name;

        /**
         * Array of elements for the current kernel.
         */
        private float[][] elements = new float[0][];

        /**
         * Returns the number of kernels in the list.
         * Used by the combox box of kernel names.
         */
        public int getSize() {
            return getKernelNames().length;
        }

        /**
         * Returns the number of rows in the kernel.
         * Used by the table of kernel values.
         */
        public int getRowCount() {
            return elements.length;
        }

        /**
         * Returns the number of columns in the model.
         * Used by the table of kernel values.
         */
        public int getColumnCount() {
            return (elements.length!=0) ? elements[0].length : 0;
        }

        /**
         * Returns {@code true} regardless of row and column index
         * Used by the table of kernel values.
         */
        public boolean isCellEditable(final int rowIndex, final int columnIndex) {
            return true;
        }

        /**
         * Returns {@code Float.class} regardless of column index
         * Used by the table of kernel values.
         */
        public Class getColumnClass(final int columnIndex) {
            return Float.class;
        }

        /**
         * Returns the value for the cell at {@code columnIndex} and {@code rowIndex}.
         * This method is automatically invoked in order to paint the kernel as a table.
         */
        public Object getValueAt(final int rowIndex, final int columnIndex) {
            return new Float(elements[rowIndex][columnIndex]);
        }

        /**
         * Set the value for the cell at {@code columnIndex} and {@code rowIndex}.
         * This method is automatically invoked when the user edited one of kernel values.
         */
        public void setValueAt(final Object value, final int rowIndex, final int columnIndex) {
            elements[rowIndex][columnIndex] = (value!=null) ? ((Number) value).floatValue() : 0;
            fireTableCellUpdated(rowIndex, columnIndex);
            findKernelName();
        }

        /**
         * Returns the kernel at the specified index.
         * Used by the combox box of kernel names.
         */
        public Object getElementAt(final int index) {
            return getKernelNames()[index];
        }

        /**
         * Returns the selected kernel name (never {@code null}).
         * Used by the combox box of kernel names.
         */
        public Object getSelectedItem() {
            return (name!=null) ? name : getString(VocabularyKeys.PERSONALIZED);
        }

        /**
         * Set the selected kernel by its name (never {@code null}).
         * Used by the combox box of kernel names.
         */
        public void setSelectedItem(final Object item) {
            final String newName = item.toString();
            if (!newName.equals(name)) {
                // 'kernel' may be null if 'item' is the "Personalized" kernel name.
                final KernelJAI kernel = (KernelJAI) kernels.get(newName);
                if (kernel != null) {
                    setKernel(kernel);
                }
                categorySelector.setSelectedItem(categories.get(newName));
                this.name = newName;
            }
        }

        /**
         * Returns the current kernel.
         *
         * @see KernelEditor#getKernel
         */
        public KernelJAI getKernel() {
            final int   height = elements.length;
            final int    width = height!=0 ? elements[0].length : 0;
            final float[] data = new float[width*height];
            int c=0; for (int j=0; j<height; j++) {
                for (int i=0; i<width; i++) {
                    data[c++] = elements[j][i];
                }
            }
            return new KernelJAI(width, height, data);
        }

        /**
         * Set the kernel. The table size will be set to the specified kernel size, add all
         * coefficients will be copied in the table. If the specified kernel matches one of
         * the kernel registered with the {@link #addKernel addKernel} method, then the kernel
         * name and category will be updated according.
         *
         * @see KernelEditor#setKernel
         */
        public void setKernel(final KernelJAI kernel) {
            final int rowCount = kernel.getHeight();
            final int colCount = kernel.getWidth();
            setKernelSize(rowCount, colCount);
            for (int j=0; j<rowCount; j++) {
                for (int i=0; i<colCount; i++) {
                    elements[j][i] = kernel.getElement(i,j);
                }
            }
            fireTableDataChanged();
        }

        /**
         * Set the size of the current kernel.
         *
         * @param width  The number of rows.
         * @param height The number of columns.
         *
         * @see KernelEditor#setKernelSize
         */
        public void setKernelSize(final int rowCount, final int colCount) {
            final int oldRowCount = elements.length;
            final int oldColCount = oldRowCount!=0 ? elements[0].length : 0;
            if (rowCount!=oldRowCount || colCount!=oldColCount) {
                elements = (float[][]) XArray.resize(elements, rowCount);
                for (int i=0; i<elements.length; i++) {
                    if (elements[i] == null) {
                        elements[i] = new float[colCount];
                    } else {
                        elements[i] = XArray.resize(elements[i], colCount);
                    }
                }
                if (colCount != oldColCount) {
                    fireTableStructureChanged();
                } else if (rowCount > oldRowCount) {
                    fireTableRowsInserted(oldRowCount, rowCount-1);
                } else if (rowCount < oldRowCount) {
                    fireTableRowsDeleted(rowCount, oldRowCount-1);
                }
                widthSelector .setValue(new Integer(colCount));
                heightSelector.setValue(new Integer(rowCount));
            }
        }

        /**
         * Returns the index of the specified kernel name in the specified category,
         * or -1 if it was not found.   This method is invoked by {@link #addKernel}
         * and {@link #removeKernel} in order to determine the range of index values
         * to give to {@link ListDataEvent}.
         *
         * @param category The kernel category. Only kernels in this category will
         *        be taken in account. This argument is usually provided by
         *        {@link KernelEditor#getKernelCategory}. {@code null}
         *        is a special value taking all categories in account.
         * @param toSearch The name of the kernel to search.
         */
        private int indexOf(final String category, final String toSearch) {
            int index = 0;
            for (final Iterator it=categories.entrySet().iterator(); it.hasNext();) {
                final Map.Entry entry = (Map.Entry) it.next();
                final String name = (String) entry.getKey();
                if (category==null || category.equals(entry.getValue())) {
                    // A kernel of the required category has been found.
                    if (toSearch.equals(name)) {
                        // Found the kernel we are looking for.
                        assert !sorted || Arrays.binarySearch(getKernelNames(), toSearch) == index;
                        return index;
                    }
                    // Right category, but wrong kernel.
                    index++;
                }
                if (sorted && name.compareTo(toSearch) >= 0) {
                    // Since kernel names are sorted, there is no
                    // need to continue the iteration past this point.
                    break;
                }
            }
            assert !sorted || Arrays.binarySearch(getKernelNames(), toSearch) < 0;
            return -1;
        }

        /**
         * Adds a kernel to the list of available kernels. Each kernel can belong to an optional
         * category. Example of categories includes "Error filters" and "Gradient masks".
         *
         * @param category The kernel's category name, which <strong>must not</strong> be
         *        {@code null}. The public method in {@link KernelEditor} is responsible
         *        for substituing a string (usually "Others" or "Personalized") in place of the
         *        null value.
         * @param name The kernel name. Kernels will be displayed in alphabetic order.
         * @param kernel The kernel. If an other kernel was registered with the same
         *        name, the previous kernel will be discarted.
         *
         * @see KernelEditor#addKernel
         */
        public void addKernel(final String category, final String name, final KernelJAI kernel) {
            sorted = false;
            if (!category.equals(categories.put(name, category))) {
                // The category doesn't already exists.
                addCategory(category);
            }
            if (kernels.put(name, kernel) != null) {
                // The new kernel replace an existing one.
                findKernelName();
            } else {
                // The kernel must be added to existing ones.
                final String cc = getKernelCategory();
                if (cc==null || category.equals(cc)) {
                    names = null; // Must be before 'indexOf'
                    final int index = indexOf(cc, name);
                    assert index >= 0 : name;
                    fireListChanged(ListDataEvent.INTERVAL_ADDED, index, index);
                }
            }
            assert kernels.size() == categories.size();
        }

        /**
         * Removes a kernel. If the kernel was the only one in
         * its category, the category is removed as well.
         *
         * @see KernelEditor#removeKernel
         */
        public void removeKernel(final KernelJAI kernel) {
            final String cc = getKernelCategory();
            for (final Iterator it=kernels.entrySet().iterator(); it.hasNext();) {
                final Map.Entry entry = (Map.Entry) it.next();
                if (kernel.equals(entry.getValue())) {
                    // Found the kernel to remove.
                    final String name     = (String) entry.getKey();
                    final int    index    = indexOf(cc, name); // Must be before any remove.
                    final String category = (String) categories.remove(name);
                    if (!categories.values().contains(category)) {
                        // No other kernel in this category.
                        categorySelector.removeItem(category);
                    }
                    it.remove();
                    if (index >= 0) {
                        names = null; // Must be after 'it.remove'
                        fireListChanged(ListDataEvent.INTERVAL_REMOVED, index, index);
                    }
                }
            }
            assert kernels.size() == categories.size();
        }

        /**
         * Removes all kernels and categories.
         *
         * @see KernelEditor#removeAllKernels
         */
        public void removeAllKernels() {
            final int size = kernels.size();
            kernels.clear();
            categories.clear();
            names = null;
            fireListChanged(ListDataEvent.INTERVAL_REMOVED, 0, size-1);
            categorySelector.removeAllItems();
            categorySelector.addItem(getResources().getString(VocabularyKeys.ALL));
        }

        /**
         * Returns a kernel by its name.
         *
         * @param  name The kernel name.
         * @return The kernel, or {@code null} if there is no kernel for the specified name.
         */
        public KernelJAI getKernel(final String name) {
            return (KernelJAI) kernels.get(name);
        }

        /**
         * Returns the array of kernel names. <strong>This method
         * returns the array by reference; do not modify!</strong>.
         *
         * @see KernelEditor#getKernelNames
         */
        public String[] getKernelNames() {
            if (names == null) {
                int count = 0;
                names = new String[kernels.size() + 1];
                final String category = getKernelCategory();
                for (final Iterator it=categories.entrySet().iterator(); it.hasNext();) {
                    final Map.Entry entry = (Map.Entry) it.next();
                    if (category==null || category.equals(entry.getValue())) {
                        names[count++] = (String) entry.getKey();
                    }
                }
                names[count++] = getString(VocabularyKeys.PERSONALIZED);
                names = (String[]) XArray.resize(names, count);
            }
            return names;
        }

        /**
         * Find the name for the current kernel. If such a name is
         * found, it will be given to the combo-box. Otherwise,
         * nothing is done.
         */
        protected void findKernelName() {
            String newName=null; // "Personalized"
            final int rowCount = elements.length;
            final int colCount = rowCount!=0 ? elements[0].length : 0;
      iter: for (final Iterator it=kernels.entrySet().iterator(); it.hasNext();) {
                final Map.Entry entry = (Map.Entry) it.next();
                final KernelJAI kernel = (KernelJAI) entry.getValue();
                if (rowCount==kernel.getHeight() && colCount==kernel.getWidth()) {
                    for (int j=0; j<rowCount; j++) {
                        for (int i=0; i<colCount; i++) {
                            if (elements[j][i] != kernel.getElement(i,j)) {
                                continue iter;
                            }
                        }
                    }
                    newName = (String) entry.getKey();
                }
            }
            if (newName == null) {
                newName = getString(VocabularyKeys.PERSONALIZED);
            }
            if (!newName.equals(name)) {
                // Set the name now in order to avoid that
                // setSelectedItem invokes setKernel again.
                this.name = newName;
                categorySelector.setSelectedItem(categories.get(newName));
                kernelSelector.setSelectedItem(newName);
                kernelSelector.repaint(); // JComboBox doesn't seems to repaint by itself.
            }
        }

        /**
         * Sorts all kernel names according the specified comparator.
         *
         * @param comparator The comparator, or {@code null} for the natural ordering.
         *
         * @see KernelEditor#sortKernelNames
         */
        public void sortKernelNames(final Comparator comparator) {
            final Map sorted = new TreeMap(comparator);
            sorted.putAll(categories);
            categories.clear();
            categories.putAll(sorted);
            names = null;
            this.sorted = (comparator == null);
            fireListChanged(ListDataEvent.CONTENTS_CHANGED, 0, categories.size()-1);
        }
        
        /**
         * Invoked when a {@link JSpinner} has changed its state.
         * This method reset the matrix size according the new
         * spinner value.
         */
        public void stateChanged(final ChangeEvent event) {
            final int rowCount = ((Number) heightSelector.getValue()).intValue();
            final int colCount = ((Number) widthSelector. getValue()).intValue();
            setKernelSize(rowCount, colCount);
            findKernelName();
        }

        /**
         * Invoked when the user selected a new kernel category.
         * The kernel list must be cleared and reconstructed.
         */
        public void itemStateChanged(final ItemEvent event) {
            if (event.getStateChange() == ItemEvent.SELECTED) {
                names = null;
                fireListChanged(ListDataEvent.CONTENTS_CHANGED, 0, categories.size());
            }
        }

        /**
         * Convenience method returning a string for the specified resource keys.
         */
        private String getString(final int key) {
            return getResources().getString(key);
        }

        /**
         * Adds a listener to the list that's notified
         * each time a change to the data model occurs.
         */
        public void addListDataListener(final ListDataListener listener) {
            listenerList.add(ListDataListener.class, listener);
        }
        
        /**
         * Removes a listener from the list that's notified
         * each time a change to the data model occurs.
         */
        public void removeListDataListener(final ListDataListener listener) {
            listenerList.remove(ListDataListener.class, listener);
        }

        /**
         * Invoked after one or more kernels are added to the model.
         *
         * @param type Must be one of {@link ListDataEvent#CONTENTS_CHANGED},
         *        {@link ListDataEvent#INTERVAL_ADDED} or {@link ListDataEvent#INTERVAL_REMOVED}.
         * @param index0 Lower index, inclusive.
         * @param index1 Upper index, <strong>inclusive</strong>.
         */
        private void fireListChanged(final int type, final int index0, final int index1) {
            ListDataEvent event = null;
            final Object[] listeners = listenerList.getListenerList();
            for (int i=listeners.length; (i-=2)>=0;) {
                if (listeners[i] == ListDataListener.class) {
                    if (event==null) {
                        event = new ListDataEvent(this, type, index0, index1);
                    }
                    final ListDataListener listener = (ListDataListener) listeners[i+1];
                    switch (type) {
                        case ListDataEvent.CONTENTS_CHANGED: listener.contentsChanged(event); break;
                        case ListDataEvent.INTERVAL_ADDED  : listener.intervalAdded  (event); break;
                        case ListDataEvent.INTERVAL_REMOVED: listener.intervalRemoved(event); break;
                    }
                }
            }
        }
    }

    /**
     * Shows a dialog box requesting input from the user. If {@code owner} is contained into a
     * {@link javax.swing.JDesktopPane}, the dialog box will appears as an internal frame. This
     * method can be invoked from any thread (may or may not be the <cite>Swing</cite> thread).
     *
     * @param  owner The parent component for the dialog box, or {@code null} if there is no parent.
     * @param  title The dialog box title.
     * @return {@code true} if user pressed the "Ok" button, or {@code false} otherwise
     *         (e.g. pressing "Cancel" or closing the dialog box from the title bar).
     */
    public boolean showDialog(final Component owner, final String title) {
        return SwingUtilities.showOptionDialog(owner, this, title);
    }

    /**
     * Show the dialog box. This method is provided only as an easy
     * way to test the dialog appearance from the command line.
     */
    public static void main(final String[] args) {
        final KernelEditor editor = new KernelEditor();
        editor.addDefaultKernels();
        editor.showDialog(null, Utilities.getShortClassName(editor));
    }
}
