/*
* 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;
// Interface utilisateur
import org.constellation.coverage.catalog.CoverageReference;
import org.constellation.resources.i18n.ResourceKeys;
import org.constellation.resources.i18n.Resources;
import org.geotoolkit.coverage.grid.GridCoverage2D;
import org.geotoolkit.coverage.processing.Operations;
import org.geotoolkit.gui.swing.ExceptionMonitor;
import org.geotoolkit.gui.swing.ProgressWindow;
import org.geotoolkit.image.io.IIOListeners;
import org.geotoolkit.internal.referencing.CRSUtilities;
import org.geotoolkit.referencing.crs.DefaultCompoundCRS;
import org.geotoolkit.referencing.crs.DefaultGeographicCRS;
import org.geotoolkit.util.Utilities;
import org.geotools.coverage.io.MetadataBuilder;
import org.geotools.resources.SwingUtilities;
import org.geotools.util.ProgressListener;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.stream.ImageOutputStream;
import javax.swing.*;
import java.awt.*;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
// Formattage
// Entrés/sorties
// Collections
// OpenGIS
// Geotools
// Constellation
/**
* Boîte de dialogue invitant l'utilisateur à sélectionner un répertoire
* de destination et un format d'image.
*
* <p> </p>
* <p align="center"><img src="doc-files/CoverageExportChooser.png"></p>
* <p> </p>
*
* @version $Id$
* @author Martin Desruisseaux
*/
public final class CoverageExportChooser extends JPanel {
/**
* Objet à utiliser pour sélectionner un répertoire de destination.
*/
private final JFileChooser chooser;
/**
* Ensemble des images à écrire. L'ordre des éléments doit être préservés.
*/
private final Set<CoverageReference> entries = new LinkedHashSet<CoverageReference>(256);
/**
* Etiquette indiquant le nombre d'images à exporter.
*/
private final JLabel count = new JLabel();
/**
* Resources pour la construction des étiquettes.
*/
private final Resources resources = Resources.getResources(getLocale());
/**
* Construit une boîte de dialogue.
*
* @param directory Répertoire de destination par défaut, ou {@code null}
* pour utiliser le répertoire du compte de l'utilisateur.
*/
public CoverageExportChooser(final File directory) {
super(new GridBagLayout());
count.setOpaque(true);
count.setBackground(Color.BLACK);
count.setForeground(Color.YELLOW);
count.setHorizontalAlignment(SwingConstants.CENTER);
///
/// Configure le paneau servant à choisir un répertoire.
///
chooser = new JFileChooser(directory);
chooser.setDialogType(JFileChooser.SAVE_DIALOG);
chooser.setDialogTitle(resources.getString(ResourceKeys.OUT_DIRECTORY));
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setControlButtonsAreShown(false);
chooser.setAcceptAllFileFilterUsed(false);
///
/// Ajoute les filtres de fichiers
///
final ImageFileFilter[] fileFilters = ImageFileFilter.getWriterFilters(null);
for (int i=0; i<fileFilters.length; i++) {
chooser.addChoosableFileFilter(fileFilters[i]);
}
///
/// Construit le paneau d'options
///
final JPanel options = new JPanel(new GridBagLayout());
final GridBagConstraints c=new GridBagConstraints();
options.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createTitledBorder(resources.getString(ResourceKeys.OPTIONS)),
BorderFactory.createEmptyBorder(/*top*/6,/*left*/9,/*bottom*/6,/*right*/9)));
c.gridx=0; c.fill=c.BOTH; c.insets.right=6;
c.gridy=0; options.add(new JLabel(resources.getString(ResourceKeys.NOT_AVAILABLE)), c);
///
/// Place les composantes
///
c.gridx=0; c.weightx=1;
c.gridy=0; c.insets.top= 6; add(count, c);
c.gridy=1; c.weighty=1; c.insets.top=15; add(chooser, c);
c.gridy=2; c.weighty=0; c.insets.top=12; add(options, c);
updateCount();
}
/**
* Met à jour l'étiquette qui indique le nombre d'images à exporter.
*/
private void updateCount() {
count.setText(resources.getString(ResourceKeys.COVERAGES_TO_EXPORT_COUNT_$1,
new Integer(entries.size())));
}
/**
* Ajoute les entrées spécifiées à la liste des images à écrire. Les
* images seront écrites dans l'ordre qu'elles apparaissent dans le
* tableau {@code entries}. Toutefois, les doublons seront ignorés.
*/
public synchronized void addEntries(final CoverageReference[] entries) {
for (int i=0; i<entries.length; i++) {
this.entries.add(entries[i]);
}
updateCount();
}
/**
* Retire les entrées spécifiées de la liste des images à écrire.
*/
public synchronized void removeEntries(final CoverageReference[] entries) {
for (int i=entries.length; --i>=0;) {
this.entries.remove(entries[i]);
}
updateCount();
}
/**
* Retire toutes les entrées de la liste des images à écrire.
*/
public synchronized void removeAllEntries() {
entries.clear();
updateCount();
}
/**
* Retourne les entrées des images qui seront à écrire.
*/
public synchronized CoverageReference[] getEntries() {
return entries.toArray(new CoverageReference[entries.size()]);
}
/**
* Retourne le répertoire dans lequel écrire les images. Ce répertoire a
* été spécifiée lors de la construction de cet objet, mais peut avoir été
* modifié par l'utilisateur.
*/
public File getDestinationDirectory() {
return chooser.getSelectedFile();
}
/**
* Fait apparaître la boîte de dialogue. Si l'utilisateur n'a pas annulé
* l'opération en cours de route, l'exportation des images sera lancée
* dans un thread en arrière-plan. Cette méthode peut donc retourner pendant
* que les exportations sont en cours. Les progrès seront affichées dans
* une fenêtre.
*
* @param owner Composante parente dans laquelle faire apparaître la
* boîte de dialogue, ou {@code null} s'il n'y en a pas.
* @param threadGroup Groupe de threads dans lequel placer celui qu'on
* va lancer.
* @return {@code true} si l'utilisateur a lancé les exportations,
* ou {@code false} s'il a annulé l'opération.
*/
public boolean showDialogAndStart(final Component owner, final ThreadGroup threadGroup) {
while (SwingUtilities.showOptionDialog(owner, this, chooser.getDialogTitle())) {
final Worker worker=new Worker(this);
if (worker.getUserConfirmation(owner)) {
try {
worker.start(threadGroup, owner);
return true;
} catch (IOException exception) {
ExceptionMonitor.show(owner, exception);
}
}
}
return false;
}
/**
* Classe ayant la charge d'exporter les images en arrière plan. Le constructeur de cette
* classe fait une copie de tous les paramètres pertinents de {@link CoverageExportChooser},
* tels qu'ils étaient au moment de la construction. Par la suite, aucune référence vers
* {@link CoverageExportChooser} n'est conservée.
*
* @version $Id$
* @author Martin Desruisseaux
*/
private static final class Worker implements Runnable, IIOReadWarningListener {
/**
* Fenêtre dans laquelle écrire les progrès de l'opération.
* Cette fenêtre ne sera créée que la première fois où elle
* sera nécessaire.
*/
private ProgressListener progress;
/**
* Encodeur à utiliser pour écrire les images. Cet encodeur
* ne sera créé que lorsque les écritures d'images démarreront.
*/
private ImageWriter writer;
/**
* Entré en cours de lecture, ou {@code null}
* s'il n'y en a pas encore.
*/
private CoverageReference current;
/**
* Liste des images à écrire.
*/
private final CoverageReference[] entries;
/**
* Répertoire de destination dans lequel
* seront écrites les images.
*/
private final File directory;
/**
* Extension des fichiers d'images. Cette extension remplacera l'extension des
* fichiers d'images sources. La chaîne de caractères {@code extension}
* ne doit pas commencer par un point. Ce champ peut être {@code null}
* s'il n'y a pas d'extension connue pour le type de fichier à écrire.
*/
private final String extension;
/**
* Objet qui avait la charge de filtrer les fichiers à afficher dans la
* boîte de dialogue. Cet objet connaît le format choisit par l'utilisateur
* et est capable de construire l'encodeur {@link ImageWriter} approprié.
*/
private final ImageFileFilter filter;
/**
* Buffer temporaire. Ce buffer est utilisé pour construire
* chacun des noms de fichier de destination des images.
*/
private final StringBuilder buffer = new StringBuilder();
/**
* Objet {@link MetadataBuilder} à utiliser pour écrire les propriétés d'un {@link GridCoverage2D}.
*/
private transient MetadataBuilder propertyParser;
/**
* Construit un objet qui procèdera aux écritures des images en arrière plan.
* Ce constructeur fera une copie des paramètres de la boîte de dialogue
* {@link CoverageExportChooser} spécifiée.
*
* @param chooser Boîte de dialogue qui demandait à l'utilisateur
* de choisir un répertoire de destination ainsi qu'un format.
*/
public Worker(final CoverageExportChooser chooser) {
synchronized (chooser) {
this.filter = (ImageFileFilter) chooser.chooser.getFileFilter();
this.entries = chooser.getEntries();
this.directory = chooser.getDestinationDirectory();
this.extension = filter.getExtension();
}
}
/**
* Retourne le nom et le chemin du fichier de destination pour l'image spécifiée.
*/
private File getDestinationFile(final int index) {
return getDestinationFile(index, extension);
}
/**
* Retourne le nom et le chemin du fichier de destination pour l'image spécifiée.
*/
private File getDestinationFile(final int index, final String extension) {
File file = entries[index].getFile();
if (file == null) {
file = new File(entries[index].getURL().getPath());
}
final String filename = file.getName();
buffer.setLength(0);
buffer.append(filename);
final int extPos = filename.lastIndexOf('.');
if (extPos >= 0) {
buffer.setLength(extPos);
}
if (extension!=null && extension.length() != 0) {
buffer.append('.');
buffer.append(extension);
}
return new File(directory, buffer.toString());
}
/**
* Vérifie si les images peuvent être écrites dans le répertoire choisi. Cette méthode
* vérifie d'abord si le répertoire est valide. Elle vérifie ensuite si le répertoire
* contient déjà des images qui risquent d'être écrasées. Si c'est le cas, alors cette
* méthode fait apparaître une boîte de dialogue qui demande à l'utilisateur de confirmer
* les écrasements. Cette méthode devrait toujours être appelée avant de lancer les exportations
* des fichiers.
*
* @param owner Composante parente dans laquelle faire apparaître les éventuelles boîtes de dialogue.
* @return {@code true} si on peut procéder aux écritures des images, ou {@code false} si
* l'utilisateur a demandé à arrêter l'opération.
*/
public boolean getUserConfirmation(final Component owner) {
final Resources resources = Resources.getResources(owner.getLocale());
if (directory==null || !directory.isDirectory()) {
SwingUtilities.showMessageDialog(owner, resources.getString(ResourceKeys.ERROR_BAD_DIRECTORY),
resources.getString(ResourceKeys.ERROR_BAD_ENTRY), JOptionPane.ERROR_MESSAGE);
return false;
}
int existing = 0;
for (int i=0; i<entries.length; i++) {
if (getDestinationFile(i).exists()) {
existing++;
}
}
if (existing != 0) {
if (!SwingUtilities.showConfirmDialog(owner, resources.getString(ResourceKeys.CONFIRM_OVERWRITE_ALL_$2, new Integer(existing),
new Integer(entries.length)), resources.getString(ResourceKeys.CONFIRM_OVERWRITE), JOptionPane.WARNING_MESSAGE))
{
return false;
}
}
return true;
}
/**
* Démarre les exportations d'images. Cette méthode fait apparaître une fenêtre
* dans laquelle seront affichées les progrès de l'opération. Elle appèle ensuite
* {@link #run} dans un thread séparé, afin de faire les écritures en arrière plan.
* <strong>Plus aucune autre méthode de {@code Worker} ne devrait être appelée
* après {@code start}.</strong>
*
* @param threadGroup Groupe de threads dans lequel placer celui qu'on va lancer.
* @param owner Composante parente dans laquelle faire apparaître la fenêtre des progrès.
* @throws IOException si une erreur a empêché le démarrage des exportations.
*/
public void start(final ThreadGroup threadGroup, final Component owner) throws IOException {
final Resources resources = Resources.getResources(owner.getLocale());
writer = filter.getImageWriter();
progress = new ProgressWindow(owner);
final Thread thread=new Thread(threadGroup, this, resources.getString(ResourceKeys.EXPORT));
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
/**
* Procède aux exportations d'images. Si une erreur survient en cours de route,
* un avertissement sera écrit dans la fenêtre des progrès. N'appelez pas cette
* méthode directement. Appelez plutôt {@link #start}, qui se chargera d'appeller
* {@code run()} dans un thread en arrière-plan.
*/
public void run() {
final IIOListeners listeners = new IIOListeners();
listeners.addIIOReadWarningListener(this);
progress.started();
for (int i=0; i<entries.length; i++) {
final CoverageReference entry = entries[i];
String name = "";
try {
name = entry.getName();
progress.setDescription(Resources.format(ResourceKeys.EXPORTING_$1, name));
progress.progress((i*100f)/entries.length);
GridCoverage2D image = entry.getCoverage(listeners).geophysics(false);
CoordinateReferenceSystem sourceCRS = image.getCoordinateReferenceSystem();
CoordinateReferenceSystem targetCRS = DefaultGeographicCRS.WGS84;
final int sourceDim = sourceCRS.getCoordinateSystem().getDimension();
final int targetDim = targetCRS.getCoordinateSystem().getDimension();
if (sourceDim > targetDim) {
final CoordinateReferenceSystem tailCRS = CRSUtilities.getSubCRS(sourceCRS, targetDim, sourceDim);
if (tailCRS != null) {
targetCRS = new DefaultCompoundCRS(targetCRS.getName().getCode(), targetCRS, tailCRS);
}
}
image = (GridCoverage2D) Operations.DEFAULT.resample(image, targetCRS);
final ImageOutputStream output = ImageIO.createImageOutputStream(getDestinationFile(i));
writer.setOutput(output);
writer.write(image.getRenderedImage());
output.close();
writeProperties(image, getDestinationFile(i, "txt"));
} catch (Exception exception) {
String message = exception.getLocalizedMessage();
if (message == null) {
message = Utilities.getShortClassName(exception);
}
progress.warningOccurred(name, null, message);
}
writer.reset();
}
progress.complete();
writer.dispose();
}
/**
* Méthode appelée automatiquement lorsqu'un avertissement
* est survenu pendant la lecture d'une image.
*/
public void warningOccurred(final ImageReader source, final String warning) {
String name = null;
final ProgressListener progress = this.progress;
if (progress != null) {
final CoverageReference entry = current;
if (entry != null) {
name = entry.getName();
}
progress.warningOccurred(name, null, warning);
}
}
/**
* Ecrit les propriétés de l'image spécifiée. L'implémentation par défaut écrit les coordonnées
* géographiques des quatres coins de l'image, sa taille, nombre de bandes, etc.
*
* @param coverage L'image pour laquelle écrire les propriétés.
* @param file Le fichier de destination. Ca sera généralement un fichier avec l'extension
* <code>".txt"</code>.
*/
protected void writeProperties(final GridCoverage2D coverage, final File file) throws IOException {
if (propertyParser == null) {
propertyParser = new MetadataBuilder();
propertyParser.setFormatPattern(Date.class, "yyyy/MM/dd HH:mm zz");
propertyParser.setFormatPattern(Number.class, "#0.######");
propertyParser.addAlias(MetadataBuilder.Z_MINIMUM, "Date de début");
propertyParser.addAlias(MetadataBuilder.Z_MAXIMUM, "Date de fin");
propertyParser.addAlias(MetadataBuilder.PROJECTION, "Projection");
propertyParser.addAlias(MetadataBuilder.ELLIPSOID, "Ellipsoïde");
propertyParser.addAlias(MetadataBuilder.Y_MAXIMUM, "Limite Nord");
propertyParser.addAlias(MetadataBuilder.Y_MINIMUM, "Limite Sud");
propertyParser.addAlias(MetadataBuilder.X_MAXIMUM, "Limite Est");
propertyParser.addAlias(MetadataBuilder.X_MINIMUM, "Limite Ouest");
propertyParser.addAlias(MetadataBuilder.Y_RESOLUTION, "Résolution en latitude");
propertyParser.addAlias(MetadataBuilder.X_RESOLUTION, "Résolution en longitude");
propertyParser.addAlias(MetadataBuilder.WIDTH, "Largeur (en pixels)");
propertyParser.addAlias(MetadataBuilder.HEIGHT, "Hauteur (en pixels)");
}
final Locale locale = Locale.getDefault();
final String lineSeparator = System.getProperty("line.separator", "\n");
final Writer out = new BufferedWriter(new FileWriter(file));
out.write('#'); out.write(lineSeparator);
out.write("# Description du format de l'image \"");
out.write(coverage.getName().toString(locale));
out.write('"');
out.write(lineSeparator);
out.write('#'); out.write(lineSeparator);
out.write(lineSeparator);
propertyParser.clear();
propertyParser.add(coverage);
propertyParser.listMetadata(out);
propertyParser.clear();
out.close();
}
}
}