version 1.1

This commit is contained in:
2024-10-09 15:06:56 +02:00
parent d81fb678e3
commit 836a412a6d
536 changed files with 63922 additions and 0 deletions
@@ -0,0 +1,122 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.logging.Level;
import javax.annotation.PreDestroy;
import org.apache.sis.util.NullArgumentException;
import org.geotoolkit.nio.IOUtilities;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
public class DocManager {
protected final Path tmpDir;
protected DocManager() {
Path tmp = null;
try {
// Init folder to put docs into.
tmp = Files.createTempDirectory("rhomeo_docs");
} catch (IOException ex) {
Rhomeo.LOGGER.log(Level.WARNING, "Impossible to prepare documentation management.", ex);
}
tmpDir = tmp;
}
public void openDocument(final URL resource) {
try {
if (isHref(resource)) {
Rhomeo.browse(resource);
}
final Path p = getPath(resource);
if (p != null)
Rhomeo.openFile(p.toFile());
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.WARNING, "Cannot open given resource : ".concat(String.valueOf(resource)), e);
}
}
private Path getPath(URL resource) throws URISyntaxException, IOException {
if (resource == null)
throw new NullArgumentException("Given resource is null !");
if ("file".equals(resource.getProtocol()))
return Paths.get(resource.toURI());
String md5 = DigestUtils.md5DigestAsHex(resource.toExternalForm().getBytes());
// HACK for win systems which are too dumb to open files without extension
final String extension = IOUtilities.extension(resource);
final Path target;
if (extension != null && !extension.isEmpty())
target = tmpDir.resolve(md5 + "." + extension);
else
target = tmpDir.resolve(md5);
if (!Files.exists(target)) {
try (final InputStream source = resource.openStream()) {
Files.copy(source, target);
}
}
return target;
}
@PreDestroy
private void clear() throws IOException {
if (tmpDir != null)
IOUtilities.deleteRecursively(tmpDir);
}
private boolean isHref(URL resource) {
return resource != null && resource.getProtocol().startsWith("http");
}
}
@@ -0,0 +1,511 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo;
import com.sun.javafx.PlatformUtil;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.UpdateInfo;
import fr.cenra.rhomeo.core.preferences.net.NetPreferences;
import fr.cenra.rhomeo.core.state.StateManager;
import fr.cenra.rhomeo.fx.FXInitialDialogPane;
import fr.cenra.rhomeo.fx.FXMainPane;
import fr.cenra.rhomeo.fx.FXSplashScreen;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.DialogEvent;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.controlsfx.dialog.ExceptionDialog;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.internal.GeotkFX;
import org.slf4j.bridge.SLF4JBridgeHandler;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* Application launcher.
*
* @author Cédric Briançon (Geomatys)
*/
public class Launcher extends Application {
/**
* Launches the application with the given arguments.
*
* @see #launch(String...)
*/
public static void main(String[] args) {
launch(args);
}
/**
* {@inheritDoc}
*/
@Override
public void start(Stage primaryStage) throws Exception {
SLF4JBridgeHandler.removeHandlersForRootLogger(); // (since SLF4J 1.6.5)
// add SLF4JBridgeHandler to j.u.l's root logger, should be done once during
// the initialization phase of your application
SLF4JBridgeHandler.install();
Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> {
// if(e instanceof Exception){
// TaskManager.INSTANCE.submit(() -> {throw (Exception) e;});
// }
// else{
RhomeoCore.LOGGER.log(Level.WARNING, "A throwable has not been caught.", e);
// }
});
// perform initialization and plugin loading tasks
final InitialLoadingTask initTask = new InitialLoadingTask();
showLoadingStage(initTask);
TaskManager.INSTANCE.submit(initTask);
}
/**
* {@inheritDoc}
*/
@Override
public void stop() throws Exception {
try {
Session.getInstance().close();
} catch (IllegalStateException ex) {
// The context was not already initialized, no need to close it.
// Not a real exception since nothing to do
}
TaskManager.INSTANCE.close();
}
/**
* Display splash screen.
*
* @param task the related task to show progress
* @throws IOException
*/
private void showLoadingStage(final InitialLoadingTask task) throws IOException {
// Initialize splash screen
final Stage splashStage = Rhomeo.newStage();
splashStage.initStyle(StageStyle.TRANSPARENT);
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fr/cenra/rhomeo/fx/FXSplashScreen.fxml"));
final GridPane root = loader.load();
final FXSplashScreen splashScreen = loader.getController();
splashScreen.uiCancel.setVisible(false);
splashScreen.uiProgressLabel.textProperty().bind(task.messageProperty());
splashScreen.uiProgressBar.progressProperty().bind(task.progressProperty());
final Scene scene = new Scene(root);
scene.getStylesheets().add("/fr/cenra/rhomeo/splashscreen.css");
scene.setFill(new Color(0, 0, 0, 0));
splashStage.setScene(scene);
splashStage.show();
// If the task succeeded, meaning the initialisation is finished or an update has been found
// and user wants to download it.
task.setOnSucceeded(evt -> Platform.runLater(() -> {
final Object value = task.getValue();
// If value is an URL, then it is an update to download.
// Otherwise, nothing to download keep going with the application start.
if (value instanceof URLAndMD5) {
final DownloadUpdateTask downloadTask = new DownloadUpdateTask((URLAndMD5) value);
splashScreen.uiProgressLabel.textProperty().unbind();
splashScreen.uiProgressLabel.textProperty().bind(downloadTask.messageProperty());
splashScreen.uiProgressBar.progressProperty().unbind();
splashScreen.uiProgressBar.progressProperty().bind(downloadTask.progressProperty());
TaskManager.INSTANCE.submit(downloadTask);
downloadTask.setOnCancelled(evt2 -> System.exit(0));
downloadTask.setOnFailed(evt2 -> Platform.runLater(() -> {
GeotkFX.newExceptionDialog("Le téléchargement de la mise à jour a échoué !", downloadTask.getException()).showAndWait();
System.exit(1);
}));
downloadTask.setOnSucceeded((evt2) -> {
Path newValue = downloadTask.getValue();
try {
Rhomeo.openFile(newValue);
Thread.sleep(1000);
} catch (Exception e) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot proceed to update !", e);
} finally {
Platform.runLater(() -> splashStage.close());
}
});
} else {
// Launches the main window here
splashStage.toFront();
splashScreen.uiLoadingPane.setVisible(false);
final FadeTransition fadeSplash = new FadeTransition(Duration.seconds(1.2), root);
fadeSplash.setFromValue(1.0);
fadeSplash.setToValue(0.0);
fadeSplash.setOnFinished(actionEvent -> {
splashStage.hide();
root.setOpacity(1.0);
try {
final Stage mainStage = createDashboardStage();
final Preferences welcomePrefs = Preferences.userNodeForPackage(FXInitialDialogPane.class);
final Stage popup;
if (welcomePrefs.getBoolean(FXInitialDialogPane.DISPLAY_RULE_KEY, true)) {
popup = Rhomeo.createWelcomePopup();
popup.initOwner(mainStage);
popup.show();
popup.requestFocus();
} else {
popup = null;
}
// HACK : display of both stages is done now, because if
// main stage is displayed before we've ended configuring
// popup, some properties (like resizing capabilities)
// looks shared.
mainStage.show();
if (popup != null) {
popup.show();
popup.requestFocus();
}
} catch (Throwable ex) {
try {
RhomeoCore.LOGGER.log(Level.WARNING, "Erreur inattendue lors de l'initialisation du panneau principal.", ex);
final ExceptionDialog exDialog = GeotkFX.newExceptionDialog("L'application a rencontré une erreur inattendue et doit fermer.", ex);
exDialog.setOnHidden((DialogEvent de) -> System.exit(1));
exDialog.show();
} catch (Throwable e) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot show error dialog to user", e);
System.exit(1);
}
}
});
fadeSplash.play();
}
}));
task.setOnCancelled(evt -> {
System.exit(0);
});
// Task cancelled, because of an error or a user cancellation action.
task.setOnFailed(evt -> {
splashScreen.uiProgressLabel.getStyleClass().remove("label");
splashScreen.uiProgressLabel.getStyleClass().add("label-error");
splashScreen.uiCancel.setVisible(true);
});
}
/**
* Show the updates confirmation window. User is asked if the update should be downloaded.
*
* @param newVersion The update version.
* @return {@code true} if the user wants to apply the update, {@code false} otherwise.
* @throws ExecutionException
* @throws InterruptedException
*/
private boolean showUpdatesStage(final UpdateInfo newVersion) throws ExecutionException, InterruptedException {
// Now that we found that an update is available, we can ask the user if the update should be downloaded.
final Task<Boolean> askForUpdate = new Task<Boolean>() {
@Override
protected Boolean call() throws Exception {
final StringBuilder builder = new StringBuilder()
.append("Version installée : ").append(Rhomeo.getVersion())
.append(System.lineSeparator())
.append("Version de la mise à jour : ").append(newVersion.getVersion())
.append(System.lineSeparator())
.append("Date de publication : ").append(newVersion.getDate() == null ? "Inconnue" : newVersion.getDate())
.append(System.lineSeparator());
final String[] releaseNote = newVersion.getReleaseNote();
if (releaseNote != null && releaseNote.length > 0) {
builder.append("Release-note : ");
for (final String str : releaseNote) {
builder.append(System.lineSeparator()).append(str);
}
}
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION,
builder.toString(), ButtonType.NO, ButtonType.YES);
alert.setHeaderText("Une mise à jour est disponible. Télécharger la mise à jour ?");
final TextArea infoArea = new TextArea(builder.toString());
infoArea.setEditable(false);
infoArea.setPrefSize(666, 444);
alert.setResizable(true);
alert.getDialogPane().setContent(infoArea);
final Optional<ButtonType> choice = alert.showAndWait();
return ButtonType.YES.equals(choice.orElse(ButtonType.NO));
}
};
Platform.runLater(() -> askForUpdate.run());
return Boolean.TRUE.equals(askForUpdate.get());
}
/**
* Main dashboard stage. Will be opened when the application will be launched.
*
* @return The main stage, never {@code null}
*/
private Stage createDashboardStage() {
final Stage mainStage = Rhomeo.newStage();
mainStage.setOnCloseRequest(event -> System.exit(0));
final FXMainPane mainPane = Session.getInstance().getBean(FXMainPane.class);
final Scene scene = new Scene(mainPane);
scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
mainStage.setScene(scene);
mainStage.initModality(Modality.NONE);
mainStage.setMinWidth(800);
mainStage.setMinHeight(600);
mainStage.setResizable(true);
mainStage.setMaximized(true);
return mainStage;
}
/**
*
* @return
*/
private URLAndMD5 checkUpdate() throws Exception {
final UpdateInfo newVersion = RhomeoCore.existingUpdate();
if (newVersion != null) {
final URL updateURL;
final String md5;
if (PlatformUtil.isWindows()) {
updateURL = newVersion.getWin32();
md5 = newVersion.getWin32MD5();
} else if (PlatformUtil.isMac()) {
updateURL = newVersion.getMacOS64();
md5 = newVersion.getMacOS64MD5();
} else if (PlatformUtil.isLinux()) {
if (newVersion.getDeb64() != null && isDeb()) {
updateURL = newVersion.getDeb64();
md5 = newVersion.getDeb64MD5();
} else {
updateURL = newVersion.getRpm64();
md5 = newVersion.getRpm64MD5();
}
} else {
updateURL = null;
md5 = null;
}
if (updateURL != null && showUpdatesStage(newVersion)) {
return new URLAndMD5(updateURL, md5);
}
}
return null;
}
private boolean isDeb() {
try {
return Runtime.getRuntime().exec("dpkg --version").waitFor() == 0;
} catch (InterruptedException|IOException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot detect if the system is a dpkg or RPM based OS.", ex);
}
return false;
}
/**
* Initial loading task, to follow with a progress bar.
* If an update has been detected, returns the download url.
*/
private final class InitialLoadingTask extends Task {
/**
* Launched when this task will be called.
*
* @return Always return {@code null}.
* @throws Exception
*/
@Override
protected Object call() throws Exception {
final int total = 4;
int inc = 0;
try {
// CHECK UPDATES ///////////////////////////////////////////////
updateProgress(inc++, total);
updateMessage("Vérification des mises à jour...");
final URLAndMD5 upURL = checkUpdate();
if (upURL != null) {
return upURL;
}
// EPSG DATABASE ///////////////////////////////////////////////
updateProgress(inc++, total);
updateMessage("Création de la base de données de projections cartographiques (EPSG) ...");
// try to create it, won't do anything if already exist
RhomeoCore.initEpsgDB();
// SPRING CONTEXT //////////////////////////////////////////////
updateProgress(inc++, total);
updateMessage("Chargement du contexte de l'application ...");
new ClassPathXmlApplicationContext(RhomeoCore.SPRING_CONTEXT_XML);
// RESTORE APPLICATION STATE ///////////////////////////////////
updateProgress(inc++, total);
updateMessage("Récupération des données...");
Session.getInstance().getBean(StateManager.class).restoreState();
// END
updateProgress(total, total);
updateMessage("Chargement terminé");
Thread.sleep(400);
} catch (Throwable ex) {
updateProgress(-1, -1);
updateMessage("Erreur inattendue : ".concat(ex.getLocalizedMessage()));
RhomeoCore.LOGGER.log(Level.WARNING, ex.getMessage(), ex);
throw ex;
}
return null;
}
}
/**
* Application update downloader.
* When completed, returns the {@linkplain Path downloaded file}.
*/
private final class DownloadUpdateTask extends Task<Path> {
private final URLAndMD5 url;
public DownloadUpdateTask(final URLAndMD5 url) {
this.url = url;
}
/**
* Download the new update.
*
* @return The {@linkplain Path downloaded update}
* @throws Exception
*/
@Override
protected Path call() throws Exception {
updateMessage("Téléchargement de la mise à jour en cours ...");
String completeURL = url.url.toExternalForm();
final Matcher matcher = RhomeoCore.EXT_PATTERN.matcher(completeURL);
final Path tmpFile = Files.createTempFile("tmpInstall", matcher.find()? matcher.group() : ".tmp");
// Now download the update in the appropriate file
final URLConnection bundleConnec = new NetPreferences().openConnection(url.url);
bundleConnec.setConnectTimeout(RhomeoCore.CONNECTION_TIMEOUT);
bundleConnec.setReadTimeout(RhomeoCore.READ_TIMEOUT);
final long totalLength = bundleConnec.getContentLengthLong();
long downloaded = 0;
final String totalReadableSize = RhomeoCore.toReadableSize(totalLength);
final MessageDigest digest = MessageDigest.getInstance("md5");
try (final InputStream input = bundleConnec.getInputStream();
final OutputStream output = Files.newOutputStream(tmpFile)) {
int readBytes;
final byte[] buffer = new byte[4096];
while ((readBytes = input.read(buffer)) >= 0) {
output.write(buffer, 0, readBytes);
digest.update(buffer, 0, readBytes);
downloaded += readBytes;
updateProgress(downloaded, totalLength);
updateMessage("Téléchargement "+ RhomeoCore.toReadableSize(downloaded) + " / " + totalReadableSize);
}
output.flush();
}
// Verify that the content was fully downloaded
if (downloaded != totalLength) {
throw new IOException("La mise à jour n'a pas été entièrement téléchargée");
}
// MD5 checksum
if (url.md5Base64 != null && !url.md5Base64.isEmpty()) {
byte[] computed = digest.digest();
if (!url.md5Base64.equals(new BigInteger(1, computed).toString(16))) {
throw new IOException("Downloaded file checksum is invalid !");
}
}
updateMessage("Mise à jour téléchargée. Veuillez cliquer sur le bouton et l'installer.");
return tmpFile;
}
}
private static class URLAndMD5 {
final URL url;
final String md5Base64;
public URLAndMD5(final URL packageURL, final String base64MD5) {
this.url = packageURL;
this.md5Base64 = base64MD5;
}
}
}
@@ -0,0 +1,758 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo;
import com.sun.javafx.PlatformUtil;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.core.CSVEncoder;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.util.ExportUtils;
import fr.cenra.rhomeo.fx.FXInitialDialogPane;
import java.awt.Color;
import java.awt.Desktop;
import java.awt.Font;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import java.util.prefs.Preferences;
import java.util.zip.ZipOutputStream;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.font.IconBuilder;
import org.geotoolkit.gui.javafx.util.ComboBoxCompletion;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.nio.ZipUtilities;
/**
* Regroups methods used by the application for handling JavaFX features.
*
* @author Cédric Briançon (Geomatys)
* @see RhomeoCore
*/
public final class Rhomeo extends RhomeoCore {
/**
* Application icon.
*/
public static final Image ICON = new Image(Rhomeo.class.getResource("/fr/cenra/rhomeo/images/logo.png").toString(), 16, 16, true, false);
public static final Image ICON_NOT_PUBLISHED = new Image(Rhomeo.class.getResource("/fr/cenra/rhomeo/images/not_published.png").toString(),
16, 16, true, true);
public static final Color GRAY_SITE = Color.decode("#898989");
public static final Color GREEN_SITE = Color.decode("#9CB35B");
public static final Color BLUE_SITE = Color.decode("#5B8AA4");
public static final Color RED_SITE = Color.decode("#D85F70");
public static final Color ORANGE_SITE = Color.decode("#FFA500");
public static final Font PROGRESS_MONITOR_ICON_FONT = IconBuilder.FONT.deriveFont(16f);
public static final Image ICON_BARS_WHITE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_BARS,24, Color.WHITE),null);
public static final Image ICON_CARET_UP = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_CARET_UP,16, GRAY_SITE),null);
public static final Image ICON_CARET_DOWN = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_CARET_DOWN,16, GRAY_SITE),null);
public static final Image ICON_CHECK_GRAY = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_CHECK,16, GRAY_SITE),null);
public static final Image ICON_CHECK_GREEN = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_CHECK,16, GREEN_SITE),null);
public static final Image ICON_CIRCLE_GRAY = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_CIRCLE,8, GRAY_SITE),null);
public static final Image ICON_COG = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_COG,24, GRAY_SITE),null);
public static final Image ICON_DOWNLOAD_BLUE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_DOWNLOAD,24, BLUE_SITE),null);
public static final Image ICON_EXCHANGE_GRAY = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_EXCHANGE,16, GRAY_SITE),null);
public static final Image ICON_EYE_GRAY = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_EYE,16, GRAY_SITE),null);
public static final Image ICON_FILE_PDF = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_FILE_PDF_O,24, RED_SITE),null);
public static final Image ICON_FILE_TEXT = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_FILE_TEXT_O,24, BLUE_SITE),null);
public static final Image ICON_INFO_BLUE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_INFO_CIRCLE,24, BLUE_SITE),null);
public static final Image ICON_INFO_GREY = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_INFO_CIRCLE,24, GRAY_SITE),null);
public static final Image ICON_LONG_ARROW_RIGHT = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_LONG_ARROW_RIGHT,48, GRAY_SITE),null);
public static final Image ICON_PENCIL_BLUE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_PENCIL,24, BLUE_SITE),null);
public static final Image ICON_PENCIL_WHITE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_PENCIL,24, Color.WHITE),null);
public static final Image ICON_STOP_RED = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_STOP,16, RED_SITE),null);
public static final Image ICON_TIMES_RED = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_TIMES,16, RED_SITE),null);
public static final Image ICON_TRASH_BLUE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_TRASH_O,24, BLUE_SITE),null);
public static final Image ICON_WARNING_ORANGE = SwingFXUtils.toFXImage(IconBuilder.createImage(FontAwesomeIcons.ICON_WARNING_ALIAS,24, ORANGE_SITE),null);
public static final String CSS_FONT_AWESOME = "rhomeo-font-awesome";
public static final String CSS_ERROR_FIELD = "error-field";
public static final String CSS_WARNING_FIELD = "warning-field";
public static final String CSS_INDICATOR_LABEL = "indicator-label";
public static final String CSS_CURRENT_STEP = "current-step";
public static final String CSS_PROTOCOL_BUTTON = "protocol-button";
public static final String CSS_PROTOCOL_POPUP_BODY = "protocol-popup-body";
/**
* Main theme path
*/
public static final String CSS_THEME_PATH = "/fr/cenra/rhomeo/theme.css";
private static final ChangeListener<String> SPINNER_LISTENER = Rhomeo::spinnerTextChanged;
/**
* Generates a stage with {@linkplain RhomeoCore#APPLICATION_TITLE application title} and
* {@linkplain #ICON application icon}.
*
* @return a new Rhomeo stage, never {@code null}
*/
public static Stage newStage() {
final Stage rhomeoStage = new Stage();
rhomeoStage.getIcons().add(ICON);
final String version = RhomeoCore.getVersion();
final String title = (version == null || version.isEmpty()) ? APPLICATION_TITLE : APPLICATION_TITLE +" ("+ version +")";
rhomeoStage.setTitle(title);
return rhomeoStage;
}
/**
* Generates the initial dialog. Will be shown when launching the application.
*
* @return The dialog stage, never {@code null}
*/
public static Stage createWelcomePopup() {
final Stage stage = Rhomeo.newStage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setResizable(true);
stage.setTitle("Accueil");
final FXInitialDialogPane dialog = new FXInitialDialogPane();
final Scene scene = new Scene(dialog);
scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
stage.setScene(scene);
stage.sizeToScene();
return stage;
}
public static void loadFXML(Parent candidate) {
final Class modelClass = null;
loadFXML(candidate, modelClass);
}
/**
* Load FXML document matching input controller. If a model class is given,
* we'll try to load a bundle for text internationalization.
* @param candidate The controller object to get FXMl for.
* @param bundleClass A class which will be used for bundle loading.
*/
public static void loadFXML(final Parent candidate, final Class bundleClass) {
ResourceBundle bundle = null;
if (bundleClass != null) {
try{
bundle = ResourceBundle.getBundle(bundleClass.getName(), Locale.FRENCH,
Thread.currentThread().getContextClassLoader());
}catch(MissingResourceException ex){
LOGGER.log(Level.INFO, "Missing bundle for : {0}", bundleClass.getName());
}
}
loadFXML(candidate, bundle);
}
public static void loadFXML(final Parent candidate, final ResourceBundle bundle) {
loadFXML(candidate, candidate.getClass(), bundle);
}
public static void loadFXML(final Parent candidate, final Class fxmlClass, final ResourceBundle bundle) {
ArgumentChecks.ensureNonNull("JavaFX controller object", candidate);
final String fxmlpath = "/"+fxmlClass.getName().replace('.', '/')+".fxml";
final URL resource = fxmlClass.getResource(fxmlpath);
if (resource == null) {
throw new RuntimeException("No FXML document can be found for path : "+fxmlpath);
}
final FXMLLoader loader = new FXMLLoader(resource);
loader.setController(candidate);
loader.setRoot(candidate);
//in special environement like osgi or other, we must use the proper class loaders
//not necessarly the one which loaded the FXMLLoader class
loader.setClassLoader(fxmlClass.getClassLoader());
if(bundle!=null) loader.setResources(bundle);
fxRunAndWait(() -> {
try {
loader.load();
} catch (IOException ex) {
throw new IllegalArgumentException(ex.getMessage(), ex);
}
});
}
/**
* Run given task in FX application thread (immediately if we're already in it),
* and wait for its result before returning.
*
* @param <T> Return TYPE of the input task.
* @param toRun Task to run in JavaFX thread.
* @return Result of the input task.
*/
public static <T> T fxRunAndWait(final Callable<T> toRun) {
return fxRunAndWait(new TaskManager.MockTask<>(toRun));
}
/**
*
* @param toRun The task to run.
*/
public static void fxRunAndWait(final Runnable toRun) {
fxRunAndWait(new TaskManager.MockTask(toRun));
}
/**
* Run given task in FX application thread (immediately if we're already in it),
* and wait for its result before returning.
*
* @param <T> Return TYPE of the input task.
* @param toRun Task to run in JavaFX thread.
* @return Result of the input task.
*/
public static <T> T fxRunAndWait(final Task<T> toRun) {
return fxRun(true, toRun);
}
/**
* Run given task in FX application thread (immediately if we're already in it).
* According to input boolean, we will return immediately or wait for the task to
* be over.
* @param wait True if we must wait for the task to end before returning, false
* to return immediately after submission.
* @param toRun The task to run into JavaFX application thread.
*/
public static void fxRun(final boolean wait, final Runnable toRun) {
fxRun(wait, new TaskManager.MockTask(toRun));
}
/**
* Run given task in FX application thread (immediately if we're already in it).
* According to input boolean, we will return immediately or wait for the task to
* be over.
* @param <T> Return TYPE of input task.
* @param wait True if we must wait for the task to end before returning, false
* to return immediately after submission.
* @return The task return value if we must wait, or we're in platform thread. Otherwise null.
* @param toRun The task to run into JavaFX application thread.
*/
public static <T> T fxRun(final boolean wait, final Callable<T> toRun) {
return fxRun(wait, new TaskManager.MockTask<>(toRun));
}
/**
* Run given task in FX application thread (immediately if we're already in it).
* According to input boolean, we will return immediately or wait for the task to
* be over.
* @param <T> Return TYPE of input task.
* @param wait True if we must wait for the task to end before returning, false
* to return immediately after submission.
* @return The task return value if we must wait, or we're in platform thread. Otherwise null.
* @param toRun The task to run into JavaFX application thread.
*/
public static <T> T fxRun(final boolean wait, final Task<T> toRun) {
if (Platform.isFxApplicationThread())
toRun.run();
else
Platform.runLater(toRun);
if (wait || Platform.isFxApplicationThread()) {
try {
return toRun.get();
} catch (RuntimeException ex) {
throw ex;
} catch (ExecutionException ex) {
if (ex.getCause() instanceof RuntimeException) {
throw (RuntimeException) ex.getCause();
} else {
throw new RhomeoRuntimeException(ex.getCause());
}
} catch (Exception e) {
throw new RhomeoRuntimeException(e);
}
} else
return null;
}
/**
* Try to open given file on system.
* @param toOpen The file open on underlying system.
* @return True if we succeeded opening file on system, false otherwise.
*/
public static Task<Boolean> openFile(final Path toOpen) {
return openFile(toOpen.toAbsolutePath().toFile());
}
/**
* Try to open given file on system.
* @param toOpen The file open on underlying system.
* @return True if we succeeded opening file on system, false otherwise.
*/
public static Task<Boolean> openFile(final File toOpen) {
return TaskManager.INSTANCE.submit("Ouverture d'un fichier", () -> {
try {
final Desktop desktop = Desktop.getDesktop();
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(toOpen);
} else if (desktop.isSupported(Desktop.Action.EDIT)) {
desktop.edit(toOpen);
} else {
throw new IOException("Impossible de communiquer avec l'OS.");
}
} catch (IOException e) {
/*
* HACK : on Windows, it seems that some file associations cannot be found
* using above method (Ex : .odt), so we try running following command as
* a fallback.
*/
if (PlatformUtil.isWindows()) {
Runtime.getRuntime().exec(new String[]{"cmd /c start", toOpen.toURI().toString()});
} else {
throw e;
}
}
return true;
});
}
public static Task<Boolean> browse(final URL href) {
return TaskManager.INSTANCE.submit("Ouverture d'un lien", () -> {
final Desktop desktop = Desktop.getDesktop();
if (desktop.isSupported(Desktop.Action.BROWSE))
desktop.browse(href.toURI());
else
throw new IOException("Impossible de communiquer avec l'OS.");
return true;
});
}
public static Task<Boolean> mail(final String mailAdress) {
return TaskManager.INSTANCE.submit("Ouverture d'un lien e-mail", () -> {
final Desktop desktop = Desktop.getDesktop();
if (desktop.isSupported(Desktop.Action.MAIL))
desktop.mail(new URI(mailAdress.startsWith("mailto")? mailAdress : "mailto:".concat(mailAdress)));
else
throw new IOException("Impossible de communiquer avec l'OS.");
return true;
});
}
/**
* Initializes an auto completable combo box, without selecting one.
*
* @param <I>
* @param comboBox
* @param items
* @see #initComboBox(ComboBox, ObservableList, Object)
*/
public static <I> void initComboBox(final ComboBox<I> comboBox, final ObservableList<I> items) {
initComboBox(comboBox, items, null);
}
/**
* Add items in the combo box and make it auto completable.
*
* @param comboBox A new combo box, not {@code null}.
* @param items List of items to add.
* @param itemSelected If not null, select this item in the list.
* @param <I> item class
*/
public static <I> void initComboBox(final ComboBox<I> comboBox, final ObservableList<I> items, final I itemSelected) {
comboBox.setItems(items);
try {
comboBox.setEditable(true);
} catch (RuntimeException re) {
// A runtime exception is launched if the editable property is already bound
// Just log and let the property bound and handled by something else.
Rhomeo.LOGGER.log(Level.FINE, re.getLocalizedMessage(), re);
}
if (itemSelected != null) {
comboBox.getSelectionModel().select(itemSelected);
}
ComboBoxCompletion.autocomplete(comboBox);
}
/**
* Generates an alert of the given type with the given text and just an OK button.
*
* @param type {@linkplain Alert.AlertType alert type}.
* @param text Text to display.
*/
public static void showAlert(final Alert.AlertType type, final String text) {
final Alert alert = new Alert(type, text, ButtonType.OK);
alert.setResizable(true);
alert.show();
}
/**
* Get the previous path stored for reopening a file chooser in it.
*
* @param packageClass Class in a specific java package on which we want to attach
* the path preference
* @return the previous path stored with the last opening of the file chooser.
*/
public static File getPreviousPath(final Class packageClass) {
final Preferences prefs = Preferences.userNodeForPackage(packageClass);
final String str = prefs.get("path", null);
if (str != null) {
final File file = new File(str);
if (file.isDirectory()) {
return file;
}
}
return null;
}
/**
* Set the path of the previously selected file into the file chooser.
*
* @param packageClass Class in a specific java package on which we want to attach
* the path preference
* @param path the path to store. Should point on a folder.
*/
public static void setPreviousPath(final Class packageClass, final File path) {
final Preferences prefs = Preferences.userNodeForPackage(packageClass);
prefs.put("path", path.getAbsolutePath());
}
/**
* Open modal popup asking for a file on the local path.
*
* @param initialFileName Initial file name to use.
* @param owner The owner of this popup.
* @param filters Filters to use. Possibly empty or null. The first item will be used as the default one.
* @return The file wished, or {@code null} if the user cancelled the popup.
*/
public static File askOutputFilePopup(final String initialFileName, final Node owner, final FileChooser.ExtensionFilter... filters) {
final FileChooser fileChooser = new FileChooser();
final File previousPath = getPreviousPath(ExportUtils.class);
if(previousPath!=null && previousPath.exists()){
fileChooser.setInitialDirectory(previousPath);
}
// Set some usefull filters
if (filters != null && filters.length > 0) {
fileChooser.getExtensionFilters().addAll(filters);
fileChooser.setSelectedExtensionFilter(filters[0]);
}
if (initialFileName != null && !initialFileName.isEmpty()) {
fileChooser.setInitialFileName(initialFileName);
}
final File outputFile = fileChooser.showSaveDialog(owner.getScene().getWindow());
if(outputFile==null) return null;
setPreviousPath(ExportUtils.class, outputFile.getParentFile());
return outputFile;
}
/**
* Try to find a combobox under given node scene tree.
* @param source The node to search into.
* @return The first found combobox, or nothing.
*/
public static Optional<ComboBox> findComboBox(final Node source) {
if (source instanceof ComboBox) {
return Optional.of((ComboBox)source);
} else if (source instanceof Parent) {
final ObservableList<Node> nodes = ((Parent)source).getChildrenUnmodifiable();
Optional<ComboBox> opt;
for (final Node n : nodes) {
opt = findComboBox(n);
if (opt.isPresent())
return opt;
}
}
return Optional.empty();
}
public static void prepareSpinners(final Node source) {
if (source instanceof Spinner) {
final Spinner spinner = (Spinner) source;
spinner.getEditor().textProperty().removeListener(SPINNER_LISTENER);
spinner.getEditor().textProperty().addListener(SPINNER_LISTENER);
final SpinnerValueFactory valueFactory = spinner.getValueFactory();
if (valueFactory instanceof SpinnerValueFactory.DoubleSpinnerValueFactory) {
final SpinnerValueFactory.DoubleSpinnerValueFactory dvf = (SpinnerValueFactory.DoubleSpinnerValueFactory) valueFactory;
dvf.setAmountToStepBy(0.01);
dvf.setConverter(new SpinnerConverter(spinner));
// HACK : To force display refresh immediately, we introduce temporary value change.
final Double oldValue = dvf.getValue();
if (oldValue == null || oldValue.isNaN() || oldValue.isInfinite())
dvf.setValue(0.0);
else dvf.setValue(oldValue + 1);
dvf.setValue(oldValue);
}
} else if (source instanceof Parent) {
final ObservableList<Node> nodes = ((Parent)source).getChildrenUnmodifiable();
for (final Node n : nodes) {
prepareSpinners(n);
}
}
}
private static void spinnerTextChanged(final ObservableValue<? extends String> obs, final String oldValue, final String newValue) {
if (obs instanceof ReadOnlyProperty) {
final Object bean = ((ReadOnlyProperty)obs).getBean();
if (bean instanceof Node) {
Parent parent = ((Node) bean).getParent();
if (parent instanceof Spinner) {
final Spinner spinner = (Spinner) parent;
try {
spinner.getValueFactory().setValue(spinner.getValueFactory().getConverter().fromString(newValue));
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.FINE, "Cannot convert seized text to numeric value", e);
}
}
}
}
}
/**
* Check column and row indices to determine if we must edit next column or
* next row. If next cell is not editable, we test the next one, etc.
*
* @param editingCell Cell currently edited. If null, this method does nothing.
*/
public static void editNextCell(final TablePosition editingCell) {
if (editingCell == null)
return;
final TableView<Object> tableView = editingCell.getTableView();
TableColumn targetCol;
int rowIndex = editingCell.getRow();
int colIndex = editingCell.getColumn();
final int maxRow = tableView.getItems().size() - 1;
final int maxCol = tableView.getColumns().size() - 1;
do {
if (colIndex >= maxCol) {
rowIndex++;
colIndex = 0;
} else {
colIndex++;
}
if (rowIndex < 0 || rowIndex > maxRow) {
targetCol = null;
} else {
targetCol = tableView.getColumns().get(colIndex);
}
} while (targetCol != null && !targetCol.isEditable());
tableView.edit(rowIndex, targetCol);
}
public static Button createProtocolDocumentButton(final Protocol protocol) {
final Button button = new Button("Fiche protocole "+ protocol.getName());
button.setGraphic(new ImageView(ICON_FILE_PDF));
button.getStyleClass().add("transparent");
button.setOnAction(event -> {
final String fileName = protocol.getName() + ".pdf";
Session.getInstance().getBean(DocManager.class).openDocument(protocol.getClass().getResource(fileName));
});
return button;
}
public static Button createModelButton(final Protocol protocol) {
final Button button = new Button("Format protocole "+ protocol.getRemarks() +" ("+ protocol.getName() +")");
button.setGraphic(new ImageView(ICON_FILE_TEXT));
button.getStyleClass().add("transparent");
button.setOnAction(event -> generateModelZip(protocol, button));
return button;
}
public static void generateModelZip(final Protocol protocol, final Node owner) {
final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
final File outputFile = Rhomeo.askOutputFilePopup(protocol.getName() +".zip", owner, zipFilter);
if (outputFile == null) {
return;
}
final Path parentPath = Paths.get(outputFile.getParentFile().getAbsolutePath());
final UUID uuid = UUID.randomUUID();
final Path folderPath = parentPath.resolve(uuid.toString());
final Class<? extends Statement> statementClass = protocol.getDataType();
try {
Files.createDirectory(folderPath);
final Path formatPath = folderPath.resolve("format.csv");
final CSVEncoder<? extends Statement> encoder = new CSVEncoder<>(formatPath, statementClass);
encoder.encode(new ArrayList<>(), true);
final Path readmePath = createReadMeFile(statementClass, folderPath);
ZipUtilities.zip(outputFile.toPath(), ZipOutputStream.DEFLATED, 9, null, formatPath, readmePath);
Files.deleteIfExists(formatPath);
Files.deleteIfExists(readmePath);
Files.deleteIfExists(folderPath);
} catch (IOException | IntrospectionException ex) {
Rhomeo.LOGGER.log(Level.INFO, "Cannot generate format file", ex);
}
}
private static Path createReadMeFile(final Class<? extends Statement> statementClass, final Path outputFolderPath) throws IOException, IntrospectionException {
final boolean bundleAvailable = InternationalResource.class.isAssignableFrom(statementClass);
final Path formatPath = outputFolderPath.resolve("readme.txt");
Files.createFile(formatPath);
String description = null;
try (final BufferedWriter bw = Files.newBufferedWriter(formatPath, StandardOpenOption.APPEND)) {
final BeanInfo beanInfo = Introspector.getBeanInfo(statementClass, Object.class);
for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
if (bundleAvailable) {
try {
description = InternationalResource.getResourceString(statementClass, desc.getName(), "tooltip");
} catch (MissingResourceException ex) {
description = null;
}
if (description == null || description.isEmpty()) {
try {
description = InternationalResource.getResourceString(statementClass, desc.getName(), "label");
} catch (MissingResourceException ex) {
description = null;
}
}
bw.write(desc.getName());
bw.write("=");
if (description == null || description.isEmpty()) {
Rhomeo.LOGGER.log(Level.WARNING, "No resources for key " + desc.getName() + ".label or " + desc.getName() + ".tooltip in bundle for class "
+ statementClass.getCanonicalName());
} else {
bw.write(description);
}
bw.newLine();
}
}
// Check if additional content is available for the format readme.
try (final InputStream in = statementClass.getResourceAsStream("readme.additional");
final InputStreamReader tmpReader = new InputStreamReader(in, StandardCharsets.UTF_8);
final BufferedReader reader = new BufferedReader(tmpReader)) {
String line;
while ((line = reader.readLine()) != null) {
bw.newLine();
bw.write(line);
}
} catch (NullPointerException e) {
LOGGER.log(Level.FINE, "No additional text defined for data description.", e);
}
}
return formatPath;
}
/**
* A special string converter for Spinners. The aim is to avoid changes in
* spinner's editor when value is updated, but matches editor text. Because
* without any check, spinner will force editor text, which could bother
* user seizing.
*/
private static class SpinnerConverter extends StringConverter<Double> {
private final Spinner target;
public SpinnerConverter(Spinner target) {
this.target = target;
}
@Override
public String toString(Double object) {
final String text = target.getEditor().getText();
Object fromString = DOUBLE_CONVERTER.fromString(text);
if ((fromString instanceof Number) && (target.getValue() instanceof Number) && ((Number)fromString).doubleValue() == ((Number)target.getValue()).doubleValue())
return text;
else return DOUBLE_CONVERTER.toString(object);
}
@Override
public Double fromString(String string) {
return (Double) DOUBLE_CONVERTER.fromString(string);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,256 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.DashboardResultItem;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.DashboardResultsManager;
import fr.cenra.rhomeo.core.result.ResultStorage;
import fr.cenra.rhomeo.core.util.ExportUtils;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
import javafx.stage.FileChooser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.File;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.TablePosition;
import org.apache.commons.net.ftp.FTPClient;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.context.annotation.Scope;
/**
* Dashboard panel displayed initially when opening the application.
*
* @author Cédric Briançon (Geomatys)
*/
@Component
@Scope("prototype")
public class FXDashboardPane extends BorderPane implements InternationalResource {
/**
* Body panel containing the data table and some action buttons.
*/
@FXML private BorderPane uiDashboardBodyPane;
@FXML
private Button uiPublish;
@FXML
private Button uiExport;
@Autowired
private FXDashboardMenuPane menuPane;
@Autowired
private Session session;
@Autowired
private DashboardResultsManager resultsManager;
@Autowired
private ResultStorage resultStorage;
FXDashboardResultsTable resultTable;
public FXDashboardPane() {
Rhomeo.loadFXML(this, FXDashboardPane.class);
}
/**
* Initializes JavaFX components after spring components are done loading.
*/
@PostConstruct
private void postConstruct() {
setTop(menuPane);
resultTable = session.createPrototype(FXDashboardResultsTable.class, true);
uiDashboardBodyPane.setCenter(resultTable);
resultTable.siteProperty().bind(menuPane.siteProperty());
uiDashboardBodyPane.visibleProperty().bind(menuPane.siteProperty().isNotNull().and(menuPane.newSiteProperty().not()));
uiDashboardBodyPane.managedProperty().bind(uiDashboardBodyPane.visibleProperty());
final ObservableList<TablePosition> selectedCells = resultTable.getSelectionModel().getSelectedCells();
uiPublish.disableProperty().bind(Bindings.createBooleanBinding(() -> {
if (selectedCells.size() != 1) {
return true;
}
TablePosition<IndicatorValuesByYearItem, ?> pos = selectedCells.get(0);
if (pos.getTableColumn() instanceof IndicatorSiteColumn) {
final String indicName = ((IndicatorSiteColumn)pos.getTableColumn()).getIndicator().getName();
final Boolean published = pos.getTableView().getItems().get(pos.getRow()).isPublished(indicName);
return published == null? true : published;
}
return true;
}, selectedCells));
uiExport.disableProperty().bind(Bindings.createBooleanBinding(() -> {
return (selectedCells.size() != 1 || !(selectedCells.get(0).getTableColumn() instanceof IndicatorSiteColumn));
}, selectedCells));
}
public ObjectProperty<Site> siteProperty() {
return menuPane.siteProperty();
}
@FXML
public void publishData(final ActionEvent evt) {
final ObservableList<TablePosition> selectedCells = resultTable.getSelectionModel().getSelectedCells();
if (resultTable == null || selectedCells.size() != 1)
return;
final DashboardResultItem item = getSelectedItem();
final IndicatorValuesByYearItem rowItem = resultTable.getItems().get(selectedCells.get(0).getRow());
if (item == null || item.isPublished())
return;
TaskManager.INSTANCE.submit("Publication ".concat(item.getIndicator()), () -> {
final FTPClient ftp = session.connectResultFTP();
try {
resultsManager.publishResults(menuPane.siteProperty().get(), Collections.singletonList(item), ftp);
} finally {
ftp.disconnect();
}
return true;
}).setOnSucceeded((evt2) -> Platform.runLater(() -> {
// Update publication state.
rowItem.getValueForIndicator(item.getIndicator()).setPublished(true);
resultTable.getItems().set(resultTable.getItems().indexOf(rowItem), rowItem);
}));
}
@FXML
public void exportData() {
if (resultTable == null || resultTable.getSelectionModel().getSelectedCells().size() != 1)
return;
final DashboardResultItem item = getSelectedItem();
if (item == null)
return;
final Indicator indic = ((IndicatorSiteColumn) resultTable.getSelectionModel().getSelectedCells().get(0).getTableColumn()).getIndicator();
final String initialFileName = new StringBuilder(item.getSite()).append('_').append(item.getIndicator()).append('_').append(item.getYear()).append(".zip").toString();
final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
final File outputFile = Rhomeo.askOutputFilePopup(initialFileName, this, zipFilter);
if (outputFile == null) {
return;
}
TaskManager.INSTANCE.submit("Export de résultats", () -> {
try (final FileSystem fs = RhomeoCore.toZipFileSystem(outputFile.toPath())) {
final Path root = fs.getRootDirectories().iterator().next();
Optional<Path> opt = resultStorage.getMainResults(item);
if (opt.isPresent())
Files.copy(opt.get(), root.resolve(opt.get().getFileName().toString()));
opt = resultStorage.getAdditionalResults(item);
if (opt.isPresent())
Files.copy(opt.get(), root.resolve(opt.get().getFileName().toString()));
// Read metadata from file, and recover memory objects needed for
// re-writing as text.
final Map[] readMds = resultStorage.readMetadata(item)
.map(ctx -> ctx.toDataContext(session))
.map(ctx -> new Map[]{session.transform(ctx.getReferences()), ctx.getUserData()})
.orElse(new Map[]{Collections.EMPTY_MAP, Collections.EMPTY_MAP});
ExportUtils.writeMetadataText(
root.resolve("metadata.txt"),
menuPane.siteProperty().get(),
indic.getProtocol(),
readMds[0],
Collections.singleton(indic),
readMds[1],
Collections.EMPTY_SET
);
}
return true;
});
}
/**
*
* @return A dashboard item reflecting data pointed by currently selected cell. Can be null.
*/
private DashboardResultItem getSelectedItem() {
if (resultTable == null) {
return null;
}
final ObservableList<TablePosition> selectedCells = resultTable.getSelectionModel().getSelectedCells();
if (resultTable.getSelectionModel().getSelectedCells().size() != 1) {
return null;
}
TablePosition pos = selectedCells.get(0);
if (!(pos.getTableColumn() instanceof IndicatorSiteColumn)) {
return null;
}
final Site site = menuPane.siteProperty().get();
final String siteName = site.getName();
final IndicatorSiteColumn col = (IndicatorSiteColumn) pos.getTableColumn();
IndicatorValuesByYearItem tmpItem = resultTable.getItems().get(pos.getRow());
final Indicator indic = col.getIndicator();
final String indicName = indic.getName();
return new DashboardResultItem(
tmpItem.getYear(), siteName, indicName, col.getCellData(tmpItem), tmpItem.isPublished(indicName)
);
}
}
@@ -0,0 +1,234 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.DashboardResultsManager;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.image.ImageView;
import org.geotoolkit.gui.javafx.util.FXTableCell;
import org.geotoolkit.gui.javafx.util.FXTableView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.TableCell;
import fr.cenra.rhomeo.api.ui.DashboardMenuItem;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import java.text.DecimalFormat;
import java.util.Optional;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javax.annotation.PostConstruct;
/**
* Dashboard table presenting results for the selected site.
*
* @author Cédric Briançon (Geomatys)
*/
@Component("fr.cenra.rhomeo.fx.FXDashboardResultsTable")
@Scope("prototype")
public class FXDashboardResultsTable extends FXTableView<IndicatorValuesByYearItem> {
@Autowired
private Session session;
@Autowired
private DashboardResultsManager resultsManager;
@Autowired
private List<DashboardMenuItem> menuItems;
private final ObjectProperty<Site> siteProperty = new SimpleObjectProperty<>();
/**
* Generate a table for the specified site.
*
* @param byCell true if the table has to use the selection by cell mode, false for generic by line selection.
*/
public FXDashboardResultsTable(final boolean byCell) {
setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
if (byCell) {
getSelectionModel().setCellSelectionEnabled(true);
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
siteProperty.addListener(this::siteChanged);
}
public ObjectProperty<Site> siteProperty() {
return siteProperty;
}
/**
* Initializes JavaFX components after spring components are done loading.
*/
@PostConstruct
public void postConstruct() {
final ContextMenu menu = new ContextMenu();
setContextMenu(menu);
for (final DashboardMenuItem menuItem : menuItems) {
final Optional<MenuItem> item = menuItem.createItem(this);
if(item.isPresent()){
menu.getItems().add(item.get());
}
}
}
private void siteChanged(final ObservableValue<? extends Site> obs, final Site oldSite, final Site newSite) {
getColumns().clear();
if (newSite == null) {
setItems(null);
setPlaceholder(new Label("Aucun site séléctionné"));
} else {
try {
setItems(resultsManager.getDashboardResultsByYear(newSite));
} catch (Exception ex) {
throw new RhomeoRuntimeException(ex);
}
if (getItems().isEmpty()) {
setPlaceholder(new Label("Aucun résultat attaché au site"));
} else {
final TableColumn<IndicatorValuesByYearItem, Integer> yearCol = new TableColumn<>("Année");
yearCol.setCellValueFactory(value -> new SimpleIntegerProperty(value.getValue().getYear()).asObject());
getColumns().add(yearCol);
final List<Indicator> indicators = session.getIndicators();
final List<Protocol> protocols = session.getProtocols();
final List<IndicatorSiteColumn> columnsToAdd = new ArrayList<>();
protocols.stream()
.filter(protocol -> protocol.isCompatible(newSite))
.sorted()
.forEach(protocol
-> indicators.stream()
.filter(indicator -> indicator.getProtocol() == protocol)
.sorted()
.forEach(indicator -> {
final IndicatorSiteColumn siteColumn = new IndicatorSiteColumn(indicator);
siteColumn.setCellValueFactory(this::getCellValueFactory);
siteColumn.setCellFactory(this::getCellFactory);
columnsToAdd.add(siteColumn);
})
);
Collections.sort(columnsToAdd);
getColumns().addAll(columnsToAdd);
}
}
}
private TableCell<IndicatorValuesByYearItem, Double> getCellFactory(final TableColumn<IndicatorValuesByYearItem, Double> input) {
// hack #61 display integers for I04/I07
final DecimalFormat format;
if (input instanceof IndicatorSiteColumn &&
(((IndicatorSiteColumn)input).getIndicator().getName().matches("I0(4|7)"))) {
format = Rhomeo.SIMPLE_DECIMAL_FORMAT;
} else {
format = Rhomeo.SECOND_DECIMAL_FORMAT;
}
final FXTableCell cell = new FXTableCell<IndicatorValuesByYearItem, Double>() {
@Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(item, empty);
setContentDisplay(ContentDisplay.RIGHT);
if (empty || item == null || Double.isNaN(item)) {
setText(null);
setGraphic(null);
} else {
final IndicatorValuesByYearItem itemLine = getItems().get(getIndex());
if (itemLine == null) {
setText(null);
setGraphic(null);
} else {
setText(format.format(item));
if (input instanceof IndicatorSiteColumn) {
final Boolean isPublished = itemLine.isPublished(((IndicatorSiteColumn) input).getIndicator().getName());
if (isPublished != null && !isPublished) {
setGraphic(new ImageView(Rhomeo.ICON_NOT_PUBLISHED));
}
}
}
}
}
};
cell.setAlignment(Pos.CENTER_RIGHT);
return cell;
}
private ObservableValue<Double> getCellValueFactory(final TableColumn.CellDataFeatures<IndicatorValuesByYearItem, Double> feature) {
final TableColumn<IndicatorValuesByYearItem, Double> col = feature.getTableColumn();
final String indicName;
if (col instanceof IndicatorSiteColumn) {
indicName = ((IndicatorSiteColumn) col).getIndicator().getName();
} else {
indicName = null;
}
final IndicatorValuesByYearItem.PublishedValue valueForIndicator
= indicName == null ? null : feature.getValue().getValueForIndicator(indicName);
if (valueForIndicator == null || valueForIndicator.getValue() == null) {
return new SimpleObjectProperty<>();
}
return new SimpleObjectProperty<>(valueForIndicator.getValue());
}
}
@@ -0,0 +1,186 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.api.InternationalDescription;
import fr.cenra.rhomeo.api.preferences.PasswordKey;
import fr.cenra.rhomeo.api.preferences.PreferenceGroup;
import fr.cenra.rhomeo.api.ui.PreferencePane;
import java.util.HashMap;
import java.util.Map;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
/**
*
* A default preference pane based on key/value structure.
*
* Each preference is displayed as a row of a {@link GridPane} in which the key
* is displayed as a {@link Label} on the left side, and the value is displayed
* in a {@link TextField} on the right side.
*
* Preferences are provided by a {@link PreferenceGroup} which must be set only
* once.
*
* @author Samuel Andrés (Geomatys)
*
* @param <T> The TYPE of {@link PreferenceGroup} keys.
*/
public class FXDefaultPreferenceGroupPane<T extends InternationalDescription> extends GridPane implements PreferencePane<T> {
private PreferenceGroup<T> preferenceGroup;
private final Map<T, StringProperty> valueFields = new HashMap<>();
/**
* Builds a {@link FXDefaultPreferenceGroupPane} based on key/value structure of
* a {@link PreferenceGroup}.
*
* @param preferenceGroup
*/
public FXDefaultPreferenceGroupPane(final PreferenceGroup<T> preferenceGroup){
this();
setPreferenceGroup(preferenceGroup);
}
/**
* Default constructor used by Spring to produce new prototype.
*
* This constructor does not fill the pane for any {@link PreferenceGroup}.
*
* The method {@link #setPreferenceGroup(PreferenceGroup)} must be used to fill the pane.
*/
public FXDefaultPreferenceGroupPane(){
final String css = FXDefaultPreferenceGroupPane.class.getResource(FXDefaultPreferenceGroupPane.class.getSimpleName()+".css").toExternalForm();
getStylesheets().add(css);
getStyleClass().add("pane");
}
@Override
public PreferenceGroup<T> getPreferenceGroup() {
return preferenceGroup;
}
/**
* Sets the preference group.
*
* Must be called only once.
*
* If the current object has been built with the default constructor (for
* instance by Spring autowiring), it must be called explicitly. On the
* contrary, this method is implicitly called by the constructor using a
* {@link PreferenceGroup} parameter.
*
* For each key of the {@link PreferenceGroup}, this method adds a new row
* to the pane, with two columns. The first one contains a label relative to
* the preference key. The second one contains a text field to edit the
* corresponding preference value.
*
* @param preferenceGroup
*/
public final void setPreferenceGroup(final PreferenceGroup<T> preferenceGroup){
if(this.preferenceGroup!=null) throw new IllegalStateException("The preference group has already been set.");
this.preferenceGroup = preferenceGroup;
int row = 0;
for(final T key : preferenceGroup.getKeys()){
final Label label = new Label(key.getLabel());
final String comment = key.getDescription();
if(comment!=null && !comment.equals("")){
label.setTooltip(new Tooltip(comment));
}
final Node editor = createEditor(key);
addRow(row++, label, editor);
}
}
private Node createEditor(final T key) {
if (key instanceof PasswordKey) {
final Button editBtn = new Button("Modifier");
final HBox container = new HBox(5, editBtn);
editBtn.setOnAction((evt) -> {
final PasswordField field = new PasswordField();
valueFields.put(key, field.textProperty());
final Button cancelBtn = new Button("Annuler");
cancelBtn.setCancelButton(true);
cancelBtn.setOnAction(evt2 -> {
valueFields.remove(key);
container.getChildren().clear();
container.getChildren().add(editBtn);
});
container.getChildren().clear();
container.getChildren().addAll(field, cancelBtn);
});
return container;
} else {
final TextField textField = new TextField(preferenceGroup.getPreference(key));
textField.setPrefWidth(300);
valueFields.put(key, textField.textProperty());
return textField;
}
}
/**
* Save the preferences.
*/
@Override
public void save(){
if(preferenceGroup!=null){
for(final T key : preferenceGroup.getKeys()){
final StringProperty value = valueFields.get(key);
if(value!=null){
preferenceGroup.setPreference(key, value.getValue());
}
}
}
}
@Override
public Node getEditor() {
return this;
}
}
@@ -0,0 +1,674 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.process.Process;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.ui.FilterTable;
import fr.cenra.rhomeo.api.ui.FilterTableSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.util.SimplePredicateChain;
import fr.cenra.rhomeo.fx.edition.FXTrackingPointTable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
*
* Default JavaFX pane to set processing parameters.
*
* For a given {@link Dataset} and a given {@link Protocol}, this pane allows to
* choose the {@link Indicator}(s) and the {@link TrackingPoint}(s) the processing
* must be computed for.
*
* @author Samuel Andrés (Geomatys)
*
* @param <T>
*/
@Component("fr.cenra.rhomeo.fx.FXDefaultProcessingPane")
@Scope("prototype")
public class FXDefaultProcessingPane<T extends Statement> extends VBox implements InternationalResource {
@FXML private GridPane uiIndicatorPane;
private final Collection<IndicatorSelection> indicatorSelections = new ArrayList<>();
@FXML private FlowPane uiYearPane;
private final Collection<YearSelection> yearSelections = new ArrayList<>();
@FXML private BorderPane uiTrackingPointPane;
private FilterTable<? extends Predicate> uiSelectionTable;
private final TableColumn<Predicate, Boolean> uiSelectionColumn;
private final Map<Predicate, BooleanProperty> filterSelection = new HashMap<>();
private final Function<Predicate, BooleanProperty> selectionComputer;
@FXML
private VBox uiProgressBox;
@FXML
private Label uiProgressTitle;
@FXML
private ProgressBar uiProgress;
@FXML
private Label uiProgressMessage;
@FXML
private HBox uiBtnBox;
private Dataset<T> dataset;
private Protocol protocol;
private ProcessContext processContext;
private Site site;
private final ChangeListener<Boolean> selectionListener = this::selectionChanged;
private final SimpleObjectProperty<Task<Set<Index>>> globalTask = new SimpleObjectProperty<>();
private SimplePredicateChain processFilter;
@Autowired
private Session session;
@Autowired
private List<FilterTableSpi> pointTableSpis;
public FXDefaultProcessingPane() {
Rhomeo.loadFXML(this, FXDefaultProcessingPane.class);
uiProgressBox.setVisible(false);
globalTask.addListener((obs, oldTask, newTask) -> {
if (oldTask != null) {
uiProgressBox.visibleProperty().unbind();
uiProgressBox.setVisible(false);
uiProgressTitle.textProperty().unbind();
uiProgressMessage.textProperty().unbind();
uiProgress.progressProperty().unbind();
uiIndicatorPane.disableProperty().unbind();
uiTrackingPointPane.disableProperty().unbind();
uiBtnBox.disableProperty().unbind();
} else {
uiProgressBox.visibleProperty().bind(newTask.runningProperty());
uiProgressTitle.textProperty().bind(newTask.titleProperty());
uiProgressMessage.textProperty().bind(newTask.messageProperty());
uiProgress.progressProperty().bind(newTask.progressProperty());
uiIndicatorPane.disableProperty().bind(newTask.runningProperty());
uiTrackingPointPane.disableProperty().bind(newTask.runningProperty());
uiBtnBox.disableProperty().bind(newTask.runningProperty());
}
});
uiSelectionColumn = new TableColumn(ResourceBundle.getBundle(FXDefaultProcessingPane.class.getCanonicalName()).getString("selectionColumn"));
selectionComputer = input -> {
final BooleanProperty bp = new SimpleBooleanProperty(input, "selection", false);
bp.addListener(selectionListener);
bp.set(true);
return bp;
};
}
@PostConstruct
public void postConstruct() {
dataset = session.getDataset();
ArgumentChecks.ensureNonNull("dataset", dataset);
final DataContext context = session.getDataContext();
protocol = context.getProtocol();
ArgumentChecks.ensureNonNull("protocol", protocol);
site = context.getSite();
ArgumentChecks.ensureNonNull("site", site);
/*
1- Indicator pane.
================*/
final List<Indicator> sortedIndicators = new ArrayList<>(session.getIndicators());
sortedIndicators.removeIf(i -> !protocol.equals(i.getProtocol()));
Collections.sort(sortedIndicators);
initIndicatorPane(sortedIndicators);
/*
2- Year pane.
============*/
final List<Integer> years = new ArrayList<>();
for(final TrackingPoint trackingPoint : dataset.getTrackingPoints()){
final int year = trackingPoint.getYear();
// Do not add the same year twice…
if(!years.contains(year)){
years.add(year);
}
}
// Sort years…
Collections.sort(years);
initYearPane(years);
/*
3- Tracking point pane.
=====================*/
initTrackingPointPane();
/*
4- LISTENERS.
=====================*/
uiSelectionTable.getItems().addListener(this::tableContentChanged);
for (final IndicatorSelection is : indicatorSelections) {
is.selectedProperty.addListener(selectionListener);
}
// FXTrackingPointPane must only display tracking point which year is selected into FXYearPane
yearSelections.forEach(yearSelection ->
yearSelection.selectedProperty().addListener(selectionListener)
);
/*
4- RESTORE PROCESS CONTEXT.
=====================*/
// Start by extracting previous indicators / years / filters
processContext = session.getProcessContext();
final Set<Indicator> previousIndics = new HashSet<>(processContext.getIndicators());
final HashSet<Integer> previousYears = new HashSet<>();
processContext.getTrackingPoints().forEach(tp -> previousYears.add(tp.getYear()));
if (processContext.getFilter() instanceof SimplePredicateChain)
processFilter = (SimplePredicateChain) processContext.getFilter();
else {
processFilter = new SimplePredicateChain();
if (processContext.getFilter() != null)
processFilter.predicates.add(processContext.getFilter());
processContext.setFilter(processFilter);
}
final Set<Predicate> previousFilters = new HashSet(processFilter.predicates);
// Restore selected indicators.
for (final IndicatorSelection is : indicatorSelections)
is.selectedProperty.set(previousIndics.contains(is.indicator));
// Find activated years
for (final YearSelection ys : yearSelections)
ys.selectedProperty.set(previousYears.contains(ys.year));
// Set same filter selection
if (!previousFilters.isEmpty())
for (final Predicate p : uiSelectionTable.getItems())
filterSelection.computeIfAbsent(p, selectionComputer).set(previousFilters.contains(p));
}
/**
*
* @return The selected {@link Indicator}s.
*/
public Collection<Indicator> getIndicators() {
return indicatorSelections.stream()
.filter(is -> is.selectedProperty().get())
.map(IndicatorSelection::getIndicator)
.collect(Collectors.toList());
}
////////////////////////////////////////////////////////////////////////////
// indicator pane // indicator pane // indicator pane // indicator pane
/**
* Init sub-pane for displaying indicators.
*/
private void initIndicatorPane(final List<Indicator> indicators){
for(int i=0; i<indicators.size(); i++){
final Indicator indicator = indicators.get(i);
final IndicatorSelection indicatorSelection = new IndicatorSelection(indicator);
indicatorSelections.add(indicatorSelection);
final CheckBox checkBox = new CheckBox();
checkBox.setAllowIndeterminate(false);
final Label label = new Label();
label.setText(indicator.getRemarks()+" - "+indicator.getProtocol().getRemarks());
indicatorSelection.selectedProperty.bindBidirectional(checkBox.selectedProperty());
final Label labelButton = new Label(indicator.getName());
labelButton.getStyleClass().add(Rhomeo.CSS_INDICATOR_LABEL);
if(!indicator.getZoneTypeCodes().contains(site.getZoneType())){
checkBox.setSelected(false);
checkBox.setDisable(true);
labelButton.setDisable(true);
final Label alert = new Label(null, new ImageView(Rhomeo.ICON_WARNING_ORANGE));
alert.setTooltip(new Tooltip("Cet indicateur n'est pas compatible avec le type de zone humide du site."));
uiIndicatorPane.addRow(i, checkBox, labelButton, label, alert);
}
else{
uiIndicatorPane.addRow(i, checkBox, labelButton, label);
}
}
}
/**
* A simple class associating a selection boolean state to an {@link Indicator}.
*/
private static class IndicatorSelection extends Selection {
private final Indicator indicator;
public IndicatorSelection(final Indicator indicator){
ArgumentChecks.ensureNonNull("indicator", indicator);
this.indicator = indicator;
}
public Indicator getIndicator(){return indicator;}
}
////////////////////////////////////////////////////////////////////////////
// year pane // year pane // year pane // year pane // year pane // year pane
/**
* Init sub-pane for displaying years.
*
* This pane is an immutable FlowPane whose children represent the state
* (selected or not) of years given to the constructor. The state of the
* years is accessible with {@link YearSelection#selectedProperty()}.
*/
private void initYearPane (final List<Integer> years){
for (final Integer year : years) {
final YearSelection yearSelection = new YearSelection(year);
yearSelections.add(yearSelection);
final CheckBox checkBox = new CheckBox();
checkBox.setAllowIndeterminate(false);
final Label label = new Label();
label.setText(year.toString());
yearSelection.selectedProperty.bindBidirectional(checkBox.selectedProperty());
uiYearPane.getChildren().add(new HBox(checkBox, label));
}
}
/**
* A simple class associating a selection boolean state to a year.
*/
private static class YearSelection extends Selection {
private final int year;
public YearSelection(final int year){
this.year = year;
}
public int getYear(){return year;}
}
////////////////////////////////////////////////////////////////////////////
// trackingPoint pane // trackingPoint pane // trackingPoint pane
/**
* Init sub-pane for displaying {@link TrackingPoint}s.
*/
private void initTrackingPointPane(){
for (final FilterTableSpi spi : pointTableSpis) {
if (spi.getProtocol() == session.getDataContext().getProtocol()) {
uiSelectionTable = spi.createEmptyTable();
break;
}
}
if (uiSelectionTable == null) {
uiSelectionTable = new FXTrackingPointTable(session);
}
uiSelectionTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
uiSelectionTable.getColumns().add(0, (TableColumn) uiSelectionColumn);
uiSelectionTable.setPrefSize(USE_COMPUTED_SIZE, USE_COMPUTED_SIZE);
uiSelectionColumn.setCellValueFactory(param -> filterSelection.computeIfAbsent(param.getValue(), selectionComputer));
uiSelectionColumn.setCellFactory(param -> {
final TableCell<Predicate, Boolean> cell = new CheckBoxTableCell<>();
cell.setEditable(true);
return cell;
});
uiSelectionColumn.setEditable(true);
uiSelectionTable.setEditable(true);
uiSelectionTable.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
uiTrackingPointPane.setCenter(uiSelectionTable);
}
/**
* Filter dataset tracking points according to current year selection, to set
* {@link #uiTrackingPointPane} content. If no year is selected, all points
* are activated.
*/
private void resetSelection() {
uiSelectionTable.getSelectionModel().clearSelection();
final Set<Integer> selectedYears = yearSelections.stream()
.filter(ys -> ys.selectedProperty.get())
.map(ys -> ys.year)
.collect(Collectors.toSet());
if (selectedYears.isEmpty()) {
uiSelectionTable.setPreFilter((input) -> false);
} else if (selectedYears.size() == yearSelections.size()) {
uiSelectionTable.setPreFilter(null);
} else {
final Function<TrackingPoint, Boolean> yearChecker = tp -> selectedYears.contains(tp.getYear());
final Predicate<Statement> preFilter = st -> {
return dataset.findPoint(st)
.map(yearChecker)
.orElse(false);
};
uiSelectionTable.setPreFilter(preFilter);
}
}
private void tableContentChanged(final ListChangeListener.Change<? extends Predicate> c) {
while (c.next()) {
if (c.wasRemoved()) {
for (final Predicate p : c.getRemoved()) {
filterSelection.computeIfAbsent(p, selectionComputer).set(false);
}
}
if (c.wasAdded()) {
for (final Predicate p : c.getAddedSubList()) {
filterSelection.computeIfAbsent(p, selectionComputer).set(true);
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// Utilities // Utilities // Utilities // Utilities // Utilities //
/**
* Utility abstract class providing a selection boolean state.
*/
private static abstract class Selection {
protected final BooleanProperty selectedProperty = new SimpleBooleanProperty(this, "selection", false);
public ReadOnlyBooleanProperty selectedProperty(){
return selectedProperty;
}
}
/**
* Update UI/related components when either year, indicator or filter
* selection change.
*
* @param obs Observable selection which change. For this method to actually
* do something, the given observable must be a {@link ReadOnlyProperty}
* with non-null {@link ReadOnlyProperty#getBean() } property.
* @param oldValue
* @param newValue
*/
private void selectionChanged(final ObservableValue<? extends Boolean> obs, Boolean oldValue, Boolean newValue) {
final Object bean;
if (obs instanceof ReadOnlyProperty) {
bean = ((ReadOnlyProperty)obs).getBean();
} else {
bean = null;
}
if (bean instanceof YearSelection) {
// Synchronize tracking points state with corresponding year.
resetSelection();
} else if (bean instanceof IndicatorSelection) {
final Indicator i = ((IndicatorSelection)bean).getIndicator();
if (Boolean.TRUE.equals(newValue)) {
processContext.getIndicators().add(i);
} else {
processContext.getIndicators().remove(i);
}
} else if (bean instanceof Predicate) {
if (Boolean.TRUE.equals(newValue)) {
processFilter.predicates.add((Predicate) bean);
} else {
processFilter.predicates.remove((Predicate) bean);
}
processContext.setFilter(null);
processContext.setFilter(processFilter);
}
}
////////////////////////////////////////////////////////////////////////////
// Process management // Process management // Process management //
@FXML
void nextStep(ActionEvent event) {
final Computer computer = new Computer();
globalTask.set(computer);
computer.stateProperty().addListener(obs -> {
if (computer.isDone()) {
switch (computer.getState()) {
case FAILED:
Rhomeo.showAlert(Alert.AlertType.ERROR, getFormatedResourceString("process-failed"));
break;
case SUCCEEDED:
session.setResults(computer.getValue());
globalTask.set(null);
session.requestWorkflowStep(WorkflowStep.RESULT);
break;
}
}
});
TaskManager.INSTANCE.submit(computer);
}
@FXML
void cancelProcess(ActionEvent event) {
final Task<Set<Index>> task = globalTask.get();
if (task != null)
task.cancel(true);
}
/**
* Wrap processes generated by requested indicators, execute them and wait
* for completion of the entire process set.
*/
private class Computer extends Task<Set<Index>> {
private final List<Process> processes = new ArrayList<>();
@Override
protected Set<Index> call() throws Exception {
updateProgress(-1, -1);
updateTitle(getResourceString("process.prepare"));
for (final Indicator indicator : getIndicators()) {
processes.addAll(indicator.createProcesses(processContext));
}
session.getAdditionalValues().clear();
if (processes.size() < 1) {
throw new IllegalStateException("Nothing to process.");
} else if (processes.size() == 1) {
final Process wrapped = processes.get(0);
Platform.runLater(() -> {
wrapped.titleProperty().addListener((obs) -> updateTitle(wrapped.getTitle()));
wrapped.messageProperty().addListener((obs) -> updateTitle(wrapped.getMessage()));
wrapped.workDoneProperty().addListener((obs) -> updateProgress(wrapped.getWorkDone(), wrapped.getTotalWork()));
});
wrapped.run();
return (Set) wrapped.get();
} else {
/*
* Compute a maximal progress for this process aggregator.
* If any process has no total work defined, the aggregator total is
* the number of processes to execute. Otherwise, its the sum of all
* process to execute.
*
* The same is done for the "work done" property. If the aggregated
* total is undefined, the work done is the number of finished
* processes. Otherwise, it's the sum of work done of each process.
*/
final Observable[] totals = new Observable[processes.size()];
final Observable[] worksDone = new Observable[processes.size()];
for (int i = 0; i < processes.size(); i++) {
totals[i] = processes.get(i).totalWorkProperty();
worksDone[i] = processes.get(i).workDoneProperty();
}
final DoubleBinding totalBinding = Bindings.createDoubleBinding(() -> {
double result = 0;
double processTotal;
for (final Process p : processes) {
processTotal = p.getTotalWork();
if (processTotal < 0) {
return (double) totals.length;
}
result += processTotal;
}
return result;
}, totals);
final DoubleBinding workDoneBinding = Bindings.createDoubleBinding(() -> {
double result = 0;
double processWork;
final boolean doCount = totalBinding.get() < 0;
for (final Process p : processes) {
if (doCount) {
if (p.isDone())
result++;
} else {
processWork = p.getWorkDone();
if (processWork >= 0) {
result += processWork;
}
}
}
return result;
}, worksDone);
Platform.runLater(() -> {
workDoneBinding.addListener((obs) -> updateProgress(workDoneBinding.get(), totalBinding.get()));
});
final Set<Index> indices = new HashSet<>();
updateTitle(getResourceString("process.go"));
for (final Process p : processes) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
TaskManager.INSTANCE.submit(p);
}
for (final Process p : processes) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
indices.addAll(p.get());
}
return indices;
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
for (final Process p : processes) {
p.cancel(true);
}
return super.cancel(mayInterruptIfRunning);
}
}
}
@@ -0,0 +1,520 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalDescription;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.data.Warning;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.AbstractParametrableIndex;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.result.IndexMapper;
import fr.cenra.rhomeo.api.result.IndexSpi;
import fr.cenra.rhomeo.api.ui.AdditionalResultSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.result.ResultWriter;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.FileSystem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
/**
* The result pane controller.
*
* @author Samuel Andrés (Geomatys)
*
*/
@Component("fr.cenra.rhomeo.fx.FXDefaultResultPane")
@Scope("prototype")
public class FXDefaultResultPane extends VBox implements InternationalResource {
/**
* Key (or key-part) that must be used to retrieve specific headers from
* {@link IndexSpi} and or base classes implementing {@link InternationalResource},
* to complementary table headers.
*/
public static final String COMPLEMENTARY_TABLE_HEADER_PROPERTY_KEY = "result-pane.complementary-table";
/**
* Fields taken into account to build columns mapping base classes into
* complementary tables.
*/
private static final List<String> BASE_FIELDS = Arrays.asList("name", "year");
@FXML private Label uiSiteLbl;
@FXML private TableView<IndexMapper<Integer>> uiSitePane;
@FXML private GridPane uiInvalidateGridPane;
@FXML private TableColumn<IndexMapper<Integer>, Integer> uiSiteYearColumn;
@FXML private ToggleButton uiDisplayValuesToggleBtn;
@FXML private ScrollPane uiAdditionalValuesScrollPane;
@FXML private VBox uiAdditionalValuesBox;
@FXML private VBox uiTableBox;
@FXML private Label uiYearsLbl;
@FXML private Label uiIndicatorLbl;
@FXML private Label uiDataStatusLbl;
@FXML private Label uiAdditionalLabel;
@Autowired
private Session session;
@Autowired
private Validator validator;
@Autowired
private ResultWriter resultWriter;
@Autowired
private List<AdditionalResultSpi> additionalSpis;
private final SimpleBooleanProperty isInvalid = new SimpleBooleanProperty(false);
/**
*
*/
public FXDefaultResultPane() {
Rhomeo.loadFXML(this, FXDefaultResultPane.class);
uiSitePane.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
uiSitePane.setFixedCellSize(25);
uiAdditionalLabel.visibleProperty().bind(Bindings.isNotEmpty(uiAdditionalValuesBox.getChildren()));
}
/**
* Initializes JavaFX components after spring components are done loading.
*/
@PostConstruct
public void postConstruct() {
final Set<Index> results = session.getResults();
ArgumentChecks.ensureNonNull("results", results);
/*
1- Schedule.
==========*/
// Determine indicators and schedule results.
final Map<Integer, IndexMapper<Integer>> mainResults = new HashMap<>();
final ArrayList<IndexSpi> mainSpis = new ArrayList<>();
final List<Indicator> indicators = new ArrayList<>();
final Map<Class, Map<Object, IndexMapper>> complementaryResults = new HashMap<>();
final Map<Integer, IndexMapper<Integer>> byYearComplementary = new HashMap<>();
final Map<Class, List<IndexSpi>> indexSpis = new HashMap<>();
for(final Index index : results){
// Seach for index indicators.
final Indicator indicator = index.getSpi().getIndicator();
if(!indicators.contains(indicator)){
indicators.add(indicator);
}
// Detect main indices for each indicator.
if (index.getSpi().isPrimary()) {
final int year = index.getYear();
final IndexMapper<Integer> mapper = mainResults.computeIfAbsent(year, (input) -> new IndexMapper<>(input));
if(!mainSpis.contains(index.getSpi()))
mainSpis.add(index.getSpi());
mapper.setValue(index.getSpi().getName(), index.getValue());
}
// Schedule AbstractParametrableIndexes by base type.
else if(index instanceof AbstractParametrableIndex) {
final AbstractParametrableIndex parametrableIndex = (AbstractParametrableIndex) index;
final Class baseClass = parametrableIndex.getBaseClass();
final Object base = parametrableIndex.getBase();
final IndexSpi indexSpi = parametrableIndex.getSpi();
if(complementaryResults.get(baseClass)==null){
complementaryResults.put(baseClass, new HashMap<>());
}
if(complementaryResults.get(baseClass).get(base)==null){
complementaryResults.get(baseClass).put(base, new IndexMapper<>(base));
}
final IndexMapper indexMapper = complementaryResults.get(baseClass).get(base);
indexMapper.setValue(indexSpi.getName(), parametrableIndex.getValue());
//
if(indexSpis.get(baseClass)==null) indexSpis.put(baseClass, new ArrayList<>());
final List<IndexSpi> spis = indexSpis.get(baseClass);
if(!spis.contains(indexSpi)) spis.add(indexSpi);
} else {
final int year = index.getYear();
final IndexMapper<Integer> mapper = byYearComplementary.computeIfAbsent(year, (input) -> new IndexMapper<>(input));
final List<IndexSpi> spis = indexSpis.computeIfAbsent(Integer.class, i -> new ArrayList<>());
if(!spis.contains(index.getSpi())) spis.add(index.getSpi());
mapper.setValue(index.getSpi().getName(), index.getValue());
}
}
// Sort indicators.
Collections.sort(indicators);
/*
2- Main indicators.
=================*/
// Build header message listing indicators
final StringBuilder sbIndicator = new StringBuilder();
sbIndicator.append("Indicateur(s) : ");
final Protocol currentProtocol = session.getDataContext().getProtocol();
boolean firstIndic = true;
for(final Indicator indicator : indicators){
if (!firstIndic) {
sbIndicator.append(", ");
}
sbIndicator.append(indicator.getName()).append(" - ").append(indicator.getRemarks());
firstIndic = false;
}
sbIndicator.append(" - ").append(currentProtocol.getRemarks());
// Create columns for main indicator pane.
uiSiteYearColumn.setCellValueFactory(value -> value.getValue().keyProperty());
mainSpis.stream().sorted().forEach(spi -> {
// Sometimes the displayed column name seems to be from the indexSpi itself, sometimes from its indicator (i.e. a specific name)
final TableColumn<IndexMapper<Integer>, Number> column = new TableColumn<>(getIndexSpiColumnName(spi));
column.setCellFactory(Rhomeo.ROUNDING_CELL_FACTORY);
column.setCellValueFactory(value -> value.getValue().valueProperty(spi.getName()));
uiSitePane.getColumns().add(column);
});
uiSitePane.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
// Feed main indicator pane.
final ObservableList<IndexMapper<Integer>> items = FXCollections.observableArrayList(mainResults.values());
uiSitePane.setItems(items);
uiSitePane.prefHeightProperty().bind(Bindings.size(items).multiply(uiSitePane.fixedCellSizeProperty()).add(40));
// Summary
uiYearsLbl.setGraphic(new ImageView(Rhomeo.ICON_CIRCLE_GRAY));
final StringBuilder sbYears = new StringBuilder();
sbYears.append("Année(s) : ");
boolean firstYear = true;
final List<Integer> years = new ArrayList<>(mainResults.keySet());
Collections.sort(years);
for (final Integer year : years) {
if (!firstYear) {
sbYears.append(", ");
}
sbYears.append(year);
firstYear = false;
}
uiYearsLbl.setText(sbYears.toString());
uiIndicatorLbl.setGraphic(new ImageView(Rhomeo.ICON_CIRCLE_GRAY));
uiIndicatorLbl.setText(sbIndicator.toString());
isInvalid.addListener((obj) -> {
if (!isInvalid.get()) {
uiDataStatusLbl.setGraphic(new ImageView(Rhomeo.ICON_CHECK_GREEN));
uiDataStatusLbl.setText("Les données sont conformes au protocole : "+ currentProtocol.getRemarks());
} else {
uiDataStatusLbl.setGraphic(new ImageView(Rhomeo.ICON_TIMES_RED));
uiDataStatusLbl.setText("Les données ne sont pas conformes au protocole : "+ currentProtocol.getRemarks());
}
});
isInvalid.set(true);
uiInvalidateGridPane.visibleProperty().bind(isInvalid);
uiInvalidateGridPane.managedProperty().bind(uiInvalidateGridPane.visibleProperty());
uiTableBox.visibleProperty().bind(isInvalid.not().or(uiDisplayValuesToggleBtn.selectedProperty()));
uiTableBox.managedProperty().bindBidirectional(uiSiteLbl.visibleProperty());
// Verify validation.
validate();
/*
3- Other indicators.
==================*/
// A table for complementary results displayed by year.
if (!byYearComplementary.isEmpty()) {
final TableView<IndexMapper<Integer>> table = new TableView<>(FXCollections.observableArrayList(byYearComplementary.values()));
final TableColumn<IndexMapper<Integer>, Integer> yearColumn = new TableColumn("Année");
yearColumn.setCellValueFactory(input -> input.getValue() == null ? null : input.getValue().keyProperty());
table.getColumns().add(yearColumn);
// index value columns
indexSpis.get(Integer.class).stream().sorted().forEach(spi -> {
// Sometimes the displayed column name seems to be from the indexSpi itself, sometimes from its indicator (i.e. a specific name)
final TableColumn<IndexMapper<Integer>, Number> column = new TableColumn<>(getIndexSpiColumnName(spi));
column.setCellFactory(Rhomeo.ROUNDING_CELL_FACTORY);
column.setCellValueFactory(value -> value.getValue().valueProperty(spi.getName()));
table.getColumns().add(column);
});
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
uiAdditionalValuesBox.getChildren().add(table);
}
// Create one TableView by base class.
for(final Class baseClass : complementaryResults.keySet()){
try {
final TableView<IndexMapper> tableView = buildIndexMapperTableView(baseClass, indexSpis.get(baseClass));
final ObservableList<IndexMapper> additionalItems = FXCollections.observableArrayList(complementaryResults.get(baseClass).values());
tableView.setItems(additionalItems);
tableView.setFixedCellSize(25);
tableView.setMinHeight(70);
tableView.setMaxHeight(300);
tableView.prefHeightProperty().bind(Bindings.size(additionalItems).multiply(tableView.fixedCellSizeProperty()).add(40));
uiAdditionalValuesBox.getChildren().add(tableView);
} catch (IntrospectionException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Could not create tableview mapping {0}.", baseClass);
}
}
final Button nextStepButton = new Button(getResourceString("proceed"));
nextStepButton.getStyleClass().add("contrast");
nextStepButton.setOnAction(value -> {
session.requestWorkflowStep(WorkflowStep.FINALIZATION);
});
nextStepButton.visibleProperty().bind(isInvalid.not());
nextStepButton.managedProperty().bind(nextStepButton.visibleProperty());
final HBox bottomHBox = new HBox(nextStepButton);
bottomHBox.setAlignment(Pos.CENTER_RIGHT);
bottomHBox.setFillHeight(true);
bottomHBox.setPrefSize(USE_PREF_SIZE, USE_PREF_SIZE);
getChildren().add(bottomHBox);
for (AdditionalResultSpi spi : additionalSpis) {
spi.getResultNode(session.getAdditionalValues()).ifPresent(node -> uiAdditionalValuesBox.getChildren().add(node));
}
}
/**
* Verify dataset validation.
*/
private void validate() {
final Runnable taskInit = () -> TaskManager.INSTANCE.submit("Validation du lot de données", () -> {
final Set<ConstraintViolation<Dataset>> violations = validator.validate(session.getProcessContext().getData());
final Iterator<ConstraintViolation<Dataset>> it = violations.iterator();
while (it.hasNext()) {
if (!it.next().getConstraintDescriptor().getPayload().contains(Warning.class)) {
return true;
}
}
return false;
}).setOnSucceeded(evt -> Platform.runLater(() -> isInvalid.set((boolean)evt.getSource().getValue())));
if (Platform.isFxApplicationThread())
taskInit.run();
else Platform.runLater(taskInit);
}
@FXML
public void exportData() {
final String initialFileName = session.getDataContext().getSite().getName().concat(".zip");
final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
final File outputFile = Rhomeo.askOutputFilePopup(initialFileName, this, zipFilter);
if (outputFile == null) {
return;
}
TaskManager.INSTANCE.submit("Export de résultats", () -> {
try (final FileSystem fs = RhomeoCore.toZipFileSystem(outputFile.toPath())) {
resultWriter.prepareWriting(fs.getRootDirectories().iterator().next()).writeMetadataText().writeResults().writeAdditionalValues();
}
return true;
});
}
/**
* Builds a complementary table for indexes associated to a given base class (for instance {@link TrackingPoint}).
*
* This method use introspection on the given class, but it only displays information from fields which name
* are specified in {@link FXDefaultResultPane#BASE_FIELDS}.
*
* @param <T>
* @param baseClass
* @return
*
* @throws IntrospectionException
*/
private <T> TableView<IndexMapper<T>> buildIndexMapperTableView(final Class<T> baseClass, final List<IndexSpi> indexSpis) throws IntrospectionException {
final BeanInfo beanInfo = Introspector.getBeanInfo(baseClass);
final TableView<IndexMapper<T>> tableView = new TableView<>();
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
// Index base columns
for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
final String dName = desc.getName();
if(BASE_FIELDS.contains(dName)){
final TableColumn<IndexMapper<T>, ?> col = new TableColumn<>(getBaseColumnName(baseClass, dName));
col.setCellValueFactory(value -> {
try {
final Method reader = desc.getReadMethod();
if(reader!=null && value!=null && value.getValue()!=null && value.getValue() instanceof IndexMapper){
final T target = ((IndexMapper<T>) value.getValue()).getKey();
if(target!=null){
return new SimpleObjectProperty(reader.invoke(target));
}
else return null;
}
else return null;
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException ex) {
RhomeoCore.LOGGER.log(Level.INFO, "Unable to build CellValueFactory for property {0}.", dName);
return null;
}
});
tableView.getColumns().add(col);
}
}
// index value columns
indexSpis.stream().sorted().forEach(spi -> {
// Sometimes the displayed column name seems to be from the indexSpi itself, sometimes from its indicator (i.e. a specific name)
final TableColumn<IndexMapper<T>, Number> column = new TableColumn<>(getIndexSpiColumnName(spi));
column.setCellValueFactory(value -> value.getValue().valueProperty(spi.getName()));
column.setCellFactory(Rhomeo.ROUNDING_CELL_FACTORY);
tableView.getColumns().add(column);
});
return tableView;
}
/**
* Builds a name for the column mapping a given base class field in a complementary table.
*
* @param baseClass
* @param propertyName
* @return
*/
private static String getBaseColumnName(final Class baseClass, final String propertyName){
// If indexSpi implements InternationalResource, try to use specific property
if(InternationalResource.class.isAssignableFrom(baseClass)){
// 1- Try to retrieve some resource specific to a complementary result table.
try{
return InternationalResource.getResourceString(baseClass, propertyName, COMPLEMENTARY_TABLE_HEADER_PROPERTY_KEY);
}
catch(MissingResourceException ex1){
// 2- Try to retrive resource mapping the property name.
try{
return InternationalResource.getResourceString(baseClass, propertyName);
}
catch(MissingResourceException ex2){
RhomeoCore.LOGGER.log(Level.FINEST, "Missing ressource to display column name from {0}", InternationalResource.class.getSimpleName());
}
}
}
// If no better can be done, return the property name.
return propertyName;
}
/**
* Builds a name for the column mapping a given {@link IndexSpi} in a complementary table.
*
* @param indexSpi
* @return
*/
private static String getIndexSpiColumnName(final IndexSpi indexSpi){
// If indexSpi implements InternationalResource, try to use specific property
if(indexSpi instanceof InternationalResource){
try{
return ((InternationalResource) indexSpi).getResourceString(COMPLEMENTARY_TABLE_HEADER_PROPERTY_KEY);
}
catch(MissingResourceException e){
RhomeoCore.LOGGER.log(Level.FINEST, "Missing ressource to display column name from {0}", InternationalResource.class.getSimpleName());
}
}
// If previous try failed and indexSpi implements InternationalDescription, use InternationalDescription label.
if(indexSpi instanceof InternationalDescription) return ((InternationalDescription) indexSpi).getLabel();
// If no other solution is found, return indexSpi name.
return indexSpi.getTitle();
}
}
@@ -0,0 +1,248 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.result.DashboardResultItem;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.DashboardResultsManager;
import fr.cenra.rhomeo.core.result.ResultStorage;
import fr.cenra.rhomeo.core.result.ResultWriter;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
/**
* @author Cédric Briançon (Geomatys)
*/
@Component("fr.cenra.rhomeo.fx.FXFinalizationPane")
@Scope("prototype")
public class FXFinalizationPane extends VBox implements InternationalResource {
@FXML
private Button addToDashboardBtn;
@FXML
private Button publishBtn;
@Autowired
private Session session;
@Autowired
private DashboardResultsManager resultsManager;
@Autowired
private ResultWriter resultWriter;
@Autowired
private ResultStorage resultStorage;
/**
* Stores if the results has been published or not.
*/
private final BooleanProperty publishedProperty = new SimpleBooleanProperty(false);
/**
* Set to true once current results are attached to dashboard.
*/
private final BooleanProperty onDashboardProperty = new SimpleBooleanProperty(false);
public FXFinalizationPane() {
Rhomeo.loadFXML(this, FXFinalizationPane.class);
}
/**
* Get back to the dashboard to start a new entry of results.
*/
@FXML
private void newForm() {
session.requestWorkflowStep(null);
}
@FXML
private void exportResults() {
final String initialFileName = new StringBuilder(session.getDataContext().getSite().getName())
.append('_').append(session.getDataContext().getProtocol().getName()).append(".zip")
.toString();
final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
final File outputFile = Rhomeo.askOutputFilePopup(initialFileName, this, zipFilter);
if (outputFile == null) {
return;
}
TaskManager.INSTANCE.submit("Export de résultats", () -> {
final Path outputPath = outputFile.toPath();
Files.deleteIfExists(outputPath);
try (final FileSystem fs = RhomeoCore.toZipFileSystem(outputPath)) {
resultWriter.prepareWriting(fs.getRootDirectories().iterator().next())
.writeMetadataText()
.writeResults()
.writeAdditionalValues();
}
return true;
});
}
/**
* Add current results to the dashboard for the current site / protocol.
*/
@FXML
private void addToDashboard() {
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION,
getFormatedResourceString("alert-add-to-dashboard"),
ButtonType.YES, ButtonType.NO);
alert.setResizable(true);
final ButtonType res = alert.showAndWait().get();
if (res == ButtonType.YES) {
final TaskManager.MockTask task = new TaskManager.MockTask("Sauvegarde des résultats", () -> {
resultStorage.store();
resultsManager.addDashboardResultsItems(getResults());
return true;
});
addToDashboardBtn.disableProperty().bind(task.runningProperty());
task.setOnSucceeded(evt -> Platform.runLater(() -> {
addToDashboardBtn.disableProperty().unbind();
addToDashboardBtn.setDisable(true);
onDashboardProperty.set(true);
}));
TaskManager.INSTANCE.submit(task);
}
}
/**
* Get current results.
*
* @return
*/
private List<DashboardResultItem> getResults() {
final List<DashboardResultItem> items = new ArrayList<>();
final Set<Index> results = session.getResults();
if (results != null && !results.isEmpty()) {
final DataContext context = session.getDataContext();
results.forEach(result -> {
if (result.getSpi().getName().equals(result.getSpi().getIndicator().getDefaultIndex())) {
// Do not keep tracking point index, just results for a year.
items.add(new DashboardResultItem(result.getYear(), context.getSite().getName(),
result.getSpi().getIndicator().getName(), result.getValue() == null ? null : result.getValue().doubleValue(),
publishedProperty.get()));
}
});
}
return items;
}
@FXML
private void publishResults(final ActionEvent evt) {
final List<DashboardResultItem> results = getResults();
final Task publish = publishSiteResults();
publishBtn.disableProperty().bind(publish.runningProperty());
publish.setOnSucceeded(event -> Platform.runLater(() -> {
publishedProperty.set(true);
publishBtn.disableProperty().unbind();
publishBtn.setDisable(true);
if (onDashboardProperty.get()) {
TaskManager.INSTANCE.submit("Mise à jour des résultats locaux", () -> {
for (final DashboardResultItem item : results)
item.setPublished(true);
resultsManager.updateResults(results);
return true;
});
}
}));
publish.setOnFailed(event -> Platform.runLater(() -> Rhomeo.showAlert(Alert.AlertType.ERROR, "La publication des résultats a échouée !")));
TaskManager.INSTANCE.submit(publish);
}
/**
* Publish site results on the FTP.
* Structure on the FTP is:
* <pre>
* results
* |- county code
* |--|- SHA1 geometry site
* |--|--|- protocol
* |--|--|--|- UUID
* |--|--|--|--|- site.geojson
* |--|--|--|--|- metadata.json
* |--|--|--|--|- result.csv
* ...
* </pre>
*
* @return Publication task
*/
private Task publishSiteResults() {
return new TaskManager.MockTask<>("Publication de résultats", () -> {
resultsManager.publishResults();
return true;
});
}
}
@@ -0,0 +1,266 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.DashboardResultItem;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import fr.cenra.rhomeo.core.CSVDecoder;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.result.ResultStorage;
import fr.cenra.rhomeo.core.result.SimpleLocationIndex;
import fr.cenra.rhomeo.core.result.SimpleYearIndex;
import java.beans.IntrospectionException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* UI to display detailed information about all computed information for a given
* year and {@link Indicator}. This includes the value of the main indicator
* {@link Index} and the potential ones.
*
* @author Samuel Andrés (Geomatys) <samuel.andres at geomatys.com>
*/
@Component("fr.cenra.rhomeo.fx.FXFullSpecificResultPane")
@Lazy
@Scope("prototype")
public class FXFullSpecificResultPane extends VBox {
private final IndicatorValuesByYearItem selectedItem;
private final Indicator indicator;
private final String siteName;
@Autowired
private ResultStorage resultStorage;
public FXFullSpecificResultPane(final IndicatorValuesByYearItem selectedItem, final Indicator indicator, final String siteName) throws IOException, IntrospectionException, InstantiationException, IllegalAccessException {
this.selectedItem = selectedItem;
this.indicator = indicator;
this.siteName = siteName;
setPadding(new Insets(10.));
setSpacing(10.);
setPrefSize(USE_COMPUTED_SIZE, USE_COMPUTED_SIZE);
}
@PostConstruct
private void postConstruct() throws IOException, IntrospectionException, InstantiationException, IllegalAccessException {
final String indicatorName = indicator.getName();
final IndicatorValuesByYearItem.PublishedValue pub = selectedItem.getValueForIndicator(indicatorName);
if (pub != null) {
final DashboardResultItem dItem = new DashboardResultItem(selectedItem.getYear(), siteName, indicatorName, pub.getValue(), pub.isPublished());
final ArrayList<? extends SimpleYearIndex> result = new ArrayList<>();
final Optional<Path> mainResults = resultStorage.getMainResults(dItem);
if (mainResults.isPresent()) {
result.addAll((List) readResult(mainResults.get()));
}
final Optional<Path> otherResults = resultStorage.getAdditionalResults(dItem);
if (otherResults.isPresent()) {
result.addAll((List) readResult(otherResults.get()));
}
buildTables(result);
}
}
/**
* Create the table view containing the result value list mapping their class.
*
* @param result
* @throws IntrospectionException
*/
private void buildTables(List<? extends SimpleYearIndex> result) throws IntrospectionException {
final List<SimpleLocationIndex> locValues = new ArrayList<>();
final Iterator<? extends SimpleYearIndex> it = result.iterator();
SimpleYearIndex current;
while (it.hasNext()) {
current = it.next();
if (current instanceof SimpleLocationIndex) {
locValues.add((SimpleLocationIndex) current);
it.remove();
}
}
if (!result.isEmpty()) {
final TableView<SimpleYearIndex> table = buildSimpleTableView(FXCollections.observableList((List)result));
getChildren().add(table);
}
if (!locValues.isEmpty()) {
final TableView<SimpleLocationIndex> table = buildLocationTableView(FXCollections.observableList(locValues));
getChildren().add(table);
}
}
/**
* Builds a {@link TableView} for tuples indexed by year.
*
* @param tuples
* @return
* @throws IntrospectionException
* @see SimpleYearIndex
*/
private TableView<SimpleYearIndex> buildSimpleTableView(ObservableList<SimpleYearIndex> values) throws IntrospectionException {
final TableView<SimpleYearIndex> table = new TableView<>();
addCommonColumns(table);
table.setItems(values);
initResizePolicy(table);
return table;
}
/**
* Configure input table resize strategy, by setting preferred height
* property to follow table item number.
*
* @param table The modified table.
*/
private void initResizePolicy(final TableView table) {
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.setMinHeight(50);
table.setFixedCellSize(30);
table.prefHeightProperty().bind(Bindings.createDoubleBinding(() -> table.getFixedCellSize() * table.getItems().size() + 30, table.getItems(), table.fixedCellSizeProperty()));
}
private void addCommonColumns(final TableView<? extends SimpleYearIndex> table) {
final TableColumn<SimpleYearIndex, String> titleColumn = new TableColumn<>("Nom");
titleColumn.setEditable(false);
titleColumn.setCellValueFactory(FXFullSpecificResultPane::getTitle);
final TableColumn<SimpleYearIndex, Number> valueColumn = new TableColumn<>("Valeur");
valueColumn.setEditable(false);
valueColumn.setCellFactory(RhomeoCore.ROUNDING_CELL_FACTORY);
valueColumn.setCellValueFactory(FXFullSpecificResultPane::getValue);
table.getColumns().add((TableColumn)titleColumn);
table.getColumns().add((TableColumn)valueColumn);
}
/**
* Builds a {@link TableView} for tuples indexed by year and location.
*
* @param tuples
* @return
* @throws IntrospectionException
* @see SimpleLocationIndex
*/
private TableView<SimpleLocationIndex> buildLocationTableView(ObservableList<SimpleLocationIndex> values) throws IntrospectionException {
final TableView<SimpleLocationIndex> table = new TableView<>();
addCommonColumns(table);
final TableColumn<SimpleLocationIndex, String> locColumn = new TableColumn<>();
if (indicator.getProtocol().getName().equalsIgnoreCase("P04"))
locColumn.setText("Habitat");
else
locColumn.setText("Point de suivi");
locColumn.setCellValueFactory(FXFullSpecificResultPane::getLocation);
table.getColumns().add(1, locColumn);
table.setItems(values);
initResizePolicy(table);
return table;
}
/**
* Get the results stored in the file which path is given as a parameter.
*
* The results are retrieved as a list of either instances of {@link SimpleLocationIndex}
* or instances of {@link SimpleYearIndex}. The method returns an entry which
* the value it the list of results, and the key is the instances' class.
*
* @param resultsPath
* @return
* @throws IOException
* @throws IntrospectionException
* @throws InstantiationException
* @throws IllegalAccessException
*/
private static List<? extends SimpleYearIndex> readResult(final Path resultsPath) throws IOException, IntrospectionException, InstantiationException, IllegalAccessException {
try {
return new CSVDecoder(resultsPath, SimpleLocationIndex.class).decode();
} catch (Exception ex) {
RhomeoCore.LOGGER.info("Impossible de lire en tant que résultat localisé. Essai de lecture en résultat non localisé.");
return new CSVDecoder(resultsPath, SimpleYearIndex.class).decode();
}
}
private static ObservableValue<String> getTitle(final TableColumn.CellDataFeatures<SimpleYearIndex, String> cdf) {
final SimpleYearIndex index = cdf.getValue();
if (index == null)
return null;
return new SimpleStringProperty(index.getName());
}
private static ObservableValue<Number> getValue(final TableColumn.CellDataFeatures<SimpleYearIndex, Number> cdf) {
final SimpleYearIndex index = cdf.getValue();
if (index == null)
return null;
return new SimpleObjectProperty<>(index.getValue());
}
private static ObservableValue<String> getLocation(final TableColumn.CellDataFeatures<SimpleLocationIndex, String> cdf) {
final SimpleLocationIndex index = cdf.getValue();
if (index == null)
return null;
return new SimpleStringProperty(index.getLocation());
}
}
@@ -0,0 +1,127 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.geotoolkit.gui.javafx.util.TaskManager;
/**
* Panel for the initial dialog when opening the application.
*
* @author Cédric Briançon (Geomatys)
*/
public class FXInitialDialogPane extends BorderPane {
public static final String DISPLAY_RULE_KEY = "display_disclaimer_on_startup";
@FXML
private Label uiVersion;
@FXML
private ImageView uiWarning;
@FXML
private CheckBox displayOnStartup;
@FXML
private Hyperlink uiEMail;
@FXML
private Hyperlink uiHref;
@FXML
private Button uiClose;
private final Preferences displayPref;
public FXInitialDialogPane() {
Rhomeo.loadFXML(this, FXInitialDialogPane.class);
displayPref = Preferences.userNodeForPackage(FXInitialDialogPane.class);
displayOnStartup.setSelected(!displayPref.getBoolean(DISPLAY_RULE_KEY, true));
displayOnStartup.selectedProperty().addListener((obs, oldValue, newValue) -> {
displayPref.putBoolean(DISPLAY_RULE_KEY, !newValue);
});
uiWarning.setImage(Rhomeo.ICON_WARNING_ORANGE);
uiEMail.setOnAction(evt -> Rhomeo.mail(uiEMail.getText()));
uiHref.setOnAction(evt -> {
try {
Rhomeo.browse(new URL(uiHref.getText()));
} catch (final MalformedURLException e) {
// mimic task failure
TaskManager.INSTANCE.submit("Ouverture d'un lien", () -> {
if (e != null)
throw e;
else
return false;
});
}
});
final String version = Rhomeo.getVersion();
if (version == null || version.isEmpty())
uiVersion.setText("Version de développement");
else
uiVersion.setText("Version ".concat(version));
Platform.runLater(() -> uiClose.requestFocus());
}
/**
* Close the dialog box when clicking on the accept button.
*/
@FXML
private void accept() {
final Stage stage = (Stage) this.getScene().getWindow();
stage.close();
}
}
@@ -0,0 +1,158 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.core.Session;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.MenuButton;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.geotoolkit.gui.javafx.util.ProgressMonitor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import org.geotoolkit.gui.javafx.util.TaskManager;
/**
* Main application panel.
*
* @author Cédric Briançon (Geomatys)
*/
@Component
public class FXMainPane extends BorderPane implements InternationalResource {
@FXML
private MenuButton uiMenuButton;
@Autowired
private FXRhomeoWizard wizard;
@Autowired
private Session session;
public FXMainPane() {
Rhomeo.loadFXML(this, FXMainPane.class);
initProgressMonitor();
uiMenuButton.setGraphic(new ImageView(Rhomeo.ICON_BARS_WHITE));
uiMenuButton.setFocusTraversable(false);
}
/**
* Initializes JavaFX components after spring components are done loading.
*/
@PostConstruct
private void postConstruct() {
setCenter(wizard);
}
/**
* Inserts the {@link ProgressMonitor} before the main menu button.
* HACK : Add an item in progress monitor list, allowing to clear list of
* tasks in error.
*/
private void initProgressMonitor() {
final ProgressMonitor progressMonitor = new ProgressMonitor(TaskManager.INSTANCE);
progressMonitor.getStylesheets().add(FXDashboardPane.class.getResource("/fr/cenra/rhomeo/ProgressMonitor.css").toString());
final HBox parent = ((HBox) uiMenuButton.getParent());
parent.getChildren().add(parent.getChildren().size()-1, progressMonitor);
}
/**
* Open preferences window.
*/
@FXML
private void showPreferences() {
final Stage prefStage = Rhomeo.newStage();
prefStage.setTitle(getResourceString("preferences"));
prefStage.initModality(Modality.APPLICATION_MODAL);
final Scene scene = new Scene(session.createPrototype(FXRhomeoPreferencePane.class));
prefStage.setScene(scene);
prefStage.setMinWidth(530);
prefStage.setMinHeight(230);
prefStage.show();
prefStage.requestFocus();
}
/**
* Open references documents page.
*/
@FXML
private void showRefDocuments() {
final Stage prefStage = Rhomeo.newStage();
prefStage.setTitle(getResourceString("ref-docs"));
prefStage.initModality(Modality.APPLICATION_MODAL);
final Scene scene = new Scene(session.createPrototype(FXRhomeoReferencePane.class));
scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
prefStage.setScene(scene);
prefStage.setMinWidth(800);
prefStage.setMinHeight(600);
prefStage.show();
prefStage.requestFocus();
}
/**
* Open the about dialog.
*/
@FXML
private void showAboutDialog() {
final Stage popup = Rhomeo.createWelcomePopup();
popup.initOwner(this.getScene().getWindow());
popup.show();
popup.requestFocus();
}
/**
* Quit the application.
*/
@FXML
private void quit() {
session.close();
System.exit(0);
}
}
@@ -0,0 +1,273 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Reference;
import fr.cenra.rhomeo.api.data.ReferenceDescription;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.ReferenceManager;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import org.geotoolkit.gui.javafx.util.FXTableView;
import org.geotoolkit.gui.javafx.util.TaskManager;
import java.beans.IntrospectionException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* Handle available versions for a reference.
*
* @param <R> the reference to display available versions.
* @author Cédric Briançon (Geomatys)
*/
public class FXProtocolReferencesPane<R extends Reference> extends BorderPane {
@FXML
private Label uiRefNameLbl;
private final FXTableView<ReferenceLineItem> referencesTable;
private final ReferenceDescription<R> description;
private static final String CSS_BUTTON_CLASS = "install-button";
public FXProtocolReferencesPane(final ReferenceDescription<R> description) throws IntrospectionException {
this.description = description;
Rhomeo.loadFXML(this);
uiRefNameLbl.setText("Référentiel "+ description.getTitle());
referencesTable = new FXTableView<>();
referencesTable.setEditable(true);
referencesTable.setMinHeight(90);
referencesTable.setPrefHeight(getMinHeight());
referencesTable.setMaxHeight(280);
referencesTable.setFixedCellSize(25);
final TableColumn<ReferenceLineItem,String> availableCol = new TableColumn<>("Versions disponibles");
availableCol.setCellValueFactory(value -> new SimpleStringProperty(String.valueOf(value.getValue().getVersion())));
referencesTable.getColumns().add(availableCol);
referencesTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
final TableColumn<ReferenceLineItem,Boolean> installedCol = new TableColumn<>("Installées");
installedCol.setCellValueFactory(value -> value.getValue().installedProperty);
installedCol.setCellFactory(param ->
new TableCell<ReferenceLineItem, Boolean>() {
@Override
protected void updateItem(Boolean item, boolean empty) {
setText(null);
setGraphic(null);
setAlignment(Pos.CENTER);
if (item != null && !item) {
final Button installBtn = new Button("Installer");
installBtn.getStyleClass().add(CSS_BUTTON_CLASS);
installBtn.setOnAction(event -> {
final ReferenceManager<R> referenceManager;
try {
referenceManager = ReferenceManager.getOrCreate(description);
} catch (IntrospectionException ex) {
throw new RhomeoRuntimeException(ex);
}
final Task installTask = referenceManager.install(((ReferenceLineItem)getTableRow().getItem()).getVersion());
installTask.setOnSucceeded(success -> {
((ReferenceLineItem)getTableRow().getItem()).setInstalled(true);
});
installBtn.disableProperty().bind(installTask.runningProperty());
TaskManager.INSTANCE.submit(installTask);
});
setGraphic(installBtn);
} else if (!empty && item != null){
setGraphic(new ImageView(Rhomeo.ICON_CHECK_GREEN));
}
}
}
);
referencesTable.getColumns().add(installedCol);
final TableColumn<ReferenceLineItem,Boolean> selectedCol = new TableColumn<>("Sélectionnées");
selectedCol.setCellValueFactory(value -> value.getValue().selectedProperty);
final ToggleGroup group = new ToggleGroup();
selectedCol.setCellFactory(param -> new RadioButtonTableCell(group));
selectedCol.setEditable(true);
referencesTable.getColumns().add(selectedCol);
final DataContext dc = Session.getInstance().getDataContext();
final ReferenceManager<R> referenceManager = ReferenceManager.getOrCreate(description);
final Set<Version> installedVersions = referenceManager.getInstalledVersions();
final Set<Version> distantVersions = referenceManager.getDistantVersions();
final HashSet<Version> allVersions = new HashSet<>(installedVersions);
allVersions.addAll(distantVersions);
final Version selectedVersion = dc.getReferences().get(description.getReferenceType());
final ObservableList<ReferenceLineItem> items = FXCollections.observableArrayList();
for (final Version ver : allVersions) {
items.add(new ReferenceLineItem(ver, installedVersions.contains(ver), ver.equals(selectedVersion)));
}
Collections.sort(items);
referencesTable.setItems(FXCollections.observableList(items));
referencesTable.setPrefHeight((items.size() + 1) * referencesTable.getFixedCellSize());
setCenter(referencesTable);
}
public ObservableList<ReferenceLineItem> getItems() {
return referencesTable.itemsProperty().get();
}
public Class<R> getReferenceClass() {
return description.getReferenceType();
}
private class RadioButtonTableCell extends TableCell<ReferenceLineItem,Boolean> {
private RadioButton radioButton;
private BooleanProperty selProperty;
/**
* Cell containing a radio button in the given group.
*
* @param group
*/
public RadioButtonTableCell(final ToggleGroup group) {
radioButton = new RadioButton();
if (group != null) {
radioButton.setToggleGroup(group);
}
}
@Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
// First remove potential binding
if (selProperty != null) {
radioButton.selectedProperty().unbindBidirectional(selProperty);
}
setGraphic(radioButton);
setAlignment(Pos.CENTER);
radioButton.visibleProperty().bind(((ReferenceLineItem)getTableRow().getItem()).installedProperty);
final ObservableValue<?> obsValue = getTableColumn().getCellObservableValue(getIndex());
if (obsValue instanceof BooleanProperty) {
selProperty = (BooleanProperty)obsValue;
// ensures the radio button selected value will follow the item value
radioButton.selectedProperty().bindBidirectional(selProperty);
}
}
}
}
class ReferenceLineItem implements Comparable<ReferenceLineItem> {
private Version version;
private BooleanProperty installedProperty;
private BooleanProperty selectedProperty;
public ReferenceLineItem() {
}
public ReferenceLineItem(Version version, boolean installed, boolean selected) {
this.version = version;
this.installedProperty = new SimpleBooleanProperty(installed);
this.selectedProperty = new SimpleBooleanProperty(selected);
}
public Version getVersion() {
return version;
}
public void setVersion(Version version) {
this.version = version;
}
public boolean isInstalled() {
return installedProperty.get();
}
public void setInstalled(boolean installed) {
this.installedProperty.set(installed);
}
public boolean isSelected() {
return selectedProperty.get();
}
public void setSelected(boolean selected) {
this.selectedProperty.set(selected);
}
@Override
public int compareTo(ReferenceLineItem o) {
if (version == null) {
return 1;
}
if (o == null) {
return -1;
}
return version.compareTo(o.getVersion());
}
}
}
@@ -0,0 +1,144 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.preferences.PreferenceGroup;
import fr.cenra.rhomeo.api.ui.PreferencePane;
import fr.cenra.rhomeo.core.Session;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javafx.scene.control.Button;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* Preference editor. This panel aggregate editors for all available
* {@link PreferenceGroup}. A discovery system allows to find the most
* appropriate editor for a given group. If we cannot find any, we use a default
* editor.
*
* @author Samuel Andrés (Geomatys)
*/
@Component("fr.cenra.rhomeo.fx.FXRhomeoPreferencePane")
@Scope("prototype")
public class FXRhomeoPreferencePane extends BorderPane implements InternationalResource {
@Autowired
private Map<String, PreferenceGroup> preferenceGroups;
@Autowired
private List<PreferencePane> availableEditors;
@Autowired
private Session session;
private final String css;
private final List<PreferencePane> preferencePanes = new ArrayList<>();
public FXRhomeoPreferencePane() {
css = FXDefaultPreferenceGroupPane.class.getResource(FXRhomeoPreferencePane.class.getSimpleName() + ".css").toExternalForm();
getStylesheets().add(css);
}
@PostConstruct
private void postConstruct() {
// Creating one tab for each preference group.
final TabPane tabPane = new TabPane();
final List<PreferenceGroup> prefGroups = new ArrayList<>(preferenceGroups.values());
Collections.sort(prefGroups);
PreferencePane prefEditor;
for (final PreferenceGroup preferenceGroup : prefGroups) {
prefEditor = null;
for (final PreferencePane editor : availableEditors) {
if (editor.getPreferenceGroup().equals(preferenceGroup)) {
prefEditor = editor;
break;
}
}
// If no specific editor has been found, a default one is used.
if (prefEditor == null) {
if (preferenceGroup.getKeys().isEmpty()) {
// A group without keys does not need to be displayed
continue;
}
final FXDefaultPreferenceGroupPane preferencePane = new FXDefaultPreferenceGroupPane();
preferencePane.setPreferenceGroup(preferenceGroup);
prefEditor = preferencePane;
}
preferencePanes.add(prefEditor);
final Tab tab = new Tab(preferenceGroup.getLabel(), prefEditor.getEditor());
tab.setTooltip(new Tooltip(preferenceGroup.getDescription()));
tabPane.getTabs().add(tab);
}
setCenter(tabPane);
// Creating preference controls
final Button ok = new Button(getResourceString("validate"));
ok.setOnAction(e -> {
for (final PreferencePane preferencePane : preferencePanes) {
preferencePane.save();
}
getScene().getWindow().hide();
});
final Button cancel = new Button(getResourceString("cancel"));
cancel.setOnAction(e -> getScene().getWindow().hide());
final HBox hBox = new HBox(cancel, ok);
hBox.getStylesheets().add(css);
hBox.getStyleClass().add("hBox");
setBottom(hBox);
}
}
@@ -0,0 +1,96 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Protocol;
import java.util.Collections;
import java.util.List;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @author Cédric Briançon (Geomatys)
*/
@Component("fr.cenra.rhomeo.fx.FXRhomeoReferencePane")
@Scope("prototype")
public class FXRhomeoReferencePane extends BorderPane {
@FXML
private GridPane uiProtocolsBtnGrid;
@FXML
private GridPane uiModelsBtnGrid;
@Autowired
private List<Protocol> protocols;
public FXRhomeoReferencePane() {
Rhomeo.loadFXML(this, FXRhomeoReferencePane.class);
}
/**
* Initializes JavaFX components after spring components are done loading.
*/
@PostConstruct
private void postConstruct() {
Collections.sort(protocols);
int modelIndex = 0;
for(int i=0; i<protocols.size(); i++){
final Protocol p = protocols.get(i);
uiProtocolsBtnGrid.add(Rhomeo.createProtocolDocumentButton(p), i%2, i/2);
if (!(p.getName().equals("P04") || p.getName().equals("P08") || p.getName().equals("P09")))
uiModelsBtnGrid.add(Rhomeo.createModelButton(p), modelIndex%2, modelIndex++/2);
}
}
@FXML
public void close() {
final Stage stage = (Stage) this.getScene().getWindow();
stage.close();
}
}
@@ -0,0 +1,526 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.ReferenceDescription;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.ui.ProtocolEditorSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.data.DataContextManager;
import fr.cenra.rhomeo.core.data.ReferenceManager;
import fr.cenra.rhomeo.fx.edition.FXDatasetPane;
import java.beans.IntrospectionException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.scene.layout.BackgroundPosition;
import javafx.scene.layout.BackgroundRepeat;
import javafx.scene.layout.BackgroundSize;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.StageStyle;
import javax.annotation.PostConstruct;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Menu banner for selected protocol page.
*
* @author Cédric Briançon (Geomatys)
*/
@Component
public class FXRhomeoWizard extends VBox {
@FXML private Label uiSelectedSiteLbl;
@FXML private Label uiSelectedProtocolLbl;
@FXML private VBox uiHeader;
@FXML private BorderPane uiBody;
@FXML private HBox uiProtocolChosenBox;
@FXML private GridPane uiProtocolStepsGrid;
@FXML private Button uiRefUsedBtn;
@FXML private Button uiStopProcessBtn;
@FXML private Button uiStep1Btn;
@FXML private Button uiStep2Btn;
@FXML private Button uiStep3Btn;
@FXML private Button uiStep4Btn;
@Autowired
private DataContextManager contextManager;
@Autowired
private Session session;
@Autowired
List<ProtocolEditorSpi> editorSpis;
private DataContext context;
private Site site;
private Protocol protocol;
private ProtocolEditorSpi editorSpi;
private final Map<WorkflowStep, Button> stepBtns;
private final String stopBtnOriginalText;
public FXRhomeoWizard() {
Rhomeo.loadFXML(this, FXRhomeoWizard.class);
// Index buttons by step
final Map<WorkflowStep, Button> tmpMap = new HashMap<>();
tmpMap.put(WorkflowStep.DATASET, uiStep1Btn);
tmpMap.put(WorkflowStep.PROCESS, uiStep2Btn);
tmpMap.put(WorkflowStep.RESULT, uiStep3Btn);
tmpMap.put(WorkflowStep.FINALIZATION, uiStep4Btn);
stepBtns = Collections.unmodifiableMap(tmpMap);
for (final Map.Entry<WorkflowStep, Button> entry : stepBtns.entrySet()) {
entry.getValue().setOnAction(evt -> session.requestWorkflowStep(entry.getKey()));
}
uiHeader.managedProperty().bind(uiHeader.visibleProperty());
final ImageView im = new ImageView(Rhomeo.ICON_EXCHANGE_GRAY);
im.setRotate(90);
uiRefUsedBtn.setGraphic(im);
uiStopProcessBtn.setGraphic(new ImageView(Rhomeo.ICON_STOP_RED));
// HACK #77 : User want a change on the button which stops process on a
// specific step, so we keep reference to its original text now.
stopBtnOriginalText = uiStopProcessBtn.getText();
// Prepare display of arrows linking step buttons. Each arrow state is bound
// to the step button following it.
final ImageView imgArrow1 = new ImageView(Rhomeo.ICON_LONG_ARROW_RIGHT);
imgArrow1.setDisable(true);
imgArrow1.visibleProperty().bind(uiStep2Btn.visibleProperty());
imgArrow1.managedProperty().bind(imgArrow1.visibleProperty());
final ImageView imgArrow2 = new ImageView(Rhomeo.ICON_LONG_ARROW_RIGHT);
imgArrow2.setDisable(true);
imgArrow2.visibleProperty().bind(uiStep3Btn.visibleProperty());
imgArrow2.managedProperty().bind(imgArrow2.visibleProperty());
final ImageView imgArrow3 = new ImageView(Rhomeo.ICON_LONG_ARROW_RIGHT);
imgArrow3.setDisable(true);
imgArrow3.visibleProperty().bind(uiStep4Btn.visibleProperty());
imgArrow3.managedProperty().bind(imgArrow3.visibleProperty());
// Limit height of arrow image, which is really too big (lot of empty space).
final Rectangle2D arrowDisplay = new Rectangle2D(
0, Rhomeo.ICON_LONG_ARROW_RIGHT.getHeight() / 4,
Rhomeo.ICON_LONG_ARROW_RIGHT.getWidth(), Rhomeo.ICON_LONG_ARROW_RIGHT.getHeight() / 2);
imgArrow1.setViewport(arrowDisplay);
imgArrow2.setViewport(arrowDisplay);
imgArrow3.setViewport(arrowDisplay);
uiProtocolStepsGrid.add(imgArrow1, 2, 0);
uiProtocolStepsGrid.add(imgArrow2, 4, 0);
uiProtocolStepsGrid.add(imgArrow3, 6, 0);
}
@PostConstruct
private void init() {
session.workflowStepProperty().addListener(this::stepChanged);
unloadDataContext();
}
private void loadDataContext() {
this.context = session.getDataContext();
this.site = context.getSite();
this.protocol = context.getProtocol();
initializeReferences();
for (final ProtocolEditorSpi spi : editorSpis) {
if (spi.getProtocol() == protocol) {
editorSpi =spi;
break;
}
}
// Deactivate step buttons for workflow parts marked as non available
// by the input SPI.
if (editorSpi != null && !editorSpi.getStepsToIgnore().isEmpty()) {
for (final WorkflowStep step : editorSpi.getStepsToIgnore()) {
final Button btn = stepBtns.get(step);
if (btn != null) {
btn.setVisible(false);
btn.setManaged(false);
}
}
}
uiSelectedSiteLbl.setText(site.getName());
uiSelectedProtocolLbl.setText(protocol.getName());
uiProtocolChosenBox.getChildren().add(uiSelectedProtocolLbl);
uiRefUsedBtn.setDisable(protocol.getReferenceTypes().isEmpty());
final List<Indicator> indicators = session.getIndicators();
indicators.stream()
.filter(indicator -> indicator.getProtocol() == protocol)
.sorted()
.forEach(indicator -> {
final Label lbl = new Label(indicator.getName());
lbl.getStyleClass().add(Rhomeo.CSS_INDICATOR_LABEL);
uiProtocolChosenBox.getChildren().add(lbl);
});
final Button pdfBtn = Rhomeo.createProtocolDocumentButton(protocol);
pdfBtn.setText(null);
pdfBtn.setPrefHeight(28);
pdfBtn.setMaxHeight(USE_PREF_SIZE);
pdfBtn.setMinHeight(USE_PREF_SIZE);
uiProtocolChosenBox.getChildren().add(pdfBtn);
uiHeader.setVisible(true);
// Generate protocol background
final String bgPath = "/fr/cenra/rhomeo/images/" + protocol.getName().toLowerCase() + ".png";
if (FXRhomeoWizard.class.getResource(bgPath) != null) {
try (final InputStream stream = FXRhomeoWizard.class.getResourceAsStream(bgPath)) {
final Background backgroundProtocol = new Background(new BackgroundImage(
new Image(stream),
BackgroundRepeat.NO_REPEAT, BackgroundRepeat.NO_REPEAT,
new BackgroundPosition(Side.LEFT, 0, true, Side.BOTTOM, 0, true), BackgroundSize.DEFAULT));
setBackground(backgroundProtocol);
} catch (IOException e) {
Rhomeo.LOGGER.log(Level.FINE, "Cannot load a background image !", e);
}
}
// #59 Reset results if user change reference version.
session.getDataContext().getReferences().addListener((Observable obs) -> {
if (session.getWorkflowStep() == WorkflowStep.RESULT || session.getWorkflowStep() == WorkflowStep.FINALIZATION)
session.requestWorkflowStep(WorkflowStep.PROCESS);
session.setResults(null);
stepBtns.get(WorkflowStep.RESULT).setDisable(true);
stepBtns.get(WorkflowStep.FINALIZATION).setDisable(true);
});
}
private void unloadDataContext() {
final Site tmpSite = site;
context = null;
site = null;
protocol = null;
uiProtocolChosenBox.getChildren().clear();
uiHeader.setVisible(false);
editorSpi = null;
for (final Button stepBtn : stepBtns.values()) {
stepBtn.setDisable(true);
stepBtn.setVisible(true);
stepBtn.getStyleClass().removeAll(Rhomeo.CSS_CURRENT_STEP);
}
final FXDashboardPane dashboard = session.getBean(FXDashboardPane.class);
dashboard.siteProperty().set(tmpSite);
// TODO : set site
uiBody.setCenter(dashboard);
setBackground(Background.EMPTY);
}
/**
* On {@link FXRhomeoWizard#uiRefUsedBtn} action.
*/
@FXML
private void showUsedRef() {
final DataContext dc = session.getDataContext();
if (dc == null) {
return;
}
final Protocol protocol = dc.getProtocol();
if (protocol == null) {
return;
}
final Stage stage = Rhomeo.newStage();
stage.setTitle("Choix des référentiels");
final VBox vbox = new VBox(10);
vbox.getStyleClass().add("white-box");
protocol.getReferenceTypes().forEach(description -> {
try {
final FXProtocolReferencesPane protocolReferences = new FXProtocolReferencesPane(description);
vbox.getChildren().add(protocolReferences);
VBox.setVgrow(protocolReferences, Priority.SOMETIMES);
} catch (IntrospectionException ex) {
throw new RhomeoRuntimeException(ex);
}
});
final Button validBtn = new Button("Valider");
validBtn.getStyleClass().add("white-button");
validBtn.setOnAction(event -> {
// #59 If user already computed indices, changing reference versions
// would delete his results, so we warn him.
if (!stepBtns.get(WorkflowStep.RESULT).isDisabled()) {
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Si vous changez de version de référentiel maintenant, vos résultats seront supprimés. Continuer ?", ButtonType.NO, ButtonType.YES);
alert.setResizable(true);
if (ButtonType.NO.equals(alert.showAndWait().orElse(ButtonType.NO)))
return;
}
vbox.getChildren().stream()
.filter(child -> child instanceof FXProtocolReferencesPane)
.map(child -> (FXProtocolReferencesPane)child)
.forEach(protoRefs ->
protoRefs.getItems().stream()
.filter(o -> o instanceof FXProtocolReferencesPane.ReferenceLineItem && ((FXProtocolReferencesPane.ReferenceLineItem) o).isSelected())
.forEach(o -> dc.getReferences().put(protoRefs.getReferenceClass(), ((FXProtocolReferencesPane.ReferenceLineItem) o).getVersion()))
);
try {
contextManager.writeDataContext(dc);
} catch (IOException ex) {
throw new RhomeoRuntimeException(ex);
} finally {
stage.close();
}
});
final Button cancelBtn = new Button("Annuler");
cancelBtn.setCancelButton(true);
cancelBtn.setOnAction(evt -> stage.close());
cancelBtn.getStyleClass().add("white-button");
final HBox validHbox = new HBox(5, cancelBtn, validBtn);
validHbox.setAlignment(Pos.CENTER_RIGHT);
vbox.getChildren().add(validHbox);
final Scene scene = new Scene(vbox);
scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
scene.getStylesheets().add("/fr/cenra/rhomeo/fx/FXProtocolReferencesPane.css");
stage.setScene(scene);
stage.setMinHeight(protocol.getReferenceTypes().size() * 130 + 80);
stage.setHeight(stage.getMinHeight());
stage.setResizable(true);
stage.initModality(Modality.APPLICATION_MODAL);
stage.initStyle(StageStyle.UNDECORATED);
stage.sizeToScene();
stage.showAndWait();
}
@FXML
private void stopProcess() {
// HACK #78 : Before stopping session, we check dataset state, for the
// user to be able to save its data before stopping.
final Node center = uiBody.getCenter();
if (center instanceof FXDatasetPane) {
Task<Boolean> saveTask = ((FXDatasetPane) center).checkSaveChange().orElse(null);
if (saveTask != null)
saveTask.setOnSucceeded(evt -> {
if (Boolean.TRUE.equals(evt.getSource().getValue()))
Platform.runLater(() -> session.requestWorkflowStep(null));
});
else
session.requestWorkflowStep(null);
} else
session.requestWorkflowStep(null);
}
private void stepChanged(final ObservableValue<? extends WorkflowStep> obs, final WorkflowStep oldStep, final WorkflowStep newStep) {
// HACK #77 : User want a change on stop button look and feel at final step.
// We reset its state before doing anything else.
uiStopProcessBtn.setText(stopBtnOriginalText);
uiStopProcessBtn.setGraphic(new ImageView(Rhomeo.ICON_STOP_RED));
if (newStep == null) {
// Clear and hide edition display. Go back to the dashboard.
unloadDataContext();
} else {
if (oldStep == null) {
// New edition started
loadDataContext();
} else {
Button btn = stepBtns.get(oldStep);
if (btn != null && btn.getStyleClass().contains(Rhomeo.CSS_CURRENT_STEP)) {
btn.getStyleClass().remove(Rhomeo.CSS_CURRENT_STEP);
}
}
// Init current step UI
Button btn = stepBtns.get(newStep);
btn.setDisable(false);
btn.getStyleClass().add(Rhomeo.CSS_CURRENT_STEP);
Node body = createNode(newStep);
if (body == null) {
switch (newStep) {
case DATASET:
body = session.getBean(FXDatasetPane.class);
break;
case PROCESS:
body = session.getBean(FXDefaultProcessingPane.class);
break;
case RESULT:
body = session.getBean(FXDefaultResultPane.class);
break;
case FINALIZATION:
body = session.getBean(FXFinalizationPane.class);
// HACK #77 : Change look and feel of stop button.
uiStopProcessBtn.setText("Retour au tableau de bord");
uiStopProcessBtn.setGraphic(new ImageView(Rhomeo.ICON));
break;
}
}
uiBody.setCenter(body);
}
}
private Node createNode(final WorkflowStep step) {
final Node node;
if (editorSpi != null) {
node = editorSpi.getEditor(step);
} else {
node = null;
}
return node;
}
/**
* Initialize references lists for further use.
* Note : For each reference of the current protocol, we update list of
* installed and available versions. If none is installed, we propose to the
* user to install the most up to date.
*
* @throws IntrospectionException
*/
private void initializeReferences() {
if (protocol == null || protocol.getReferenceTypes().isEmpty())
return;
for (final ReferenceDescription description : protocol.getReferenceTypes()) {
try {
final ReferenceManager referenceManager = ReferenceManager.getOrCreate(description);
final Task<ReferenceManager> task = referenceManager.refresh();
task.setOnSucceeded(event -> {
// If we haven't any version installed for this reference, then we propose to install the newest.
if (referenceManager.getInstalledVersions().isEmpty() && !referenceManager.getDistantVersions().isEmpty()) {
Platform.runLater(() -> {
final StringBuilder builder = new StringBuilder("Aucune version du référentiel suivant n'est installée :")
.append(System.lineSeparator())
.append(description.getTitle())
.append(System.lineSeparator())
.append(System.lineSeparator())
.append("Voulez-vous télécharger la plus récente ?");
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION, builder.toString(), ButtonType.NO, ButtonType.YES);
alert.setHeaderText("Référentiel non installé");
alert.setResizable(true);
// If agreed, we download newest version. When it's done, we set it as used version.
if (ButtonType.YES.equals(alert.showAndWait().orElse(ButtonType.NO))) {
final Version max = Collections.max((Set<Version>) referenceManager.getDistantVersions());
TaskManager.INSTANCE.submit(referenceManager.install(max)).setOnSucceeded(evt -> {
final DataContext dc = session.getDataContext();
if (dc != null)
Platform.runLater(() -> dc.getReferences().put(description.getReferenceType(), max));
});
}
});
} else {
final Version selectedVersion = context.getReferences().get(description.getReferenceType());
// If at least one local version exists for this reference and nothing has been selected, then select the biggest one.
// This way, for each reference a version should be selected.
if (selectedVersion == null && !referenceManager.getInstalledVersions().isEmpty()) {
final Version max = Collections.max((Set<Version>) referenceManager.getInstalledVersions());
context.getReferences().put(description.getReferenceType(), max);
}
}
});
task.setOnFailed(event -> {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot update reference versions !", event.getSource().getException());
final Version selectedVersion = context.getReferences().get(description.getReferenceType());
// If at least one local version exists for this reference and nothing has been selected, then select the biggest one.
// This way, for each reference a version should be selected.
if (selectedVersion == null && !referenceManager.getInstalledVersions().isEmpty()) {
final Version max = Collections.max((Set<Version>) referenceManager.getInstalledVersions());
context.getReferences().put(description.getReferenceType(), max);
}
});
TaskManager.INSTANCE.submit(task);
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.WARNING, "Cannot initialize reference !", e);
}
}
}
}
@@ -0,0 +1,60 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.GridPane;
/**
* Created by cedr on 06/04/16.
*/
public class FXSplashScreen extends GridPane {
@FXML public GridPane uiLoadingPane;
@FXML public Label uiProgressLabel;
@FXML public ProgressBar uiProgressBar;
@FXML public Button uiCancel;
@FXML
public void closeApp() {
System.exit(0);
}
}
@@ -0,0 +1,139 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import java.util.HashMap;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.springframework.stereotype.Component;
import fr.cenra.rhomeo.api.ui.DashboardMenuItem;
import java.util.Map;
import java.util.Optional;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
public class GraphDashboardItem implements DashboardMenuItem {
private static final String GRAPH_TITLE = "Évolution des valeurs indicatrices";
@Override
public Optional<MenuItem> createItem(final TableView<IndicatorValuesByYearItem> target) {
final MenuItem graphItem = new MenuItem("Graphique d'évolution");
graphItem.setOnAction(evt -> displayGraph(target));
graphItem.visibleProperty().bind(Bindings.size(target.getSelectionModel().getSelectedIndices()).greaterThan(1));
return Optional.of(graphItem);
}
/**
* Display the evolution graph in a modal window.
*/
private static void displayGraph(final TableView<IndicatorValuesByYearItem> source) {
if (source.getScene() == null || source.getScene().getWindow() == null)
return;
final Stage chartStage = new Stage(StageStyle.UTILITY);
chartStage.initModality(Modality.WINDOW_MODAL);
chartStage.initOwner(source.getScene().getWindow());
chartStage.setTitle(GRAPH_TITLE);
chartStage.setScene(new Scene(createChart(source)));
chartStage.show();
}
/**
* Create a point chart to display selected indicator values from dashboard.
* @return A chart to display selected values.
*/
private static XYChart<Number, Number> createChart(final TableView<IndicatorValuesByYearItem> source) {
final Map<String, XYChart.Series<Number, Number>> series = new HashMap<>();
final ObservableList<TablePosition> selectedCells = source.getSelectionModel().getSelectedCells();
Double cellData;
int year;
IndicatorSiteColumn col;
int minYear = Integer.MAX_VALUE;
int maxYear = Integer.MIN_VALUE;
double minValue = Double.MAX_VALUE;
double maxValue = Double.MIN_VALUE;
for (final TablePosition pos : selectedCells) {
if (pos.getTableColumn() instanceof IndicatorSiteColumn) {
col = (IndicatorSiteColumn) pos.getTableColumn();
cellData = col.getCellData(pos.getRow());
if (cellData != null && Double.isFinite(cellData)) {
year = source.getItems().get(pos.getRow()).getYear();
minYear = StrictMath.min(year, minYear);
maxYear = StrictMath.max(year, maxYear);
minValue = StrictMath.min(cellData, minValue);
maxValue = StrictMath.max(cellData, maxValue);
series.computeIfAbsent(col.getIndicator().getTitle(), (title) -> {
final XYChart.Series<Number, Number> s = new XYChart.Series<>();
s.setName(title);
return s;
}).getData().add(new XYChart.Data<>(year, cellData));
}
}
}
final XYChart<Number, Number> chart = new ScatterChart<>(
new NumberAxis("Année", minYear - 1, maxYear + 1, 5),
new NumberAxis("Valeur", minValue - 0.5, maxValue + 0.5, 0.5)
);
chart.setTitle(GRAPH_TITLE);
chart.getData().addAll(series.values());
chart.getData().sort((o1, o2) -> o1.getName().compareTo(o2.getName()));
chart.setPadding(new Insets(10));
return chart;
}
}
@@ -0,0 +1,299 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.data.ReferenceDescription;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.DashboardResultItem;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.result.ResultStorage;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Function;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.geotoolkit.gui.javafx.util.TaskManager;
/**
* Table column for indicators.
* Column present in the {@link FXDashboardResultsTable}.
*
* @author Cédric Briançon (Geomatys)
*/
public class IndicatorSiteColumn extends TableColumn<IndicatorValuesByYearItem, Double> implements Comparable<IndicatorSiteColumn> {
private final Indicator indicator;
private final ChangeListener<ObservableList> itemPropertyChanged;
private final InvalidationListener referenceCheckTrigger;
private final ObjectProperty<Task> checkTaskProperty;
/**
* HACK : Cannot find any property to put graphic right to text in a column
* header, so we put a label in the header to contain both.
*/
private final Label header;
public IndicatorSiteColumn(final Indicator indicator) {
this.indicator = indicator;
header = new Label(indicator.getName());
header.setContentDisplay(ContentDisplay.RIGHT);
setGraphic(header);
itemPropertyChanged = this::itemPropertyChanged;
referenceCheckTrigger = obs -> checkReferences();
tableViewProperty().addListener(this::tableChanged);
tableViewProperty().addListener(referenceCheckTrigger);
checkTaskProperty = new SimpleObjectProperty<>();
checkTaskProperty.addListener((obs, oldVal, newVal) -> {
if (oldVal != null && oldVal.isRunning())
oldVal.cancel();
if (newVal != null && !newVal.isRunning())
TaskManager.INSTANCE.submit(newVal);
});
}
public Indicator getIndicator() {
return indicator;
}
@Override
public int compareTo(IndicatorSiteColumn o) {
if (o == null) {
return -1;
}
if (indicator == null) {
return 1;
}
return indicator.compareTo(o.getIndicator());
}
/**
* Update listeners when parent table changes.
* @param obs
* @param oldVal
* @param newVal
*/
private void tableChanged(final ObservableValue<? extends TableView> obs, TableView oldVal, TableView newVal) {
if (oldVal != null) {
oldVal.itemsProperty().removeListener(itemPropertyChanged);
oldVal.itemsProperty().removeListener(referenceCheckTrigger);
}
if (newVal != null) {
newVal.itemsProperty().addListener(itemPropertyChanged);
newVal.itemsProperty().addListener(referenceCheckTrigger);
}
}
/**
* Update listeners when parent table items change.
* @param obs
* @param oldVal
* @param newVal
*/
private void itemPropertyChanged(final ObservableValue<? extends ObservableList> obs, ObservableList oldVal, ObservableList newVal) {
if (oldVal != null)
oldVal.removeListener(referenceCheckTrigger);
if (newVal != null)
newVal.addListener(referenceCheckTrigger);
}
/**
* Browse column indices to find all reference versions used for their computing.
* If we find multiple versions for a same reference, we will display a little
* warning to the user.
*/
private void checkReferences() {
final ObservableList<IndicatorValuesByYearItem> items;
if (getTableView() == null || (items = getTableView().getItems()) == null || items.isEmpty())
return;
final Site site;
if (getTableView() instanceof FXDashboardResultsTable)
site = ((FXDashboardResultsTable)getTableView()).siteProperty().get();
else
site = null;
if (site == null)
return;
final ReferenceChecker checker = new ReferenceChecker(site, items);
checker.setOnSucceeded(evt -> Platform.runLater(() -> {
final Map<ReferenceDescription, Map<Version, Set<Integer>>> md = checker.getValue();
if (multipleVersionsInUse(md)) {
final ImageView imgView = new ImageView(Rhomeo.ICON_WARNING_ORANGE);
final Tooltip tt = new Tooltip("Les versions de référentiels ne sont pas homogènes.");
tt.setContentDisplay(ContentDisplay.BOTTOM);
tt.getStyleClass().add("info-popup");
tt.setGraphic(createConflictView(md));
final Label label = new Label(null, imgView);
label.setTooltip(tt);
header.setGraphic(label);
}
}));
checker.setOnFailed(evt -> Platform.runLater(() -> {
// TODO. For now, do as if nothing happened
}));
checkTaskProperty.set(checker);
}
private VBox createConflictView(final Map<ReferenceDescription, Map<Version, Set<Integer>>> md) {
final VBox vbox = new VBox(10);
vbox.setFillWidth(true);
vbox.setStyle("font-size: 12pt;");
for (Map.Entry<ReferenceDescription, Map<Version, Set<Integer>>> entry : md.entrySet()) {
if (entry.getValue().size() < 2)
continue;
final BorderPane bp = new BorderPane();
final Label title = new Label(entry.getKey().getTitle());
title.getStyleClass().add("section-title");
bp.setTop(title);
final GridPane gp = new GridPane();
bp.setCenter(gp);
gp.getColumnConstraints().addAll(
new ColumnConstraints(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Priority.NEVER, HPos.LEFT, true),
new ColumnConstraints(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE, Priority.ALWAYS, HPos.CENTER, true)
);
final Map<Version, Set<Integer>> vMap = entry.getValue();
int rowIndex = 0;
for (Map.Entry<Version, Set<Integer>> vEntry : vMap.entrySet()) {
gp.add(new Label(vEntry.getKey().toString()), 0, rowIndex);
final StringJoiner sj = new StringJoiner(", ");
vEntry.getValue().forEach(val -> sj.add(String.valueOf(val)));
gp.add(new Label(sj.toString()), 1, rowIndex++);
}
vbox.getChildren().add(bp);
}
return vbox;
}
private boolean multipleVersionsInUse(final Map<ReferenceDescription, Map<Version, Set<Integer>>> metadata) {
for (final Map.Entry<ReferenceDescription, Map<Version, Set<Integer>>> entry : metadata.entrySet()) {
if (entry.getValue().size() > 1)
return true;
}
return false;
}
/**
* Read metadata associated to given items and column indicator, to find all
* versions used for references.
*/
private class ReferenceChecker extends Task<Map<ReferenceDescription, Map<Version, Set<Integer>>>> {
final List<IndicatorValuesByYearItem> items;
final Site site;
ReferenceChecker(final Site site, final List<IndicatorValuesByYearItem> items) {
this.site = site;
this.items = items;
}
@Override
protected Map<ReferenceDescription, Map<Version, Set<Integer>>> call() throws Exception {
final Session session = Session.getInstance();
final ResultStorage storage = session.getBean(ResultStorage.class);
final Map<ReferenceDescription, Map<Version, Set<Integer>>> refVersions = new HashMap<>();
for (final IndicatorValuesByYearItem item : items) {
if (isCancelled())
break;
final IndicatorValuesByYearItem.PublishedValue tmpVal = item.getValueForIndicator(indicator.getName());
if (tmpVal != null) {
final DashboardResultItem tmpItem = new DashboardResultItem(item.getYear(), site.getName(), indicator.getName(), tmpVal.getValue(), tmpVal.isPublished());
storage.readMetadata(tmpItem)
.map(sc -> sc.toDataContext(session))
.map(ctx -> session.transform(ctx.getReferences()))
.ifPresent(map -> merge(map, refVersions, item.getYear()));
}
}
return refVersions;
}
void merge(final Map<ReferenceDescription, Version> source, final Map<ReferenceDescription, Map<Version, Set<Integer>>> dest, int year) {
for (final Map.Entry<ReferenceDescription, Version> entry : source.entrySet()) {
dest.computeIfAbsent(entry.getKey(), MAP_CREATOR)
.computeIfAbsent(entry.getValue(), SET_CREATOR)
.add(year);
}
}
}
private static final Function<Version, Set<Integer>> SET_CREATOR = input -> new HashSet<>();
private static final Function<ReferenceDescription, Map<Version, Set<Integer>>> MAP_CREATOR = input -> new HashMap<>();
}
@@ -0,0 +1,208 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.IndicatorValuesByYearItem;
import fr.cenra.rhomeo.api.ui.DashboardMenuItem;
import fr.cenra.rhomeo.core.RhomeoRuntimeException;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p04.I04;
import fr.cenra.rhomeo.protocol.p04.I07;
import fr.cenra.rhomeo.protocol.p08.I12;
import fr.cenra.rhomeo.protocol.p09.I13;
import java.beans.IntrospectionException;
import java.io.IOException;
import java.util.Optional;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
*
* @author Samuel Andrés (Geomatys) <samuel.andres at geomatys.com>
*/
@Component
public class MultiIndicatorsDashboardItem implements DashboardMenuItem {
@Autowired
private Session session;
@Override
public Optional<MenuItem> createItem(final TableView<IndicatorValuesByYearItem> table) {
if (!(table instanceof FXDashboardResultsTable))
return Optional.empty();
final MenuItem graphItem = new MenuItem("Voir tous les indicateurs");
graphItem.setOnAction(evt -> {
try {
displayStage((FXDashboardResultsTable) table);
} catch (IOException | IntrospectionException | InstantiationException | IllegalAccessException ex) {
throw new RhomeoRuntimeException("Unable to display results.", ex);
}
});
graphItem.visibleProperty().bind(Bindings.size(table.getSelectionModel().getSelectedIndices()).isEqualTo(1)
.and(isValidIndice(table.getSelectionModel().getSelectedCells())));
return Optional.of(graphItem);
}
/**
* Customized binding to test indicator column.
*
* @param op
* @return
*/
public static BooleanBinding isValidIndice(final ObservableList<TablePosition> op) {
if (op == null) {
throw new NullPointerException("List cannot be null.");
}
return new BooleanBinding() {
{
super.bind(op);
}
@Override
public void dispose() {
super.unbind(op);
}
@Override
protected boolean computeValue() {
if(op.size()!=1) return false;
final TablePosition cell = op.get(0);
final TableColumn tableColumn = cell.getTableColumn();
if(tableColumn instanceof IndicatorSiteColumn && ((IndicatorSiteColumn) tableColumn).getIndicator()!=null){
final String ind = ((IndicatorSiteColumn) tableColumn).getIndicator().getName();
if (I04.NAME.equals(ind) || I07.NAME.equals(ind) || I12.NAME.equals(ind) || I13.NAME.equals(ind)) {
return tableColumn.getCellData(cell.getRow()) != null;
}
}
return false;
}
@Override
//@ReturnsUnmodifiableCollection
public ObservableList<?> getDependencies() {
return FXCollections.singletonObservableList(op);
}
};
}
/**
* Display the evolution graph in a modal window.
*/
private void displayStage(final FXDashboardResultsTable table) throws IOException, IntrospectionException, InstantiationException, IllegalAccessException {
if (table.getScene() == null
|| table.getScene().getWindow() == null
|| table.getSelectionModel().getSelectedCells()==null
|| table.getSelectionModel().getSelectedCells().size()!=1)
return;
final Site site = table.siteProperty().get();
final TableColumn<IndicatorValuesByYearItem, ?> tableColumn = table.getSelectionModel().getSelectedCells().get(0).getTableColumn();
if(site!=null && site.getName()!=null && tableColumn!=null && tableColumn instanceof IndicatorSiteColumn){
final Indicator indicator = ((IndicatorSiteColumn) tableColumn).getIndicator();
final IndicatorValuesByYearItem selectedItem = table.getSelectionModel().getSelectedItem();
final Stage chartStage = new Stage(StageStyle.TRANSPARENT);
chartStage.initModality(Modality.WINDOW_MODAL);
chartStage.initOwner(table.getScene().getWindow());
final FXFullSpecificResultPane fullResultPane = session.createPrototype(FXFullSpecificResultPane.class,
selectedItem,
indicator,
site.getName());
final Label label = new Label(indicator.getName() + " ("+selectedItem.getYear()+")");
label.setAlignment(Pos.CENTER);
label.setMaxWidth(Double.MAX_VALUE);
label.getStyleClass().add("text-bold");
final BorderPane root = new BorderPane();
root.setPrefSize(400, Region.USE_COMPUTED_SIZE);
root.getStyleClass().add("white-box");
root.setTop(label);
root.setCenter(fullResultPane);
final Button closeBtn = new Button("Fermer");
closeBtn.setCancelButton(true);
closeBtn.setOnAction(evt -> chartStage.close());
closeBtn.getStyleClass().add("white-button");
final HBox hBox = new HBox(closeBtn);
hBox.setAlignment(Pos.CENTER_RIGHT);
root.setBottom(hBox);
final Scene scene = new Scene(root);
scene.getStylesheets().add(Rhomeo.CSS_THEME_PATH);
scene.getStylesheets().add("/fr/cenra/rhomeo/fx/FXProtocolReferencesPane.css");
chartStage.setMaxHeight(400);
chartStage.setMaxWidth(700);
chartStage.setScene(scene);
chartStage.sizeToScene();
chartStage.show();
}
}
}
@@ -0,0 +1,396 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.DataContext;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.data.TrackingPoint;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p02.P02;
import fr.cenra.rhomeo.protocol.p05.P05;
import fr.cenra.rhomeo.protocol.p06.P06;
import fr.cenra.rhomeo.protocol.p07.P07;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javax.annotation.PostConstruct;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
*
* This pane is a factorisation of common attributes and functionalities of
* {@link P02}, {@link P05}, {@link P06} and {@link P07} {@link StatementEditor}s.
*
* It displays a summary of a "current" {@link TrackingPoint} one the one hand,
* and a list of {@link T} on the other hand.
*
* The pane inherits Borderpane and must contain at least :
*
* 1- {@link TrackingPoint} attribute summary :
*
* - a {@link TextField} ({@link AbstractCustomStatementEditor#uiName}) mapping
* the name of the current {@link TrackingPoint}.
* - a {@link DatePicker} field ({@link AbstractCustomStatementEditor#uiDate}),
* mapping the date of the current selected statement.
* - a {@link TextArea} field ({@link AbstractCustomStatementEditor#uiRemarks}),
* mapping the remarks of the current selected statement.
*
* 2- A {@link StatementTable} of the {@link T}s of the selected {@link TrackingPoint}.
*
* /!\ Inherited classes should be {@link Component}s, as a post-construct phase
* and an autowired field are defined here.
*
* @author Samuel Andrés (Geomatys) <samuel.andres at geomatys.com>
* @param <T>
*/
public abstract class AbstractCustomStatementEditor<T extends Statement> extends BorderPane implements StatementEditor<T> {
@Autowired
protected Session session;
// Tracking point attributes
@FXML protected TextField uiName;
@FXML protected DatePicker uiDate;
@FXML protected TextArea uiRemarks;
// Statement control
@FXML protected BorderPane uiObsContainer;
@FXML protected Button uiDelete;
protected StatementTable<T> uiStatementTable;
protected final Class<T> dataType;
/**
* An internal field we use to know if the typed name has changed when user
* exit the name field.
*/
private String oldName = null;
/**
* An internal field we use to know if the typed date has changed when user
* exit the date field.
*/
private LocalDate oldDate = null;
/**
* An internal field we use to know if the typed remarks have changed when
* user exit the remarks field.
*/
private String oldRemarks = null;
private final StringProperty firstEditionColumn = new SimpleStringProperty();
protected AbstractCustomStatementEditor(final Class<T> dataType) {
super();
ArgumentChecks.ensureNonNull("Data type", dataType);
this.dataType = dataType;
}
/**
* Default {@link PostConstruct} AbstractCustomStatementEditor.
*/
@PostConstruct
protected void init() {
try {
final Class<? extends Statement> dataType = session.getDataContext().getProtocol().getDataType();
if (getDataType().isAssignableFrom(dataType)) {
// Default behaviour : ignore some statement attributes.
uiStatementTable = new StatementTable(dataType, getFieldsToIgnore());
uiStatementTable.setMinHeight(30);
uiStatementTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
final InvalidationListener listener = (Observable obs) -> {
for (final TableColumn col : uiStatementTable.getColumns()) {
if (col.isEditable() && (col instanceof PropertyColumn)) {
firstEditionColumn.set(((PropertyColumn)col).descriptor.getName());
break;
}
}
};
uiStatementTable.getColumns().addListener(listener);
listener.invalidated(uiStatementTable.getColumns());
uiObsContainer.setCenter((Node) uiStatementTable);
uiName.focusedProperty().addListener(this::nameFocusChanged);
uiDate.focusedProperty().addListener(this::dateFocusChanged);
uiRemarks.focusedProperty().addListener(this::remarksFocusChanged);
uiStatementTable.itemsProperty().addListener(this::itemsChanged);
itemsChanged(uiStatementTable.itemsProperty(), null, null);
}
} catch (Exception e) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot initialize statement table !", e);
final Label label = new Label("Impossible d'afficher la table des relevés.");
label.getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
uiObsContainer.setCenter(label);
}
}
@Override
public ObservableList<T> getItems() {
return uiStatementTable.getItems();
}
@Override
public void setItems(ObservableList<T> items) {
uiStatementTable.setItems(items);
}
@Override
public ObjectProperty<ObservableList<T>> itemsProperty() {
return uiStatementTable.itemsProperty();
}
/**
*
* @return A list of properties from {@link #dataType} which must not be
* included into edition table.
*/
protected String[] getFieldsToIgnore() {
return new String[]{"trackingPoint", "date", "remarks"};
}
/**
* Default behaviour dataType return behaviour.
*
* Returns the data type of the {@link Protocol} of the current {@link Session}
* {@link DataContext}.
*
* @return
*/
@Override
public Class<T> getDataType() {
return dataType;
}
/**
* Default behaviour to create a new {@link T}.
*
* Creates a new statement initializing common attributes from {@link TrackingPoint}.
*
* @return
* @throws ReflectiveOperationException
*/
protected T initStatement() throws ReflectiveOperationException {
// TODO : try to acquire a prototype ?
final T data = (T) session.getDataContext().getProtocol().getDataType().newInstance();
if (uiName.getText() != null && !uiName.getText().trim().isEmpty()) {
data.setTrackingPoint(uiName.getText());
}
if (uiDate.getValue() != null) {
data.setDate(uiDate.getValue());
}
data.setRemarks(uiRemarks.getText());
return data;
}
/**
* Default behaviour on adding {@link T} event.
*
* Creates a new statement and focus on it.
*
* @param evt
* @throws ReflectiveOperationException
*/
@FXML
protected void addStatement(final ActionEvent evt) throws ReflectiveOperationException {
if (uiStatementTable == null || uiStatementTable.getItems() == null)
return;
final T data = initStatement();
uiStatementTable.getItems().add(data);
final TablePosition pos = new TablePosition(uiStatementTable, uiStatementTable.getItems().size() - 1, null);
// HACK : It appears that table cells are refreshed on next pulse, so we wait for it before asking for focus.
TaskManager.INSTANCE.submit(() -> {
synchronized (pos) {
try {
pos.wait(100);
} catch (InterruptedException ex) {
Rhomeo.LOGGER.log(Level.WARNING, null, ex);
}
}
Platform.runLater(() -> Rhomeo.editNextCell(pos));
});
}
/**
* Default behaviour of deleting {@link T} event.
*
* @param evt
*/
@FXML
protected void deleteStatements(final ActionEvent evt) {
if (uiStatementTable == null || uiStatementTable.getItems() == null)
return;
// Defensive copy
final List<Integer> indices = new ArrayList<>(uiStatementTable.getSelectionModel().getSelectedIndices());
for (final int index : indices) {
uiStatementTable.getItems().remove(index);
}
}
@Override
public void focusOn(T item, String propertyName) {
uiStatementTable.focusOn(item, propertyName);
}
/**
* Default behaviour on changing current {@link TrackingPoint}.
*
* @param obs
* @param oldItems
* @param newItems
*/
protected void itemsChanged(final ObservableValue<? extends List> obs, final List<T> oldItems, final List<T> newItems) {
uiDelete.disableProperty().unbind();
if (newItems != null && !newItems.isEmpty()) {
final T first = newItems.get(0);
uiName.setText(first.getTrackingPoint());
uiDate.setValue(first.getDate());
uiRemarks.setText(first.getRemarks());
if (uiStatementTable != null) {
uiStatementTable.getSelectionModel().selectFirst();
}
} else {
uiName.setText(null);
uiDate.setValue(null);
uiRemarks.clear();
}
if (uiStatementTable != null) {
uiDelete.disableProperty().bind(uiStatementTable.getSelectionModel().selectedItemProperty().isNull());
}
uiName.requestFocus();
}
/**
* Default behaviour when the focus has changed on {@link AbstractCustomStatementEditor#uiName}.
*
* @param obs
* @param oldValue
* @param newValue
*/
protected void nameFocusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
final String name = uiName.getText();
if (Boolean.TRUE.equals(newValue)) {
oldName = name;
} else if (Boolean.FALSE.equals(newValue) && !Rhomeo.equivalent(oldName, name)) {
// Update statements. Use defensive copy because each change of name can update list of available items.
final Statement[] toUpdate = uiStatementTable.getItems().toArray(new Statement[uiStatementTable.getItems().size()]);
for (int i = 0 ; i < toUpdate.length ; i++) {
toUpdate[i].setTrackingPoint(name);
}
}
}
/**
* Default behaviour when the focus has changed on {@link AbstractCustomStatementEditor#uiDate}.
*
* @param obs
* @param oldValue
* @param newValue
*/
protected void dateFocusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
final LocalDate date = uiDate.getValue();
if (Boolean.TRUE.equals(newValue)) {
oldDate = date;
} else if (!(oldDate == date || oldDate != null && date != null && oldDate.isEqual(date))) {
// Update statements. Use defensive copy because each change of name can update list of available items.
final Statement[] toUpdate = uiStatementTable.getItems().toArray(new Statement[uiStatementTable.getItems().size()]);
for (int i = 0 ; i < toUpdate.length ; i++) {
toUpdate[i].setDate(date);
}
}
}
/**
* Default behaviour when the focus has changed on {@link AbstractCustomStatementEditor#uiName}.
*
* @param obs
* @param oldValue
* @param newValue
*/
protected void remarksFocusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final Boolean newValue) {
final String remarks = uiRemarks.getText();
if (Boolean.TRUE.equals(newValue)) {
oldRemarks = remarks;
} else if (Boolean.FALSE.equals(newValue) && !Rhomeo.equivalent(oldRemarks, remarks)) {
for (final Statement st : uiStatementTable.getItems()) {
st.setRemarks(remarks);
}
}
}
}
@@ -0,0 +1,53 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import javafx.scene.control.TableCell;
/**
* A table cell which knows what value to commit when editing.
* @author Alexis Manin (Geomatys)
*/
public abstract class AutoCommitableTableCell<T, S> extends TableCell<T, S> {
/**
* Trigger a commit, as {@link #commitEdit(java.lang.Object) }. But its the
* cell which determines the value to commit.
*/
abstract void commitEdit();
}
@@ -0,0 +1,125 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.core.RhomeoCore;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.time.LocalDate;
import java.util.Objects;
import java.util.function.Predicate;
/**
*
* @author Alexis Manin (Geomatys)
*/
public class ByNameAndDateFilter implements Predicate<Statement>, Comparable<ByNameAndDateFilter>, Externalizable {
protected static final long serialVersionUID = 1L;
protected String name;
protected LocalDate date;
public ByNameAndDateFilter() {}
public ByNameAndDateFilter(String name, LocalDate date) {
this.name = name;
this.date = date;
}
@Override
public boolean test(Statement t) {
if (!Objects.equals(name, t.getTrackingPoint()))
return false;
return RhomeoCore.checkEquality(date, t.getDate());
}
public String getName() {
return name;
}
public LocalDate getDate() {
return date;
}
@Override
public int compareTo(ByNameAndDateFilter o) {
final int nameComparison = getName().compareTo(o.getName());
if (nameComparison != 0)
return nameComparison;
return getDate().compareTo(o.getDate());
}
@Override
public int hashCode() {
int hash = 3;
hash = 61 * hash + Objects.hashCode(this.name);
hash = 61 * hash + Objects.hashCode(this.date);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final ByNameAndDateFilter other = (ByNameAndDateFilter) obj;
if (!Objects.equals(this.name, other.name))
return false;
return date == other.date || date != null && other.date != null && date.isEqual(other.date);
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name == null? "" : name);
RhomeoCore.writeDate(date, out);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
date = RhomeoCore.readDate(in);
}
}
@@ -0,0 +1,282 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import com.sun.javafx.PlatformUtil;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.preferences.PreferenceGroup;
import fr.cenra.rhomeo.api.ui.PreferencePane;
import fr.cenra.rhomeo.core.CSVMapper;
import fr.cenra.rhomeo.core.CSVMappingBuilder;
import fr.cenra.rhomeo.preferences.csv.CSVKey;
import fr.cenra.rhomeo.preferences.csv.CSVPreferences;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.logging.Level;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.StringConverter;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* A panel to edit CSV format preferences for dataset import / export. You can
* also acquire a {@link CSVMappingBuilder} from a configuration typed in a popup
* by calling {@link #showConfigurationForm(javafx.stage.Window) }.
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class CSVConfigurationPane extends VBox implements PreferencePane<CSVKey> {
private static final String INFO_TEXT = new StringBuilder("L'échappement des chaînes de caractère est optionnel. Le caractère d'échappement est \" et doit entourer la chaine complète.")
.append(System.lineSeparator())
.append("Pour utiliser le caractère \" dans les messages échappés, il doit être doublé.")
.append(System.lineSeparator())
.append("Ex : \"Ceci est un texte \"\"échappé\"\".\"")
.toString();
@FXML
private ComboBox<Charset> uiEncoding;
@FXML
private TextField uiText;
@FXML
private CheckBox uiTab;
@FXML
private Label uiInfo;
@FXML
private Button uiCancel;
@FXML
private Button uiApply;
@Autowired
private CSVPreferences prefs;
private CSVMappingBuilder builder;
private CSVConfigurationPane() {
super();
Rhomeo.loadFXML(this);
Charset defaultCharset;
if (PlatformUtil.isWindows())
try {
defaultCharset = Charset.forName("windows-1252");
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.FINE, "No charset found for windows-1252.", e);
defaultCharset = StandardCharsets.ISO_8859_1;
}
else
defaultCharset = StandardCharsets.UTF_8;
Rhomeo.initComboBox(uiEncoding, FXCollections.observableArrayList(Charset.availableCharsets().values()).sorted(), defaultCharset);
uiEncoding.setConverter(new CharsetStringConverter());
uiText.disableProperty().bind(uiTab.selectedProperty());
uiText.textProperty().addListener(this::textChanged);
uiInfo.setGraphic(new ImageView(Rhomeo.ICON_INFO_BLUE));
uiInfo.setText(INFO_TEXT);
}
@PostConstruct
private void init() {
init(prefs.getPreference(CSVPreferences.CHARSET_KEY), prefs.getPreference(CSVPreferences.SEPARATOR_KEY));
}
private void init(final String charset, final String separator) {
// Restore default values from preferences.
if (separator.equals("\t")) {
uiTab.setSelected(true);
} else {
uiText.setText(separator);
}
if (charset != null)
uiEncoding.setValue(Charset.forName(charset));
}
private void initButtons() {
uiApply.setVisible(true);
uiCancel.setVisible(true);
uiApply.setOnAction(this::createBuilder);
uiApply.disableProperty().bind(Bindings.createBooleanBinding(() ->
uiText.getCharacters().length() != 1 && uiEncoding.getValue() == null
, uiText.textProperty(), uiEncoding.valueProperty()));
uiCancel.setCancelButton(true);
}
/**
* Ensure that typed separator is a single character.
*
* @param obs
* @param oldValue
* @param newValue
*/
private void textChanged(final ObservableValue<? extends String> obs, final String oldValue, final String newValue) {
if (newValue != null && newValue.length() > 1) {
uiText.setText(newValue.substring(newValue.length() -1));
}
}
private void createBuilder(final ActionEvent e) {
if (uiEncoding.getValue() == null || (!uiTab.isSelected() && uiText.getText() == null || uiText.getText().isEmpty()))
return;
final char separator = uiTab.isSelected()? '\t' : uiText.getText().charAt(uiText.getText().length() -1);
builder = new CSVMappingBuilder()
.withSeparator(separator)
.withEncoding(uiEncoding.getValue());
}
/**
* Show a dialog allowing user to configure the basic parameters for CSV
* mapping (separator, encoding).
*
* /!\ MUST BE CALLED FROM FX-THREAD.
*
* @return A csv mapper builder if user confirmed its configuration, Nothing
* if he cancelled.
*/
public static Optional<CSVMappingBuilder> showConfigurationForm() {
return showConfigurationForm(null);
}
/**
* Show a dialog allowing user to configure the basic parameters for CSV
* mapping (separator, encoding).
*
* /!\ MUST BE CALLED FROM FX-THREAD.
*
* @param owner The owner of the dialog to display. If the owner is null,
* then displayed stage is application modal. Otherwise, the modality is
* limited only on owner window.
* @return A csv mapper builder if user confirmed its configuration, Nothing
* if he cancelled.
*/
public static Optional<CSVMappingBuilder> showConfigurationForm(final Window owner) {
final CSVConfigurationPane content = new CSVConfigurationPane();
content.init(StandardCharsets.UTF_8.name(), String.valueOf(CSVMapper.DEFAULT_SEPARATOR));
content.initButtons();
final Stage stage = new Stage(StageStyle.UTILITY);
final EventHandler<ActionEvent> btnListener = (evt) -> stage.close();
content.uiApply.addEventHandler(ActionEvent.ACTION, btnListener);
content.uiCancel.addEventHandler(ActionEvent.ACTION, btnListener);
if (owner != null) {
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
} else {
stage.initModality(Modality.APPLICATION_MODAL);
}
stage.setScene(new Scene(content));
stage.setTitle("Paramètres de lecture");
stage.showAndWait();
return Optional.ofNullable(content.builder);
}
@Override
public void save() {
if (uiText.getText() != null && !uiText.getText().isEmpty()) {
final char separator = uiTab.isSelected() ? '\t' : uiText.getText().charAt(uiText.getText().length() - 1);
prefs.setPreference(CSVPreferences.SEPARATOR_KEY, String.valueOf(separator));
}
if (uiEncoding.getValue() != null)
prefs.setPreference(CSVPreferences.CHARSET_KEY, uiEncoding.getValue().name());
}
@Override
public PreferenceGroup<CSVKey> getPreferenceGroup() {
return prefs;
}
@Override
public Node getEditor() {
return this;
}
/**
* A simple converter between string and charset types.
*/
private static class CharsetStringConverter extends StringConverter<Charset> {
@Override
public String toString(Charset object) {
if (object == null)
return null;
return object.displayName();
}
@Override
public Charset fromString(String string) {
if (string == null || (string = string.trim()).isEmpty())
return null;
return Charset.forName(string);
}
}
}
@@ -0,0 +1,702 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.ObjectWrapper;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.api.ui.FilterTable;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.core.CSVDecoder;
import fr.cenra.rhomeo.core.CSVEncoder;
import fr.cenra.rhomeo.core.CSVMapper;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.fx.validation.FXValidationPane;
import fr.cenra.rhomeo.preferences.csv.CSVPreferences;
import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.SelectionMode;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javax.annotation.PostConstruct;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.internal.GeotkFX;
import org.geotoolkit.util.collection.CloseableIterator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import fr.cenra.rhomeo.api.ui.FilterTableSpi;
import fr.cenra.rhomeo.core.state.StateManager;
import java.util.concurrent.atomic.AtomicBoolean;
import javafx.beans.Observable;
/**
* Default editor for {@link Datasset} edition. For {@link Statement} edition,
* we try to inject a {@link StatementEditor} specific to the input {@link Protocol#getDataType()
* }.
*
* IMPORTANT : Input {@link StatementEditor} does not use directly {@link Dataset#getItems()
* }, to avoid sudden changes in the editor caused by an event sourced from
* dataset. However, to keep a responsive dataset, we mirror editor data in
* dataset on the fly. It is needed for state saving (see {@link StateManager) and other mechanisms as validation.
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
@Lazy
public class FXDatasetPane extends VBox {
private static final Pattern CSV_SUFFIX = Pattern.compile("(?i)\\.csv$");
@Autowired
private CSVPreferences csvPrefs;
@Autowired
private Session session;
@Autowired
private List<FilterTableSpi> pointTableSpis;
private FilterTable<Predicate> uiGroupTable;
private StatementEditor<Statement> uiStatementEditor;
@Autowired
private FXValidationPane uiValidator;
@FXML
private Button uiDisplayDocumentation;
@FXML
private Button uiDeleteDataset;
@FXML
private BorderPane uiPointListContainer;
@FXML
private Button uiAddPoint;
@FXML
private Button uiDeletePoints;
@FXML
private BorderPane uiPointEditorContainer;
@FXML
private BorderPane uiValidationContainer;
@FXML
private Button uiExportDataset;
@FXML
private Button uiProceed;
protected final ResourceBundle bundle;
private final ListChangeListener itemListener = this::itemsChanged;
private ObservableList<Statement> dataList;
private final AtomicBoolean changeDetected = new AtomicBoolean(false);
public FXDatasetPane() {
bundle = ResourceBundle.getBundle(FXDatasetPane.class.getCanonicalName());
Rhomeo.loadFXML(this, FXDatasetPane.class);
}
@PostConstruct
private void init() {
ArgumentChecks.ensureNonNull("Session containing data", session);
ArgumentChecks.ensureNonNull("Data context", session.getDataContext());
ArgumentChecks.ensureNonNull("Target protocol", session.getDataContext().getProtocol());
ArgumentChecks.ensureNonNull("Target dataset", session.getDataset());
for (final FilterTableSpi spi : pointTableSpis) {
if (spi.getProtocol() == session.getDataContext().getProtocol()) {
uiGroupTable = spi.createEmptyTable();
break;
}
}
if (uiGroupTable == null) {
uiGroupTable = (FilterTable) new FXTrackingPointTable(session);
}
uiGroupTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
uiGroupTable.setMinHeight(50);
uiGroupTable.setFixedCellSize(25);
final IntegerBinding size = Bindings.size(uiGroupTable.getItems());
uiGroupTable.prefHeightProperty().bind(
Bindings.createDoubleBinding(() -> {
if (size.get() < 1) {
return 50d;
}
return size.get() * uiGroupTable.getFixedCellSize() + 30;
}, size, uiGroupTable.fixedCellSizeProperty())
);
Optional<StatementEditor> optEditor = session.findEditor(session.getDataContext().getProtocol().getDataType());
if (optEditor.isPresent()) {
uiStatementEditor = optEditor.get();
if (uiStatementEditor instanceof Region) {
((Region)uiStatementEditor).setMinHeight(50);
}
uiStatementEditor.itemsProperty().addListener(this::itemsPropertyChanged);
uiGroupTable.getSelectionModel().selectedItemProperty().addListener((obs, oldGroup, newGroup) -> {
if (newGroup == null) {
uiStatementEditor.setItems(FXCollections.observableArrayList());
} else {
uiStatementEditor.setItems(FXCollections.observableArrayList(session.getDataset().getItems().filtered(newGroup)));
}
});
uiStatementEditor.setItems(FXCollections.observableArrayList());
uiPointEditorContainer.setCenter((Node) uiStatementEditor);
} else {
uiAddPoint.setVisible(false);
uiAddPoint.setManaged(false);
}
uiPointListContainer.setCenter(uiGroupTable);
uiPointEditorContainer.managedProperty().bind(uiPointEditorContainer.visibleProperty());
uiValidationContainer.setCenter(uiValidator);
uiValidator.getWarningList().getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
uiValidator.getWarningList().getSelectionModel().selectedItemProperty().addListener(this::selectedErrorChanged);
uiValidator.getErrorList().getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
uiValidator.getErrorList().getSelectionModel().selectedItemProperty().addListener(this::selectedErrorChanged);
/*
* HACK : Error list force focus on itself after click. So we
* have to repeat the selectedErrorChanged on mouse released.
*/
final EventHandler<MouseEvent> handler = evt -> {
final ConstraintViolation violation;
if (evt.getSource() == uiValidator.getWarningList()) {
violation = uiValidator.getWarningList().getSelectionModel().getSelectedItem();
} else if (evt.getSource() == uiValidator.getErrorList()) {
violation = uiValidator.getErrorList().getSelectionModel().getSelectedItem();
} else {
violation = null;
}
if (violation != null)
selectedErrorChanged(null, null, violation);
};
uiValidator.getErrorList().addEventHandler(MouseEvent.MOUSE_RELEASED, handler);
uiValidator.getWarningList().addEventHandler(MouseEvent.MOUSE_RELEASED, handler);
/*
* Disability
*/
BooleanBinding emptyDataset = Bindings.isEmpty(session.getDataset().getItems());
uiDeleteDataset.disableProperty().bind(emptyDataset);
uiExportDataset.disableProperty().bind(emptyDataset);
uiProceed.disableProperty().bind(emptyDataset);
uiDeletePoints.disableProperty().bind(uiGroupTable.getSelectionModel().selectedItemProperty().isNull());
// Icons
uiDeleteDataset.setGraphic(new ImageView(Rhomeo.ICON_TRASH_BLUE));
uiDeletePoints.setGraphic(new ImageView(Rhomeo.ICON_TRASH_BLUE));
uiDisplayDocumentation.setGraphic(new ImageView(Rhomeo.ICON_FILE_TEXT));
session.getDataset().getItems().addListener(this::changeDetected);
}
@FXML
void addPoint(ActionEvent event) {
// No tracking point selected. However, a null value will allow creation
// of a fresh one. We force editor visibility.
uiGroupTable.getSelectionModel().clearSelection();
uiStatementEditor.setItems(FXCollections.observableArrayList());
}
@FXML
void deleteDataset(ActionEvent event) {
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION, bundle.getString("dataset.delete.confirm"), ButtonType.NO, ButtonType.YES);
alert.setResizable(true);
if (ButtonType.YES.equals(alert.showAndWait().orElse(ButtonType.NO))) {
// issue #79 : focused filter is not suppressed.
uiGroupTable.getSelectionModel().clearSelection();
session.getDataset().getItems().clear();
if (uiStatementEditor != null)
uiStatementEditor.setItems(FXCollections.observableArrayList());
uiValidator.reset();
}
}
@FXML
void deletePoints(ActionEvent event) {
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION, bundle.getString("point.delete.confirm"), ButtonType.NO, ButtonType.YES);
alert.setResizable(true);
if (ButtonType.YES.equals(alert.showAndWait().orElse(ButtonType.NO))) {
// Defensive copy
final ArrayList<Predicate> selected = new ArrayList<>(uiGroupTable.getSelectionModel().getSelectedItems());
// HACK : filter table won't delete entries if they're focused.
uiGroupTable.getSelectionModel().clearSelection();
if (!selected.isEmpty()) {
Predicate filter = selected.get(0);
for (int i = 1; i < selected.size(); i++) {
filter = filter.or(selected.get(i));
}
session.getDataset().getItems().removeIf(filter);
}
}
}
@FXML
void displayDocumentation(ActionEvent event) {
final Protocol protocol = session.getDataContext().getProtocol();
Rhomeo.generateModelZip(protocol, uiDisplayDocumentation);
}
@FXML
Task<Boolean> exportDataset(ActionEvent event) {
final File chosen = chooseCSV(uiExportDataset.getText(), true);
if (chosen != null) {
final TaskManager.MockTask export = new TaskManager.MockTask(bundle.getString("dataset.export.task"), () -> {
final char sep;
final String separator = csvPrefs.getPreference(CSVPreferences.SEPARATOR_KEY);
if (separator == null || separator.isEmpty())
sep = CSVMapper.DEFAULT_SEPARATOR;
else
sep = separator.charAt(separator.length() - 1);
// Check if we must transform data when encoding.
Class dataType = session.getDataContext().getProtocol().getDataType();
Iterator toEncode = session.getDataset().getItems().iterator();
Optional<ObjectWrapper> opt = session.getWrapper(dataType);
if (opt.isPresent()) {
dataType = opt.get().getOutputType();
toEncode = new Transformer(opt.get(), toEncode);
}
final CSVEncoder encoder = new CSVEncoder(
chosen.toPath(), dataType, sep, Charset.forName(csvPrefs.getPreference(CSVPreferences.CHARSET_KEY)), null);
encoder.encode(toEncode, true);
return true;
});
export.setOnSucceeded(evt -> changeDetected.set(false));
export.setOnFailed(evt -> Platform.runLater(() -> {
GeotkFX.newExceptionDialog(bundle.getString("dataset.export.error"), export.getException());
}));
export.runningProperty().addListener((obs, oldValue, newValue) -> {
setDisable(newValue);
// TODO : show a loading popup
});
return TaskManager.INSTANCE.submit(export);
}
return null;
}
@FXML
void importDataset(ActionEvent event) {
Task previousTask = null;
if (!session.getDataset().getItems().isEmpty()) {
final String contentText = bundle.getString("save.before.import");
final Alert saveCurrent = new Alert(Alert.AlertType.WARNING, contentText, ButtonType.CANCEL, ButtonType.NO, ButtonType.YES);
saveCurrent.setResizable(true);
ButtonType choice = saveCurrent.showAndWait().orElse(ButtonType.CANCEL);
if (ButtonType.CANCEL.equals(choice)) {
return;
} else if (ButtonType.YES.equals(choice)) {
previousTask = exportDataset(event);
if (previousTask == null) {
return; // user cancelled saving. Stop the entire process.
}
}
}
final File input = chooseCSV(bundle.getString("dataset.import.choice"), false);
if (input == null)
return;
/* On dataset change, the trigger serving to notify user on possible
* unsaved entries. But, as we are just importing data, we must keep
* the same state as before the task.
*/
final boolean previousStatus = changeDetected.get();
final TaskManager.MockTask<String> importTask = new TaskManager.MockTask(bundle.getString("dataset.import.task"), () -> {
final HashSet<String> ignorable = new HashSet<>(2);
ignorable.add("trackingPoint");
ignorable.add("remarks");
final char sep;
final String separator = csvPrefs.getPreference(CSVPreferences.SEPARATOR_KEY);
if (separator == null || separator.isEmpty())
sep = CSVMapper.DEFAULT_SEPARATOR;
else
sep = separator.charAt(separator.length() - 1);
final CSVDecoder decoder = new CSVDecoder(
input.toPath(),
session.getDataContext().getProtocol().getDataType(),
sep,
Charset.forName(csvPrefs.getPreference(CSVPreferences.CHARSET_KEY)),
ignorable
);
// Check doublons.
int integrated = 0;
int doublons = 0;
final ObservableList dataset = session.getDataset().getItems();
final HashSet doublonDetector = new HashSet(dataset);
try (final CloseableIterator it = decoder.decodeLazy()) {
Object read;
while (it.hasNext()) {
read = it.next();
if (doublonDetector.add(read)) {
integrated++;
} else {
doublons++;
}
}
}
// Update dataset with new elements.
doublonDetector.removeAll(dataset);
final TaskManager.MockTask t = new TaskManager.MockTask(() -> dataset.addAll(doublonDetector));
Platform.runLater(t);
t.get();
if (doublons <= 0)
return String.format(bundle.getString("dataset.import.result.no-doublon"), integrated);
else
return String.format(bundle.getString("dataset.import.result"), integrated, doublons);
});
importTask.setOnFailed(evt -> Platform.runLater(() -> {
GeotkFX.newExceptionDialog(bundle.getString("dataset.import.error"), importTask.getException());
}));
importTask.setOnSucceeded(evt -> Platform.runLater(() -> {
changeDetected.set(previousStatus);
Rhomeo.showAlert(Alert.AlertType.INFORMATION, importTask.getValue());
}));
if (previousTask != null) {
final SimpleObjectProperty<Worker.State> previousState = new SimpleObjectProperty<>();
previousState.addListener((obs, oldState, newState) -> {
if (Worker.State.SUCCEEDED.equals(newState)) {
TaskManager.INSTANCE.submit(importTask);
}
});
previousState.bind(previousTask.stateProperty());
} else {
TaskManager.INSTANCE.submit(importTask);
}
}
@FXML
void nextStep(ActionEvent event) {
Task<Boolean> previousTask = checkSaveChange().orElse(null);
final Runnable proceed = () -> {
setProcessContext(session);
/*
* Check if we're able to jump to the next step. If not, we try to guess
* why, and inform user of what is wrong.
*/
if (!session.requestWorkflowStep(WorkflowStep.PROCESS)) {
if (session.getDataContext().getReferences().size() < session.getDataContext().getProtocol().getReferenceTypes().size()) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Les versions des référentiels associés au protocole ne sont pas définies !");
} else {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante. Veuillez vérifier votre lot de données.");
}
}
};
if (previousTask != null) {
previousTask.setOnSucceeded(evt -> {
if (Boolean.TRUE.equals(evt.getSource().getValue()))
Platform.runLater(proceed);
});
} else {
proceed.run();
}
}
/**
* If changes have been detected in currently edited dataset, ask to user if
* he wants to save them, and launch export process if he agrees.
*
* /!\ You must call this method from FX thread.
*
* @return An empty object if there's no change to save. Otherwise, a task
* is returned, which returns false if user cancelled saving, or true if
* data has been successfully written.
*/
public Optional<Task<Boolean>> checkSaveChange() {
if (!changeDetected.get()) {
return Optional.empty();
}
final String contentText = bundle.getString("save.before.next");
final Alert saveCurrent = new Alert(Alert.AlertType.CONFIRMATION, contentText, ButtonType.CANCEL, ButtonType.NO, ButtonType.YES);
saveCurrent.setResizable(true);
ButtonType choice = saveCurrent.showAndWait().orElse(ButtonType.CANCEL);
if (ButtonType.YES.equals(choice)) {
final Task exportTask = exportDataset(null);
if (exportTask != null)
return Optional.of(exportTask);
} else if (ButtonType.NO.equals(choice)) {
return Optional.empty();
}
final TaskManager.MockTask t = new TaskManager.MockTask(() -> false);
t.run();
return Optional.of(t);
}
/**
* When a change is detected on input dataset, we notify user that changes
* occurred and some data may not have been saved.
*
* @param obs
*/
private void changeDetected(final Observable obs) {
/* If dataset has been emptied, we clear change state. We check it here,
* so it is valid either if user deleted it manually or via "clear dataset"
* button.
*/
if (session.getDataset().getItems().isEmpty())
changeDetected.set(false);
else
changeDetected.set(true);
}
/**
* When we change items of inner {@link StatementEditor}, we update
* listeners registered on it.
*
* @param obs
*/
private void itemsPropertyChanged(final Observable obs) {
if (dataList != null)
dataList.removeListener(itemListener);
dataList = uiStatementEditor.getItems();
if (dataList != null) {
dataList.addListener(itemListener);
}
}
/**
* Report changes from editor list to dataset.
* @param c
*/
private void itemsChanged(final ListChangeListener.Change<Statement> c) {
ObservableList dataset = session.getDataset().getItems();
while (c.next()) {
if (c.wasRemoved()) {
// Delete strict reference, because if the user has added multiple new
// objects in the table, they could be equal.
final ArrayList removedCopy = new ArrayList(c.getRemoved());
dataset.removeIf(item -> {
final Iterator it = removedCopy.iterator();
while (it.hasNext()) {
if (item == it.next()) {
it.remove();
return true;
}
}
return false;
});
}
if (c.wasAdded()) {
dataset.addAll(c.getAddedSubList());
}
}
}
/**
* Utility method to set {@link Session}'s {@link ProcessContext}.
*
* @param session
*/
public static void setProcessContext(final Session session){
final ProcessContext processContext;
if (session.getProcessContext() != null) {
processContext = session.getProcessContext();
// We keep previous selection, but clean tracking points which does
// not exist anymore. After this operation, if we've got no more
// point selected, we reset selection.
// processContext.getTrackingPoints().retainAll(session.getDataset().getTrackingPoints());
// if (processContext.getTrackingPoints().isEmpty()) {
// processContext.getTrackingPoints().addAll(session.getDataset().getTrackingPoints());
// }
} else {
// Create a process context. By default, we activate all available indicators for the edited protocol.
processContext = new ProcessContext(session.getDataset(), session.getDataContext());
session.getIndicators().stream()
.filter(i -> i.getProtocol() == session.getDataContext().getProtocol())
.forEach(i -> processContext.getIndicators().add(i));
//processContext.getTrackingPoints().addAll(session.getDataset().getTrackingPoints());
}
session.setProcessContext(processContext);
}
/**
* On error selection, we try to retrieve the origin statement, then focus
* on it in the statement editor.
*
* @param obs
* @param oldValue
* @param newValue
*/
private void selectedErrorChanged(final ObservableValue<? extends ConstraintViolation> obs, final ConstraintViolation oldValue, final ConstraintViolation newValue) {
if (newValue != null) {
if (newValue.getLeafBean() instanceof Statement) {
final Statement st = (Statement) newValue.getLeafBean();
/*
* Find group related to the target statement. We
* need it to initialize statement editor.
*/
for (final Predicate p : uiGroupTable.getItems()) {
if (p.test(st)) {
uiGroupTable.getSelectionModel().select(p);
break;
}
}
final Iterator<Path.Node> it = newValue.getPropertyPath().iterator();
String pName = null;
while (it.hasNext()) {
pName = it.next().getName();
}
uiStatementEditor.focusOn(st, pName);
}
}
}
/**
* Display a file chooser to allow user to choose a CSV file to open or save.
* @param title The title to display on the file chooser header.
* @param saveMode True to show file chooser in save mode (see {@link FileChooser#showSaveDialog(javafx.stage.Window) }).
* False to use opening mode (See {@link FileChooser#showOpenDialog(javafx.stage.Window) }.
*
* @return The file selected by user. Can be null if user cancelled.
*/
private File chooseCSV(final String title, final boolean saveMode) {
final FileChooser chooser = new FileChooser();
chooser.setTitle(title);
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Fichier CSV", "*.csv"));
final File previousPath = Rhomeo.getPreviousPath(FXDatasetPane.class);
if (previousPath != null) {
chooser.setInitialDirectory(previousPath);
}
File result = saveMode?
chooser.showSaveDialog(getScene().getWindow()):
chooser.showOpenDialog(getScene().getWindow());
if (result != null) {
Rhomeo.setPreviousPath(FXDatasetPane.class, result.getParentFile());
if (!CSV_SUFFIX.matcher(result.getName()).find()) {
result = new File(result.getParentFile(), result.getName().concat(".csv"));
}
}
return result;
}
private static class Transformer<I, O> implements Iterator<O> {
final ObjectWrapper<I, O> wrapper;
final Iterator<I> source;
public Transformer(ObjectWrapper<I, O> wrapper, Iterator<I> source) {
this.wrapper = wrapper;
this.source = source;
}
@Override
public boolean hasNext() {
return source.hasNext();
}
@Override
public O next() {
return wrapper.apply(source.next());
}
}
}
@@ -0,0 +1,160 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.api.ui.StatementEditorSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p02.list.PhysionomyFlore;
import fr.cenra.rhomeo.protocol.p02.FloreStatement;
import java.util.Arrays;
import java.util.List;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.util.StringConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class FXFloreEditor extends AbstractCustomStatementEditor<FloreStatement> {
@FXML
private ComboBox<PhysionomyFlore> uiPhysio;
public FXFloreEditor() {
super(FloreStatement.class);
Rhomeo.loadFXML(this, FXFloreEditor.class);
Rhomeo.initComboBox(uiPhysio, FXCollections.observableList(Arrays.asList(PhysionomyFlore.values())));
uiPhysio.valueProperty().addListener(this::physionomyChanged);
uiPhysio.setConverter(new StringConverter<PhysionomyFlore>() {
@Override
public String toString(PhysionomyFlore object) {
if (object == null)
return null;
return new StringBuilder(object.getCode()).append(" - ").append(object.getValue()).toString();
}
@Override
public PhysionomyFlore fromString(String string) {
if (string == null || (string = string.trim()).isEmpty()) {
return null;
}
return RhomeoCore.getByCode(PhysionomyFlore.class, string.split("\\s")[0]).orElse(null);
}
});
}
@Override
protected void init() {
super.init();
if (uiStatementTable != null)
uiStatementTable.sortColumns(Arrays.asList("cd_nom", "abundance"));
}
@Override
protected String[] getFieldsToIgnore() {
final String[] toIgnore = super.getFieldsToIgnore();
final String[] overrided = new String[toIgnore.length + 2];
overrided[0] = "physionomy";
overrided[1] = "strate";
System.arraycopy(toIgnore, 0, overrided, 2, toIgnore.length);
return overrided;
}
@Override
protected FloreStatement initStatement() throws ReflectiveOperationException {
final FloreStatement data = super.initStatement();
if (uiPhysio.getValue() instanceof PhysionomyFlore) {
((FloreStatement) data).setPhysionomy(uiPhysio.getValue().getCode());
}
return data;
}
/*
* UI LISTENERS
*/
private void physionomyChanged(final ObservableValue<? extends PhysionomyFlore> obs, final PhysionomyFlore oldPhysio, final PhysionomyFlore newPhysio) {
final String newCode = newPhysio == null ? null : newPhysio.getCode();
if (uiStatementTable == null || uiStatementTable.getItems() == null)
return;
for (int i = 0; i < uiStatementTable.getItems().size(); i++) {
uiStatementTable.getItems().get(i).physionomyProperty().set(newCode);
}
}
@Override
protected void itemsChanged(final ObservableValue<? extends List> obs, final List<FloreStatement> oldItems, final List<FloreStatement> newItems) {
super.itemsChanged(obs, oldItems, newItems);
if (newItems != null && !newItems.isEmpty()) {
uiPhysio.setValue(RhomeoCore.getByCode(PhysionomyFlore.class, newItems.get(0).getPhysionomy()).orElse(null));
} else {
uiPhysio.setValue(null);
}
}
@Component
public static class Spi implements StatementEditorSpi<FloreStatement> {
@Autowired
Session session;
@Override
public StatementEditor<FloreStatement> createEditor() {
return session.createPrototype(FXFloreEditor.class);
}
@Override
public Class<FloreStatement> getDataType() {
return FloreStatement.class;
}
}
}
@@ -0,0 +1,79 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.api.ui.StatementEditorSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p05.OrthopteraStatement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Samuel Andrés (Geomatys) <samuel.andres at geomatys.com>
*/
@Component
@Scope("prototype")
public class FXOrthopteraEditor extends AbstractCustomStatementEditor<OrthopteraStatement> {
public FXOrthopteraEditor() {
super(OrthopteraStatement.class);
Rhomeo.loadFXML(this, FXFloreEditor.class);
}
@Component
public static class Spi implements StatementEditorSpi<OrthopteraStatement> {
@Autowired
Session session;
@Override
public StatementEditor<OrthopteraStatement> createEditor() {
return session.createPrototype(FXOrthopteraEditor.class);
}
@Override
public Class<OrthopteraStatement> getDataType() {
return OrthopteraStatement.class;
}
}
}
@@ -0,0 +1,87 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.api.ui.StatementEditorSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p07.P07Statement;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class FXP07Editor extends AbstractCustomStatementEditor<P07Statement> {
public FXP07Editor() {
super(P07Statement.class);
Rhomeo.loadFXML(this, FXFloreEditor.class);
}
@Override
protected void init() {
super.init();
if (uiStatementTable != null)
uiStatementTable.sortColumns(Arrays.asList("cd_nom", "number"));
}
@Component
public static class Spi implements StatementEditorSpi<P07Statement> {
@Autowired
Session session;
@Override
public StatementEditor<P07Statement> createEditor() {
return session.createPrototype(FXP07Editor.class);
}
@Override
public Class<P07Statement> getDataType() {
return P07Statement.class;
}
}
}
@@ -0,0 +1,207 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.ui.FilterTable;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p01.P01;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.util.Callback;
/**
* A simple table whose aim is to display tracking point information.
* @author Alexis Manin (Geomatys)
*/
public class FXTrackingPointTable extends FilterTable<ByNameAndDateFilter> {
private static final Callback<TableColumn<ByNameAndDateFilter, LocalDate>, TableCell<ByNameAndDateFilter, LocalDate>> DATE_CELL_FACTORY = FXTrackingPointTable::createDateCell;
private final Session session;
private final TableColumn<ByNameAndDateFilter, Integer> nbColumn;
private final FilteredList<Statement> dataset;
public FXTrackingPointTable(final Session session) {
this.session = session;
final TableColumn<ByNameAndDateFilter, String> nameColumn = new TableColumn("Nom");
nameColumn.setCellValueFactory(input -> new SimpleStringProperty(input.getValue().getName()));
final TableColumn<ByNameAndDateFilter, LocalDate> dateColumn = new TableColumn("Date");
dateColumn.setCellValueFactory(input -> (ObservableValue) new SimpleObjectProperty<>(input.getValue().getDate()));
dateColumn.setCellFactory(DATE_CELL_FACTORY);
// HACK for column name (we should not do it that way, but time...)
String nbTitle = "Nombre";
if (session.getDataContext() != null && session.getDataContext().getProtocol() != null) {
final Protocol p = session.getDataContext().getProtocol();
if (p instanceof P01)
nbTitle = "Nombre d'horizons";
else
nbTitle = "Nombre d'observations";
}
nbColumn = new TableColumn(nbTitle);
nbColumn.setCellValueFactory(this::getSubsetSize);
getColumns().addAll(nameColumn, dateColumn, nbColumn);
setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
setItems(FXCollections.observableArrayList());
dataset = session.getDataset().getItems().filtered(null); // Prepare a filtered list to allow user setting filters.
dataset.addListener(this::datasetChanged);
addFilters(dataset);
}
@Override
public Predicate<? super Statement> getPreFilter() {
return dataset.getPredicate();
}
@Override
public void setPreFilter(final Predicate<Statement> filter) {
dataset.setPredicate(filter);
}
@Override
public ObjectProperty<Predicate<? super Statement>> preFilterProperty() {
return dataset.predicateProperty();
}
private ObservableValue<Integer> getSubsetSize(final TableColumn.CellDataFeatures<ByNameAndDateFilter, Integer> cellFeature) {
// If given value and current dataset are not null, we set column value as tracking point subset size.
if (cellFeature.getValue() != null && session != null && session.getDataset() != null) {
return (ObservableValue) Bindings.size(dataset.filtered(cellFeature.getValue()));
} else {
return new SimpleObjectProperty<>(null);
}
}
/**
* Every time session dataset changes, we update list of available groups.
* @param c Changes which occurred on surveyed data.
*/
private void datasetChanged(final ListChangeListener.Change<? extends Statement> c) {
while (c.next()) {
if (c.wasUpdated()) {
removeObsolete();
addFilters(c.getList().subList(c.getFrom(), c.getTo()));
} else {
if (c.wasRemoved()) {
removeObsolete();
}
addFilters(c.getAddedSubList());
}
}
}
/**
* Delete from table all useless entries. I.e all entries which don't have
* any statement associated anymore, and not being currently edited.
*/
private void removeObsolete() {
/* HACK : To find obsolete groups, we just browse count column, and
* delete matching items for each count equal to 0.
*/
final Iterator<ByNameAndDateFilter> it = getItems().iterator();
Integer nb;
ByNameAndDateFilter current;
while (it.hasNext()) {
current = it.next();
// Even if the current group is empty, we don't delete
// it if user is working with it.
if (current.equals(getSelectionModel().getSelectedItem()))
continue;
nb = nbColumn.getCellData(current);
if (nb == null || nb == 0)
it.remove();
}
}
/**
* Add filters matching given data name/date pairs.
* @param dataset Data which will serve to build list of filters to add.
*/
private void addFilters(final List<? extends Statement> dataset) {
if (dataset == null || dataset.isEmpty())
return;
final HashSet<ByNameAndDateFilter> set = new HashSet<>();
for (final Statement st : dataset) {
set.add(new ByNameAndDateFilter(st.getTrackingPoint(), st.getDate()));
}
synchronized (this) {
set.removeAll(getItems()); // Ensure no doublon is added.
getItems().addAll(set);
}
}
private static TableCell<ByNameAndDateFilter, LocalDate> createDateCell(final TableColumn<ByNameAndDateFilter, LocalDate> column) {
return new TableCell<ByNameAndDateFilter, LocalDate>() {
@Override
protected void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
if (empty)
setText(null);
else if (item == null)
setText(" - ");
else
setText(item.toString());
}
};
}
}
@@ -0,0 +1,192 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.core.RhomeoCore;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import org.apache.sis.util.ArgumentChecks;
/**
*
* @author Alexis Manin (Geomatys)
*/
public class ObjectTable<T> extends TableView<T> {
private final Class<T> dataType;
private final BeanInfo beanInfo;
/**
* Create a new table to expose data type properties.
*
* @param objectType Type of row item objects.
* @param blackList A set of properties to ignore when building table columns.
*
* @throws IntrospectionException
*/
public ObjectTable(final Class objectType, String... blackList) throws IntrospectionException {
ArgumentChecks.ensureNonNull("Data type", objectType);
dataType = objectType;
beanInfo = Introspector.getBeanInfo(objectType, Object.class);
final Set<String> toIgnore;
if (blackList == null || blackList.length <= 0) {
toIgnore = Collections.EMPTY_SET;
} else {
toIgnore = new HashSet(Arrays.asList(blackList));
}
// Index methods by name to find them easily
final MethodDescriptor[] mds = beanInfo.getMethodDescriptors();
final Map<String, Method> methods = new HashMap<>(mds.length);
for (final MethodDescriptor md : mds) {
methods.put(md.getName(), md.getMethod());
}
final boolean bundleAvailable = InternationalResource.class.isAssignableFrom(dataType);
boolean editable = false;
for (final PropertyDescriptor desc : beanInfo.getPropertyDescriptors()) {
if (toIgnore.contains(desc.getName()))
continue;
try {
if (bundleAvailable) {
try {
desc.setDisplayName(InternationalResource.getResourceString((Class) dataType, desc.getName(), "label"));
desc.setShortDescription(InternationalResource.getResourceString((Class) dataType, desc.getName(), "tooltip"));
} catch (Exception e) {
RhomeoCore.LOGGER.log(Level.FINE, "No bundle available for property ".concat(desc.getName()), e);
}
}
final PropertyColumn<T> col = new PropertyColumn<>(desc, methods.get(desc.getName().concat("Property")));
if (col.isEditable())
editable = true;
getColumns().add(col);
} catch (IllegalArgumentException e) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot create a table column for property ".concat(desc.getName()), e);
}
}
setEditable(editable);
setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
}
/**
* Try to sort table columns according to the names given in input list. Names
* in input list must be a property name, or (only for columns not working with object properties)
* the text of the column. Order in the list will define the order of the columns.
*
* @param orderedNames Order to apply on this table columns.
*/
public void sortColumns(final String... orderedNames) {
if (orderedNames == null || orderedNames.length < 1)
return;
sortColumns(Arrays.asList(orderedNames));
}
public void sortColumns(final List<String> orderedNames) {
if (orderedNames == null || orderedNames.isEmpty())
return;
getColumns().sort((TableColumn<T, ?> o1, TableColumn<T, ?> o2) -> {
final String firstName, secondName;
if (o1 instanceof PropertyColumn) {
firstName = ((PropertyColumn)o1).descriptor.getName();
} else {
firstName = o1.getText();
}
if (o2 instanceof PropertyColumn) {
secondName = ((PropertyColumn)o2).descriptor.getName();
} else {
secondName = o2.getText();
}
final int firstOccur = orderedNames.indexOf(firstName);
final int secondOccur = orderedNames.indexOf(secondName);
// Second property not in the given list. No change in ordering.
if (secondOccur < 0)
return -1;
// First property not found. Moved to the back.
else if (firstOccur < 0)
return 1;
else
return orderedNames.indexOf(firstName) - orderedNames.indexOf(secondName);
});
}
public Class<T> getDataType() {
return dataType;
}
public void focusOn(T item, String propertyName) {
final int rowIndex = getItems().indexOf(item);
if (rowIndex >= 0) {
if (propertyName == null || (propertyName = propertyName.trim()).isEmpty()) {
getSelectionModel().select(item);// No property given. Select row.
} else {
// Try to find the column of the property, to focus / edit queried cell.
for (final TableColumn col : getColumns()) {
if (col instanceof PropertyColumn && ((PropertyColumn) col).descriptor.getName().equals(propertyName)) {
if (col.isEditable()) {
edit(rowIndex, col);
} else {
getFocusModel().focus(rowIndex, col);
}
return;
}
}
}
}
}
}
@@ -0,0 +1,104 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.api.ui.StatementEditorSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p01.PedologyStatement;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TableView;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class P01Editor extends AbstractCustomStatementEditor<PedologyStatement> {
public P01Editor() {
super(PedologyStatement.class);
Rhomeo.loadFXML(this, P01Editor.class);
}
@Override
protected void init() {
super.init();
if (uiStatementTable != null) {
uiStatementTable.setMinHeight(30);
uiStatementTable.setMaxSize(Double.MAX_VALUE, USE_COMPUTED_SIZE);
uiStatementTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
uiStatementTable.sortColumns("numberH", "depth", "limits", "color", "value", "chroma", "texture", "structure", "rawElements", "roots", "stains", "abundance", "size", "shape", "humidity", "compactness", "plasticity", "adhesiveness", "friability", "alterationMo", "vonPost");
}
}
@Override
protected void deleteStatements(ActionEvent evt) {
final Alert alert = new Alert(Alert.AlertType.CONFIRMATION, ResourceBundle.getBundle(P01Editor.class.getCanonicalName()).getString("statement.delete.confirm"), ButtonType.NO, ButtonType.YES);
alert.setResizable(true);
if (ButtonType.YES.equals(alert.showAndWait().orElse(ButtonType.NO))) {
super.deleteStatements(evt);
}
}
@Component
public static class Spi implements StatementEditorSpi<PedologyStatement> {
@Autowired
Session session;
@Override
public StatementEditor<PedologyStatement> createEditor() {
return session.createPrototype(P01Editor.class);
}
@Override
public Class<PedologyStatement> getDataType() {
return PedologyStatement.class;
}
}
}
@@ -0,0 +1,222 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Statement;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import static fr.cenra.rhomeo.fx.edition.FXDatasetPane.setProcessContext;
import fr.cenra.rhomeo.protocol.p04.I04;
import fr.cenra.rhomeo.protocol.p04.I07;
import fr.cenra.rhomeo.protocol.p04.P04Statement;
import java.beans.IntrospectionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.gui.javafx.util.TaskManager;
/**
*
* @author Alexis Manin (Geomatys)
*/
public class P04Editor extends BorderPane {
@FXML
private Button uiFinalize;
@FXML
private BorderPane uiTableContainer;
@FXML
private Button uiDelete;
private final Session session;
private final StatementTable<P04Statement> table;
DoubleBinding sizeBinding = null;
public P04Editor(final Session session) throws IntrospectionException {
ArgumentChecks.ensureNonNull("Session", session);
this.session = session;
Rhomeo.loadFXML(this);
table = new StatementTable<>(P04Statement.class, "trackingPoint", "date", "remarks");
table.sortColumns("year", "habitat", "humine", "acids", "pt", "cot");
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
uiTableContainer.setCenter(table);
if (session.getDataset() != null) {
table.setItems(session.getDataset().getItems());
table.setFixedCellSize(32);
sizeBinding = Bindings.createDoubleBinding(() -> {
final int elementNb = table.getItems().size() < 1? 1 : table.getItems().size();
return table.getFixedCellSize() * elementNb + 30; // Header size is not counted in element numbers, so we add a hard-coded size for it.
}, table.getItems(), table.fixedCellSizeProperty());
table.prefHeightProperty().bind(sizeBinding);
}
uiDelete.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());
uiFinalize.disableProperty().bind(Bindings.isEmpty(table.getItems()));
}
@FXML
void addStatement(ActionEvent event) throws ReflectiveOperationException {
session.getDataset().getItems().add(session.getDataContext().getProtocol().getDataType().newInstance());
}
@FXML
void deleteSelection(ActionEvent event) {
// Defensive copy
final Collection<Statement> selectedItems = new ArrayList<>(table.getSelectionModel().getSelectedItems());
// Delete strict reference, because if the user has added multiple new
// objects in the table, they could be equal.
if (!selectedItems.isEmpty()) {
session.getDataset().getItems().removeIf(item -> {
for (final Statement selected : selectedItems) {
if (selected == item)
return true;
}
return false;
});
}
}
@FXML
void validate(ActionEvent event) {
final Validator validator = session.getBean(Validator.class);
final Set<ConstraintViolation<Dataset>> result = validator.validate(session.getDataset());
if (result != null && !result.isEmpty()) {
final StringBuilder builder = new StringBuilder("La saisie n'est pas valide : ");
int index;
for (final ConstraintViolation cv : result) {
builder.append(System.lineSeparator()).append(" - ");
index = -1;
final Object leafBean = cv.getLeafBean();
for (int i = 0 ; i < table.getItems().size() ; i++) {
if (leafBean == table.getItems().get(i)) {
index = i;
break;
}
}
if (index >= 0) {
builder.append("Ligne ").append(index + 1).append(" : ");
}
builder.append(cv.getMessage());
}
Rhomeo.showAlert(Alert.AlertType.WARNING, builder.toString());
return;
}
// Get P04 indicators
Indicator i04 = null;
Indicator i07 = null;
for (final Indicator i : session.getIndicators()) {
if (I04.NAME.equals(i.getName())) {
i04 = i;
if (i07 != null)
break;
} else if (I07.NAME.equals(i.getName())) {
i07 = i;
if (i04 != null)
break;
}
}
// Ask indicators to convert seized data into indices.
final List<fr.cenra.rhomeo.api.process.Process> processes = new ArrayList<>();
if (i04 != null) {
i04.createProcesses(new ProcessContext(session.getDataset(), session.getDataContext())).forEach(p -> {
TaskManager.INSTANCE.submit(p);
processes.add(p);
});
}
if (i07 != null) {
i07.createProcesses(new ProcessContext(session.getDataset(), session.getDataContext())).forEach(p -> {
TaskManager.INSTANCE.submit(p);
processes.add(p);
});
}
// Aggregate / sort results.
final Task<Set<Index>> t = new TaskManager.MockTask("Traitement des résultats...", () -> {
Set<Index> indices = new HashSet<>();
for (final fr.cenra.rhomeo.api.process.Process p : processes) {
try {
indices.addAll(p.get());
} catch (Exception ex) {
Rhomeo.LOGGER.log(Level.WARNING, "A process failed", ex);
}
}
return indices;
});
// Proceed to finalization.
t.setOnSucceeded((evt) -> {
session.setResults(t.getValue());
session.requestWorkflowStep(WorkflowStep.FINALIZATION);
});
t.setOnFailed((evt) -> Rhomeo.showAlert(Alert.AlertType.WARNING, "Une erreur s'est produite. Impossible de procéder à la finalisation."));
TaskManager.INSTANCE.submit(t);
setProcessContext(session);
}
}
@@ -0,0 +1,109 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.ui.ProtocolEditorSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.protocol.p04.P04;
import java.beans.IntrospectionException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import javafx.scene.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
public class P04EditorSpi implements ProtocolEditorSpi {
@Autowired
@Qualifier(P04.NAME)
Protocol p04;
@Autowired
Session session;
final Set<WorkflowStep> toIgnore;
private P04EditorSpi() {
final HashSet<WorkflowStep> steps = new HashSet<>(2);
steps.add(WorkflowStep.PROCESS);
steps.add(WorkflowStep.RESULT);
toIgnore = Collections.unmodifiableSet(steps);
}
@Override
public Protocol getProtocol() {
return p04;
}
@Override
public Node getEditor(WorkflowStep step) {
Node result = null;
switch (step) {
case DATASET: {
try {
result = new P04Editor(session);
} catch (IntrospectionException ex) {
Rhomeo.LOGGER.log(Level.WARNING, "Cannot initialize edition panel for P04", ex);
}
}
break;
case PROCESS:
case RESULT:
throw new IllegalStateException("Queried step is invalid for this protocol wizard !");
}
return result;
}
@Override
public Set<WorkflowStep> getStepsToIgnore() {
return toIgnore;
}
}
@@ -0,0 +1,161 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.ui.StatementEditor;
import fr.cenra.rhomeo.api.ui.StatementEditorSpi;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.protocol.p06.Habitat;
import fr.cenra.rhomeo.protocol.p06.OdonataStatement;
import java.util.Arrays;
import java.util.List;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.util.StringConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class P06Editor extends AbstractCustomStatementEditor<OdonataStatement> {
@FXML
private ComboBox<Habitat> uiHabitat;
public P06Editor() {
super(OdonataStatement.class);
Rhomeo.loadFXML(this, P06Editor.class);
Rhomeo.initComboBox(uiHabitat, FXCollections.observableList(Arrays.asList(Habitat.values())));
uiHabitat.valueProperty().addListener(this::habitatChanged);
uiHabitat.setConverter(new StringConverter<Habitat>() {
@Override
public String toString(Habitat object) {
if (object == null)
return null;
return new StringBuilder(object.getCode()).append(" - ").append(object.getValue()).toString();
}
@Override
public Habitat fromString(String string) {
if (string == null || (string = string.trim()).isEmpty()) {
return null;
}
return RhomeoCore.getByCode(Habitat.class, string.split("\\s")[0]).orElse(null);
}
});
}
@Override
protected void init(){
super.init();
if (uiStatementTable != null)
uiStatementTable.sortColumns(Arrays.asList("cd_nom", "behavior"));
}
@Override
protected String[] getFieldsToIgnore() {
String[] fieldsToIgnore = super.getFieldsToIgnore();
fieldsToIgnore = Arrays.copyOf(fieldsToIgnore, fieldsToIgnore.length +1);
fieldsToIgnore[fieldsToIgnore.length -1] = "habitat";
return fieldsToIgnore;
}
@Override
protected OdonataStatement initStatement() throws ReflectiveOperationException {
final OdonataStatement data = super.initStatement();
if (uiHabitat.getValue() instanceof Habitat) {
data.setHabitat(uiHabitat.getValue().getCode());
}
return data;
}
/*
* UI LISTENERS
*/
private void habitatChanged(final ObservableValue<? extends Habitat> obs, final Habitat oldPhysio, final Habitat newPhysio) {
final String newCode = newPhysio == null? null : newPhysio.getCode();
if (uiStatementTable == null || uiStatementTable.getItems() == null)
return;
for (int i = 0 ; i < uiStatementTable.getItems().size() ; i++) {
uiStatementTable.getItems().get(i).habitatProperty().set(newCode);
}
}
@Override
protected void itemsChanged(final ObservableValue<? extends List> obs, final List<OdonataStatement> oldItems, final List<OdonataStatement> newItems) {
super.itemsChanged(obs, oldItems, newItems);
if (newItems == null || newItems.isEmpty()) {
uiHabitat.setValue(null);
} else {
uiHabitat.setValue(RhomeoCore.getByCode(Habitat.class, newItems.get(0).getHabitat()).orElse(null));
}
}
@Component
public static class Spi implements StatementEditorSpi<OdonataStatement> {
@Autowired
Session session;
@Override
public StatementEditor<OdonataStatement> createEditor() {
return session.createPrototype(P06Editor.class);
}
@Override
public Class<OdonataStatement> getDataType() {
return OdonataStatement.class;
}
}
}
@@ -0,0 +1,728 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import com.vividsolutions.jts.geom.Geometry;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.Version;
import fr.cenra.rhomeo.api.annotations.RefersTo;
import fr.cenra.rhomeo.api.data.Reference;
import fr.cenra.rhomeo.api.data.Warning;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.data.reference.Taxref;
import fr.cenra.rhomeo.core.util.RelationResolver;
import java.beans.PropertyDescriptor;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.collections.MapChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.util.StringConverter;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.apache.sis.parameter.ParameterBuilder;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Numbers;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.gui.javafx.parameter.FXValueEditor;
import org.geotoolkit.gui.javafx.parameter.FXValueEditorSpi;
import org.geotoolkit.gui.javafx.util.ComboBoxCompletion;
import org.opengis.parameter.ParameterDescriptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* A column whose aim is to display and offer edition capabilities on a specific
* POJO property. The display / edition capabilities are computed using reflection.
*
* @author Alexis Manin (Geomatys)
* @param <T> Type of object found in a row of the parent table.
*/
@Component
@Scope("prototype")
public class PropertyColumn<T> extends TableColumn<T, Object> {
static final KeyHandler KEY_HANDLER = new KeyHandler();
static final StringConverter BASIC_CONVERTER = new BasicConverter();
@Autowired
private Validator validator;
@Autowired
RelationResolver resolver;
/**
* Descriptor of the property to manage in the column (property extracted
* from a row item).
*/
final PropertyDescriptor descriptor;
/**
* A boolean indicating if {@link #observableGetter} implements {@link WritableValue}
* interface.
*/
final boolean isWritableValue;
/**
* A method extracted from input {@link PropertyDescriptor} to write target
* property.
*/
final Method writeMethod;
/**
* A method extracted from input {@link PropertyDescriptor} to read target
* property.
*/
final Method readMethod;
/**
* A method with no parameter, whose aim is to send back an observable
* object on target property.
*/
final Method observableGetter;
/**
* An annotation extracted from getters to know if the property to manage
* is referring to another object.
*/
final RefersTo referent;
public final SimpleObjectProperty<FXValueEditor> editorProperty = new SimpleObjectProperty<>();
// Hack for issue #75
final boolean isSpecies;
/**
* A weak bound to the converter used for transformation of column values
* in text. We use weak reference here mainly because of application
* {@link Reference} which are loaded asynchronously (user can change used
* version, etc.). Also, it avoid to keep a (possibly) consequent memory
* chunk when not needed.
*/
private WeakReference<StringConverter> textConverterRef;
public PropertyColumn(final PropertyDescriptor desc, final Method observableGetter) {
super();
ArgumentChecks.ensureNonNull("Property descriptor", desc);
Session.getInstance().injectDependencies(this);
descriptor = desc;
writeMethod = desc.getWriteMethod();
readMethod = desc.getReadMethod();
this.observableGetter = observableGetter;
/*
* Analyze given parameters to determine how to handle cell content.
*/
boolean tmpIsProperty = false;
boolean isObservableValue = false;
if (observableGetter != null) {
Class<?> observableType = observableGetter.getReturnType();
if (WritableValue.class.isAssignableFrom(observableType)) {
tmpIsProperty = true;
} else if (ObservableValue.class.isAssignableFrom(observableType)) {
isObservableValue = true;
}
}
isWritableValue = tmpIsProperty;
if (isWritableValue || isObservableValue) {
setCellValueFactory((param) -> {
try {
return (ObservableValue) observableGetter.invoke(param.getValue());
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot acces observable property ".concat(desc.getName()), ex);
return null;
}
});
} else if (readMethod != null) {
readMethod.setAccessible(true);
setCellValueFactory((param) -> {
try {
return new SimpleObjectProperty<>(readMethod.invoke(param.getValue()));
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot acces property ".concat(desc.getName()), ex);
return null;
}
});
} else {
throw new IllegalArgumentException("Cannot extract values from given property descriptor !");
}
RefersTo tmpReferent = null;
if (readMethod != null) {
tmpReferent = readMethod.getAnnotation(RefersTo.class);
}
if (tmpReferent == null && observableGetter != null) {
tmpReferent = observableGetter.getAnnotation(RefersTo.class);
}
referent = tmpReferent;
// Hack #75
isSpecies = referent == null? false : referent.property().equals(Taxref.ATT_CD_NOM);
/* When editing a property pointing on a reference, we listen on data
* context changes to update choice list when user change the version
* used.
*/
if (referent != null && Reference.class.isAssignableFrom(referent.type())) {
try {
Session.getInstance().getDataContext().getReferences().addListener((MapChangeListener.Change<? extends Class<Reference>, ? extends Version> change) -> {
if (referent.type().equals(change.getKey())) {
initEditor();
}
});
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.FINE, "Cannot listen on data context changes.", e);
}
}
initEditor();
setCellFactory(this::createPropertyCell);
editableProperty().bind(editorProperty.isNotNull());
setText(desc.getDisplayName());
String remarks = desc.getShortDescription();
if (remarks != null && !remarks.equals(desc.getDisplayName()) && !(remarks = remarks.trim()).isEmpty()) {
final Label label = new Label(FontAwesomeIcons.ICON_INFO_CIRCLE);
label.setTooltip(new Tooltip(remarks));
label.setStyle("-fx-font-family: FontAwesome; -fx-font-size:16;");
setGraphic(label);
}
// # 86 : Compare species using their name
if (isSpecies) {
setComparator((o1, o2) -> {
if (o1 == o2) // both null or same reference
return 0;
else if (o1 == null)
return 1;
else if (o2 == null)
return -1;
else if (o1.equals(o2))
return 0;
final StringConverter converter = getOrCreateTextConverter();
final String s1 = converter.toString(o1);
final String s2 = converter.toString(o2);
if (s1 == s2) // both null or same reference
return 0;
else if (s1 == null)
return 1;
else if (s2 == null)
return -1;
return s1.compareTo(s2);
});
}
}
private void initEditor() {
/* If there is writing possibility, we try to setup an editor for the
* column cells.
*/
if (isWritableValue || writeMethod != null) {
Set possibleValues = null;
if (referent != null) {
try {
possibleValues = resolver.getPossibleValues(referent);
} catch (Exception ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot load choice list for property ".concat(descriptor.getName()), ex);
}
}
final ParameterDescriptor parameterDescriptor;
final ParameterBuilder builder = new ParameterBuilder().addName(descriptor.getName()).setRequired(false);
if (possibleValues != null) {
parameterDescriptor = builder.createEnumerated((Class) Numbers.primitiveToWrapper(descriptor.getPropertyType()), possibleValues.toArray(), null);
} else {
parameterDescriptor = builder.create(descriptor.getPropertyType(), null);
}
Optional<FXValueEditor> editor = FXValueEditorSpi.findEditor(parameterDescriptor);
if (editor.isPresent()) {
// If given editor is a combo box, we activate auto-completion on it.
final FXValueEditor tmpEditor = editor.get();
Optional<ComboBox> optCombo = Rhomeo.findComboBox(tmpEditor.getComponent());
if (optCombo.isPresent()) {
ComboBox cBox = optCombo.get();
// See ComboBoxListViewSkin
cBox.getProperties().put("comboBoxRowsToMeasureWidth", 7);
setConverter(cBox);
try {
cBox.setEditable(true);
} catch (RuntimeException e) {
// A runtime exception is launched if the editable property is already bound
// Just log and let the property bound and handled by something else.
Rhomeo.LOGGER.log(Level.FINE, e.getLocalizedMessage(), e);
}
ComboBoxCompletion.autocomplete(cBox);
// HACK : Force auto-completion to reset when starting edition.
cBox.focusedProperty().addListener((obs, oldVal, newVal) -> {
if (newVal)
cBox.fireEvent(new KeyEvent(KeyEvent.KEY_RELEASED, "", "", KeyCode.UNDEFINED, false, false, false, false));
});
} else {
Rhomeo.prepareSpinners(tmpEditor.getComponent());
}
editorProperty.set(tmpEditor);
}
}
}
private PropertyCell createPropertyCell(TableColumn<T, Object> column) {
return new PropertyCell();
}
public PropertyDescriptor getTarget() {
return descriptor;
}
private void setConverter(ComboBox cBox) {
final StringConverter baseConverter = getOrCreateTextConverter();
final StringConverter converter = new StringConverter() {
@Override
public String toString(Object object) {
return baseConverter.toString(object);
}
@Override
public Object fromString(String string) {
if (string == null || string.isEmpty())
return null;
final Object firstGuess = baseConverter.fromString(string);
if (firstGuess == null) {
for (final Object o : cBox.getItems()) {
if (Objects.equals(string, o.toString()))
return o;
}
}
return null;
}
};
cBox.setConverter(converter);
}
/**
* Create a converter for the elements of this column.
* @return A converter to display this column elements as text. Never null.
*/
private StringConverter getOrCreateTextConverter() {
StringConverter converter = textConverterRef == null? null : textConverterRef.get();
if (converter != null)
return converter;
if (referent != null) {
try {
final Set targets = resolver.getTargetObjects(referent);
final Function extractor = RelationResolver.acquireExtractor(referent);
final HashMap<Object, String> refMap = new HashMap<>(targets.size());
for (final Object o : targets) {
refMap.put(extractor.apply(o), o.toString());
}
converter = new StringConverter() {
@Override
public String toString(Object object) {
if (object == null || (isSpecies && object.equals(0)))
return null;
final String str = refMap.get(object);
if (str != null)
return str;
return BASIC_CONVERTER.toString(object);
}
@Override
public Object fromString(String string) {
if (string == null || string.isEmpty())
return null;
for (final Map.Entry<Object, String> entry : refMap.entrySet()) {
if (string.equals(entry.getValue())) {
return entry.getKey();
}
}
return BASIC_CONVERTER.fromString(string);
}
};
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.FINE, "Cannot tune combobox cells.", e);
}
}
if (converter == null) {
return BASIC_CONVERTER;
} else {
textConverterRef = new WeakReference<>(converter);
return converter;
}
}
/**
* A cell for rendering and edition of a property.
*/
private class PropertyCell extends AutoCommitableTableCell<T, Object> {
/**
* A boolean to know if we're in edition state or not. We store a local
* variable here, because when calling {@link TableCell#startEdit() },
* the edition flag is updated AFTER item update. If we do not want to
* update the view twice on state change, we have to locally store edition
* state, then calling super.startEdit().
*/
boolean edition = false;
EventHandler<MouseEvent> enteredOnError;
final ChangeListener<Boolean> focusListener = this::focusChanged;
@Override
public void cancelEdit() {
edition = false;
super.cancelEdit();
updateItem(getTableColumn().getCellData(getTableRow().getIndex()), false);
removeKeyHandling();
}
@Override
public void commitEdit() {
commitEdit(editorProperty.get().valueProperty().getValue());
}
@Override
public void commitEdit(Object newValue) {
if (isWritableValue) {
WritableValue property = (WritableValue) getTableColumn().getCellObservableValue(getTableRow().getIndex());
property.setValue(newValue);
} else {
try {
writeMethod.invoke(getTableRow().getItem(), newValue);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot commit value !", ex);
}
}
edition = false;
super.commitEdit(newValue);
removeKeyHandling();
editorProperty.get().getComponent().removeEventHandler(KeyEvent.KEY_RELEASED, KEY_HANDLER);
}
@Override
public void startEdit() {
edition = true; // Set to true for view update.
super.startEdit();
if (!(edition = isEditing()))
return;
final FXValueEditor editor = editorProperty.get();
if (editor == null ||
!(isWritableValue || writeMethod != null)) {
throw new IllegalStateException("Cannot start edition on not editable cell.");
}
// Init editor value from cell content.
editor.valueProperty().setValue(getItem());
addKeyHandling();
}
@Override
protected void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (edition && !empty) {
setText(null);
Node node = editorProperty.get().getComponent();
if (node instanceof Region) {
final Region r = (Region) node;
r.setMinSize(0, 0);
r.prefWidthProperty().bind(prefWidthProperty());
r.prefHeightProperty().bind(prefHeightProperty());
}
setGraphic(node);
Optional<ComboBox> opt = Rhomeo.findComboBox(node);
if (opt.isPresent()) {
node = opt.get();
}
node.requestFocus();
node.focusedProperty().addListener(focusListener);
} else {
removeGraphic();
if (empty) {
setText(null);
} else {
setText(getOrCreateTextConverter().toString(item));
}
}
checkValue(item, empty);
}
private void focusChanged(final ObservableValue<? extends Boolean> obs, final Boolean oldValue, final boolean newValue) {
if (edition && !newValue) {
commitEdit();
}
}
/**
* Remove the graphic attached to the cell, freeing its previously bound
* properties in the same time.
*/
private void removeGraphic() {
Node graphic = getGraphic();
if (graphic != null) {
if (graphic instanceof Region) {
final Region r = (Region) graphic;
r.prefWidthProperty().unbind();
r.prefHeightProperty().unbind();
}
Optional<ComboBox> opt = Rhomeo.findComboBox(graphic);
if (opt.isPresent()) {
graphic = opt.get();
}
graphic.focusedProperty().removeListener(focusListener);
}
setGraphic(null);
}
public void checkValue(final Object value, final boolean isEmpty) {
final Object rowItem;
if (isEmpty || getTableRow() == null) {
rowItem = null;
} else {
rowItem = getTableRow().getItem();
}
final Set<ConstraintViolation> result;
if (rowItem == null) { // Empty cell
result = Collections.EMPTY_SET;
} else {
result = validator.validateValue((Class) rowItem.getClass(),
PropertyColumn.this.descriptor.getName(),
value
);
}
if (!result.isEmpty()) {
int warningNumber = 0;
for (final ConstraintViolation cv : result) {
if (cv.getConstraintDescriptor().getPayload().contains(Warning.class))
warningNumber++;
}
if (warningNumber >= result.size()) {
if (!getStyleClass().contains(Rhomeo.CSS_WARNING_FIELD)) {
getStyleClass().add(Rhomeo.CSS_WARNING_FIELD);
}
} else if (!getStyleClass().contains(Rhomeo.CSS_ERROR_FIELD)) {
getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
}
// Activate a tootltip to give user information about what happens.
createErrorTooltip(result).ifPresent(tip -> setTooltip(tip));
} else {
setTooltip(null);
getStyleClass().removeAll(Rhomeo.CSS_ERROR_FIELD, Rhomeo.CSS_WARNING_FIELD);
}
}
/**
* Create a tooltip listing error message of input violations.
* @param errors Errors to display.
* @return An empty optional if input list was null or empty. A tooltip
* with input error messages otherwise.
*/
private Optional<Tooltip> createErrorTooltip(final Collection<ConstraintViolation> errors) {
if (errors == null || errors.isEmpty())
return Optional.empty();
final String text;
if (errors.size() == 1) {
text = errors.iterator().next().getMessage();
} else {
final Iterator<ConstraintViolation> it = errors.iterator();
final StringBuilder builder = new StringBuilder("∙ ");
if (it.hasNext())
builder.append(it.next().getMessage());
while (it.hasNext()) {
builder.append(System.lineSeparator()).append("∙ ").append(it.next().getMessage());
}
text = builder.toString();
}
final Tooltip tip = new Tooltip(text);
tip.getStyleClass().add("info-popup");
return Optional.of(tip);
}
/**
* Set the {@link #KEY_HANDLER} to listen on key events on the given
* cell. The event listener is meant to listen for actions on a cell
* being in edition mode.
*
* Note : If the handler was already listening on another cell, its
* unregistered before setting input cell.
*
* @param target The table cell to activate Key event handling on.
*/
private void addKeyHandling() {
if (KEY_HANDLER.target != null) {
removeKeyHandling();
}
KEY_HANDLER.target = this;
addEventHandler(KeyEvent.KEY_RELEASED, KEY_HANDLER);
addEventFilter(KeyEvent.ANY, KEY_HANDLER);
editorProperty.get().getComponent().addEventHandler(KeyEvent.KEY_RELEASED, KEY_HANDLER);
}
/**
* Set the {@link #KEY_HANDLER} to stop listening on given cell.
*
* @param target The cell to deactivate key event handling for.
*/
private void removeKeyHandling() {
if (KEY_HANDLER != null && KEY_HANDLER.target != null) {
KEY_HANDLER.target.removeEventHandler(KeyEvent.KEY_RELEASED, KEY_HANDLER);
KEY_HANDLER.target.removeEventFilter(KeyEvent.ANY, KEY_HANDLER);
editorProperty.get().getComponent().removeEventHandler(KeyEvent.KEY_RELEASED, KEY_HANDLER);
KEY_HANDLER.target = null;
}
}
}
/**
* Listens on keyboard key release on a given table cell. Default listeners are :
* - ENTER : commit edit
* - TAB : commit edit then edit next column. If no more column is available,
* start editing first column of the next row.
* - ESC : cancel edit.
*/
static class KeyHandler implements EventHandler<KeyEvent> {
AutoCommitableTableCell target;
@Override
public void handle(KeyEvent evt) {
if (target == null)
return;
if (KeyEvent.KEY_RELEASED.equals(evt.getEventType())) {
switch (evt.getCode()) {
case ENTER:
target.commitEdit();
evt.consume();
break;
case TAB:
final TableView tableView = target.getTableView();
TablePosition currentCell = tableView.getEditingCell();
if (currentCell == null)
currentCell = new TablePosition(tableView, 0, null);
target.commitEdit();
Rhomeo.editNextCell(currentCell);
evt.consume();
break;
case ESCAPE:
target.cancelEdit();
evt.consume();
}
} else {
// Filter events
switch (evt.getCode()) {
case TAB:
evt.consume();
}
}
}
}
private static class BasicConverter extends StringConverter<Object> {
@Override
public String toString(Object object) {
if (object instanceof Geometry) {
return "Objet trop complexe pour affichage";
} else if (object != null && Numbers.isFloat(object.getClass())) {
return RhomeoCore.SECOND_DECIMAL_FORMAT.format(object);
} else {
return String.valueOf(object);
}
}
@Override
public Object fromString(String string) {
return null;
}
}
}
@@ -0,0 +1,60 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.edition;
import fr.cenra.rhomeo.api.data.Statement;
import java.beans.IntrospectionException;
import javafx.collections.transformation.SortedList;
/**
*
* @author Alexis Manin (Geomatys)
* @param <T> Type of row items.
*/
public class StatementTable<T extends Statement> extends ObjectTable<T> {
public StatementTable(Class objectType, String... blackList) throws IntrospectionException {
super(objectType, blackList);
itemsProperty().addListener((obs, oldValue, newValue) -> {
if (newValue instanceof SortedList) {
((SortedList)newValue).comparatorProperty().bind(comparatorProperty());
}
});
}
}
@@ -0,0 +1,282 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p08;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.data.reference.GeoReferential;
import fr.cenra.rhomeo.core.data.reference.GeoReferentials;
import java.util.ArrayList;
import java.util.StringJoiner;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.geotoolkit.gui.javafx.util.FXTableView;
import org.geotoolkit.gui.javafx.util.TaskManager;
/**
* Table to download referential data for a site.
*
* @author Johann Sorel (Geomatys)
*/
public class FXGeoReferentialTable extends VBox {
private static final Image CHECKED = new Image("/fr/cenra/rhomeo/fx/p08/u74.png");
private final GeoReferentials geoRef;
private final Site site;
private final TableView<GeoReferential> table = new FXTableView<>();
private final Button downloadButton = new Button("Télécharger");
private final ProgressIndicator progress = new ProgressIndicator();
private final String[] requiredTypes;
final TableColumn<GeoReferential,Boolean> colAvailable;
public FXGeoReferentialTable(GeoReferentials geoRef,Site site, String ... requieredTypes){
this.geoRef = geoRef;
this.site = site;
this.requiredTypes = requieredTypes;
progress.setProgress(-1);
progress.setVisible(false);
progress.setPrefSize(32, 32);
progress.setMinSize(32, 32);
downloadButton.getStyleClass().add("create-button");
downloadButton.setStyle("-fx-text-fill: #FFFFFF;");
downloadButton.managedProperty().bind(downloadButton.visibleProperty());
downloadButton.setPadding(new Insets(5, 20, 5, 20));
final HBox box = new HBox(progress,downloadButton);
box.setPadding(new Insets(5, 5, 5, 5));
box.setAlignment(Pos.CENTER_RIGHT);
getChildren().addAll(table, box);
//table structure
final TableColumn<GeoReferential, Integer> colAnnees = new TableColumn<>("Années disponibles");
colAvailable = new TableColumn<>("Années téléchargées sur le poste");
final TableColumn<GeoReferential,Boolean> colDownload = new TableColumn<>("Années à télécharger");
colAnnees.setCellValueFactory(param -> new SimpleIntegerProperty(param.getValue().getYear()).asObject());
colAvailable.setCellValueFactory((TableColumn.CellDataFeatures<GeoReferential, Boolean> param) -> param.getValue().completed(requiredTypes));
colAvailable.setCellFactory((TableColumn<GeoReferential, Boolean> param) -> new AvailableCell());
colDownload.setCellValueFactory((TableColumn.CellDataFeatures<GeoReferential, Boolean> param) -> param.getValue().toDownloadProperty());
colDownload.setCellFactory((TableColumn<GeoReferential, Boolean> param) -> new DownloadCell());
table.getColumns().addAll(colAnnees, colAvailable, colDownload);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
downloadButton.setOnAction(this::downloadRef);
updateRefTable();
}
private void updateRefTable(){
final Task<ObservableList<GeoReferential>> update = new Task<ObservableList<GeoReferential>>() {
@Override
protected ObservableList<GeoReferential> call() throws Exception {
updateTitle("Mise à jour des références");
final ObservableList<GeoReferential> referentials = geoRef.getReferentials(site);
//remove referentials which don't have all required data
for (int i = referentials.size() - 1; i >= 0; i--) {
if (!referentials.get(i).areAvailable(requiredTypes)) {
referentials.remove(i);
}
}
return referentials;
}
};
update.setOnSucceeded(evt -> Platform.runLater(() -> {
table.setItems(update.getValue());
}));
update.setOnFailed(evt -> Platform.runLater(() -> {
final Throwable ex = evt.getSource().getException();
table.setPlaceholder(new Label(ex.getLocalizedMessage()));
table.setItems(FXCollections.emptyObservableList());
RhomeoCore.LOGGER.log(Level.WARNING, ex.getLocalizedMessage(),ex);
}));
progress.visibleProperty().bind(update.runningProperty());
TaskManager.INSTANCE.submit(update);
}
private void downloadRef(ActionEvent event) {
final Task<?> task = new Task() {
@Override
protected Object call() throws Exception {
int selectedYears = 0;
final ArrayList<Integer> failedYears = new ArrayList();
for (GeoReferential ref : table.getItems()) {
if (ref.toDownloadProperty().get()) {
selectedYears++;
updateTitle("Téléchargement ".concat(String.valueOf(ref.getYear())));
try {
geoRef.downloadReferential(site, ref.getYear());
} catch (IllegalArgumentException e) {
failedYears.add(ref.getYear());
}
}
}
if (selectedYears < 1) {
Platform.runLater(() -> {
final Alert alert = new Alert(Alert.AlertType.WARNING, "Aucune année séléctionnée pour le téléchargement.", ButtonType.CLOSE);
alert.setResizable(true);
alert.show();
});
} else if (!failedYears.isEmpty()) {
final String msg;
if (failedYears.size() < selectedYears) {
final StringJoiner builder = new StringJoiner(", ", "Aucun référentiel n'est disponible pour la zone demandée en ", ".");
failedYears.stream()
.map(year -> String.valueOf(year))
.forEach(year -> builder.add(year));
msg = builder.toString();
} else {
msg = "Aucun référentiel n'est disponible pour les années demandées";
}
Platform.runLater(() -> {
final Alert alert = new Alert(Alert.AlertType.WARNING, msg, ButtonType.CLOSE);
alert.setResizable(true);
alert.show();
});
}
return true;
}
};
progress.visibleProperty().bind(task.runningProperty());
downloadButton.visibleProperty().bind(task.runningProperty().not());
// TODO : display message on illegal argument exception : no data available.
final EventHandler e = (EventHandler) (Event event1) -> updateRefTable();
task.setOnSucceeded(e);
task.setOnFailed(evt -> {
if (evt.getSource().getException() instanceof IllegalArgumentException) {
final Alert alert = new Alert(Alert.AlertType.WARNING, "Aucun référentiel n'est disponible pour la zone demandée en ", ButtonType.CLOSE);
alert.setResizable(true);
alert.show();
}
e.handle(evt);
});
task.setOnCancelled(e);
TaskManager.INSTANCE.submit(task);
}
public ObservableList<GeoReferential> getItems() {
return table.getItems();
}
private static class AvailableCell extends TableCell<GeoReferential, Boolean>{
public AvailableCell() {
}
@Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if(Boolean.TRUE.equals(item)){
setGraphic(new ImageView(CHECKED));
}else{
setText(null);
setGraphic(null);
}
}
}
private class DownloadCell extends TableCell<GeoReferential, Boolean> {
private final CheckBox checkBox = new CheckBox();
private DownloadCell() {
setGraphic(checkBox);
checkBox.setOnAction((ActionEvent event) -> {
TableRow row = tableRowProperty().getValue();
if (row == null)
return;
final GeoReferential r = (GeoReferential) row.getItem();
if (r != null) {
r.toDownloadProperty().set(checkBox.isSelected());
}
});
}
@Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
checkBox.setVisible(!empty);
checkBox.setSelected(Boolean.TRUE.equals(item));
TableRow row = tableRowProperty().getValue();
if (row == null)
return;
final GeoReferential r = (GeoReferential) row.getItem();
if (r != null) {
checkBox.setVisible(!colAvailable.getCellData(r));
}
}
}
}
@@ -0,0 +1,220 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p08;
import com.vividsolutions.jts.geom.Geometry;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.core.BeanUtils;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.data.county.CountyRepository;
import fr.cenra.rhomeo.core.data.reference.GeoReferential;
import fr.cenra.rhomeo.core.data.reference.GeoReferentials;
import fr.cenra.rhomeo.core.util.GeometryUtils;
import fr.cenra.rhomeo.protocol.p08.I12;
import java.awt.Color;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javax.annotation.PostConstruct;
import org.geotoolkit.data.FeatureStoreUtilities;
import org.geotoolkit.data.bean.BeanFeature;
import org.geotoolkit.display2d.GO2Utilities;
import org.geotoolkit.feature.Feature;
import org.geotoolkit.feature.FeatureBuilder;
import org.geotoolkit.gui.javafx.render2d.FXMapFrame;
import org.geotoolkit.map.FeatureMapLayer;
import org.geotoolkit.map.MapBuilder;
import org.geotoolkit.map.MapContext;
import org.geotoolkit.map.MapItem;
import org.geotoolkit.map.MapLayer;
import org.geotoolkit.style.MutableStyleFactory;
import org.opengis.feature.Property;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.expression.PropertyName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Johann Sorel (Geomatys)
*/
@Component
@Scope("prototype")
public class FXP08DatasetPane extends VBox {
@Autowired
private Session session;
@Autowired
private GeoReferentials geoRef;
@Autowired
@Qualifier(I12.NAME)
private Indicator indicator;
@FXML
private Button next;
@FXML
private BorderPane tableSpace;
@Autowired(required = false)
private CountyRepository countyRepository;
private FXGeoReferentialTable table;
public FXP08DatasetPane() {
Rhomeo.loadFXML(this);
}
@PostConstruct
private void postConstruct() {
table = new FXGeoReferentialTable(geoRef, session.getDataContext().getSite(),
GeoReferentials.TYPE_ZONE_HYDRO,
GeoReferentials.TYPE_TACHE_ARTIF,
GeoReferentials.TYPE_TACHE_URBAINE);
tableSpace.setCenter(table);
}
@FXML
void nextStep(ActionEvent event) {
//check there is at least one year of data available
int nbAvailable = 0;
final ObservableList<GeoReferential> items = table.getItems();
for(GeoReferential ref : items){
//we need zone hydro,tache artif and tache_urbaine
if(ref.zoneHydroLocal().get()
&& ref.tacheArtifLocal().get()
&& ref.tacheUrbaineLocal().get()) nbAvailable++;
}
if (nbAvailable <= 0) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante : Aucun réferentiel téléchargé.");
return;
}
final ProcessContext ctx = new ProcessContext(session.getDataset(), session.getDataContext());
ctx.getIndicators().add(indicator);
session.setProcessContext(ctx);
if (!session.requestWorkflowStep(WorkflowStep.PROCESS)) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante. Veuillez vérifier votre lot de données.");
}
}
@FXML
private void showMap() throws Exception {
final Site currentSite = session.getDataContext().getSite();
// Should not be null when called here
if (currentSite == null) {
throw new IllegalArgumentException("Currently selected site was null here, but shouldn't be");
}
// If counties are available, we display them on the map.
final MapContext context = MapBuilder.createContext();
final MutableStyleFactory sf = GO2Utilities.STYLE_FACTORY;
if (countyRepository != null) {
try {
context.layers().add(MapBuilder.createFeatureLayer(countyRepository.getFeatures(), sf.style(sf.lineSymbolizer())
));
} catch (Exception e) {
Rhomeo.LOGGER.log(Level.WARNING, "Cannot show county layer", e);
}
}
final BeanFeature feature = new BeanFeature(currentSite, BeanUtils.createMapping(Site.class, null));
final MapLayer layer = MapBuilder.createFeatureLayer(FeatureStoreUtilities.collection(feature), sf.style(sf.polygonSymbolizer(null, sf.fill(Color.red), null)));
layer.setName("Site");
context.layers().add(layer);
context.setAreaOfInterest(layer.getBounds());
Geometry buffer = GeometryUtils.computeBuffer(currentSite.getGeometry());
final FeatureBuilder fb = new FeatureBuilder(feature.getType());
for (final Property p : feature.getProperties()) {
if (p.getValue() instanceof Geometry)
fb.setPropertyValue(p.getName(), buffer);
else
fb.setPropertyValue(p.getName(), p.getValue());
}
final Feature bufferFeature = fb.buildFeature("buffer");
final FeatureMapLayer bufferLayer = MapBuilder.createFeatureLayer(FeatureStoreUtilities.collection(bufferFeature));
bufferLayer.setName("Buffer");
context.layers().add(1, bufferLayer);
final FilterFactory2 ff = GO2Utilities.FILTER_FACTORY;
final PropertyName geomProp = ff.property("geom");
MapItem zoneHydro = geoRef.getWFSLayers(Optional.of(ff.intersects(geomProp, ff.literal(buffer))), GeoReferentials.TYPE_ZONE_HYDRO);
zoneHydro = zoneHydro.items().get(0);
MapItem wfsLayers = geoRef.getWFSLayers(Optional.of(ff.bbox(null, ((MapLayer)zoneHydro).getBounds())), GeoReferentials.TYPE_TACHE_ARTIF, GeoReferentials.TYPE_TACHE_URBAINE, GeoReferentials.TYPE_RPG);
wfsLayers.setName("BBox filtered");
context.items().add(0, wfsLayers);
final List<Geometry> geomCollection = ((FeatureMapLayer)zoneHydro).getCollection().stream()
.map(feat -> feat.getDefaultGeometryProperty().getValue())
.filter(geom -> geom instanceof Geometry)
.map(geom -> (Geometry) geom)
.collect(Collectors.toList());
Geometry filterGeom = GO2Utilities.JTS_FACTORY.createGeometryCollection(geomCollection.toArray(new Geometry[geomCollection.size()])).union();
wfsLayers = geoRef.getWFSLayers(Optional.of(ff.intersects(geomProp, ff.literal(filterGeom))), GeoReferentials.TYPE_TACHE_ARTIF, GeoReferentials.TYPE_TACHE_URBAINE, GeoReferentials.TYPE_RPG);
wfsLayers.setName("Polygon filtered");
context.items().add(1, wfsLayers);
context.items().add(0, zoneHydro);
FXMapFrame.show(context);
}
}
@@ -0,0 +1,310 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p08;
import com.vividsolutions.jts.geom.Geometry;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.util.ExportUtils;
import fr.cenra.rhomeo.core.util.GeometryUtils;
import fr.cenra.rhomeo.protocol.p08.I12;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import fr.cenra.rhomeo.api.process.Process;
import fr.cenra.rhomeo.fx.p09.FXP09ProcessPane;
import fr.cenra.rhomeo.core.data.reference.GeoReferential;
import fr.cenra.rhomeo.core.data.reference.GeoReferentials;
import fr.cenra.rhomeo.protocol.p08.I12Process;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.BitSet;
import java.util.Optional;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ChoiceDialog;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.internal.GeotkFX;
/**
*
* @author Johann Sorel (Geomatys)
*/
@Component
@Scope("prototype")
public class FXP08ProcessPane extends VBox {
@Autowired
private Session session;
@Autowired
private GeoReferentials geoRef;
@Autowired
@Qualifier(I12.NAME)
private Indicator indicator;
@FXML
private Button next;
@FXML
private Button uiExport;
@FXML
private HBox yearBoxes;
private final List<CheckBox> checkBoxes = new ArrayList<>();
private final SimpleObjectProperty<Task<Set<Index>>> globalTask = new SimpleObjectProperty<>();
private BitSet activatedYears;
private final Preferences prefs;
public FXP08ProcessPane(){
Rhomeo.loadFXML(this);
prefs = Preferences.userNodeForPackage(FXP08ProcessPane.class);
}
@PostConstruct
private void postConstruct() {
//liste available referential state for this site
final ObservableList<GeoReferential> refs;
try {
refs = geoRef.getLocalReferentials(session.getDataContext().getSite())
.filtered(ref -> ref.hasLocalTypes(
GeoReferentials.TYPE_ZONE_HYDRO,
GeoReferentials.TYPE_TACHE_ARTIF,
GeoReferentials.TYPE_TACHE_URBAINE)
)
.sorted();
} catch (IllegalArgumentException | IOException ex) {
// If we cannot load georeferentials, it's no use to go further.
GeotkFX.newExceptionDialog("Impossible de déterminer la liste des réferentiels disponibles. La lecture sur le système a échouée.", ex).show();
session.requestWorkflowStep(WorkflowStep.DATASET);
return;
}
final ByteBuffer yearsAsBytes = ByteBuffer.wrap(new byte[refs.size() * (Integer.SIZE / Byte.SIZE)]);
final IntBuffer years = yearsAsBytes.asIntBuffer();
for (final GeoReferential ref : refs) {
years.put(ref.getYear());
}
years.position(0);
/* Restore selection state from serialized bitset if possible. To
* determine if restoration is possible, we ensure there's data stored
* in preferences, and also that they match current edited site / years.
* Otherwise, we set a new bitset, with default selection on types 17
* and 18.
* We do the same with years, except that by default, they're all selected.
*/
final String siteName = session.getDataContext().getSite().getName();
final boolean restore = prefs.get(FXP09ProcessPane.SITE_NAME, "").equals(siteName)
&& ByteBuffer.wrap(prefs.getByteArray(FXP09ProcessPane.AVAILABLE_YEARS, new byte[0])).asIntBuffer().equals(years);
final byte[] yearSelection;
if (restore && (yearSelection = prefs.getByteArray(FXP09ProcessPane.YEAR_SELECTION, null)) != null) {
activatedYears = BitSet.valueOf(yearSelection);
} else {
activatedYears = new BitSet(refs.size());
activatedYears.set(0, activatedYears.length());
}
// A new session is starting. We initialize preferences for it.
if (!restore) {
prefs.put(FXP09ProcessPane.SITE_NAME, siteName);
prefs.putByteArray(FXP09ProcessPane.AVAILABLE_YEARS, yearsAsBytes.array());
}
for (GeoReferential ref : refs) {
final CheckBox check = new CheckBox(String.valueOf(ref.getYear()));
final int index = checkBoxes.size();
check.setSelected(activatedYears.get(index));
check.selectedProperty().addListener((obs, oldValue, newValue) -> {
yearSelectionChanged(index, newValue == null? false : newValue);
});
yearBoxes.getChildren().add(check);
checkBoxes.add(check);
}
}
private void yearSelectionChanged(final int index, final boolean value) {
activatedYears.set(index, value);
prefs.putByteArray(FXP09ProcessPane.YEAR_SELECTION, activatedYears.toByteArray());
}
@FXML
void exportGeometries(ActionEvent event) {
final Site site = session.getDataContext().getSite();
final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
final File outputFile = Rhomeo.askOutputFilePopup(site.getName()+".zip", this, zipFilter);
if (outputFile == null) {
return;
}
int tmpSelectedYear = Integer.MIN_VALUE;
for(final CheckBox c : checkBoxes) {
if (c.isSelected()) {
tmpSelectedYear = Math.max(tmpSelectedYear, Integer.parseInt(c.getText()));
}
}
final int selectedYear = tmpSelectedYear;
final ChoiceDialog<ExportUtils.ExportType> dialog = new ChoiceDialog<>(ExportUtils.ExportType.GJSON, ExportUtils.ExportType.values());
dialog.setTitle("Format d'export");
dialog.setHeaderText("Choisissez un format de sortie");
dialog.setResizable(true);
dialog.showAndWait().ifPresent(type -> {
Task<Boolean> t = TaskManager.INSTANCE.submit("Export", () -> {
final Geometry buffer = GeometryUtils.computeBuffer(site.getGeometry());
final Optional<Geometry> territory;
if (selectedYear > Integer.MIN_VALUE) {
territory = geoRef.getLocalReferential(site, selectedYear).unionIntersecting(GeoReferentials.TYPE_ZONE_HYDRO, site.getGeometry());
} else {
territory = Optional.empty();
}
ExportUtils.exportSiteAndBuffer(outputFile, site, buffer, territory, type);
return true;
});
uiExport.disableProperty().unbind();
uiExport.disableProperty().bind(t.runningProperty());
});
}
@FXML
void nextStep(ActionEvent event) {
//check there is at least one year of data available
int nbAvailable = 0;
for(CheckBox c : checkBoxes){
if(c.isSelected()) nbAvailable++;
}
if (nbAvailable==0) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante : veuillez sélectionner une année.");
return;
}
final Computer computer = new Computer();
globalTask.set(computer);
computer.stateProperty().addListener(obs -> {
if (computer.isDone()) {
setDisable(false);
switch (computer.getState()) {
case FAILED:
Rhomeo.showAlert(Alert.AlertType.ERROR, "process-failed");
break;
case SUCCEEDED:
session.setResults(computer.getValue());
globalTask.set(null);
session.requestWorkflowStep(WorkflowStep.RESULT);
break;
}
}
});
TaskManager.INSTANCE.submit(computer);
}
private class Computer extends Task<Set<Index>> {
private final List<Process> processes = new ArrayList<>();
@Override
protected Set<Index> call() throws Exception {
processes.addAll(indicator.createProcesses(session.getProcessContext()));
final I12Process process = (I12Process) processes.get(0);
//configure selected years
for(CheckBox c : checkBoxes){
if(c.isSelected())process.getYears().add(Integer.valueOf(c.getText()));
}
updateProgress(-1, -1);
updateTitle("I12");
Platform.runLater(() -> {
process.titleProperty().addListener((obs) -> updateTitle(process.getTitle()));
process.messageProperty().addListener((obs) -> updateTitle(process.getMessage()));
process.workDoneProperty().addListener((obs) -> updateProgress(process.getWorkDone(), process.getTotalWork()));
});
process.run();
return (Set) process.get();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
for (final Process p : processes) {
p.cancel(true);
}
return super.cancel(mayInterruptIfRunning);
}
}
}
@@ -0,0 +1,88 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p08;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.ui.ProtocolEditorSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.protocol.p08.P08;
import java.util.Collections;
import java.util.Set;
import javafx.scene.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* Protocol 08 editor spi.
*
* @author Johann Sorel (Geomatys)
*/
@Component
public class P08ProtocolEditorSpi implements ProtocolEditorSpi{
@Autowired
private Session session;
@Autowired
@Qualifier(P08.NAME)
private Protocol protocol;
@Override
public Protocol getProtocol() {
return protocol;
}
@Override
public Node getEditor(WorkflowStep step) {
switch(step){
case DATASET : return session.getBean(FXP08DatasetPane.class);
case PROCESS : return session.getBean(FXP08ProcessPane.class);
default : return null;
}
}
@Override
public Set<WorkflowStep> getStepsToIgnore() {
return Collections.EMPTY_SET;
}
}
@@ -0,0 +1,125 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p09;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.process.ProcessContext;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.fx.p08.FXGeoReferentialTable;
import fr.cenra.rhomeo.core.data.reference.GeoReferential;
import fr.cenra.rhomeo.core.data.reference.GeoReferentials;
import fr.cenra.rhomeo.protocol.p09.I13;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Johann Sorel (Geomatys)
*/
@Component
@Scope("prototype")
public class FXP09DatasetPane extends VBox {
@Autowired
private Session session;
@Autowired
private GeoReferentials geoRef;
@Autowired
@Qualifier(I13.NAME)
private Indicator indicator;
@FXML
private Button next;
@FXML
private BorderPane tableSpace;
private FXGeoReferentialTable table;
public FXP09DatasetPane() {
Rhomeo.loadFXML(this);
}
@PostConstruct
private void postConstruct(){
table = new FXGeoReferentialTable(geoRef, session.getDataContext().getSite(),
GeoReferentials.TYPE_ZONE_HYDRO,
GeoReferentials.TYPE_RPG);
tableSpace.setCenter(table);
}
@FXML
void nextStep(ActionEvent event) {
//check there is at least one year of data available
int nbAvailable = 0;
final ObservableList<GeoReferential> items = table.getItems();
for(GeoReferential ref : items){
//we need zone hydro,tache artif and tache_urbaine
if(ref.zoneHydroLocal().get()
&& ref.rpgLocal().get()) nbAvailable++;
}
if (nbAvailable==0) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante : la saisie n'est pas valide.");
return;
}
final ProcessContext ctx = new ProcessContext(session.getDataset(), session.getDataContext());
ctx.getIndicators().add(indicator);
session.setProcessContext(ctx);
if (!session.requestWorkflowStep(WorkflowStep.PROCESS)) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante. Veuillez vérifier votre lot de données.");
}
}
}
@@ -0,0 +1,383 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p09;
import com.vividsolutions.jts.geom.Geometry;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.api.process.Indicator;
import fr.cenra.rhomeo.api.result.Index;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.core.data.reference.GeoReferential;
import fr.cenra.rhomeo.core.data.reference.GeoReferentials;
import fr.cenra.rhomeo.core.util.GeometryUtils;
import fr.cenra.rhomeo.protocol.p09.I13;
import fr.cenra.rhomeo.protocol.p09.I13Process;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javax.annotation.PostConstruct;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import fr.cenra.rhomeo.api.process.Process;
import fr.cenra.rhomeo.core.util.ExportUtils;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.BitSet;
import java.util.Optional;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.layout.GridPane;
import org.geotoolkit.internal.GeotkFX;
/**
*
* @author Johann Sorel (Geomatys)
*/
@Component
@Scope("prototype")
public class FXP09ProcessPane extends VBox {
public static final String SITE_NAME = "site_name";
public static final String AVAILABLE_YEARS = "available_years";
private static final String TYPE_SELECTION = "type_selection";
public static final String YEAR_SELECTION = "year_selection";
private static final String[] TYPES = new String[]{
null,
"01 - Blé tendre",
"02 - Maïs grain et ensilage",
"03 - Orge",
"04 - Autres céréales",
"05 - Colza",
"06 - Tournesol",
"07 - Autres oleagineux",
"08 - Protéagineux",
"09 - Plantes à fibres",
"10 - Semences",
"11 - Gel (surfaces gelées sans production)",
"12 - Gel industriel",
"13 - Autres gels",
"14 - Riz",
"15 - Légumineuses à grains",
"16 - Fourrage",
"17 - Estives landes",
"18 - Prairies permanentes",
"19 - Prairies temporaires",
"20 - Vergers",
"21 - Vignes",
"22 - Fruits à coque",
"23 - Oliviers",
"24 - Autres cultures industrielles",
"25 - Légumes-fleurs",
"26 - Canne à sucre",
"27 - Arboriculture",
"28 - Divers"
};
@Autowired
private Session session;
@Autowired
private GeoReferentials geoRef;
@Autowired
@Qualifier(I13.NAME)
private Indicator indicator;
@FXML
private Button next;
@FXML
private Button uiExport;
@FXML
private HBox yearBoxes;
@FXML
private GridPane typeBoxes;
private final List<CheckBox> yearCheckBoxes = new ArrayList<>();
private final List<CheckBox> typeCheckBoxes = new ArrayList<>();
private final SimpleObjectProperty<Task<Set<Index>>> globalTask = new SimpleObjectProperty<>();
private BitSet activatedYears;
private BitSet activatedTypes;
private final Preferences prefs;
public FXP09ProcessPane() {
Rhomeo.loadFXML(this);
prefs = Preferences.userNodeForPackage(FXP09ProcessPane.class);
}
@PostConstruct
private void postConstruct() {
//liste available referential state for this site
final ObservableList<GeoReferential> refs;
try {
refs = geoRef.getLocalReferentials(session.getDataContext().getSite())
.filtered(ref -> ref.hasLocalTypes(GeoReferentials.TYPE_ZONE_HYDRO, GeoReferentials.TYPE_RPG))
.sorted();
} catch (IllegalArgumentException | IOException ex) {
// If we cannot load georeferentials, it's no use to go further.
GeotkFX.newExceptionDialog("Impossible de déterminer la liste des réferentiels disponibles. La lecture sur le système a échouée.", ex).show();
session.requestWorkflowStep(WorkflowStep.DATASET);
return;
}
final ByteBuffer yearsAsBytes = ByteBuffer.wrap(new byte[refs.size() * (Integer.SIZE / Byte.SIZE)]);
final IntBuffer years = yearsAsBytes.asIntBuffer();
for (final GeoReferential ref : refs) {
years.put(ref.getYear());
}
years.position(0);
/* Restore selection state from serialized bitset if possible. To
* determine if restoration is possible, we ensure there's data stored
* in preferences, and also that they match current edited site / years.
* Otherwise, we set a new bitset, with default selection on types 17
* and 18.
* We do the same with years, except that by default, they're all selected.
*/
final String siteName = session.getDataContext().getSite().getName();
final boolean restore = prefs.get(SITE_NAME, "").equals(siteName)
&& ByteBuffer.wrap(prefs.getByteArray(AVAILABLE_YEARS, new byte[0])).asIntBuffer().equals(years);
final byte[] yearSelection;
if (restore && (yearSelection = prefs.getByteArray(YEAR_SELECTION, null)) != null) {
activatedYears = BitSet.valueOf(yearSelection);
} else {
activatedYears = new BitSet(refs.size());
activatedYears.set(0, activatedYears.length());
}
final byte[] typeSelection;
if (restore && (typeSelection = prefs.getByteArray(TYPE_SELECTION, null)) != null) {
activatedTypes = BitSet.valueOf(typeSelection);
} else {
activatedTypes = new BitSet(TYPES.length);
activatedTypes.set(17, true);
activatedTypes.set(18, true);
activatedTypes.set(19, true);
}
// A new session is starting. We initialize preferences for it.
if (!restore) {
prefs.put(SITE_NAME, siteName);
prefs.putByteArray(AVAILABLE_YEARS, yearsAsBytes.array());
}
// Init UI. For checkboxes, each time selection change, we update preferences.
for (GeoReferential ref : refs) {
final CheckBox check = new CheckBox(String.valueOf(ref.getYear()));
final int index = yearCheckBoxes.size();
check.setSelected(activatedYears.get(index));
check.selectedProperty().addListener((obs, oldValue, newValue) -> {
yearSelectionChanged(index, newValue == null? false : newValue);
});
yearBoxes.getChildren().add(check);
yearCheckBoxes.add(check);
}
double best = 0;
typeCheckBoxes.add(null);
for(int i=1;i<29;i++){
final CheckBox box = new CheckBox(TYPES[i]);
box.setSelected(activatedTypes.get(i));
typeCheckBoxes.add(box);
GridPane.setColumnIndex(box, (i-1)%4);
GridPane.setRowIndex(box, ((i-1)-((i-1)%4)));
typeBoxes.getChildren().add(box);
best = Math.max(best, box.getWidth());
final int index = i;
box.selectedProperty().addListener((obs, oldValue, newValue) -> {
boxSelectionChanged(index, newValue == null? false : newValue);
});
}
}
private void yearSelectionChanged(final int index, final boolean value) {
activatedYears.set(index, value);
prefs.putByteArray(YEAR_SELECTION, activatedYears.toByteArray());
}
private void boxSelectionChanged(final int index, final boolean value) {
activatedTypes.set(index, value);
prefs.putByteArray(TYPE_SELECTION, activatedTypes.toByteArray());
}
@FXML
void exportGeometries(ActionEvent event) {
final Site site = session.getDataContext().getSite();
final FileChooser.ExtensionFilter zipFilter = new FileChooser.ExtensionFilter("Archive zip", "*.zip");
final File outputFile = Rhomeo.askOutputFilePopup(site.getName()+".zip", this, zipFilter);
if (outputFile == null) {
return;
}
int tmpSelectedYear = Integer.MIN_VALUE;
for(final CheckBox c : yearCheckBoxes) {
if (c.isSelected()) {
tmpSelectedYear = Math.max(tmpSelectedYear, Integer.parseInt(c.getText()));
}
}
final int selectedYear = tmpSelectedYear;
final ChoiceDialog<ExportUtils.ExportType> dialog = new ChoiceDialog<>(ExportUtils.ExportType.GJSON, ExportUtils.ExportType.values());
dialog.setTitle("Format d'export");
dialog.setHeaderText("Choisissez un format de sortie");
dialog.setResizable(true);
dialog.showAndWait().ifPresent(type -> {
Task<Boolean> t = TaskManager.INSTANCE.submit("Export", () -> {
final Geometry buffer = GeometryUtils.computeBuffer(site.getGeometry());
final Optional<Geometry> territory;
if (selectedYear > Integer.MIN_VALUE) {
territory = geoRef.getLocalReferential(site, selectedYear).unionIntersecting(GeoReferentials.TYPE_ZONE_HYDRO, site.getGeometry());
} else {
territory = Optional.empty();
}
ExportUtils.exportSiteAndBuffer(outputFile, site, buffer, territory, type);
return true;
});
uiExport.disableProperty().unbind();
uiExport.disableProperty().bind(t.runningProperty());
});
}
@FXML
void nextStep(ActionEvent event) {
//check there is at least one year of data available
int nbAvailable = 0;
for(CheckBox c : yearCheckBoxes){
if(c.isSelected()) nbAvailable++;
}
if (nbAvailable==0) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de passer à l'étape suivante : veuillez sélectionner une année.");
return;
}
final Computer computer = new Computer();
globalTask.set(computer);
computer.stateProperty().addListener(obs -> {
if (computer.isDone()) {
setDisable(false);
switch (computer.getState()) {
case FAILED:
Rhomeo.showAlert(Alert.AlertType.ERROR, "process-failed");
break;
case SUCCEEDED:
session.setResults(computer.getValue());
globalTask.set(null);
session.requestWorkflowStep(WorkflowStep.RESULT);
break;
}
}
});
TaskManager.INSTANCE.submit(computer);
}
private class Computer extends Task<Set<Index>> {
private final List<Process> processes = new ArrayList<>();
@Override
protected Set<Index> call() throws Exception {
processes.addAll(indicator.createProcesses(session.getProcessContext()));
final I13Process process = (I13Process) processes.get(0);
//configure selected years
for(CheckBox c : yearCheckBoxes){
if(c.isSelected())process.getYears().add(Integer.valueOf(c.getText()));
}
//configure excluded types
for(int i=1;i<29;i++){
if(typeCheckBoxes.get(i).isSelected()){
process.getNoCount().add(i);
}
}
updateProgress(-1, -1);
updateTitle("I13");
Platform.runLater(() -> {
process.titleProperty().addListener((obs) -> updateTitle(process.getTitle()));
process.messageProperty().addListener((obs) -> updateTitle(process.getMessage()));
process.workDoneProperty().addListener((obs) -> updateProgress(process.getWorkDone(), process.getTotalWork()));
});
process.run();
return (Set) process.get();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
for (final Process p : processes) {
p.cancel(true);
}
return super.cancel(mayInterruptIfRunning);
}
}
}
@@ -0,0 +1,88 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.p09;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.ui.ProtocolEditorSpi;
import fr.cenra.rhomeo.core.Session;
import fr.cenra.rhomeo.core.WorkflowStep;
import fr.cenra.rhomeo.protocol.p09.P09;
import java.util.Collections;
import java.util.Set;
import javafx.scene.Node;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* Protocol 09 editor spi.
*
* @author Johann Sorel (Geomatys)
*/
@Component
public class P09ProtocolEditorSpi implements ProtocolEditorSpi{
@Autowired
private Session session;
@Autowired
@Qualifier(P09.NAME)
private Protocol protocol;
@Override
public Protocol getProtocol() {
return protocol;
}
@Override
public Node getEditor(WorkflowStep step) {
switch(step){
case DATASET : return session.getBean(FXP09DatasetPane.class);
case PROCESS : return session.getBean(FXP09ProcessPane.class);
default : return null;
}
}
@Override
public Set<WorkflowStep> getStepsToIgnore() {
return Collections.EMPTY_SET;
}
}
@@ -0,0 +1,83 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.validation;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.core.RhomeoCore;
import java.util.Iterator;
import java.util.logging.Level;
import javafx.scene.control.ListCell;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
/**
* A simple cell which render a constraint violation message in cell text.
* @author Alexis Manin (Geomatys)
*/
public class ConstraintViolationListCell extends ListCell<ConstraintViolation> {
@Override
protected void updateItem(ConstraintViolation item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
final StringBuilder msgBuilder = new StringBuilder();
if (item.getLeafBean() instanceof InternationalResource) {
final Iterator<Path.Node> it = item.getPropertyPath().iterator();
String pName = null;
while (it.hasNext())
pName = it.next().getName();
if (pName != null) {
try {
msgBuilder.append(InternationalResource.getResourceString((Class)item.getLeafBean().getClass(), pName, "label"))
.append(" : ");
} catch (Exception e) {
RhomeoCore.LOGGER.log(Level.FINE, "Cannot find resource ".concat(pName), e);
}
}
}
setText(msgBuilder.append(item.getMessage()).toString());
}
}
}
@@ -0,0 +1,259 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.fx.validation;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.data.Dataset;
import fr.cenra.rhomeo.api.data.Warning;
import fr.cenra.rhomeo.core.Session;
import java.util.HashSet;
import java.util.ResourceBundle;
import java.util.Set;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.StackPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Callback;
import javax.annotation.PostConstruct;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class FXValidationPane extends StackPane {
@Autowired
private Session session;
@Autowired
private Validator validator;
@FXML
private Label uiStateIcon;
@FXML
private Label uiStateLabel;
@FXML
private Button uiValidate;
@FXML
private Hyperlink uiErrorContainer;
@FXML
private Hyperlink uiWarningContainer;
@FXML
private ProgressIndicator uiProgress;
private final ListView<ConstraintViolation> uiErrorList;
private final ListView<ConstraintViolation> uiWarningList;
private final ObservableList errors;
private final ObservableList warnings;
private final BooleanBinding noError;
private Stage errorStage;
private Stage warningStage;
public FXValidationPane() {
super();
Rhomeo.loadFXML(this, FXValidationPane.class);
errors = FXCollections.observableArrayList();
warnings = FXCollections.observableArrayList();
uiErrorList = new ListView<>(errors);
uiErrorList.setMaxHeight(200);
final Insets insets = new Insets(4);
uiErrorList.setPadding(insets);
uiWarningList = new ListView<>(warnings);
uiWarningList.setPadding(insets);
uiWarningList.setMaxHeight(200);
final Callback factory = (list) -> new ConstraintViolationListCell();
uiErrorList.setCellFactory(factory);
uiWarningList.setCellFactory(factory);
// Update display on error detection
noError = Bindings.isEmpty(errors);
uiErrorContainer.visibleProperty().bind(noError.not());
uiErrorContainer.managedProperty().bind(uiErrorContainer.visibleProperty());
final BooleanBinding noWarning = Bindings.isEmpty(warnings);
uiWarningContainer.visibleProperty().bind(noWarning.not());
uiWarningContainer.managedProperty().bind(uiWarningContainer.visibleProperty());
noError.addListener((obs) -> {
if (session.getDataset().getItems().isEmpty()) {
uiStateIcon.setText(null);
uiStateLabel.setText(null);
} else if (noError.get()) {
uiStateIcon.setText(FontAwesomeIcons.ICON_CHECK);
uiStateIcon.setStyle("-fx-font-family: FontAwesome; -fx-font-size: 24; -fx-text-fill:#9CB35B");
uiStateLabel.setText(ResourceBundle.getBundle(FXValidationPane.class.getCanonicalName()).getString("dataset.valid"));
} else {
uiStateIcon.setText(FontAwesomeIcons.ICON_TIMES);
uiStateIcon.setStyle("-fx-font-family: FontAwesome; -fx-font-size: 24; -fx-text-fill:#D85F70");
uiStateLabel.setText(ResourceBundle.getBundle(FXValidationPane.class.getCanonicalName()).getString("dataset.invalid"));
}
});
}
@PostConstruct
private void init() {
uiValidate.disableProperty().bind(Bindings.isEmpty(session.getDataset().getItems()));
}
@FXML
void validate(ActionEvent event) {
TaskManager.INSTANCE.submit("Validation du lot de données", () -> {
Rhomeo.fxRun(false, () -> {
setDisable(true);
uiProgress.setVisible(true);
});
try {
Set<ConstraintViolation<Dataset>> violations = validator.validate(session.getDataset());
final HashSet tmpErrors = new HashSet();
final HashSet tmpWarnings = new HashSet();
for (final ConstraintViolation violation : violations) {
if (findWarning(violation)) {
tmpWarnings.add(violation);
} else {
tmpErrors.add(violation);
}
}
Rhomeo.fxRun(false, () -> {
errors.setAll(tmpErrors);
warnings.setAll(tmpWarnings);
uiStateLabel.setVisible(true);
noError.invalidate();
});
} finally {
Rhomeo.fxRun(false, () -> {
setDisable(false);
uiProgress.setVisible(false);
});
}
});
}
private static boolean findWarning(final ConstraintViolation violation) {
for (final Object p : violation.getConstraintDescriptor().getPayload()) {
if (p == Warning.class) {
return true;
}
}
return false;
}
public ListView<ConstraintViolation> getErrorList() {
return uiErrorList;
}
public ListView<ConstraintViolation> getWarningList() {
return uiWarningList;
}
/**
* Empty this panel content.
*/
public void reset() {
warnings.clear();
errors.clear();
noError.invalidate();
}
@FXML
private void showWarnings(final ActionEvent e) {
if (warningStage == null) {
warningStage = createErrorStage("Avertissements", uiWarningList);
}
warningStage.show();
warningStage.requestFocus();
}
@FXML
private void showErrors(final ActionEvent e) {
if (errorStage == null) {
errorStage = createErrorStage("Erreurs", uiErrorList);
}
errorStage.show();
errorStage.requestFocus();
}
private Stage createErrorStage(final String title, final ListView<ConstraintViolation> errors) {
final Stage s = new Stage(StageStyle.UNIFIED);
if (title != null)
s.setTitle(title);
s.initModality(Modality.NONE);
if (getScene() != null)
s.initOwner(getScene().getWindow());
final Scene scene = new Scene(errors);
s.setScene(scene);
s.setAlwaysOnTop(false);
s.sizeToScene();
return s;
}
}
@@ -0,0 +1,232 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.preferences;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.preferences.PreferenceGroup;
import fr.cenra.rhomeo.api.ui.PreferencePane;
import fr.cenra.rhomeo.core.RhomeoCore;
import fr.cenra.rhomeo.core.preferences.ftp.FTPAccess;
import fr.cenra.rhomeo.core.preferences.ftp.FTPPreferences;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.logging.Level;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.PasswordField;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.util.StringConverter;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.internal.GeotkFX;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
*
* @author Alexis Manin (Geomatys)
*/
@Component
@Scope("prototype")
public class FXFTPConfiguration extends BorderPane implements PreferencePane {
@FXML
private ComboBox<FTPPreferences.ACCESS_POINTS> uiAccess;
@Autowired
FTPPreferences prefs;
@FXML
private TextField uiURL;
@FXML
private Spinner<Integer> uiPort;
@FXML
private TextField uiDir;
@FXML
private TextField uiLogin;
@FXML
private PasswordField uiPassword;
@FXML
private Button uiTest;
private FTPAccess access;
public FXFTPConfiguration() {
Rhomeo.loadFXML(this);
uiPort.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 65535, 21, 1));
Rhomeo.initComboBox(uiAccess, FXCollections.observableArrayList(Arrays.asList(FTPPreferences.ACCESS_POINTS.values())));
uiAccess.setEditable(false);
uiAccess.setConverter(new StringConverter<FTPPreferences.ACCESS_POINTS>() {
@Override
public String toString(FTPPreferences.ACCESS_POINTS object) {
if (object == null)
return null;
return object.getTitle();
}
@Override
public FTPPreferences.ACCESS_POINTS fromString(String string) {
if (string == null)
return null;
for (final FTPPreferences.ACCESS_POINTS ap : FTPPreferences.ACCESS_POINTS.values()) {
if (string.equals(ap))
return ap;
}
return null;
}
});
uiAccess.valueProperty().addListener(this::accessChanged);
}
@FXML
void cancel(ActionEvent event) {
if (access == null)
clearForm();
else
fillFromAcccess();
}
@FXML
void checkConnection(ActionEvent event) {
final String adress = uiURL.getText();
if (adress == null || adress.isEmpty()) {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de tester la connexion si le champs d'adresse est vide !");
}
final int port = uiPort.getValue();
final String workDir = uiDir.getText();
final String login = uiLogin.getText();
final String pw = uiPassword.getText();
final TaskManager.MockTask t = new TaskManager.MockTask(() -> FTPAccess.createClient(adress, port, workDir, login, pw));
t.setOnSucceeded((evt) -> Platform.runLater(() -> {
if (t.getValue() != null) {
Rhomeo.showAlert(Alert.AlertType.INFORMATION, "Connexion établie avec succès !");
} else {
Rhomeo.showAlert(Alert.AlertType.WARNING, "Aucune réponse du serveur !");
}
}));
t.setOnFailed(evt -> Platform.runLater(() -> GeotkFX.newExceptionDialog("Impossible de se connecter au service FTP", t.getException()).show()));
t.runningProperty().addListener((obs, oldValue, newValue) -> uiTest.setDisable(newValue));
TaskManager.INSTANCE.submit("Test de connexion", t);
}
private void accessChanged(final ObservableValue<? extends FTPPreferences.ACCESS_POINTS> obs, FTPPreferences.ACCESS_POINTS oldAccess, FTPPreferences.ACCESS_POINTS newAccess) {
if (newAccess == null) {
access = null;
clearForm();
} else {
access = prefs.getAccess(newAccess);
fillFromAcccess();
}
}
private void clearForm() {
uiDir.clear();
uiLogin.clear();
uiPassword.clear();
uiPort.getValueFactory().setValue(21);
uiURL.clear();
}
private void fillFromAcccess() {
uiDir.setText(access.getWorkingDir());
uiLogin.setText(access.getLogin());
try {
uiPassword.setText(access.getPassword());
} catch (GeneralSecurityException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot read password for FTP access", ex);
Rhomeo.showAlert(Alert.AlertType.ERROR, "Impossible de lire le mot de passe enregistré");
}
uiPort.getValueFactory().setValue(access.getPort());
uiURL.setText(access.getAdress());
}
private void fillAccess() {
if (access == null)
return;
access.setPort(uiPort.getValue());
access.setAdress(uiURL.getText());
access.setWorkingDir(uiDir.getText());
access.setLogin(uiLogin.getText());
try {
access.setPassword(uiPassword.getText());
} catch (GeneralSecurityException ex) {
RhomeoCore.LOGGER.log(Level.WARNING, "Cannot write password for FTP access", ex);
GeotkFX.newExceptionDialog("Impossible d'enregistrer le mot de passe", ex).show();
}
}
@Override
public PreferenceGroup getPreferenceGroup() {
return prefs;
}
@Override
public Node getEditor() {
return this;
}
@Override
public void save() {
fillAccess();
}
}
@@ -0,0 +1,77 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.preferences.csv;
import fr.cenra.rhomeo.api.preferences.InternationalPreferenceKey;
import org.apache.sis.util.ArgumentChecks;
/**
*
* @author Alexis Manin (Geomatys)
*/
public class CSVKey implements InternationalPreferenceKey {
private final String keyId;
private final String defaultValue;
public CSVKey(final String netKey) {
this(netKey, null);
}
public CSVKey(final String netKey, final String defaultValue) {
ArgumentChecks.ensureNonNull("Key", netKey);
this.keyId = netKey;
this.defaultValue = defaultValue;
}
@Override
public String name() {
return keyId;
}
@Override
public String getKey() {
return keyId;
}
@Override
public String getDefaultValue() {
return defaultValue;
}
}
@@ -0,0 +1,74 @@
/**
* Copyright © CENRA (2016)
*
* remi.clement@espaces-naturels.fr
*
* This software is a multi-platform and portable application based on
* the scientifical toolbox to monitor wetlands. With this toolbox it’s
* possible to calculate indicators «RhoMéO» to monitor quality,
* fonctionalities and urban and agricultural pressures of wetlands.
* Indicators are calculated using naturalist data entered and/or imported,
* and geographic data downloaded from a geographic server.
*
* This software is governed by the CeCILL license under French law and
* abiding by the rules of distribution of free software. You can use,
* modify and/ or redistribute the software under the terms of the CeCILL
* license as circulated by CEA, CNRS and INRIA at the following URL
* "http://www.cecill.info".
*
* As a counterpart to the access to the source code and rights to copy,
* modify and redistribute granted by the license, users are provided only
* with a limited warranty and the software's author, the holder of the
* economic rights, and the successive licensors have only limited
* liability.
*
* In this respect, the user's attention is drawn to the risks associated
* with loading, using, modifying and/or developing or reproducing the
* software by the user in light of its specific status of free software,
* that may mean that it is complicated to manipulate, and that also
* therefore means that it is reserved for developers and experienced
* professionals having in-depth computer knowledge. Users are therefore
* encouraged to load and test the software's suitability as regards their
* requirements in conditions enabling the security of their systems and/or
* data to be ensured and, more generally, to use and operate it in the
* same conditions as regards security.
*
* The fact that you are presently reading this means that you have had
* knowledge of the CeCILL license and that you accept its terms.
*/
package fr.cenra.rhomeo.preferences.csv;
import fr.cenra.rhomeo.api.preferences.UserPackagePreferenceGroup;
import fr.cenra.rhomeo.core.CSVMapper;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.stereotype.Component;
/**
* Display preferences of encoding and other CSV formatting parameters. This is
* needed for dataset import/export.
*
* @author Alexis Manin (Geomatys)
*/
@Component
public class CSVPreferences extends UserPackagePreferenceGroup<CSVKey> {
public static final CSVKey CHARSET_KEY = new CSVKey("charset", StandardCharsets.UTF_8.name());
public static final CSVKey SEPARATOR_KEY = new CSVKey("separator", String.valueOf(CSVMapper.DEFAULT_SEPARATOR));
private static final List<CSVKey> KEYS = Collections.unmodifiableList(Arrays.asList(new CSVKey[] {
CHARSET_KEY, SEPARATOR_KEY
}));
@Override
public List getKeys() {
return KEYS;
}
@Override
public int getPriority() {
return 3;
}
}
@@ -0,0 +1,109 @@
/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2015, Geomatys
*
* 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.geotoolkit.gui.javafx.parameter;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.control.DatePicker;
import org.apache.sis.util.UnconvertibleObjectException;
/**
* COPIED FROM GEOTOOLKIT 4.0.0-M0082. Aim is to simplify edition.
*
* @author Alexis Manin (Geomatys)
*/
public class FXDateEditor extends FXValueEditor {
static final Class[] SUPPORTED_CLASSES = new Class[]{Date.class, LocalDate.class, LocalDateTime.class};
private final SimpleObjectProperty dateProperty = new SimpleObjectProperty();
private final DatePicker editor = new DatePicker();
public FXDateEditor(final Spi spi) {
super(spi);
dateProperty.addListener((ObservableValue observable, Object oldValue, Object newValue) -> {
if (editor.valueProperty().isBound())
return; // Do not throw exception, because user has set editor property as a target for one of his own property.
if (newValue == null)
editor.valueProperty().set(null);
else if (newValue instanceof LocalDateTime)
editor.valueProperty().set(((LocalDateTime)newValue).toLocalDate());
else if (newValue instanceof LocalDate)
editor.valueProperty().set((LocalDate)newValue);
else if (newValue instanceof Date) {
editor.valueProperty().set(new Timestamp(((Date)newValue).getTime()).toLocalDateTime().toLocalDate());
} else throw new UnconvertibleObjectException("Cannot convert from "+newValue.getClass() + " to "+LocalDate.class);
});
editor.valueProperty().addListener((ObservableValue<? extends LocalDate> observable, LocalDate oldValue, LocalDate newValue) -> {
if (dateProperty.isBound())
return; // Do not throw exception, because user has set exposed property as a target for one of his own property.
if (newValue == null) {
dateProperty.set(null);
} else {
Class valueClass = getValueClass();
if (valueClass == null)
dateProperty.set(newValue); // TODO : throw an exception ? editor has not been configured, but user already started to work with it.
else if (LocalDateTime.class.isAssignableFrom(valueClass))
dateProperty.set(newValue.atStartOfDay());
else if (LocalDate.class.isAssignableFrom(valueClass))
dateProperty.set(newValue);
else if (Date.class.isAssignableFrom(valueClass))
dateProperty.set(Timestamp.valueOf(newValue.atStartOfDay()));
}
});
}
@Override
public Property valueProperty() {
return dateProperty;
}
@Override
public Node getComponent() {
return editor;
}
/**
* SPI
*/
public static final class Spi extends FXValueEditorSpi {
@Override
public boolean canHandle(Class binding) {
for (final Class c : SUPPORTED_CLASSES) {
if (c.isAssignableFrom(binding))
return true;
}
return false;
}
@Override
public FXValueEditor createEditor() {
return new FXDateEditor(this);
}
}
}
@@ -0,0 +1,388 @@
package org.geotoolkit.gui.javafx.util;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Background;
import javafx.scene.layout.Border;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import org.apache.sis.util.ArgumentChecks;
import org.geotoolkit.font.FontAwesomeIcons;
import org.geotoolkit.internal.GeotkFX;
/**
* A JavaFX component which display progress and encountered errors for all tasks
* submitted on a specific task manager.
*
* The last submitted task is displayed in an Hbox. To see other running tasks
* and tasks in error, there's two {@link MenuButton}. Each display custom menu
* items containing information about a task.
*
* The {@link ProgressMonitor} is skinnable using a css stylesheet and the specific
* following css classes:
* {@link ProgressMonitor.CURRENT_TASK_CSS_CLASS}
* {@link ProgressMonitor.CURRENT_TASK_GRAPHIC_CSS_CLASS}
* {@link ProgressMonitor.ERROR_TASK_CSS_CLASS}
* {@link ProgressMonitor.ERROR_TASK_GRAPHIC_CSS_CLASS}
* {@link ProgressMonitor.PROGRESS_MONITOR_CSS_CLASS}
* {@link ProgressMonitor.CANCEL_BUTTON_CSS_CLASS}
* {@link ProgressMonitor.TASK_PROGRESS_CSS_CLASS}
* {@link ProgressMonitor.TASK_PROGRESS_GRAPHIC_CSS_CLASS}
* {@link ProgressMonitor.MENU_ITEM_CSS_CLASS}
* {@link ProgressMonitor.CLEAR_MENU_ITEM_CSS_CLASS}
*
*
* {@link ProgressMonitor} labels can be parametrized using {@link ResourceBundle}
* properties defined by {@link ProgressMonitor.ResourceKey}.
*
* @author Alexis Manin (Geomatys)
*/
public class ProgressMonitor extends HBox {
private static String ICON_LABEL_FONT_FAMILY = "-fx-font-family: FontAwesome Regular;";
/**
* The css classes associated to the {@link ProgressMonitor} nodes.
*/
public static final String CURRENT_TASK_CSS_CLASS="geotk-progressMonitor-runningTasks";
public static final String CURRENT_TASK_GRAPHIC_CSS_CLASS="geotk-progressMonitor-runningTasks-graphic";
public static final String ERROR_TASK_CSS_CLASS="geotk-progressMonitor-tasksInError";
public static final String ERROR_TASK_GRAPHIC_CSS_CLASS="geotk-progressMonitor-tasksInError-graphic";
public static final String PROGRESS_MONITOR_CSS_CLASS="geotk-progressMonitor";
public static final String CANCEL_BUTTON_CSS_CLASS="geotk-progressMonitor-cancelButton";
public static final String TASK_PROGRESS_CSS_CLASS="geotk-progressMonitor-taskProgress";
public static final String TASK_PROGRESS_GRAPHIC_CSS_CLASS="geotk-progressMonitor-taskProgress-graphic";
public static final String MENU_ITEM_CSS_CLASS="geotk-progressMonitor-menuItem";
public static final String CLEAR_MENU_ITEM_CSS_CLASS="geotk-progressMonitor-clearMenuItem";
private TaskProgress lastTask;
private final MenuButton runningTasks;
private final MenuButton tasksInError;
private final TaskManager taskRegistry;
static {
// Load Font Awesome.
final Font font = FXUtilities.FONTAWESOME;
}
/**
* The base constructor of progress monitors.
*
* @param registry The {@link TaskManager} followed by this progress monitor.
*/
public ProgressMonitor(final TaskManager registry) {
ArgumentChecks.ensureNonNull("Input task registry", registry);
taskRegistry = registry;
final Label runningIcon = new Label(FontAwesomeIcons.ICON_GEARS_ALIAS);
runningIcon.setStyle(ICON_LABEL_FONT_FAMILY);
runningIcon.getStyleClass().add(CURRENT_TASK_GRAPHIC_CSS_CLASS);
runningTasks = new MenuButton(GeotkFX.getString(ProgressMonitor.class, "currentTasksLabel"), runningIcon);
final Label errorIcon = new Label(FontAwesomeIcons.ICON_EXCLAMATION_CIRCLE);
errorIcon.setStyle(ICON_LABEL_FONT_FAMILY);
errorIcon.getStyleClass().add(ERROR_TASK_GRAPHIC_CSS_CLASS);
tasksInError = new MenuButton(GeotkFX.getString(ProgressMonitor.class, "errorTasksLabel"), errorIcon);
final Tooltip runninTasksTooltip = new Tooltip(GeotkFX.getString(ProgressMonitor.class, "currentTasksTooltip"));
runningTasks.setTooltip(runninTasksTooltip);
final Tooltip tasksInErrorTooltip = new Tooltip(GeotkFX.getString(ProgressMonitor.class, "errorTasksTooltip"));
tasksInError.setTooltip(tasksInErrorTooltip);
final SimpleListProperty runningTasksProp = new SimpleListProperty(taskRegistry.getSubmittedTasks());
final SimpleListProperty failedTasksProp = new SimpleListProperty(taskRegistry.getTasksInError());
// Hide list of tasks if there's no information available.
runningTasks.visibleProperty().bind(runningTasksProp.sizeProperty().greaterThan(1));
tasksInError.visibleProperty().bind(failedTasksProp.emptyProperty().not());
// Display number of tasks on menu button.
runningTasks.textProperty().bind(runningTasksProp.sizeProperty().asString());
tasksInError.textProperty().bind(failedTasksProp.sizeProperty().asString());
// Set default visible task the last one submitted.
lastTask = new TaskProgress();
lastTask.taskProperty().bind(runningTasksProp.valueAt(runningTasksProp.sizeProperty().subtract(1)));
// Do not reserve size for hidden components.
runningTasks.managedProperty().bind(runningTasks.visibleProperty());
tasksInError.managedProperty().bind(tasksInError.visibleProperty());
lastTask.managedProperty().bind(lastTask.visibleProperty());
initTasks();
getChildren().addAll(lastTask, runningTasks, tasksInError);
minWidthProperty().bind(prefWidthProperty());
prefWidthProperty().set(USE_COMPUTED_SIZE);
getStyleClass().add(PROGRESS_MONITOR_CSS_CLASS);
runningTasks.getStyleClass().add(CURRENT_TASK_CSS_CLASS);
tasksInError.getStyleClass().add(ERROR_TASK_CSS_CLASS);
}
/**
* Fill panel with currently submitted tasks. Add listeners on
* {@link TaskManager} to be aware of new events.
*/
private void initTasks() {
final MenuItem clearErrorItem = new MenuItem(GeotkFX.getString(ProgressMonitor.class, "cleanErrorList"));
clearErrorItem.setOnAction(evt -> taskRegistry.getTasksInError().clear());
final Label icon = new Label(FontAwesomeIcons.ICON_TRASH_O);
icon.setStyle(ICON_LABEL_FONT_FAMILY);
clearErrorItem.setGraphic(icon);
clearErrorItem.getStyleClass().add(CLEAR_MENU_ITEM_CSS_CLASS);
tasksInError.getItems().add(clearErrorItem);
tasksInError.getItems().add(new SeparatorMenuItem());
// Listen on current running tasks
final ObservableList<Task> tmpSubmittedTasks = taskRegistry.getSubmittedTasks();
tmpSubmittedTasks.addListener((ListChangeListener.Change<? extends Task> c) -> {
final Set<Task> toAdd = new HashSet<>();
final Set<Task> toRemove = new HashSet<>();
storeChanges(c, toAdd, toRemove);
Platform.runLater(() -> {
for (final Task task : toAdd) {
final CustomMenuItem item = new CustomMenuItem(new TaskProgress(task));
item.setHideOnClick(false);
runningTasks.getItems().add(item);
}
// remove Ended tasks
runningTasks.getItems().removeIf(new GetItemsForTask(toRemove));
});
});
// Check failed tasks.
final ObservableList<Task> tmpTasksInError = taskRegistry.getTasksInError();
tmpTasksInError.addListener((ListChangeListener.Change<? extends Task> c) -> {
final Set<Task> toAdd = new HashSet<>();
final Set<Task> toRemove = new HashSet<>();
storeChanges(c, toAdd, toRemove);
Platform.runLater(() -> {
for (final Task task : toAdd) {
tasksInError.getItems().add(new ErrorMenuItem(task));
}
// remove Ended tasks
tasksInError.getItems().removeIf(new GetItemsForTask(toRemove));
});
});
final Runnable initPanel = () -> {
final int nbSubmitted = tmpSubmittedTasks.size();
// do not add last task to our menu, it will be used on main display.
for (int i = 0; i < nbSubmitted; i++) {
final CustomMenuItem item = new CustomMenuItem(new TaskProgress(tmpSubmittedTasks.get(i)));
item.setHideOnClick(false);
runningTasks.getItems().add(item);
}
for (final Task t : tmpTasksInError) {
tasksInError.getItems().add(new ErrorMenuItem(t));
}
};
if (Platform.isFxApplicationThread()) {
initPanel.run();
} else {
Platform.runLater(initPanel);
}
}
/**
* Store all objects depicted by a {@link ListChangeListener} into given
* collections.
*
* @param c The {@link ListChangeListener.Change} containing list content delta.
* @param added The collection to store new added objects into.
* @param removed Add removed objects in it.
*/
private static void storeChanges(final ListChangeListener.Change c, final Collection added, final Collection removed) {
while (c.next()) {
final List<? extends Task> addedSubList = c.getAddedSubList();
final List<? extends Task> removeSubList = c.getRemoved();
if (addedSubList != null && !addedSubList.isEmpty()) {
added.addAll(addedSubList);
}
final Iterator<? extends Task> it = removeSubList.iterator();
while (it.hasNext()) {
final Task current = it.next();
if (!added.remove(current)) {
removed.add(current);
}
}
}
}
/**
* The node giving information about a specific task. Allow to see title,
* description and current progress, as to cancel the task.
*/
private static class TaskProgress extends HBox {
private final Label title = new Label();
private final Tooltip description = new Tooltip();
private final ProgressBar progress = new ProgressBar();
private final Button cancelButton;
private final ObjectProperty<Task> taskProperty = new SimpleObjectProperty<>();
TaskProgress() {
this(null);
}
TaskProgress(final Task t) {
final Label icon = new Label(FontAwesomeIcons.ICON_BAN);
icon.setStyle(ICON_LABEL_FONT_FAMILY);
icon.getStyleClass().add(TASK_PROGRESS_GRAPHIC_CSS_CLASS);
cancelButton = new Button("", icon);
taskProperty.addListener((ObservableValue<? extends Task> observable, Task oldValue, Task newValue) -> {
if (Platform.isFxApplicationThread()) {
taskUpdated();
} else {
Platform.runLater(()->taskUpdated());
}
});
taskProperty.set(t);
getChildren().addAll(title, progress, cancelButton);
getStyleClass().add(TASK_PROGRESS_CSS_CLASS);
cancelButton.getStyleClass().add(CANCEL_BUTTON_CSS_CLASS);
}
public ObjectProperty<Task> taskProperty() {
return taskProperty;
}
public Task getTask() {
return taskProperty.get();
}
public synchronized void taskUpdated() {
title.textProperty().unbind();
progress.progressProperty().unbind();
description.textProperty().unbind();
cancelButton.setOnAction(null);
final Task t =taskProperty.get();
if (t != null) {
title.textProperty().bind(t.titleProperty());
description.textProperty().bind(t.messageProperty());
progress.progressProperty().bind(t.progressProperty());
cancelButton.setOnAction((ActionEvent e) -> t.cancel());
setVisible(true);
} else {
setVisible(false);
}
}
}
/**
* A simple menu items for failed tasks. Display an exception Dialog when clicked.
*/
private class ErrorMenuItem extends MenuItem {
final Task failedTask;
ErrorMenuItem(final Task failedTask) {
ArgumentChecks.ensureNonNull("task in error", failedTask);
this.failedTask = failedTask;
// No need for binding here. Task failed, its state should not change anymore.
String title = failedTask.getTitle();
if (title == null || title.isEmpty())
title = GeotkFX.getString(ProgressMonitor.class, "anonymOperation");
setText(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm"))+" - "+title);
final Label icon = new Label(FontAwesomeIcons.ICON_TRASH_O);
icon.setStyle(ICON_LABEL_FONT_FAMILY);
final Button deleteButton = new Button("", icon);
deleteButton.setBorder(Border.EMPTY);
deleteButton.setPadding(Insets.EMPTY);
deleteButton.setBackground(Background.EMPTY);
deleteButton.setOnAction(e -> {
taskRegistry.getTasksInError().remove(failedTask);
e.consume();
});
setGraphic(deleteButton);
final Dialog d = GeotkFX.newExceptionDialog(failedTask.getMessage(), failedTask.getException());
d.setResizable(true);
setOnAction((ActionEvent ae) -> d.show());
getStyleClass().add(MENU_ITEM_CSS_CLASS);
}
public Task getTask() {
return failedTask;
}
}
/**
* A simple {@link Predicate} which return current monitor progress bars
* which are focused on one of the given tasks.
*/
private static class GetItemsForTask implements Predicate<MenuItem> {
private final Collection<? extends Task> tasks;
GetItemsForTask(final Collection<? extends Task> taskFilter) {
ArgumentChecks.ensureNonNull("Input filter tasks", taskFilter);
tasks = taskFilter;
}
@Override
public boolean test(MenuItem item) {
if (item instanceof CustomMenuItem) {
final Node content = ((CustomMenuItem) item).getContent();
return (content instanceof TaskProgress
&& tasks.contains(((TaskProgress) content).getTask()));
} else if (item instanceof ErrorMenuItem) {
return tasks.contains(((ErrorMenuItem) item).getTask());
}
return false;
}
}
}