/**
 * 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 com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import fr.cenra.rhomeo.DocManager;
import fr.cenra.rhomeo.Rhomeo;
import fr.cenra.rhomeo.api.InternationalResource;
import fr.cenra.rhomeo.api.data.Protocol;
import fr.cenra.rhomeo.api.data.Site;
import fr.cenra.rhomeo.core.BeanUtils;
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.county.County;
import fr.cenra.rhomeo.core.list.HumidZoneType;
import fr.cenra.rhomeo.core.list.OdonateZoneBio;
import fr.cenra.rhomeo.core.list.OrthoptereZoneBio;
import fr.cenra.rhomeo.core.util.ExportUtils;
import fr.cenra.rhomeo.core.data.site.SiteImpl;
import fr.cenra.rhomeo.core.data.site.DuplicatedKeyException;
import fr.cenra.rhomeo.core.data.site.SiteRepository;

import java.beans.IntrospectionException;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.logging.Level;
import java.util.regex.Matcher;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
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.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.PopupWindow;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.StringConverter;
import javax.annotation.PostConstruct;
import javax.xml.stream.XMLStreamException;
import org.apache.sis.storage.DataStoreException;
import org.controlsfx.control.StatusBar;
import org.geotoolkit.data.FeatureCollection;
import org.geotoolkit.data.FeatureIterator;
import org.geotoolkit.data.FeatureStoreUtilities;
import org.geotoolkit.data.bean.BeanFeature;
import org.geotoolkit.data.kml.model.KmlException;
import org.geotoolkit.data.query.Query;
import org.geotoolkit.data.query.QueryBuilder;
import org.geotoolkit.data.shapefile.ShapefileFeatureStore;
import org.geotoolkit.display2d.GO2Utilities;
import org.geotoolkit.feature.Feature;
import org.geotoolkit.feature.Property;
import org.geotoolkit.gui.javafx.layer.FXFeatureTable;
import org.geotoolkit.gui.javafx.render2d.FXCoordinateBar;
import org.geotoolkit.gui.javafx.render2d.FXGeoToolBar;
import org.geotoolkit.gui.javafx.render2d.FXMap;
import org.geotoolkit.gui.javafx.render2d.FXNavigationBar;
import org.geotoolkit.gui.javafx.render2d.FXScaleBarDecoration;
import org.geotoolkit.gui.javafx.render2d.navigation.FXPanHandler;
import org.geotoolkit.gui.javafx.util.TaskManager;
import org.geotoolkit.map.FeatureMapLayer;
import org.geotoolkit.map.MapBuilder;
import org.geotoolkit.map.MapContext;
import org.geotoolkit.map.MapLayer;
import org.opengis.filter.Id;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.opengis.util.FactoryException;
import org.opengis.util.GenericName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import static fr.cenra.rhomeo.core.RhomeoCore.CODE_PATTERN;
import fr.cenra.rhomeo.core.data.county.CountyRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import javafx.beans.binding.Bindings;
import org.apache.sis.referencing.CommonCRS;
import org.geotoolkit.data.kml.model.AbstractGeometry;
import org.geotoolkit.data.kml.model.Kml;
import org.geotoolkit.data.kml.model.MultiGeometry;
import org.geotoolkit.data.kml.xml.KmlReader;
import org.geotoolkit.feature.FeatureBuilder;
import org.geotoolkit.feature.FeatureTypeBuilder;
import org.geotoolkit.feature.type.AttributeDescriptor;
import org.geotoolkit.feature.type.FeatureType;
import org.geotoolkit.feature.type.PropertyDescriptor;
import org.geotoolkit.geometry.jts.JTS;

/**
 * Dashboard panel menu handling protocol and site choice.
 * Will be put in the top part of the {@link FXDashboardPane}.
 *
 * @author Cédric Briançon (Geomatys)
 * @author Alexis Manin    (Geomatys)
 */
@Component
@Scope("prototype")
public class FXDashboardMenuPane extends BorderPane implements InternationalResource {

    @FXML private ImageView uiConfigSiteImg;
    @FXML private ImageView uiCaretSiteImg;
    @FXML private Label uiSelectedSiteLbl;

    /**
     * Protocols banner.
     */
    @FXML private HBox protocolsBannerBox;

    /**
     * Selected site banner.
     */
    @FXML private ComboBox<String> uiSitesList;
    @FXML private ToggleButton uiInfoSiteToggleBtn;
    @FXML private BorderPane uiProtocolSitePane;
    @FXML private HBox uiToCollapseButtonsSiteHbox;
    @FXML private ToggleButton uiNewSiteToggleBtn;
    @FXML private BorderPane uiSiteInfoPane;

    @FXML private GridPane uiInfoSiteGrid;

    /**
     * Fields for info site form.
     */
    @FXML private TextField uiNameSiteTxt;
    @FXML private TextField uiReferentSiteTxt;
    @FXML private Button uiImportContourBtn;
    @FXML private ComboBox<County> uiDepartmentSiteComboBox;
    @FXML private TextField uiStructureSiteTxt;
    @FXML private ComboBox<HumidZoneType> uiTypeZhSiteComboBox;
    @FXML private ComboBox<OdonateZoneBio> uiZbOdoSiteComboBox;
    @FXML private ComboBox<OrthoptereZoneBio> uiZbOrthoSiteComboBox;

    /**
     * Buttons for selected site.
     */
    @FXML private HBox uiInfoSelectedSiteBtnsBox;
    @FXML private ToggleButton uiEditSelectedSiteToggleBtn;
    @FXML private Button uiDownloadSelectedSiteBtn;
    @FXML private Button uiDeleteSelectedSiteBtn;

    /**
     * Action buttons on selected / new site.
     */
    @FXML private Button uiCancelBtn;
    @FXML private Button uiCreateSiteBtn;

    @Autowired
    private Session session;

    @Autowired
    private SiteRepository siteRepository;

    @Autowired(required = false)
    private CountyRepository countyRepository;

    @Autowired
    private DocManager docManager;

    /**
     * Geometry of the selected / new site.
     */
    private final ObjectProperty<MultiPolygon> geomToAdd = new SimpleObjectProperty<>();

    /**
     * Selected site property.
     */
    private final ObjectProperty<Site> selectedSiteProperty;

    /**
     * List of loaded counties. Can be null if an error happens while loading counties.
     */
    private Map<String, County> counties;

    private final BooleanBinding odoTypeFilled;
    private final BooleanBinding odoTypeNotEditable;
    private final BooleanBinding odoTypeNotEditableAndNotNew;
    private final BooleanBinding orthoTypeFilled;
    private final BooleanBinding orthoTypeEditable;
    private final BooleanBinding orthoTypeEditableOrNewSite;
    private final BooleanBinding notEditing;
    private final BooleanBinding notNewSite;

    public FXDashboardMenuPane() {
        Rhomeo.loadFXML(this, FXDashboardMenuPane.class);

        uiConfigSiteImg.setImage(Rhomeo.ICON_COG);
        uiCaretSiteImg.setImage(Rhomeo.ICON_CARET_UP);

        // Stores the site panel open or not property.
        final BooleanProperty showSitesPaneProperty = new SimpleBooleanProperty(true);

        uiProtocolSitePane.visibleProperty().bind(showSitesPaneProperty);
        uiProtocolSitePane.managedProperty().bind(uiProtocolSitePane.visibleProperty());

        // Bind selected site
        selectedSiteProperty = new SimpleObjectProperty<>(this, "selected site");
        uiSitesList.valueProperty().addListener(this::siteNameChanged);
        selectedSiteProperty.addListener(this::siteChanged);
        selectedSiteProperty.addListener(((observable, oldValue, newValue) -> {
            if (oldValue != null) {
                uiSelectedSiteLbl.textProperty().unbind();
            }

            if (newValue != null && newValue.getName() != null && !newValue.getName().isEmpty()) {
                uiSelectedSiteLbl.textProperty().bind(
                        new SimpleStringProperty("SITE : ").concat(uiSitesList.getEditor().textProperty()));
            } else {
                uiSelectedSiteLbl.setText(null);
            }
        }));

        // Display selected site name only if one has been selected and the site panel is collapsed.
        uiSelectedSiteLbl.visibleProperty().bind(
                showSitesPaneProperty.not().and(selectedSiteProperty.isNotNull()));
        uiSelectedSiteLbl.managedProperty().bind(uiSelectedSiteLbl.visibleProperty());

        notNewSite = uiNewSiteToggleBtn.selectedProperty().not();

        // Display info site button if an existing one has been selected
        uiInfoSiteToggleBtn.visibleProperty().bind(selectedSiteProperty.isNotNull().and(notNewSite));

        // Allow to collapse site information pane
        uiToCollapseButtonsSiteHbox.setOnMouseClicked(event -> {
            final boolean showPane = showSitesPaneProperty.get();
            if (showPane) {
                uiCaretSiteImg.setImage(Rhomeo.ICON_CARET_DOWN);
            } else {
                uiCaretSiteImg.setImage(Rhomeo.ICON_CARET_UP);
            }
            showSitesPaneProperty.setValue(!showPane);
        });

        // Sub panel that should open only if its main panel (uiProtocolSitePane) is already opened
        // and either user request to create a new site or to view info on an existing one.
        uiSiteInfoPane.visibleProperty().bind(uiNewSiteToggleBtn.selectedProperty().or(uiInfoSiteToggleBtn.selectedProperty().and(uiInfoSiteToggleBtn.visibleProperty())));
        uiSiteInfoPane.managedProperty().bind(uiSiteInfoPane.visibleProperty());

        // Display action buttons only in the new site mode or editing an existing site mode
        uiCreateSiteBtn.visibleProperty().bind(uiNewSiteToggleBtn.selectedProperty().or(uiEditSelectedSiteToggleBtn.selectedProperty()));
        uiCreateSiteBtn.managedProperty().bind(uiCreateSiteBtn.visibleProperty());
        uiCancelBtn.visibleProperty().bind(uiCreateSiteBtn.visibleProperty());
        uiCancelBtn.managedProperty().bind(uiCancelBtn.visibleProperty());

        /*
         * When new site button state change, we initiate or cancel complete form.
         */
        uiNewSiteToggleBtn.selectedProperty().addListener((obs, old, newVal) -> {
            final Site selected = selectedSiteProperty.get();
            final boolean isNewSite = selected != null && (selected.getName() == null || selected.getName().trim().isEmpty());
            if (newVal) {
                /* Before setting a new site, we ensure user won't lose data on
                 * a previously edited object.
                 */
                if (!isNewSite) {
                    if (confirmCancelling(selected)) {
                        final Site newSite = new SiteImpl(null);
                        selectedSiteProperty.set(newSite);
                    } else {
                        uiNewSiteToggleBtn.setSelected(false); // a rollback has been decided
                    }
                }
            } else {
                // If the button has been de-activated, and we're editing a new
                // site, we clear selection. However, if user ask for a rollback,
                // we re-activate the button.
                if (isNewSite) {
                    if (confirmCancelling(selected)) {
                        selectedSiteProperty.set(null);
                    } else {
                        uiNewSiteToggleBtn.setSelected(true); // a rollback has been decided
                    }
                }
            }
            uiCreateSiteBtn.setText(newVal ? getResourceString("create-site") : getResourceString("save"));
            uiImportContourBtn.setText(newVal ? getResourceString("import-contour") : getResourceString("view-contour"));
        });

        // Add PDF file images in the site form
        final Button typeZhBtn = createPdfButton("Typologie_Habitats.pdf");
        uiInfoSiteGrid.add(typeZhBtn, 6, 1);
        final Button typeZbOdonateBtn = createPdfButton("zones_biogeo_odonates.pdf");
        uiInfoSiteGrid.add(typeZbOdonateBtn, 6, 2);
        final Button typeZbOrthoBtn = createPdfButton("zones_biogeo_orthopteres.pdf");
        uiInfoSiteGrid.add(typeZbOrthoBtn, 6, 3);

        // Button info for selected site
        uiInfoSiteToggleBtn.selectedProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue) {
                uiCreateSiteBtn.setText(getResourceString("save"));
                uiImportContourBtn.setText(getResourceString("view-contour"));
                uiImportContourBtn.setGraphic(new ImageView(Rhomeo.ICON_EYE_GRAY));
                uiEditSelectedSiteToggleBtn.setSelected(false);
                uiInfoSiteToggleBtn.setGraphic(new ImageView(Rhomeo.ICON_INFO_BLUE));
            } else {
                uiInfoSiteToggleBtn.setGraphic(new ImageView(Rhomeo.ICON_INFO_GREY));
            }
        });
        uiInfoSiteToggleBtn.setGraphic(new ImageView(Rhomeo.ICON_INFO_GREY));
        uiInfoSiteToggleBtn.setTooltip(new Tooltip(getResourceString("view-info-site-tooltip")));

        // Panel containing buttons for selected site.
        uiInfoSelectedSiteBtnsBox.visibleProperty().bind(uiInfoSiteToggleBtn.selectedProperty().and(notNewSite));
        uiInfoSelectedSiteBtnsBox.managedProperty().bind(uiInfoSelectedSiteBtnsBox.visibleProperty());
        // Buttons images for selected site actions.
        uiDownloadSelectedSiteBtn.setGraphic(new ImageView(Rhomeo.ICON_DOWNLOAD_BLUE));
        uiDownloadSelectedSiteBtn.setTooltip(new Tooltip(getResourceString("download-selected-site-tooltip")));
        uiDeleteSelectedSiteBtn.setGraphic(new ImageView(Rhomeo.ICON_TRASH_BLUE));
        uiDeleteSelectedSiteBtn.setTooltip(new Tooltip(getResourceString("delete-selected-site-tooltip")));
        uiEditSelectedSiteToggleBtn.setGraphic(new ImageView(Rhomeo.ICON_PENCIL_BLUE));
        uiEditSelectedSiteToggleBtn.setTooltip(new Tooltip(getResourceString("edit-selected-site-tooltip")));
        uiEditSelectedSiteToggleBtn.selectedProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue) {
                uiEditSelectedSiteToggleBtn.setGraphic(new ImageView(Rhomeo.ICON_PENCIL_WHITE));
            } else {
                uiEditSelectedSiteToggleBtn.setGraphic(new ImageView(Rhomeo.ICON_PENCIL_BLUE));
            }
        });

        geomToAdd.addListener((observable, oldValue, newValue) -> {
            if (uiNewSiteToggleBtn.isSelected()) {
                uiImportContourBtn.setGraphic(newValue != null ? new ImageView(Rhomeo.ICON_CHECK_GRAY) : null);
            }
        });

        // Bind editable form fields
        notEditing = uiEditSelectedSiteToggleBtn.selectedProperty().not();
        uiNameSiteTxt.disableProperty().bind(notEditing.and(notNewSite));
        uiReferentSiteTxt.disableProperty().bind(notEditing.and(notNewSite));
        uiStructureSiteTxt.disableProperty().bind(notEditing.and(notNewSite));
        // These combo boxes are only editable in the case of a new site.
        uiDepartmentSiteComboBox.disableProperty().bind(notNewSite);
        uiTypeZhSiteComboBox.disableProperty().bind(notNewSite);
        // The two fields below are editable until the target site values are empty (see Gitlab issue #37).
        odoTypeFilled = Bindings.createBooleanBinding(() -> {
            final Site ss = selectedSiteProperty.get();
            if (ss == null)
                return false;
            final Optional<OdonateZoneBio> odoOpt = RhomeoCore.getByCode(OdonateZoneBio.class, ss.getOdonateType());
            return odoOpt.isPresent() && !OdonateZoneBio.ZGLOBAL.equals(odoOpt.get());
        }, selectedSiteProperty, uiZbOdoSiteComboBox.valueProperty());
        odoTypeNotEditable = notEditing.or(odoTypeFilled);
        odoTypeNotEditableAndNotNew = notNewSite.and(odoTypeNotEditable);
        uiZbOdoSiteComboBox.disableProperty().bind(odoTypeNotEditableAndNotNew);
        orthoTypeFilled = Bindings.createBooleanBinding(() -> {
            final Site ss = selectedSiteProperty.get();
            if (ss == null)
                return false;
            return RhomeoCore.getByCode(OrthoptereZoneBio.class, ss.getOrthoptereType()).isPresent();
        }, selectedSiteProperty, uiZbOrthoSiteComboBox.valueProperty());
        orthoTypeEditable = notEditing.or(orthoTypeFilled);
        orthoTypeEditableOrNewSite = notNewSite.and(orthoTypeEditable);
        uiZbOrthoSiteComboBox.disableProperty().bind(orthoTypeEditableOrNewSite);

        // Set converters to avoid bad behavior on seizure
        uiTypeZhSiteComboBox.setConverter(new StringConverter<HumidZoneType>() {
            @Override
            public String toString(HumidZoneType object) {
                return object == null? null : object.toString();
            }

            @Override
            public HumidZoneType fromString(String string) {
                if (string == null || (string = string.trim()).isEmpty()) {
                    return null;
                }

                final Matcher matcher = CODE_PATTERN.matcher(string);
                if (matcher.find()) {
                    return HumidZoneType.getByCode(matcher.group());
                }

                return null;
            }
        });

        uiZbOdoSiteComboBox.setConverter(new StringConverter<OdonateZoneBio>() {
            @Override
            public String toString(OdonateZoneBio object) {
                return object == null? null : object.toString();
            }

            @Override
            public OdonateZoneBio fromString(String string) {
                if (string == null || (string = string.trim()).isEmpty()) {
                    return null;
                }

                final Matcher matcher = CODE_PATTERN.matcher(string);
                if (matcher.find()) {
                    return OdonateZoneBio.getByCode(matcher.group());
                }

                return null;
            }
        });

        uiZbOrthoSiteComboBox.setConverter(new StringConverter<OrthoptereZoneBio>() {
            @Override
            public String toString(OrthoptereZoneBio object) {
                return object == null? null : object.toString();
            }

            @Override
            public OrthoptereZoneBio fromString(String string) {
                if (string == null || (string = string.trim()).isEmpty()) {
                    return null;
                }

                final Matcher matcher = CODE_PATTERN.matcher(string);
                if (matcher.find()) {
                    return OrthoptereZoneBio.getByCode(matcher.group());
                }

                return null;
            }
        });

        // Fill lists
        Rhomeo.initComboBox(uiTypeZhSiteComboBox, FXCollections.observableArrayList(HumidZoneType.values()));
        Rhomeo.initComboBox(uiZbOdoSiteComboBox, FXCollections.observableArrayList(OdonateZoneBio.values()));
        Rhomeo.initComboBox(uiZbOrthoSiteComboBox, FXCollections.observableArrayList(OrthoptereZoneBio.values()));
    }

    /**
     * Initializes JavaFX components after spring components are done loading.
     */
    @PostConstruct
    private void postConstruct() {
        Rhomeo.initComboBox(uiSitesList, siteRepository.findNames());
        final EventHandler<MouseEvent> filter = evt -> {
            if (MouseEvent.MOUSE_PRESSED.equals(evt.getEventType()) ||
                    MouseEvent.MOUSE_CLICKED.equals(evt.getEventType())) {
                final Site site = siteProperty().get();
                if (site == null || site.equals(prepareSiteFromForm(null)))
                    return;

                if (confirmCancelling(site)) {
                    selectedSiteProperty.set(null);
                    uiSitesList.requestFocus();
                } else {
                    evt.consume();
                }
            }
        };
        uiSitesList.addEventFilter(MouseEvent.ANY, filter);

        // Load county choice
        if (countyRepository == null) {
            uiDepartmentSiteComboBox.disableProperty().unbind();
            uiDepartmentSiteComboBox.setDisable(true);
            Rhomeo.showAlert(Alert.AlertType.WARNING, "Impossible de charger la liste des départements. Il sera impossible de créer de nouveaux sites.");
        } else {
            counties = countyRepository.getAll();
            final String countyCode = siteProperty().get() == null ? null : siteProperty().get().getCountyCode();
            Rhomeo.initComboBox(uiDepartmentSiteComboBox, FXCollections.observableArrayList(counties.values()).sorted(), countyCode == null? null : counties.get(countyCode));
            uiDepartmentSiteComboBox.setConverter(countyRepository.getStringConverter());
        }

        session.getProtocols().stream()
                .sorted()
                .forEach(protocol -> {
                    final Button protocolButton = new Button(protocol.getName());
                    protocolButton.getStyleClass().add(Rhomeo.CSS_PROTOCOL_BUTTON);

                    // Prepare tooltip destined to display bound indicators.
                    final StringJoiner chainBuilder = new StringJoiner(" / ");
                    session.getIndicators().stream()
                            .filter(indicator -> indicator.getProtocol() == protocol)
                            .sorted()
                            .forEach(indicator -> chainBuilder.add(indicator.getTitle().concat(" (").concat(indicator.getRemarks()).concat(")")));
                    final Tooltip tt = new Tooltip(chainBuilder.toString());
                    tt.getStyleClass().add(Rhomeo.CSS_PROTOCOL_POPUP_BODY);
                    tt.setAnchorLocation(PopupWindow.AnchorLocation.CONTENT_BOTTOM_LEFT);
                    // Force tooltip to show on the upper right corner of the
                    // associated protocol button.
                    tt.setOnShowing(evt -> {
                        final Bounds parentBounds = protocolButton.getBoundsInLocal();
                        final Point2D screen = protocolButton.localToScreen(parentBounds.getMaxX(), parentBounds.getMinY());
                        tt.setAnchorX(screen.getX() > 10? screen.getX() -10 : screen.getX());
                        tt.setAnchorY(screen.getY() + 10);
                    });


                    protocolButton.setTooltip(tt);

                    // Configure protocol button behavior.
                    selectedSiteProperty.addListener((observable, oldValue, newValue) -> {
                        protocolButton.setDisable(newValue == null || !protocol.isCompatible(newValue));
                    });
                    protocolButton.setDisable(true);
                    protocolButton.setOnAction(event -> {
                        startEdition(selectedSiteProperty.get(), protocol);
                    });
                    protocolsBannerBox.getChildren().add(protocolButton);
                    protocolsBannerBox.disableProperty().bind(uiNewSiteToggleBtn.selectedProperty());
                });
    }

    /**
     * Expose the selected site property.
     *
     * @return a property, never {@code null}.
     */
    public ObjectProperty<Site> siteProperty() {
        return selectedSiteProperty;
    }

    /**
     * Expose the new site selected property.
     *
     * @return a property, never {@code null}
     */
    public BooleanProperty newSiteProperty() {
        return uiNewSiteToggleBtn.selectedProperty();
    }

    /**
     * Switch to the selected protocol view.
     *
     * @param site The selected site, should not be {@code null}
     * @param protocol The protocol to show, should not be {@code null}
     */
    public void startEdition(final Site site, final Protocol protocol) {
        // Defines the selected context
        session.startEdition(site, protocol);
        session.requestWorkflowStep(WorkflowStep.DATASET);
    }

    /**
     * Exports the current site as :
     * - either a .zip file containing shapefile resources,
     * - or a .kml file.
     */
    @FXML
    private void downloadSelectedSite() throws DataStoreException, XMLStreamException, IOException, KmlException, TransformException {

        final Site site = selectedSiteProperty.get();

        if(site==null) return;

        final FileChooser.ExtensionFilter[] filters = new FileChooser.ExtensionFilter[] {
                new FileChooser.ExtensionFilter("Archive zip", "*.zip"),
                new FileChooser.ExtensionFilter("Shapefile", "*.shp"),
                new FileChooser.ExtensionFilter("Keyhole Markup Language", "*.kml"),
                new FileChooser.ExtensionFilter("Tous", "*")
        };
        final File outputFile = Rhomeo.askOutputFilePopup(site.getName()+".zip", this, filters);
        if(outputFile==null) return;

        Rhomeo.setPreviousPath(ExportUtils.class, outputFile.getParentFile());

        if(outputFile.getName().endsWith(".shp")){
            ExportUtils.writeShp(site, outputFile);
        }
        else if(outputFile.getName().endsWith(".kml")){
            ExportUtils.writeKml(site, outputFile);
        }
        else if(outputFile.getName().endsWith(".zip")){
            ExportUtils.writeZip(site, outputFile);
        }
        else {
            Rhomeo.showAlert(Alert.AlertType.ERROR, getResourceString("alert-site-download"));
        }
    }

    /**
     * Delete selected site if the user validates this operation.
     */
    @FXML
    private void deleteSelectedSite() {
        final Site selectedSite = selectedSiteProperty.get();
        final String siteName = selectedSite.getName();
        final Alert alert = new Alert(Alert.AlertType.CONFIRMATION,
                getFormatedResourceString("alert-site-deleted", siteName),
                ButtonType.YES, ButtonType.NO);
        alert.setResizable(true);
        final ButtonType res = alert.showAndWait().get();
        if (res == ButtonType.YES) {
            siteRepository.delete(siteName);
            try {
                session.serializeDeleteSite(siteName);
            } catch (IOException | IntrospectionException | ReflectiveOperationException ex) {
                throw  new RhomeoRuntimeException(ex);
            }
            cancel();
        }
    }

    /**
     * Import of a site into the system. Ask user for a shapefile as contour for this site.
     *
     * @throws MalformedURLException if the specified shapefile path is invalid
     * @throws DataStoreException if unable to read the shapefile specified
     */
    @FXML
    private void importSite() throws MalformedURLException, DataStoreException {
        uiNewSiteToggleBtn.setSelected(true);
        // If no rollback has been performed, we proceed to the import.
        if (uiNewSiteToggleBtn.isSelected()) {
            if (!importShapefile()) {
                cancel();
            }
        }
    }

    /**
     * Ask user for a shapefile to use as contour for the site, or view the selected site SHP.
     *
     * @throws MalformedURLException if the specified shapefile path is invalid
     * @throws DataStoreException if unable to read the shapefile specified
     */
    @FXML
    private void importOrViewContour() throws MalformedURLException, DataStoreException {
        // Create / import new contour mode
        if (uiNewSiteToggleBtn.isSelected()) {
            importShapefile();

        } else {
            // View the selected site contour
            final Stage viewShpStage = createShpViewStage();
            viewShpStage.initOwner(this.getScene().getWindow());
            viewShpStage.show();
            viewShpStage.requestFocus();
        }
    }

    private boolean importShapefile() throws MalformedURLException {
        final FileChooser fileChooser = new FileChooser();
        fileChooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("ShapeFile (*.shp)", "*.shp", "*.SHP"),
                new FileChooser.ExtensionFilter("Keyhole Markup Language (*.kml)", "*.kml", "*.KML")
        );
        final File prevPath = Rhomeo.getPreviousPath(FXDashboardMenuPane.class);
        if (prevPath != null) {
            fileChooser.setInitialDirectory(prevPath);
        }
        final File file = fileChooser.showOpenDialog(getScene().getWindow());
        if (file != null) {
            Rhomeo.setPreviousPath(FXDashboardMenuPane.class, file.getParentFile());
        } else {
            Rhomeo.LOGGER.log(Level.INFO, "Un fichier .shp doit être choisi.");
            return false;
        }

        final Task<Feature> chooseFeature = choose(file.toURI());
        final Stage loadingStage = new Stage(StageStyle.TRANSPARENT);
        loadingStage.initModality(Modality.APPLICATION_MODAL);
        loadingStage.initOwner(this.getScene().getWindow());
        final ProgressIndicator pi = new ProgressIndicator();
        pi.setBackground(Background.EMPTY);
        final Scene scene = new Scene(pi);
        scene.setFill(null);
        loadingStage.setScene(scene);
        loadingStage.show();
        chooseFeature.runningProperty().addListener((obs, oldValue, newValue) -> {
            if (!newValue)
                loadingStage.close();
        });

        chooseFeature.setOnFailed((evt) -> Platform.runLater(()
                -> Rhomeo.showAlert(Alert.AlertType.ERROR, "Une erreur s'est produite lors de la lecture du fichier.")
        ));

        chooseFeature.setOnSucceeded((evt) -> Platform.runLater(() -> {
            final Feature feature = chooseFeature.getValue();
            if (feature == null) {
                return;
            }

            Geometry tmpGeom = feature.getDefaultGeometryProperty() == null? null : (Geometry) feature.getDefaultGeometryProperty().getValue();
            if (tmpGeom instanceof LinearRing) {
                tmpGeom = tmpGeom.getFactory().createPolygon((LinearRing) tmpGeom);
            }
            if (tmpGeom instanceof Polygon) {
                tmpGeom = new MultiPolygon(new Polygon[]{(Polygon) tmpGeom}, tmpGeom.getFactory());
            }

            if (tmpGeom instanceof MultiPolygon) {
                geomToAdd.set((MultiPolygon) tmpGeom);
            } else {
                Rhomeo.showAlert(Alert.AlertType.ERROR, "Impossible d'importer la donnée car sa géométrie n'est pas un polygone.");
            }

            // update fx UI
            final Property nameProp = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.NAME, "nom", "nom_site", "nomSite", "siteName", "site_name");
            if (nameProp != null && nameProp.getValue() != null) {
                uiNameSiteTxt.setText(nameProp.getValue().toString());
            }
            final Property refProp = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.REFERENT);
            if (refProp != null && refProp.getValue() != null) {
                uiReferentSiteTxt.setText(refProp.getValue().toString());
            }
            final Property orgProp = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.ORG);
            if (orgProp != null && orgProp.getValue() != null) {
                uiStructureSiteTxt.setText(orgProp.getValue().toString());
            }
            final Property dep = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.COUNTY, "dept");
            County sourceCounty = dep == null || dep.getValue() == null ? null : counties.get(dep.getValue().toString().trim());
            if (geomToAdd.get() != null && countyRepository != null) {
                List<County> matching = countyRepository.get(geomToAdd.get());
                if (!matching.isEmpty() && !matching.contains(sourceCounty))
                    sourceCounty = matching.get(0);
            }

            if (sourceCounty != null) {
                uiDepartmentSiteComboBox.setValue(sourceCounty);
            } else {
                uiDepartmentSiteComboBox.setValue(null);
            }

            final Property type = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.TYPE, "code_ty_01", "type_zone");
            if (type != null && type.getValue() != null && !type.getValue().toString().isEmpty()) {
                uiTypeZhSiteComboBox.getSelectionModel().select(HumidZoneType.getByCode(type.getValue().toString()));
            }
            final Property odo = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.ODONATE);
            if (odo != null && odo.getValue() != null && !odo.getValue().toString().isEmpty()) {
                uiZbOdoSiteComboBox.getSelectionModel().select(OdonateZoneBio.getByCode(odo.getValue().toString()));
            }
            final Property ortho = getByNameIgnoreCase(feature, Rhomeo.FT_PROPERTIES.ORTHOPTERE);
            if (ortho != null && ortho.getValue() != null && !ortho.getValue().toString().isEmpty()) {
                uiZbOrthoSiteComboBox.getSelectionModel().select(OrthoptereZoneBio.getByCode(ortho.getValue().toString()));
            }
        }));

        TaskManager.INSTANCE.submit(chooseFeature);
        return true;
    }

    private Property getByNameIgnoreCase(final Feature source, final Rhomeo.FT_PROPERTIES p, final String... fallback) {
        for (final Property prop : source.getProperties()) {
            if (prop.getName().tip().toString().equalsIgnoreCase(p.name()))
                return prop;
        }

        if (fallback != null && fallback.length > 0) {
            final HashSet<String> lcNames = new HashSet<>(fallback.length);
            for (final String str : fallback) {
                if (str != null && !str.isEmpty())
                    lcNames.add(str.toLowerCase());
            }

            for (final Property prop : source.getProperties()) {
                if (lcNames.contains(prop.getName().tip().toString().toLowerCase()))
                    return prop;
            }
        }

        return null;
    }

    private Task<Feature> choose(final URI shpFile) {
        return new TaskManager.MockTask<>("Lecture d'un fichier", () -> {
            Feature feature = null;
            final CoordinateReferenceSystem siteCRS = Rhomeo.getSiteCRS();

            // First, we try to read a KML file. If it fails, we'll try shapefile format.
            Exception kmlError;
            try {
                KmlReader kmlReader = new KmlReader();
                kmlReader.setInput(shpFile);
                kmlReader.setUseNamespace(false);
                return mapKml(kmlReader.read(), siteCRS);
            } catch (Exception e) {
                kmlError = e;
            }

            try (final ShapefileFeatureStore shpStore = new ShapefileFeatureStore(shpFile, "no namespace")) {
                final Set<GenericName> names = shpStore.getNames();
                if (names != null && !names.isEmpty()) {
                    final GenericName layerName = names.iterator().next();
                    final Query baseQuery = QueryBuilder.all(layerName);
                    final FeatureCollection col = shpStore.createSession(false)
                            .getFeatureCollection(baseQuery);

                    if (col.getFeatureType().getCoordinateReferenceSystem() == null) {
                        Rhomeo.showAlert(Alert.AlertType.ERROR, "Aucune projection n'a été trouvée dans le fichier. Il est impossible d'importer les données.");
                        throw new IllegalArgumentException("No valid CRS in file ".concat(shpFile.toString()));
                    }

                    if (col.isEmpty()) {
                        feature = null;
                    } else if (col.size() == 1) {
                        try (final FeatureIterator it = col.subCollection(QueryBuilder.reprojected(layerName, siteCRS)).iterator()) {
                            if (it.hasNext()) {
                                feature = it.next();
                            }
                        }
                    } else {
                        final FeatureMapLayer tmpLayer = MapBuilder.createFeatureLayer(col);
                        final FXFeatureTable choiceTable = new FXFeatureTable();
                        choiceTable.setLoadAll(true);
                        choiceTable.setEditable(false);
                        choiceTable.init(tmpLayer); // TODO : pagination

                        feature = Rhomeo.fxRunAndWait(() -> {
                            final Alert a = new Alert(Alert.AlertType.CONFIRMATION, null, ButtonType.CANCEL, ButtonType.OK);
                            a.getDialogPane().setContent(choiceTable);
                            a.setHeaderText("Le fichier contient plusieurs entrées. Veuillez choisir celle à importer.");
                            a.setResizable(true);
                            if (ButtonType.OK.equals(a.showAndWait().orElse(ButtonType.CANCEL))) {
                                final Id selectionFilter = tmpLayer.getSelectionFilter();
                                if (selectionFilter != null) {
                                    final QueryBuilder builder = new QueryBuilder(layerName);
                                    builder.setFilter(selectionFilter);
                                    builder.setCRS(siteCRS);
                                    try (final FeatureIterator it = col.subCollection(builder.buildQuery()).iterator()) {
                                        if (it.hasNext()) {
                                            return it.next();
                                        }
                                    }
                                }
                            }
                            return null;
                        });
                    }
                }
            } catch (Exception e) {
                e.addSuppressed(kmlError);
                throw e;
            }

            return feature;
        });
    }

    private Feature mapKml(final Kml kml, final CoordinateReferenceSystem targetCRS) throws Exception {
        final Feature kmlFeature = kml.getAbstractFeature();
        final FeatureTypeBuilder builder = new FeatureTypeBuilder();
        builder.setName("site");
        GenericName geomName = null;
        for (final PropertyDescriptor pType : kmlFeature.getType().getDescriptors()) {
            if (AbstractGeometry.class.isAssignableFrom(pType.getType().getBinding())) {
                builder.add(pType.getName(), Geometry.class);
                builder.setDefaultGeometry(pType.getName());
                geomName = pType.getName();
            } else if (pType instanceof AttributeDescriptor) {
                builder.add(pType.getName(), pType.getType().getBinding(), 1, 1, true, null);
            }
        }

        final FeatureType targetType = builder.buildSimpleFeatureType();
        final FeatureBuilder target = new FeatureBuilder(targetType);
        for (final Property p : kmlFeature.getProperties()) {
            if (p.getValue() == null)
                continue;
            if (p.getName().equals(geomName)) {
                final Geometry geom = convert(p.getValue());
                if (geom != null) {
                    JTS.setCRS(geom, CommonCRS.WGS84.normalizedGeographic());
                    target.setPropertyValue(p.getName(), JTS.convertToCRS(geom, targetCRS));
                }
            } else {
                target.setPropertyValue(p.getName(), p.getValue());
            }
        }

        return target.buildFeature(kmlFeature.getIdentifier().getID());
    }

    /**
     * Note : Return only polygons or multi-polygons.
     * @param source The object to convert into polygon or multi-polygon
     * @return Converted geometry, or null.
     * @throws Exception
     */
    private static Geometry convert(final Object source) {
        if (source instanceof Polygon)
            return (Geometry) source;
        else if (source instanceof MultiGeometry) {
            final MultiGeometry kmlMulti = (MultiGeometry) source;
            final List<Polygon> geoms = new ArrayList<>(0);

            //loop on geometry to add id on a GeometryList
            for (AbstractGeometry abstractGeometry : kmlMulti.getGeometries()) {
                if (abstractGeometry instanceof Polygon) {
                    geoms.add((Polygon) abstractGeometry);
                }
            }
            return new GeometryFactory().createMultiPolygon(geoms.toArray(new Polygon[geoms.size()]));

        } else
            return null;
    }

    /**
     * Creates a {@linkplain Stage stage} with a map showing the currently selected site geometry.
     *
     * @return A stage with the map, never {@code null}
     */
    private Stage createShpViewStage() {
        final Site currentSite = selectedSiteProperty.get();
        // Should not be null when called here
        if (currentSite == null) {
            throw new IllegalArgumentException("Currently selected site was null here, but shouldn't be");
        }

        final Stage stage = Rhomeo.newStage();
        stage.setResizable(true);
        stage.setTitle(currentSite.getName());
        stage.setWidth(1024);
        stage.setHeight(768);

        final FXMap map = new FXMap(false);
        map.addDecoration(new FXScaleBarDecoration());
        map.setHandler(new FXPanHandler(false));
        final FXCoordinateBar coordBar = new FXCoordinateBar(map);
        coordBar.setCrsButtonVisible(false);
        // Remove the first item among left items in the status bar, should be the timeline button
        ((StatusBar)coordBar.getChildren().get(0)).getLeftItems().remove(0);

        final FXNavigationBar navBar = new FXNavigationBar(map);
        // Remove the go to coordinate button
        ((HBox)navBar.getItems().get(0)).getChildren().remove(4);
        final FXGeoToolBar geoToolBar = new FXGeoToolBar(map);
        final BorderPane fillPane = new BorderPane();
        fillPane.setMaxWidth(Double.MAX_VALUE);

        final GridPane topgrid = new GridPane();
        navBar.setMaxHeight(Double.MAX_VALUE);
        geoToolBar.setMaxHeight(Double.MAX_VALUE);
        topgrid.add(navBar, 0, 0);
        topgrid.add(geoToolBar, 1, 0);
        final ColumnConstraints col2 = new ColumnConstraints();
        col2.setHgrow(Priority.ALWAYS);
        topgrid.getColumnConstraints().addAll(new ColumnConstraints(), col2);

        // If counties are available, we display them on the map.
        final MapContext context = MapBuilder.createContext();

        if (countyRepository != null) {
            try {
                context.layers().add(MapBuilder.createFeatureLayer(
                        countyRepository.getFeatures(), GO2Utilities.STYLE_FACTORY.style(GO2Utilities.STYLE_FACTORY.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));
        context.layers().add(layer);
        context.setAreaOfInterest(layer.getBounds());
        map.getContainer().setContext(context);
        try {
            map.getCanvas().setObjectiveCRS(Rhomeo.getSiteCRS());
            map.getCanvas().setVisibleArea(layer.getBounds());
        } catch (FactoryException | TransformException | java.awt.geom.NoninvertibleTransformException e) {
            throw new IllegalArgumentException("Unable to defines canvas CRS", e);
        }

        final Scene scene = new Scene(new BorderPane(map, topgrid, null, coordBar, null));
        stage.setScene(scene);

        return stage;
    }

    /**
     * Create or update a site, depending if we are viewing the information panel as a new or existing site.
     */
    @FXML
    private void createOrUpdateSite() {
        // Check data validity
        if (!verifyMandatoryParameters()) {
            return;
        }

        Site site = selectedSiteProperty.get();
        final String oldName = site == null? null : site.getName();
        site = prepareSiteFromForm((SiteImpl)site);

        try {
            // If the site had no name initially, it's a new one. Otherwise, it
            // already existed.
            if (oldName == null || uiNewSiteToggleBtn.isSelected()) {
                siteRepository.create(site);

            } else {
                siteRepository.update(site, oldName);
                try {
                    session.serializeUpdateSiteName(site, oldName);
                } catch (IOException | IntrospectionException | ReflectiveOperationException ex) {
                    throw  new RhomeoRuntimeException(ex);
                }
            }

            // Update selection will set the editor in read-only mode, and ensure
            // that combo-box is aware of the created site.
            selectedSiteProperty.set(null);
            selectedSiteProperty.set(site);

        } catch (DuplicatedKeyException e) {
            Rhomeo.showAlert(Alert.AlertType.WARNING, getFormatedResourceString("alert-site-createOrUpdate", site.getName()));
        }
    }

    /**
     * Verify that all mandatory parameters are filled.
     *
     * @return {@code True} if all parameters are filled, {@code false} if one is missing.
     */
    private boolean verifyMandatoryParameters() {
        // Verify mandatory fields
        boolean result = true;
        if (uiNameSiteTxt.getText() == null || uiNameSiteTxt.getText().trim().isEmpty()) {
            uiNameSiteTxt.getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
            Rhomeo.showAlert(Alert.AlertType.WARNING, "Un nom doît avoir été renseigné pour créer un nouveau site.");
            result = false;
        } else {
            uiNameSiteTxt.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        }

        if (geomToAdd.get() == null) {
            uiImportContourBtn.getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
            if (result) {
                Rhomeo.showAlert(Alert.AlertType.WARNING, "Un contour doît avoir été importé pour créer un nouveau site.");
                result = false;
            }
        } else {
            uiImportContourBtn.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        }

        if (uiDepartmentSiteComboBox.getSelectionModel().isEmpty()) {
            uiDepartmentSiteComboBox.getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
            if (result) {
                Rhomeo.showAlert(Alert.AlertType.WARNING, "Un département doît avoir été sélectionné pour créer un nouveau site.");
                result = false;
            }
        } else {
            uiDepartmentSiteComboBox.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        }

        if (uiTypeZhSiteComboBox.getSelectionModel().isEmpty()) {
            uiTypeZhSiteComboBox.getStyleClass().add(Rhomeo.CSS_ERROR_FIELD);
            if (result) {
                Rhomeo.showAlert(Alert.AlertType.WARNING, "Un type de zone humide doît avoir été sélectionné pour créer un nouveau site.");
                result = false;
            }
        } else {
            uiTypeZhSiteComboBox.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        }
        return result;
    }

    /**
     * Cancel site creation.
     */
    @FXML
    private void cancel() {
        if (confirmCancelling(siteProperty().get())) {
            selectedSiteProperty.set(null);
        }
    }

    /**
     * Clears the site form.
     */
    private void clearForm() {
        uiNameSiteTxt.setText("");
        uiReferentSiteTxt.setText("");
        uiStructureSiteTxt.setText("");
        uiTypeZhSiteComboBox.setValue(null);
        uiDepartmentSiteComboBox.setValue(null);
        uiZbOdoSiteComboBox.setValue(null);
        uiZbOrthoSiteComboBox.setValue(null);
        geomToAdd.set(null);

        uiNameSiteTxt.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        uiImportContourBtn.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        uiDepartmentSiteComboBox.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
        uiTypeZhSiteComboBox.getStyleClass().remove(Rhomeo.CSS_ERROR_FIELD);
    }

    /**
     * Generates a new {@linkplain SiteImpl site} using values filled in the site information form.
     *
     * @param site site to update if not {@code null}. If {@code null}, create a new one.
     * @return A new site, or the same instance with values updated from the form.
     * @throws IllegalArgumentException if the geometry is invalid.
     */
    private SiteImpl prepareSiteFromForm(final SiteImpl site) {
        final SiteImpl siteToPrepare;
        if (site == null) {
            siteToPrepare = new SiteImpl(geomToAdd.get());
        } else {
            siteToPrepare = site;
            siteToPrepare.setGeometry(geomToAdd.get());
        }
        siteToPrepare.setName(uiNameSiteTxt.getText());
        siteToPrepare.setReferent(uiReferentSiteTxt.getText());
        siteToPrepare.setOrganization(uiStructureSiteTxt.getText());

        final County selectedItem = uiDepartmentSiteComboBox.getSelectionModel().getSelectedItem();
        if (selectedItem != null) {
            siteToPrepare.setCountyCode(selectedItem.getCode());
        }

        if (!uiTypeZhSiteComboBox.getSelectionModel().isEmpty()) {
            siteToPrepare.setZoneType(uiTypeZhSiteComboBox.getSelectionModel().getSelectedItem().getCode());
        }
        if (!uiZbOdoSiteComboBox.getSelectionModel().isEmpty()) {
            siteToPrepare.setOdonateType(uiZbOdoSiteComboBox.getSelectionModel().getSelectedItem().getCode());
        }
        if (!uiZbOrthoSiteComboBox.getSelectionModel().isEmpty()) {
            siteToPrepare.setOrthoptereType(uiZbOrthoSiteComboBox.getSelectionModel().getSelectedItem().getCode());
        }

        return siteToPrepare;
    }

    /**
     * Fill the site information form using values coming from a site.
     * Reverse of {@link #prepareSiteFromForm(SiteImpl)}.
     *
     * @param site site
     */
    private void fillFormFromSite(final Site site) {
        uiNameSiteTxt.setText(site.getName());
        uiReferentSiteTxt.setText(site.getReferent());
        uiStructureSiteTxt.setText(site.getOrganization());
        geomToAdd.set(site.getGeometry());

        final String cc = site.getCountyCode();
        County sourceCounty = cc == null || counties == null? null : counties.get(cc.trim());
        if (site.getGeometry() != null && countyRepository != null) {
            List<County> matching = countyRepository.get(site.getGeometry());
            if (!matching.isEmpty() && !matching.contains(sourceCounty))
                sourceCounty = matching.get(0);
        }

        if (sourceCounty != null) {
            uiDepartmentSiteComboBox.setValue(sourceCounty);
        } else {
            uiDepartmentSiteComboBox.setValue(null);
        }

        if (site.getZoneType() != null && !site.getZoneType().isEmpty()) {
            uiTypeZhSiteComboBox.getSelectionModel().select(HumidZoneType.getByCode(site.getZoneType()));
        }
        if (site.getOdonateType() != null && !site.getOdonateType().isEmpty()) {
            uiZbOdoSiteComboBox.getSelectionModel().select(OdonateZoneBio.getByCode(site.getOdonateType()));
        }
        if (site.getOrthoptereType() != null && !site.getOrthoptereType().isEmpty()) {
            uiZbOrthoSiteComboBox.getSelectionModel().select(OrthoptereZoneBio.getByCode(site.getOrthoptereType()));
        }
    }

    private void siteNameChanged(final ObservableValue<? extends String> obs, String oldSite, String newSite) {
        Site site = selectedSiteProperty.get();
        if (site != null && Rhomeo.equivalent(newSite, site.getName()))
            return;

        if (newSite == null || newSite.trim().isEmpty())
            selectedSiteProperty.set(null);
        else
            selectedSiteProperty.set(siteRepository.findOne(newSite));
    }

    private synchronized void siteChanged(final ObservableValue<? extends Site> obs, Site oldValue, Site newValue) {
        clearForm();

        if (newValue != null) {
            fillFormFromSite(newValue);
            if (newValue.getName() != null) {
                uiNewSiteToggleBtn.setSelected(false);
            }
            uiSitesList.setValue(newValue.getName());
        } else {
            uiNewSiteToggleBtn.setSelected(false);
            uiSitesList.setValue(null);
        }
    }

    private boolean confirmCancelling(final Site toCancel) {
        return Rhomeo.fxRunAndWait(() -> {
            if (toCancel != null && !toCancel.equals(prepareSiteFromForm(null))) {
                final Alert modifications = new Alert(Alert.AlertType.CONFIRMATION, getResourceString("unsaved-modification"), ButtonType.NO, ButtonType.YES);
                modifications.setResizable(true);
                if (ButtonType.NO.equals(modifications.showAndWait().orElse(ButtonType.NO))) {
                    return false;
                }
            }

            return true;
        });
    }

    /**
     * Generates a PDF button that will open the PDF file when clicked.
     *
     * @param pdfPath Path of the PDF to open.
     * @return A button, never {@code null}
     */
    private Button createPdfButton(final String pdfPath) {
        final Button button = new Button();
        button.setGraphic(new ImageView(Rhomeo.ICON_FILE_PDF));
        button.getStyleClass().add("transparent");
        button.setOnAction(event ->
                docManager.openDocument(FXDashboardMenuPane.class.getResource(pdfPath))
        );
        return button;
    }
}
