/* ----------------------------------------
 * Origin  : GFI INFORMATIQUE 
 * Project : CDPP Propagation Tool 
 * ----------------------------------------
 * History 
 * Version : 01/05/2015 : SCA : V2.1
 *  Creation 
 * Version : 24/03/2016 : SCA : V2.2 (PSWS-H2020)
 *  Set a default J-map image if not exists
 * ----------------------------------------
 */
package fr.tlse.irap.pt.client.controller.tools.jmap;

import java.util.Date;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.tlse.irap.pt.client.component.CustomComponent;
import fr.tlse.irap.pt.client.component.ICustomComponent;
import fr.tlse.irap.pt.client.controller.RootController;
import fr.tlse.irap.pt.client.controller.common.FilterKeyEvent;
import fr.tlse.irap.pt.client.controller.common.Formatter;
import fr.tlse.irap.pt.client.controller.common.SpaceElementCollection;
import fr.tlse.irap.pt.client.controller.common.Style;
import fr.tlse.irap.pt.client.controller.common.constant.TOOLTIP;
import fr.tlse.irap.pt.client.controller.dialogbox.DialogBox;
import fr.tlse.irap.pt.client.controller.eclipticPlane.EclipticPlaneController;
import fr.tlse.irap.pt.client.controller.tools.IToolController.JMapTool;
import fr.tlse.irap.pt.client.controller.tools.IToolController.Tool;
import fr.tlse.irap.pt.client.controller.tools.fitParameter.FitParameterComponent;
import fr.tlse.irap.fit.Fit;
import fr.tlse.irap.fit.FitCatalog;
import fr.tlse.irap.fit.FitPoint;
import fr.tlse.irap.map.JMap;
import fr.tlse.irap.pt.client.services.ServiceFactory;
import fr.tlse.irap.spaceElement.Probe;
import fr.tlse.irap.spaceElement.SpaceElement;
import fr.tlse.irap.spaceElement.Sun;
import fr.tlse.irap.tool.corotation.Corotation;
import fr.tlse.irap.tool.corotation.CorotationEnd;
import fr.tlse.irap.tool.radial.CMEType;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.Cursor;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;

public class JMapCorotationComponent extends AnchorPane implements IJMapMenu, ICustomComponent {

    protected static final Logger log = LoggerFactory.getLogger(JMapCorotationComponent.class);

    @FXML
    protected GridPane gridPane;
    @FXML
    protected JMapComponent jMapComponent;
    @FXML
    protected TextField beta;
    @FXML
    protected TextField blobsTimeInterval;
    @FXML
    protected TextField rotPeriod;
    @FXML
    protected TextField posAngle;
    @FXML
    protected TextField speed;

    private BooleanProperty hasChanged = new SimpleBooleanProperty();
    private BooleanProperty hasBetaChanged = new SimpleBooleanProperty();
    private BooleanProperty hasBlobsTimeIntervalChanged = new SimpleBooleanProperty();

    private JMapCorotationModel jMapCorotationModel = new JMapCorotationModel();
    private JMapModel jMapModel;
    private JMapPosition jMapPosition;
    private JMapCompute jMapCompute;
    private ObjectProperty<Date> currentDate;
    private IJMapMenu jMapMenu;

    private JMapWorker worker = new JMapWorker();

    private FitParameterComponent fitParameterComponent;
    private EclipticPlaneController eclipticPlaneComponent;

    public JMapCorotationComponent() {
        super();
        CustomComponent.initCustomComponent(this);

        jMapMenu = this;
        jMapCompute = jMapCorotationModel.jMapCompute;
        jMapPosition = jMapCompute.jMapPosition;
        jMapModel = jMapComponent.jMapModel;

        Sun sun = new Sun();
        jMapComponent.setStartSource(sun);
        jMapPosition.setStartSource(sun);

        // Create tooltips
        speed.setTooltip(new Tooltip(TOOLTIP.SPEED));
        rotPeriod.setTooltip(new Tooltip(TOOLTIP.ROT_PERIOD));
        blobsTimeInterval.setTooltip(new Tooltip(TOOLTIP.JMAP_BLOB_INTERVAL));
        beta.setTooltip(new Tooltip(TOOLTIP.JMAP_BETA));

        jMapCorotationModel.getSpeed().bindBidirectional(speed.textProperty());
        jMapCorotationModel.getRotPeriod().bindBidirectional(rotPeriod.textProperty());
        jMapCorotationModel.getBlobsTimeInterval().bindBidirectional(blobsTimeInterval.textProperty());
        jMapCorotationModel.getBeta().bindBidirectional(beta.textProperty());

        blobsTimeInterval.addEventFilter(KeyEvent.ANY, new FilterKeyEvent(blobsTimeInterval, 8, 6, 24, Formatter.getFormatter(), hasBlobsTimeIntervalChanged, blobsTimeInterval.focusedProperty()));
        beta.addEventFilter(KeyEvent.ANY, new FilterKeyEvent(beta, 0, 359, Formatter.getFormatter(), hasBetaChanged, beta.focusedProperty()));

        hasBetaChanged.addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    // update from view
                    jMapCorotationModel.update();
                    // Notify change
                    getHasChanged().set(true);
                    getHasChanged().set(false);
                }
            }
        });

        hasBlobsTimeIntervalChanged.addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (!newValue) {
                    // update from view
                    jMapCorotationModel.updateBlobsInt();
                    // reset fit parameter
                    if (fitParameterComponent != null) {
                        fitParameterComponent.reset();
                    }
                    // Launch automatic compute 
                    if (jMapCorotationModel.getJMapTool() == JMapTool.INSITU) {
                        computeInSitu();
                    } else if (jMapCorotationModel.getJMapTool() == JMapTool.FIT) {
                        setFitFromCatalog();
                    } else {
                        computeClick();
                    }
                }
            }
        });

        // Listen to the change of JMapComponent
        jMapComponent.getHasChanged().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                if (newValue) {
                    if (jMapCorotationModel.getJMapTool().equals(JMapTool.CLICK)) {
                        // compute fit from click points
                        computeClick();
                    } else if (jMapCorotationModel.getJMapTool().equals(JMapTool.FIT)) {
                        // set selected fit from catalog
                        setFitFromCatalog();
                    }
                }
            }
        });

    }

    /**
     * @param eclipticPlaneComponent
     * @param selectedDate
     * @param jMapTool
     */
    public void initComponentWith(EclipticPlaneController eclipticPlaneComponent, ObjectProperty<Date> selectedDate) {
        if (this.currentDate == null) {
            this.eclipticPlaneComponent = eclipticPlaneComponent;
            // bind jmap current date to the selected start date
            currentDate = selectedDate;
            // listen for start date change
            currentDate.addListener(new InvalidationListener() {
                public void invalidated(Observable observable) {
                    // clear positions of all observers and sources
                    if (jMapPosition.isBindStartDate()) {
                        jMapPosition.setDates(currentDate.getValue());
                        worker.updateImage();
                    } else {
                        jMapPosition.bindStartDate();
                    }
                }
            });
            // setting initialization
            jMapPosition.setDates(currentDate.get());
            jMapComponent.initComponent(jMapPosition);
            jMapComponent.initMenu(jMapMenu, jMapCorotationModel.getJMapTool(), Tool.COROTATION);
            SpaceElementCollection.getHasChanged().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> observedBoolean, Boolean oldValue, Boolean newValue) {
                    if (!newValue) {
                        jMapComponent.initMenu(jMapMenu, jMapCorotationModel.getJMapTool(), Tool.COROTATION);
                    }
                }
            });
            jMapModel.reset();
            worker.updateImage();
        }
    }

    /**
     *
     * @param startSourceProperty
     */
    public void setStartSource(ObjectProperty<SpaceElement> startSourceProperty) {
        jMapComponent.setStartSource(startSourceProperty.get());
        jMapPosition.setStartSource(startSourceProperty.get());
        // bind start property change
        startSourceProperty.addListener(new ChangeListener<SpaceElement>() {
            @Override
            public void changed(ObservableValue<? extends SpaceElement> observable, SpaceElement oldValue, SpaceElement newValue) {
                jMapComponent.setStartSource(newValue);
                jMapPosition.setStartSource(newValue);
                // set beta edit mode 
                Style.setEditMode(beta, jMapCorotationModel.getJMapTool() == JMapTool.INSITU ? (jMapPosition.getStartSource() instanceof Sun) : false);
            }
        });
        Style.setEditMode(beta, jMapCorotationModel.getJMapTool() == JMapTool.INSITU ? (jMapPosition.getStartSource() instanceof Sun) : false);
    }

    /**
     * configure component according to jMapTool
     *
     * @param jMapTool
     */
    public void setJMapTool(JMapTool jMapTool) {
        jMapCorotationModel.setJMapTool(jMapTool);
        jMapComponent.setJMapTool(jMapTool, Tool.COROTATION);
        // set beta edit mode 
        Style.setEditMode(beta, jMapTool == JMapTool.INSITU ? (jMapPosition.getStartSource() instanceof Sun) : false);
        if (fitParameterComponent != null) {
            fitParameterComponent.reset();
        }
        // Launch automatic compute 
        if (jMapCorotationModel.getJMapTool() == JMapTool.INSITU) {
            //log.debug("Set JMap tool => call ComputeInSitu()");
            computeInSitu();
        } else if (jMapCorotationModel.getJMapTool() == JMapTool.FIT) {
            computeCatalog();
        }
    }

    /**
     * configure fit result tab
     *
     * @param fitResultTab
     */
    public void setFitResultTab(Tab fitResultTab) {
        if (jMapCorotationModel.getJMapTool() == JMapTool.INSITU) {
            fitResultTab.setDisable(true);
        } else {
            fitResultTab.setDisable(false);
        }
    }

    /**
     * Update Corotation model from JMapCorotationModel
     */
    public void updateCorotation(Corotation corotationModel) {
        jMapCorotationModel.updateCorotation(corotationModel);
    }

    /**
     * Update view from Corotation
     *
     * @param corotation
     */
    public void updateView(Corotation corotation) {
        jMapCorotationModel.updateViewFrom(corotation);

        if (jMapCorotationModel.getJMapTool() == JMapTool.INSITU) {
            // Launch compute InSitu
            //log.debug("Update view from Corotation=> call ComputeInSitu()");
            computeInSitu();
        }
    }

    /**
     * Update View from EndModel
     */
    public void updateView(CorotationEnd endModel, boolean endIsVisible) {
        if (endIsVisible) {
            jMapPosition.setEndSource(endModel.getEndSource());
            jMapPosition.setImpactTime(endModel.getEndTime().getTime());
        } else {
            jMapPosition.setEndSource(null);
            jMapPosition.setImpactTime(null);
        }
        jMapComponent.resetView();
    }

    /**
     * Accessors
     */
    public JMapCorotationModel getModel() {
        return jMapCorotationModel;
    }

    public JMapComponent getJMapComponent() {
        return jMapComponent;
    }

    public void updateEclipticFOV() {
        eclipticPlaneComponent.updateFovWith(jMapComponent.jMapModel);
    }

    /**
     * ***********************************************************************
     ********************* jMapComponent menu actions ************************
     ************************************************************************
     */
    @Override
    public void runShowPlanet(SpaceElement spaceElement) {
        jMapComponent.showHideElement(spaceElement);
    }

    @Override
    public void runShowAllPlanets() {
        jMapComponent.showAllPlanets();
    }

    @Override
    public void runShowStartEnd() {
        jMapComponent.showStartEnd();
    }

    @Override
    public void runShowAllProbes() {
        jMapComponent.showAllProbes();
    }

    @Override
    public void runShowProbe(SpaceElement spaceElement) {
        jMapComponent.showHideElement(spaceElement);
    }

    @Override
    public void runShowAllComets() {
        jMapComponent.showAllComets();
    }

    @Override
    public void runShowComet(SpaceElement spaceElement) {
        jMapComponent.showHideElement(spaceElement);
    }

    @Override
    public void runZoomP1() {
        jMapComponent.zoomP1();
    }

    @Override
    public void runZoomP2() {
        jMapComponent.zoomP2();
    }

    @Override
    public void runResetZoom() {
        jMapComponent.resetZoom();
    }

    @Override
    public void runReset() {
        jMapComponent.resetStartEnd();
    }

    @Override
    public void runChooseCatalog(boolean fromLinkcat, CMEType cirType) {
    }

    @Override
    public void runSelectFit() {
        jMapComponent.setSelectFitInProgress(true);
    }

    @Override
    public void runRemoveLastPoint() {
        jMapComponent.removeLastPoint();
    }

    @Override
    public void runFitPoints() {
        jMapComponent.fitPoints();
    }

    /**
     * ***********************************************************************
     ************************ Specific menu actions **************************
     ************************************************************************
     */
    @Override
    public void runClickPoints() {
        jMapCorotationModel.clearView();
        jMapComponent.startClickPoints();
        fitParameterComponent.reset();
    }

    @Override
    public void runClickPointsFile() {
        if (jMapComponent.updloadClickPoints()) {
            jMapCorotationModel.clearView();
        }
    }

    @Override
    public void runSavePoints() {
        jMapComponent.saveClickPoints();
    }

    /**
     * ***********************************************************************
     **************************** choose map menu ****************************
     ************************************************************************
     */
    @Override
    public void runChooseMap(String source, String type) {
        worker.updateImage(source, type);
    }

    /**
     * Workers sub-class - Upload data from server and display on IHM
     */
    private class JMapWorker {

        private JMap jMap = null;
        private Probe observer = null;
        private boolean defaultImage;
        private boolean newSource;

        /**
         * Update J-map image from observer at the current date.
         */
        public void updateImage(String source, String type) {
            jMapModel.update(source, type);
            newSource = true;
            update();
        }

        /**
         * Update J-map image from a new date
         */
        public void updateImage() {
            // clear positions of obs and sources
            jMapPosition.clearPositionsObservers();
            jMapPosition.clearPositionsSources();
            newSource = false;
            update();
        }

        public void update() {
            final Task<Object> updateImageWorker = this.updateImageWorker();
            updateImageWorker.stateProperty().addListener(new ChangeListener<Worker.State>() {
                @Override
                public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {

                    if (newState == Worker.State.SUCCEEDED) {
                        // thread is done with success
                        defaultImage = false;
                        if (jMapComponent.updateImage(jMap)) {
                            // update view
                            jMapModel.updateView();
                            jMapPosition.setObserverInSitu(jMapModel.getObserver());
                        }
                        // Reset JMap and Fit parameter components
                        jMapComponent.resetView();
                        fitParameterComponent.reset();
                        updateEclipticFOV();
                        endCursor();

                        // clear all fit series
                        jMapComponent.clearBlobsSeries();
                        jMapComponent.clearFitSeries();
                        jMapComponent.clearCatalogSeries();

                        //log.debug("Image has changed => call Compute()");
                        if (jMapCorotationModel.getJMapTool() == JMapTool.INSITU) {
                            computeInSitu();
                        } else if (jMapCorotationModel.getJMapTool() == JMapTool.FIT) {
                            computeCatalog();
                        }
//                    } else {
                    } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                        // thread is aborted
                        if (observer == null) {
                            DialogBox.warn(jMapModel.getObserver() + " probe is not available at this date !");
                            jMapModel.reset();
                            //update();
                        } else if (!defaultImage) {
                            if (newSource) {
                                DialogBox.warn("Image not available !");
                            }
                            // get default image
                            defaultImage = true;
                            update();
                        } else {
                            defaultImage = false;
                            jMapComponent.clearImage();
                        }
                        if (updateImageWorker.getException() != null) {
                            log.info("Jmap not found - " + updateImageWorker.getException().getMessage());
                        }
                        endCursor();
                    }
                }
            });
            new Thread(updateImageWorker).start();
            waitCursor();
        }

        private Task<Object> updateImageWorker() {
            return new Task<Object>() {
                @Override
                protected Object call() {
                    observer = null;
                    // Get position of observer at time tStart
                    observer = jMapPosition.getPositionObserver(jMapModel.getObserver());

                    if (defaultImage) {
                        // Load default J-map for observer between t1 and t2
                        jMap = ServiceFactory.getJMapService().getDefaultJMap(observer, jMapModel.getType(), jMapPosition.getT1(), jMapPosition.getT2());
                    } else {
                        // Load J-map for observer between t1 and t2
                        jMap = ServiceFactory.getJMapService().getJMap(observer, jMapModel.getType(), jMapPosition.getT1(), jMapPosition.getT2());
                    }
                    if (jMap == null || jMap.getHeader() == null) {
                        jMap = null;
                        throw new NullPointerException("No JMap Found");
                    }
                    return true;
                }
            };
        }
    }

    /**
     * Compute Click
     */
    public void computeClick() {
        final Task<Object> computeClickWorker = this.computeClickWorker();
        computeClickWorker.stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                if (newState == Worker.State.SUCCEEDED) {
                    // update view
                    jMapCorotationModel.updateView();
                    // Add fit in situ series
                    jMapComponent.addFitSeries(jMapCompute.getFitPoints());
                    // Compute Blobs Pattern
                    computeBlobs();
                    // notify change
                    getHasChanged().set(true);
                    getHasChanged().set(false);
                    try {
                        // redraw FitParameter fitPoints plot
                        fitParameterComponent.setFitParam(
                                jMapCompute.getBetaStart(),
                                jMapCompute.getSpeedSolarWind(),
                                jMapCompute.getFitPoints(),
                                jMapCompute.getClickPoints(),
                                jMapCompute.getFitError());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    // Clear
                    jMapCompute.setClickPoints(null);
                    jMapCompute.setFitPoints(null);
                    endCursor();

                } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                    // aborted
                    DialogBox.error("Compute Click has failed  !");
                    if (computeClickWorker.getException() != null) {
                        log.info("Compute Click failed - " + computeClickWorker.getException().getMessage());
                    }
                    endCursor();
                }
            }
        });
        new Thread(computeClickWorker).start();
        waitCursor();
    }

    private Task<Object> computeClickWorker() {
        return new Task<Object>() {
            @Override
            protected Object call() {
                jMapCompute.setClickPoints(jMapComponent.getClickPoints());
                // compute click algorithm
                jMapCompute.computeClick();
                // set position of observer in situ from computed time tStart 
                jMapPosition.setPositionObsInSitu();
                // compute Fit
                jMapCompute.computeFit();
                //log.debug("Compute Click successful ended - ");
                return true;
            }
        };
    }

    /**
     * ComputeBlobs => Compute blobs and display series in catalog chart
     */
    private void computeBlobs() {
        // Compute Blobs Pattern
        jMapCompute.computeBlobs();
        // Add blobs series
        jMapComponent.clearBlobsSeries();
        for (List<FitPoint> fitPoints : jMapCompute.getBlobs()) {
            jMapComponent.addBlobSeries(fitPoints);
        }
    }

    /**
     * Compute In Situ
     */
    public void computeInSitu() {
        final Task<Object> computeInSituWorker = this.computeInSituWorker();
        computeInSituWorker.stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                if (newState == Worker.State.SUCCEEDED) {
                    // update view
                    jMapCorotationModel.updateViewInSitu();
                    // Add fit in situ series
                    jMapComponent.addFitSeries(jMapCompute.getFitPoints());
                    // Compute blobs
                    computeBlobs();
                    // Clear
                    jMapCompute.setFitPoints(null);
                    endCursor();

                } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                    endCursor();
                }
            }
        });
        new Thread(computeInSituWorker).start();
        waitCursor();
    }

    private Task<Object> computeInSituWorker() {
        return new Task<Object>() {
            @Override
            protected Object call() {
                // Compute InSitu
                jMapCompute.computeInSitu();

                return true;
            }
        };
    }

    /**
     * Compute Catalog
     */
    public void computeCatalog() {
        final Task<Object> computeCatalogWorker = this.computeCatalogWorker();
        computeCatalogWorker.stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                if (newState == Worker.State.SUCCEEDED) {
                    // clearance
                    jMapComponent.clearFitSeries();
                    jMapComponent.clearCatalogSeries();
                    // Add computed fit series
                    if (jMapCompute.getFitCatalog() != null) {
                        for (Fit fit : jMapCompute.getFitCatalog()) {
                            jMapComponent.addCatalogSeries(fit.getFitPoints());
                        }
                    }
                    RootController.endCursor();

                } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                    // aborted
                    log.info("Compute Catalog failed - " + computeCatalogWorker.getException().getMessage());
                    DialogBox.error("Compute Catalog has failed  !");
                    //endCursor();
                    RootController.endCursor();
                }
            }
        });
        new Thread(computeCatalogWorker).start();
        RootController.waitCursor();
    }

    private Task<Object> computeCatalogWorker() {
        return new Task<Object>() {
            @Override
            protected Object call() {
                // Load catalog parametes of fits (time, beta, speed) for obsInSitu between t1 and t2
                FitCatalog fitCatalog = ServiceFactory.getFitService().fetchFitsCIR(jMapPosition.getT1(), jMapPosition.getT2(), jMapPosition.getObsInSitu().getName());
                // Compute fits from catalog parameters
                jMapCompute.computeFits(fitCatalog.getFits());
                //log.debug("Compute J-Map Catalog successful ended - ");
                return true;
            }
        };
    }

    /**
     * Select Fit from Catalog
     */
    public void setFitFromCatalog() {
        final Task<Object> setFitFromCatalogWorker = this.setFitFromCatalogWorker();
        setFitFromCatalogWorker.stateProperty().addListener(new ChangeListener<Worker.State>() {
            @Override
            public void changed(ObservableValue<? extends Worker.State> observableValue, Worker.State oldState, Worker.State newState) {
                if (newState == Worker.State.SUCCEEDED) {
                    // update view
                    jMapCorotationModel.updateView();
                    // Compute Blobs Pattern
                    computeBlobs();
                    // notify change
                    getHasChanged().set(true);
                    getHasChanged().set(false);
                    try {
                        // redraw FitParameter fitPoints plot
                        fitParameterComponent.setFitParam(
                                jMapPosition.getObsInSitu().getName(),
                                jMapCompute.getFitCatalog().get(jMapComponent.getSelectedFit()));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    endCursor();

                } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                    // aborted
                    log.info("Compute Catalog failed - " + setFitFromCatalogWorker.getException().getMessage());
                    DialogBox.error("Compute Catalog has failed  !");
                    endCursor();
                }
            }
        });
        new Thread(setFitFromCatalogWorker).start();
        waitCursor();
    }

    private Task<Object> setFitFromCatalogWorker() {
        return new Task<Object>() {
            @Override
            protected Object call() {
                // update computed data from selected fit
                jMapCompute.updateFromFit(jMapComponent.getSelectedFit());
                // set position of observer in situ from selected time tStart 
                jMapPosition.setPositionObsInSitu();
                return true;
            }
        };
    }

    private void waitCursor() {
        this.setCursor(Cursor.WAIT);
        gridPane.setDisable(true);
    }

    private void endCursor() {
        this.setCursor(null);
        gridPane.setDisable(false);
    }

    public void setFitParameterComponent(FitParameterComponent fitParameterComponent) {
        this.fitParameterComponent = fitParameterComponent;
    }

    public BooleanProperty getHasChanged() {
        return hasChanged;
    }

    public void setHasChanged(BooleanProperty hasChanged) {
        this.hasChanged = hasChanged;
    }

}
