/*
* Constellation - An open source and standard compliant SDI
* http://www.constellation-sdi.org
*
* Copyright 2014 Geomatys.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.constellation.gui.observation;
// J2SE dependencies
import org.constellation.catalog.CatalogException;
import org.constellation.catalog.Element;
import org.constellation.coverage.catalog.Catalog;
import org.constellation.coverage.catalog.Layer;
import org.constellation.coverage.model.Descriptor;
import org.constellation.coverage.model.Operation;
import org.constellation.coverage.model.RegionOfInterest;
import org.constellation.resources.XArray;
import org.geotoolkit.gui.swing.DisjointLists;
import org.geotools.resources.SwingUtilities;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.prefs.Preferences;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
// AWT/Swing dependencies
// Geotools dependencies
// J2SE dependencies
/**
* Sélectionne des descripteurs parmis une liste.
*
* @version $Id$
* @author Martin Desruisseaux
*/
@SuppressWarnings("serial")
public class DescriptorChooser extends JPanel {
/**
* Une liste d'éléments mémorisée sour forme de tableau. Ce modèle sera utilisé
* par les listes de la première étape.
*
* @version $Id$
* @author Martin Desruisseaux
*/
private static final class Model extends AbstractListModel {
/**
* Les éléments dans cette liste.
*/
private Element[] elements = new Element[0];
/** Spécifie l'ensemble des éléments à affecter à cette liste. */
public void setElements(final Set<? extends Element> newElements) {
final int newLength = newElements.size();
final int length = (elements != null) ? Math.max(elements.length, newLength) : newLength;
elements = newElements.toArray(new Element[newLength]);
if (length != 0) {
fireContentsChanged(this, 0, length-1);
}
}
/** Retourne le nombre d'éléments dans cette liste. */
public int getSize() {
return elements.length;
}
/** Retourne l'élément à l'index spécifié. */
public Element getElementAt(final int index) {
return elements[index];
}
}
/**
* Action exécuté lorsque l'un des boutons est appuyé.
*
* @version $Id$
* @author Martin Desruisseaux
*/
private final class Action implements ActionListener {
public void actionPerformed(final ActionEvent event) {
final Object source = event.getSource();
if (source == previous) {
setStep(SELECT_PROPERTIES);
return;
}
if (source == next) {
if (SELECT_DESCRIPTORS.equals(currentStep)) {
previous.setEnabled(false);
next .setEnabled(false);
saveSelection();
execute();
} else {
setStep(SELECT_DESCRIPTORS);
}
return;
}
if (source == cancel) {
cancel();
return;
}
}
}
//////////////////////////////// fin des classes internes ///////////////////////////////////
/**
* Nom de la première étape, qui consiste à choisir les {@linkplain Layer couches},
* {@linkplain Operation opérations} et {@link RegionOfInterest positions relatives}.
* Ce nom peut être spécifié en argument à {@link #setStep} pour sélectionner les
* listes à afficher.
*/
public static final String SELECT_PROPERTIES = "SelectProperties";
/**
* Nom de la seconde étape, qui consiste à choisir les {@linkplain Descriptor descripteurs}
* eux-mêmes. Ce nom peut être spécifié en argument à {@link #setStep} pour sélectionner les
* listes à afficher.
*/
public static final String SELECT_DESCRIPTORS = "SelectDescriptors";
/**
* Le nom de la feuille de préférences pour les derniers descripteurs utilisés.
*/
private static final String LAST_DESCRIPTORS = "LastDescriptors";
/**
* Ensemble des {@linkplain Descriptor descripteurs} dont on veut répartir les composantes
* dans les trois listes {@code phenomenons}, {@code operations} et {@code offsets}.
*/
private final Map<Descriptor,Boolean> descriptors = new LinkedHashMap<Descriptor,Boolean>();
/**
* Ensemble des {@linkplain Layer couches} d'images.
*/
private final Map<Layer,Boolean> layers = new LinkedHashMap<Layer,Boolean>();
/**
* Ensemble des {@linkplain Operation opérations}.
*/
private final Map<Operation,Boolean> operations = new LinkedHashMap<Operation,Boolean>();
/**
* Ensemble des {@linkplain RegionOfInterest positions spatio-temporelles relatives}.
*/
private final Map<RegionOfInterest,Boolean> offsets = new LinkedHashMap<RegionOfInterest,Boolean>();
/**
* Liste des {@linkplain Layer couches} d'images.
*/
private final JList layerList;
/**
* Liste des {@linkplain Operation opérations}.
*/
private final JList operationList;
/**
* Liste des {@linkplain RegionOfInterest positions spatio-temporelles relatives}.
*/
private final JList offsetList;
/**
* List des {@linkplain Descriptor descripteurs}.
*/
private final DisjointLists descriptorList;
/**
* L'étape courante, comme une des constantes {@link #SELECT_PROPERTIES} ou
* {@link #SELECT_DESCRIPTORS}.
*/
private String currentStep;
/**
* La composante graphique qui affichera les différentes étapes.
*/
private final JPanel cards;
/**
* Le bouton pour revenir à l'étape précédente.
*/
private final JButton previous;
/**
* Le bouton pour aller à l'étape suivante.
*/
private final JButton next;
/**
* Le bouton pour annuler.
*/
private final JButton cancel;
/**
* Les préférences à utiliser pour mémoriser les derniers descripteurs utilisés.
*/
private final Preferences preferences = Preferences.userNodeForPackage(DescriptorChooser.class);
/**
* Construit une nouvelle composante initialement vide.
*/
public DescriptorChooser() {
super(new BorderLayout(0,9));
cards = new JPanel(new CardLayout());
/*
* Construction de l'étape 1.
*/
JPanel step = new JPanel(new BorderLayout(0,9));
JPanel pane = new JPanel(new GridLayout(1,3,9,0));
pane.add(new JLabel("Couches d'images", JLabel.CENTER));
pane.add(new JLabel("Opérations", JLabel.CENTER));
pane.add(new JLabel("Décalages", JLabel.CENTER));
step.add(pane, BorderLayout.NORTH);
pane = new JPanel(new GridLayout(1,3,9,0));
pane.add(new JScrollPane(layerList = new JList(new Model())));
pane.add(new JScrollPane(operationList = new JList(new Model())));
pane.add(new JScrollPane(offsetList = new JList(new Model())));
step.add(pane, BorderLayout.CENTER);
cards.add(step, SELECT_PROPERTIES);
/*
* Construction de l'étape 2.
*/
descriptorList = new DisjointLists();
descriptorList.setAutoSortEnabled(false);
descriptorList.setFont(new Font("Arial Unicode MS", Font.PLAIN, 16));
cards.add(descriptorList, SELECT_DESCRIPTORS);
/*
* Construction de la barre des boutons.
*/
add(cards, BorderLayout.CENTER);
pane = new JPanel(new GridLayout(1,3,9,0));
pane.add(previous = new JButton("Précédent"));
pane.add(next = new JButton("Suivant"));
pane.add(cancel = new JButton("Annuler"));
pane.setBorder(BorderFactory.createEmptyBorder(15,0,0,0));
Box box = Box.createHorizontalBox();
box.add(Box.createHorizontalGlue());
box.add(pane);
add(box, BorderLayout.SOUTH);
setBorder(BorderFactory.createCompoundBorder(getBorder(),
BorderFactory.createEmptyBorder(12, 12, 12, 12)));
/*
* Définition des actions.
*/
final Action action = new Action();
previous.addActionListener(action);
next .addActionListener(action);
cancel .addActionListener(action);
setStep(SELECT_PROPERTIES);
}
/**
* Construit une nouvelle composante initialisée avec l'ensemble des descripteurs spécifié.
* Les descripteurs qui avaient été choisis par l'utilisateur lors de la dernière utilisation
* de cette composante seront automatiquement sélectionnés.
*/
public DescriptorChooser(final Collection<Descriptor> descriptors) {
this();
addDescriptor(descriptors);
loadSelection();
}
/**
* Ajoute à cette composante l'ensemble des descripteurs spécifié. Aucun des nouveaux
* descripteurs ne sera initiallement sélectionné. Afin de pré-sélectionner ceux qui
* avaient été choisis par l'utilisateur lors de la dernière utilisation de cette
* composante, appelez {@link #loadSelection}.
*/
public void addDescriptor(final Collection<Descriptor> toAdd) {
boolean seriesModified = false;
boolean operationModified = false;
boolean offsetModified = false;
for (final Descriptor descriptor : toAdd) {
if (add(descriptors, descriptor)) {
seriesModified |= add(layers, descriptor.getLayer());
operationModified |= add(operations, descriptor.getOperation());
offsetModified |= add(offsets, descriptor.getRegionOfInterest());
}
}
if (seriesModified) setElements(layerList, layers);
if (operationModified) setElements(operationList, operations);
if (offsetModified) setElements(offsetList, offsets);
}
/**
* Ajoute à l'ensemble spécifié l'élément spécifié. Si l'élément n'existait pas déjà, il sera
* ajouté comme un élément non-sélectionné. S'il existait déjà, son état sera laissé inchangé.
*/
private static <T extends Element> boolean add(final Map<T,Boolean> map, final T element) {
final Boolean old = map.put(element, FALSE);
if (old == null) {
return true;
}
if (old) {
if (map.put(element, old) != FALSE) {
throw new AssertionError(element);
}
}
return false;
}
/**
* Affecte au modèle de la liste spécifiée l'ensemble des éléments spécifié.
* La sélection sera définie en fonction des valeurs du dictionnaire.
*/
private static void setElements(final JList list, final Map<? extends Element, Boolean> elements) {
final Model model = (Model) list.getModel();
model.setElements(elements.keySet());
copySelection(elements, list);
}
/**
* Sélectionne les éléments de la liste spécifiée en fonction du dictionnaire spécifié. Cette
* méthode peut être interprétée comme une copie de la sélection du dictionnaire {@code elements}
* <em>vers</em> la liste {@code list}.
*/
private static <T extends Element> void copySelection(final Map<T,Boolean> elements, final JList list) {
final ListModel model = list.getModel();
final int size = model.getSize();
int selected[] = new int[size];
int count = 0;
for (int i=0; i<size; i++) {
if (TRUE.equals(elements.get(model.getElementAt(i)))) {
selected[count++] = i;
}
}
selected = XArray.resize(selected, count);
list.setSelectedIndices(selected);
}
/**
* Met à jour l'état du dictionnaire spécifié en fonction de la sélection de la liste spécifiée.
* Cette méthode peut être interprétée comme une copie de la sélection de la liste {@code list}
* <em>vers</em> le dictionnaire {@code elements}.
*/
private static <T extends Element> void copySelection(final JList list, final Map<T,Boolean> elements) {
final ListModel model = list.getModel();
final ListSelectionModel selection = list.getSelectionModel();
final int size = model.getSize();
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
final T element = (T) model.getElementAt(i);
if (elements.put(element, selection.isSelectedIndex(i)) == null) {
throw new AssertionError(element);
}
}
}
/**
* Met à jour les champs internes (notamment {@link #descriptors}) en fonction de la sélection
* des listes de couches, procédures et décalages spatio-temporelles. Cette opération intervient
* typiquement lorsque l'utilisateur appuie sur le bouton "Suivant".
*/
private void commitPropertiesSelection() {
copySelection(layerList, layers);
copySelection(operationList, operations);
copySelection(offsetList, offsets);
final Set<Descriptor> selected = new HashSet<Descriptor>();
final Set<Descriptor> confirmed = new HashSet<Descriptor>();
for (final Map.Entry<Descriptor,Boolean> entry : descriptors.entrySet()) {
final Descriptor descriptor = entry.getKey();
final boolean isSelected = TRUE.equals(layers .get(descriptor.getLayer ())) &&
TRUE.equals(operations.get(descriptor.getOperation ())) &&
TRUE.equals(offsets .get(descriptor.getRegionOfInterest()));
if (isSelected) {
if (!selected.add(descriptor)) {
throw new AssertionError(descriptor);
}
}
if (entry.getValue()) {
if (!confirmed.add(descriptor)) {
throw new AssertionError(descriptor);
}
}
entry.setValue(isSelected);
}
descriptorList.clear();
descriptorList.addElements(selected);
descriptorList.selectElements(confirmed);
}
/**
* Met à jour les champs internes (notamment {@link #layers}, {@link #operations} et {@link #offsets})
* en fonction de la sélection des descripteurs. Cette opération intervient typiquement lorsque
* l'utilisateur appuie sur le bouton "Précédent".
* <p>
* Notez que cette méthode n'efface pas la sélection précédente des listes {@link #layers},
* {@link #operations} et {@link #offsets}. Elle peut seulement l'étendre. Si vous souhaitez
* effacer les sélections précédentes au préalable, appelez d'abord
* <code>{@link #selectAll selectAll}(..., FALSE)</code>.
*/
private void commitDescriptorSelection() {
@SuppressWarnings("unchecked")
final Set<Descriptor> selected = new HashSet<Descriptor>(descriptorList.getElements(true));
for (final Map.Entry<Descriptor,Boolean> entry : descriptors.entrySet()) {
final Descriptor descriptor = entry.getKey();
final boolean isSelected = selected.contains(descriptor);
entry.setValue(isSelected);
if (isSelected) {
if (layers .put(descriptor.getLayer(), TRUE) == null ||
operations.put(descriptor.getOperation(), TRUE) == null ||
offsets .put(descriptor.getRegionOfInterest(), TRUE) == null)
{
throw new AssertionError(descriptor);
}
}
}
copySelection(layers, layerList );
copySelection(operations, operationList);
copySelection(offsets, offsetList );
}
/**
* Affecte la valeur spécifiée à toutes les entrées du dictionnaire spécifié.
*/
private static <T extends Element> void selectAll(final Map<T,Boolean> elements, final Boolean isSelected) {
for (final Map.Entry<T,Boolean> entry : elements.entrySet()) {
entry.setValue(isSelected);
}
}
/**
* Sélectionne les descripteurs qui avaient été choisies par l'utilisateur lors de
* la dernière utilisation de cette composante. Cette sélection est puisée dans
* les {@linkplain Preferences préférences}.
*/
public void loadSelection() {
final String last = preferences.get(LAST_DESCRIPTORS, null);
if (last == null) {
return;
}
final Set<String> selected = new HashSet<String>();
final Set<Descriptor> confirmed = new HashSet<Descriptor>();
final StringTokenizer tokens = new StringTokenizer(last, "\t\n\r\f");
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().trim();
if (token.length() != 0) {
selected.add(token);
}
}
for (final Map.Entry<Descriptor,Boolean> entry : descriptors.entrySet()) {
final Descriptor descriptor = entry.getKey();
if (selected.contains(descriptor.getName())) {
entry.setValue(TRUE);
if (!confirmed.add(descriptor)) {
throw new AssertionError(descriptor);
}
}
}
final Set<Descriptor> ensureAdded = new HashSet<Descriptor>(confirmed);
ensureAdded.removeAll(descriptorList.getElements(true ));
ensureAdded.removeAll(descriptorList.getElements(false));
descriptorList.addElements(ensureAdded);
descriptorList.selectElements(confirmed);
commitDescriptorSelection();
}
/**
* Sauvegarde la sélection de l'utilisateur dans les {@linkplain Preferences préférences}.
* Cette sélection pourra être récupérée plus tard avec {@link #loadSelection}. Cette méthode
* est appelée automatiquement lorsque l'utilisateur appuie sur le bouton "Exécuter".
*/
public void saveSelection() {
final StringBuilder buffer = new StringBuilder();
for (final Descriptor descriptor : getDescriptors(true)) {
if (buffer.length() != 0) {
buffer.append('\t');
}
buffer.append(descriptor.getName());
}
preferences.put(LAST_DESCRIPTORS, buffer.toString());
}
/**
* Spécifie l'étape à afficher. L'argument {@code step} doit être une des constantes
* suivantes: {@link #SELECT_PROPERTIES} ou {@link #SELECT_DESCRIPTORS}.
*/
public void setStep(final String step) {
if (SELECT_PROPERTIES.equals(step)) {
commitDescriptorSelection();
previous.setEnabled(false);
next.setText("Suivant");
} else if (SELECT_DESCRIPTORS.equals(step)) {
commitPropertiesSelection();
previous.setEnabled(true);
next.setText("Exécuter");
} else {
throw new IllegalArgumentException(step);
}
((CardLayout) cards.getLayout()).show(cards, step);
currentStep = step;
}
/**
* Si {@code selected} est {@code true}, retourne l'ensemble des descripteurs sélectionnés par
* l'utilisateur. Sinon, retourne l'ensemble des descripteurs qui ne sont pas sélectionnés.
*/
public Set<Descriptor> getDescriptors(final boolean selected) {
if (SELECT_PROPERTIES.equals(currentStep)) {
commitPropertiesSelection();
}
@SuppressWarnings("unchecked")
final Set<Descriptor> s = new HashSet<Descriptor>(descriptorList.getElements(selected));
return s;
}
/**
* Méthode appelée automatiquement lorsque l'utilisateur a appuyé sur le bouton "Exécuter".
* L'implémentation par défaut ne fait qu'appeller {@link #dispose}. Les classes dérivées
* devrait surcharger cette méthode afin d'exécuter l'action qu'elles souhaite effectuer
* à partir de l'{@linkplain #getSelectedDescriptors ensemble des descripteurs sélectionnés}.
*/
protected void execute() {
dispose();
}
/**
* Méthode appelée automatique lorsque l'utilisateur a appuyé sur le bouton "Annuler".
* L'implémentation par défaut ne fait qu'appeller {@link #dispose}. Les classes dérivées
* devrait surcharger cette méthode afin d'interrompre l'action lancée par {@link #execute}.
*/
protected void cancel() {
dispose();
}
/**
* Fait disparaître la fenêtre parente. Cette méthode est appelée automatiquement lorsque
* l'utilisateur appuie sur le bouton "Annuler". Elle peut aussi être appelée lorsque
* l'exécution de la tâche (celle qui est lancée après la sélection des descripteurs)
* est terminée.
*/
public void dispose() {
previous.setEnabled(false);
next .setEnabled(false);
cancel .setEnabled(false);
Container parent = this;
while ((parent = parent.getParent()) != null) {
if (parent instanceof JInternalFrame) {
((JInternalFrame) parent).dispose();
return;
}
if (parent instanceof Window) {
((Window) parent).dispose();
return;
}
}
}
/**
* Affiche cette composante graphique. Si {@code owner} est nul, cette composante graphique
* sera affiché dans son propre {@link JFrame}. Sinon, elle apparaître comme une boîte de
* dialogue ou une fenêtre interne, en fonction du type de {@code owner}.
*/
public void show(final Component owner) {
final Component frame = SwingUtilities.toFrame(owner, this, "Sélection de descripteurs", null);
frame.setVisible(true);
}
/**
* Affiche cette composante avec l'ensemble des descripteurs de la base de données par défaut.
* Cette méthode est utilisée principalement à des fins de tests.
*
* @throws CatalogException si une erreur est survenue lors de l'interrogation du catalogue.
*/
public static void main(String[] args) throws CatalogException {
final Catalog observations = Catalog.getDefault();
final DescriptorChooser chooser = new DescriptorChooser(observations.getDescriptors());
chooser.show(null);
}
}