/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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;
* version 2.1 of the License.
*
* 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;
import javax.media.jai.KernelJAI;
import javax.media.jai.operator.ConvolveDescriptor; // For Javadoc
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.GridBagConstraints;
import java.awt.IllegalComponentStateException;
import static java.awt.GridBagConstraints.*;
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;
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;
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;
import org.geotools.resources.XArray;
import org.geotools.resources.Classes;
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> </p>
* <p align="center"><img src="doc-files/KernelEditor.png"></p>
* <p> </p>
*
* @since 2.3
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see GradientKernelEditor
* @see ConvolveDescriptor
* @see org.geotools.coverage.processing.operation.GradientMagnitude
*/
@SuppressWarnings("serial")
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=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=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=REMAINDER; c.fill=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<String> 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 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$
* @author Martin Desruisseaux (IRD)
*/
private final class Model extends AbstractTableModel implements ComboBoxModel,
ChangeListener, ItemListener
{
/**
* Dictionnary of kernels by their name.
*/
private final Map<String,KernelJAI> kernels = new HashMap<String,KernelJAI>();
/**
* List of categories by kernel's name.
*/
private final Map<String,String> categories = new LinkedHashMap<String,String>();
/**
* {@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.
*/
@Override
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.
*/
@Override
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 Float.valueOf(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.
*/
@Override
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 = 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 = 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<Map.Entry<String,KernelJAI>> it=kernels.entrySet().iterator(); it.hasNext();) {
final Map.Entry<String,KernelJAI> entry = it.next();
if (kernel.equals(entry.getValue())) {
// Found the kernel to remove.
final String name = entry.getKey();
final int index = indexOf(cc, name); // Must be before any remove.
final String category = 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 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 = 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<String> comparator) {
final Map<String,String> sorted = new TreeMap<String,String>(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, Classes.getShortClassName(editor));
}
}