/* ----------------------------------------
 * Origin  : Akkodis
 * Project : CDPP Propagation Tool 
 * ----------------------------------------
 * History 
 * Version : V2.12
 * DM-ID : Redmine#11103 : 25/03/2025 : LBE : Radio spectro interface added
 * Version : V2.13
 * DM-ID : Redmine#12165 : 27/08/2025 : LBE : Min pts to fit added
 * DM-ID : redmine#12153 : 09/09/2025 : LBE : added WIND radio spectro
 * DM-ID : redmine#12357 : 24/10/2025 : LBE : Fixed fit data clear openjfx bug
 * ----------------------------------------
 */
package fr.tlse.irap.pt.client.controller.tools.radio;

import fr.tlse.irap.fit.Fit;
import fr.tlse.irap.fit.FitCatalog;
import fr.tlse.irap.pt.client.controller.tools.plot.*;
import java.util.ArrayList;
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.Formatter;
import fr.tlse.irap.pt.client.controller.common.Item;
import fr.tlse.irap.pt.client.controller.common.MapMenu;
import fr.tlse.irap.pt.client.controller.tools.IToolController;
import fr.tlse.irap.pt.client.controller.tools.color.IColorTable;
import fr.tlse.irap.pt.client.controller.tools.color.UcarColorTable;
import fr.tlse.irap.pt.client.controller.tools.color.UcarColorTableType;
import fr.tlse.irap.pt.client.controller.tools.jmap.ClickPointsFileHandler;
import static fr.tlse.irap.pt.client.controller.tools.radio.RadioBurstAnalysis.linearRegression;
import fr.tlse.irap.pt.client.controller.tools.radio.RadioRadialModel.eDensityModel;
import fr.tlse.irap.fit.FitPoint;
import fr.tlse.irap.pt.client.controller.dialogbox.DialogBox;
import fr.tlse.irap.pt.client.controller.tools.jmap.JMapCompute;
import fr.tlse.irap.pt.client.services.ServiceFactory;
import fr.tlse.irap.spectro.Spectro;
import fr.tlse.irap.spectro.SpectroWrapper;
import fr.tlse.irap.tool.radial.CMEType;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import java.util.logging.Level;
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.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.stage.FileChooser;
import javafx.util.Pair;

public class RadioRadialComponent extends GridPane implements ICustomComponent, IRadioMenu {

    private static final Logger log = LoggerFactory.getLogger(RadioRadialComponent.class);

    private RadioRadialModel radioModel = new RadioRadialModel();
    private BooleanProperty hasChanged = new SimpleBooleanProperty();

    /**
     * Listened value from radialDashBoardComponent
     */
    private ObjectProperty<Date> startDate;
    private StringProperty speedStrProp;

    /**
     * List of spectro data
     */
    private SpectroWrapper fluxList = new SpectroWrapper();

    /**
     *
     */
    StringProperty source = new SimpleStringProperty("STEREO-A");
    /**
     *
     */
    StringProperty type = new SimpleStringProperty("WAVES");

    /**
     * To record click to fit
     */
    private RadioFitParameterComponent fitParameterComponent;

    /* Selection properties */
    MenuItem savePointsItem, removePointItem, fitPointsItem;
    FitPoint selectedP = new FitPoint();

    boolean isClickPointsInProgress = false;
    /**
     * Contains Slope/intercept result in m/s and seconds from 1970
     */
    double[] currFitResult = null;

    /**
     * Mann conversion arrays to convert Radius to density
     */
    double[] rMann = null;
    double[] neMann = null;

    /**
     * Series defined for the fitChart - [0]: click points - [1]: fit points
     * computed by click
     */
    Series<Number, Number> seriesClick = new Series<Number, Number>();
    Series<Number, Number> seriesFit = new Series<Number, Number>();

    /**
     * current color table
     */
    private IColorTable colorTable = null;

    @FXML
    protected GridPane root;
    @FXML
    protected BorderPane pane;
    //@FXML
    //protected Pane spectroPane;
    @FXML
    protected LineChart<Number, Number> radioChart;
    @FXML
    protected ImageView spectroImg;
    @FXML
    protected TextField legend;

    @FXML
    protected Label sourceLabel;
    @FXML
    protected Label typeLabel;
    @FXML
    protected TextField fitSpeedTf;
    @FXML
    protected TextField fitTStartTf;
    @FXML
    protected TextField fitErrorTf;
    @FXML
    protected NumberAxis xAxis;
    @FXML
    protected LogarithmicAxis yAxis;

    @FXML
    protected Pane spectroColorMap;
    @FXML
    protected ImageView spectroColorMapImg;
    @FXML
    protected LogarithmicAxis spectroColorMapAxis;

    private Double minX, maxX;

    private MapMenu radioMenu;

    //cursor lines
    private Line hLine;
    private Line vLine;
    // selection properties
    private boolean zoomSelection = false;
    private boolean isT1Selected = false;
    private long selectedT1, selectedT2;
    private JMapCompute jMapCompute;

    //private double fluxLineWidth = 0.5;
    /**
     * Display radio data time/freq. Only available in radial mode
     */
    public RadioRadialComponent() {
        super();
        CustomComponent.initCustomComponent(this);
        //Set fits catalog default values
        jMapCompute = new JMapCompute();
        jMapCompute.setLinkcatMode(false);
        jMapCompute.setCmeType(CMEType.FIXED_POINT);

        try {
            double[][] lMannModel = getRMannData();
            rMann = Arrays.stream(lMannModel).mapToDouble(row -> row[0]).toArray();
            neMann = Arrays.stream(lMannModel).mapToDouble(row -> row[1]).toArray();
        } catch (IOException ex) {
            log.error("Error loading mann data file.");
        }

        // Init x-axis
        NumberAxis.DefaultFormatter xAxisFormatter = new NumberAxis.DefaultFormatter(xAxis) {
            @Override
            public String toString(Number object) {
                return Formatter.getTdf().format(object);
            }
        };
        xAxis.setTickLabelFormatter(xAxisFormatter);

        // Create cross lines 
        hLine = new Line(0, 0, 10, 0);
        hLine.setStrokeWidth(0.7);
        hLine.getStrokeDashArray().addAll(4.0);
        vLine = new Line(0, 0, 10, 0);
        vLine.setStrokeWidth(0.7);
        vLine.getStrokeDashArray().addAll(4.0);
        pane.getChildren().add(hLine);
        pane.getChildren().add(vLine);

        //add clickToFit 
        seriesClick.setName("ClickPoints");
        seriesFit.setName("FitLine");
        radioChart.getData().add(seriesClick); // points to fit
        radioChart.getData().add(seriesFit); // fit line

        //bind textx components to values
        sourceLabel.textProperty().bindBidirectional(source);
        typeLabel.textProperty().bindBidirectional(type);

        // bind other charts axis 
        // Init context menu
        loadMenu();

        //mouse on right nrjs list panel
        // Manage mouse events
        pane.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {
                mouseClick(mouseEvent);
            }
        });
        pane.addEventFilter(MouseEvent.MOUSE_MOVED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {
                mouseMove(mouseEvent);
            }
        });
    }

    /**
     * configure component according to jMapTool
     *
     * @param pJMapTool
     */
    public void setJMapTool(IToolController.JMapTool pJMapTool) {

        radioModel.setJMapTool(pJMapTool);
        loadMenu();

        if (fitParameterComponent != null) {
            fitParameterComponent.reset();
        }
        // Launch automatic compute 
        if (radioModel.getJMapTool() == IToolController.JMapTool.INSITU) {
            clearFitResults();
            //if not in catalog, remove fit catalog lines
            clearCatalogSeries();
            computeInSitu(radioModel.getDate(), radioModel.getSpeed());
        } else if (radioModel.getJMapTool() == IToolController.JMapTool.CLICK) {
            //if not in catalog, remove fit catalog lines
            clearCatalogSeries();
            clearFitResults();
            computeInSitu(radioModel.getDate(), radioModel.getSpeed());
        } else if (radioModel.getJMapTool() == IToolController.JMapTool.FIT) {
            clearFitResults();
            getCatalogData();
        }
    }

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

    /*
     * Bind start date to update
     */
    /**
     *
     * @param pStartDate
     * @param pSpeed
     */
    public void initWith(ObjectProperty<Date> pStartDate, StringProperty pSpeed) {

        startDate = pStartDate;
        speedStrProp = pSpeed;
        // init the Component from the selected start date
        radioModel.setDates(startDate.get());
        radioModel.setCmeDate(radioModel.getDate());
        minX = Math.floor((double) (radioModel.getT1()));
        maxX = Math.ceil((double) (radioModel.getT2()));
        resizeXAxis(minX, maxX);
        loadMenu();
        getRadioData();

        // listen for change date
        startDate.addListener(new InvalidationListener() {
            public void invalidated(Observable observable) {
                // resize date axis according to new start date
                radioModel.setDates(startDate.get());
                minX = Math.floor((double) (radioModel.getT1()));
                maxX = Math.ceil((double) (radioModel.getT2()));
                resizeXAxis(minX, maxX);
                // redraw chart
                fluxList.getTimedFluxes().clear();
                loadMenu();//reinit menus
                //clearFitSeries();
                if (radioModel.getJMapTool() == IToolController.JMapTool.INSITU) {
                    computeInSitu(radioModel.getDate(), radioModel.getSpeed());
                }
                if (radioModel.getJMapTool() == IToolController.JMapTool.FIT) {
                    getCatalogData();
                    computeInSitu(radioModel.getDate(), radioModel.getSpeed());
                }
                getRadioData();
            }
        });

        // listen for speed change
        pSpeed.addListener(new InvalidationListener() {
            public void invalidated(Observable observable) {
                log.debug("Speed change found in RadioRadialComponent");
                radioModel.setSpeed(Double.valueOf(pSpeed.get()));
                if (radioModel.getJMapTool() == IToolController.JMapTool.INSITU
                        || radioModel.getJMapTool() == IToolController.JMapTool.FIT) {
                    computeInSitu(radioModel.getDate(), radioModel.getSpeed());
                }
            }
        });

    }

    /**
     * Mouse click event
     *
     * @param evt
     */
    private void mouseClick(MouseEvent evt) {

        double xAxisShift = xAxis.getLayoutX();
        double yAxisShift = yAxis.getLayoutY() + ((Node) yAxis.getParent()).getLayoutY();
//        double xSpAxisShift = xSpectraAxis.getLayoutX();
//        double ySpAxisShift = ySpectraAxis.getLayoutY() + ((Node) ySpectraAxis.getParent()).getLayoutY();
//        double yVdaAxisShift = yVdaAxis.getLayoutY() + ((Node) yVdaAxis.getParent()).getLayoutY();
        // Treat only if mouse is inside axes
        if (evt.getY() > yAxisShift && evt.getY() < yAxisShift + yAxis.getHeight()
                && evt.getX() > xAxisShift && evt.getX() < xAxisShift + xAxis.getWidth()) {

            if (evt.getButton() == MouseButton.PRIMARY) {
                radioMenu.hide();
            } else if (evt.getButton() == MouseButton.SECONDARY) {
                radioMenu.show(pane, evt.getScreenX(), evt.getScreenY());
            }

            radioMenu.getMenu(RadioRadialltemBuilder.ZOOM).setVisible(true);

            double xAxisStep = xAxis.getWidth() / (xAxis.getUpperBound() - xAxis.getLowerBound());
            long XX = (long) (((evt.getX() - xAxisShift - 1) / xAxisStep) + xAxis.getLowerBound());
            double YY = getYValue(evt);

            //System.out.println("***** Mouse Click XX/YY " + XX + "/" + YY+ " getY(): "+evt.getY()+" lowerbound: "+yVdaAxis.getLowerBound());
            selectedP.setTime(XX);
            selectedP.setElongation(YY);
            if (isClickPointsInProgress && evt.getButton() == MouseButton.PRIMARY) {
                /* add point in the click series */
                addClickPoint(selectedP);
            }

            if (zoomSelection) {
                if (!isT1Selected) {
                    selectedT1 = XX;
                    isT1Selected = true;
                    //System.out.println("** t1 selected " + XX);
                    //t1Item.setText(VPlotItemBuilder.ZOOM_T1 + Formatter.getSdf().format(selectedT1));
                } else {
                    //System.out.println("** t2 selected " + XX);
                    selectedT2 = XX;
                    //t2Item.setText(VPlotItemBuilder.ZOOM_T2 + Formatter.getSdf().format(selectedT2));
                    if (selectedT2 > selectedT1) {
                        resizeXAxis(selectedT1, selectedT2);
                        resetZoomFields();
                        // allow reset
                        // resetItem.setDisable(false);
                    }
                    isT1Selected = false;
                    zoomSelection = false;
                }
            }
//			}
        }

        // Treat spectro colormap click
        double xColMapShift = spectroColorMap.getLayoutX();
        double yColMapShift = spectroColorMap.getLayoutY() + ((Node) spectroColorMap.getParent()).getLayoutY();
        if (radioChart.isVisible() && evt.getY() > yColMapShift && evt.getY() < yColMapShift + spectroColorMap.getHeight()
                && evt.getX() > xColMapShift && evt.getX() < xColMapShift + spectroColorMap.getWidth()) {

            changeColorMinMax(evt.getX(), evt.getY());

        }

    }

    /**
     * Mouse move event
     *
     * @param evt
     */
    private void mouseMove(MouseEvent evt) {
        double xAxisShift = xAxis.getLayoutX();
        double yAxisShift = yAxis.getLayoutY() + ((Node) yAxis.getParent()).getLayoutY();
        // Treat only if mouse is inside axis
        if (evt.getY() > yAxisShift && evt.getY() < yAxisShift + yAxis.getHeight()
                && evt.getX() > xAxisShift && evt.getX() < xAxisShift + xAxis.getWidth()) {

            double xAxisStep = xAxis.getWidth() / (xAxis.getUpperBound() - xAxis.getLowerBound());
            long XX = (long) (((evt.getX() - xAxisShift - 1) / xAxisStep) + xAxis.getLowerBound());
            double YY = getYValue(evt);

            //move cursor
            hLine.setStartX(xAxisShift);
            hLine.setEndX(xAxisShift + xAxis.getWidth());
            vLine.setStartY(yAxisShift);
            vLine.setEndY(yAxisShift + yAxis.getHeight());
            hLine.setStartY(evt.getY());
            hLine.setEndY(evt.getY());
            vLine.setStartX(evt.getX());
            vLine.setEndX(evt.getX());

            // Display mouse coordinates
            legend.setText("Time : " + Formatter.getSdf().format(XX) + " , Freq : " + Formatter.getFormatter2D().format(YY));

        }
    }

    /**
     * Get chart Y value depending on spectro
     *
     * @param evt Mouse click event.
     * @return
     */
    private double getYValue(MouseEvent evt) {

//            double yAxisStep = ySpectraAxis.getHeight() / (ySpectraAxis.getUpperBound() - ySpectraAxis.getLowerBound());
//            double lYY = ((evt.getY() - yAxisShift - 1) / yAxisStep) + ySpectraAxis.getLowerBound();
//            lYRet = ySpectraAxis.getUpperBound() - lYY + ySpectraAxis.getLowerBound(); // invert YAxis
        double lYRet = 0;
        double yAxisShift = yAxis.getLayoutY() + ((Node) yAxis.getParent()).getLayoutY();
//        double yAxisStep = yAxis.getHeight() / (Math.log10(yAxis.getUpperBound()) - Math.log10(yAxis.getLowerBound()));
//        double lYY = ((evt.getY() - yAxisShift - 1) / yAxisStep) + Math.log10(yAxis.getLowerBound());

        double yAxisUpper = Math.log10(yAxis.getUpperBound());
        double yAxisLower = Math.log10(yAxis.getLowerBound());

        double yAxisStep = yAxis.getHeight() / Math.abs(yAxisUpper - yAxisLower);
        double lLogYY = yAxisUpper + ((evt.getY() - yAxisShift - 1) / yAxisStep);
        //lYRet = yAxisUpper - lLogYY + yAxisLower; // invert YAxis
        lYRet = Math.pow(10, lLogYY);

        //lYRet = lYY;
        return lYRet;
    }

    /**
     * Show Dialog to change min/max color
     */
    private void changeColorMinMax(double pX, double pY) {
        Dialog<Pair<String, String>> dialog = new Dialog<>();
        dialog.setTitle("Choose min/max Color");

        // Set the button types.
        ButtonType okButtonType = new ButtonType("OK", ButtonData.OK_DONE);
        dialog.getDialogPane().getButtonTypes().addAll(okButtonType, ButtonType.CANCEL);

        GridPane gridPane = new GridPane();
        gridPane.setHgap(10);
        gridPane.setVgap(10);
        gridPane.setPadding(new Insets(20, 150, 10, 10));

        TextField lMin = new TextField("" + spectroColorMapAxis.getLowerBound());
        //lMin.setPromptText(""+spectroColorMapAxis.getLowerBound());
        TextField lMax = new TextField("" + spectroColorMapAxis.getUpperBound());
        //lMax.setPromptText(""+spectroColorMapAxis.getUpperBound());

        gridPane.add(lMin, 0, 0);
        //gridPane.add(new Label(" / "), 1, 0);
        gridPane.add(lMax, 1, 0);

        dialog.getDialogPane().setContent(gridPane);

        // Request focus on the first field by default.
        //Platform.runLater(() -> lMin.requestFocus());
        //set position
        dialog.getDialogPane().getScene().getWindow().setX(pX);
        dialog.getDialogPane().getScene().getWindow().setY(pY);
        dialog.setX(pX);
        dialog.setY(pY);

        // Convert the result to a min-max-pair when the login button is clicked.
        dialog.setResultConverter(dialogButton -> {
            if (dialogButton == okButtonType) {
                String lStrMin = lMin.getText().isEmpty() ? "" + spectroColorMapAxis.getLowerBound() : lMin.getText();
                String lStrMax = lMax.getText().isEmpty() ? "" + spectroColorMapAxis.getUpperBound() : lMax.getText();
                return new Pair<>(lStrMin, lStrMax);
            }
            return null;
        });

        Optional<Pair<String, String>> result = dialog.showAndWait();

        result.ifPresent(pair -> {
            //System.out.println("From=" + pair.getKey() + ", To=" + pair.getValue());
            setMinMaxSpectraColor(Double.valueOf(pair.getKey()), Double.valueOf(pair.getValue()));

        });
    }

    /**
     * Add the items to the menu of sep spectro plot.
     */
    private void loadMenu() {
        // load contextual menu
        radioMenu = new MapMenu();
        List<Item> menus = new ArrayList<>();
        //menus.add(RadioRadialltemBuilder.getShowSpectraItem(radioMenu.getMap(), this));

        menus.add(RadioRadialltemBuilder.getChooseMapItem(radioMenu.getMap(), this));

        if (radioModel.getJMapTool() == IToolController.JMapTool.FIT) {
            menus.add(RadioRadialltemBuilder.getChooseCatalogItem(radioMenu.getMap(), this));
        }
        if (radioModel.getJMapTool() == IToolController.JMapTool.CLICK) {
            menus.add(RadioRadialltemBuilder.getAddFitItem(radioMenu.getMap(), this));
        }
        if (radioModel.getJMapTool() == IToolController.JMapTool.INSITU) {
            menus.add(RadioRadialltemBuilder.getAddInSituItem(radioMenu.getMap(), this));
        }
        menus.add(RadioRadialltemBuilder.getZoomItem(radioMenu.getMap(), this));

        radioMenu.addAll(menus);

        /* Init addFit menu */
        if (radioModel.getJMapTool() == IToolController.JMapTool.CLICK) {
            menus.add(RadioRadialltemBuilder.getAddFitItem(radioMenu.getMap(), this));
            Menu addFitMenu = radioMenu.getMenu(RadioRadialltemBuilder.ADD_FIT);
            removePointItem = addFitMenu.getItems().get(1);
            savePointsItem = addFitMenu.getItems().get(3);
            fitPointsItem = addFitMenu.getItems().get(4);
            removePointItem.setDisable(true);
            savePointsItem.setDisable(true);
            fitPointsItem.setDisable(true);
        }

        /* Init zoom menu */
        Menu zoomMenu = radioMenu.getMenu(VPlotItemBuilder.ZOOM);
        //t1Item = zoomMenu.getItems().get(0);
        //t2Item = zoomMenu.getItems().get(1);
        //resetItem = zoomMenu.getItems().get(2);

        zoomMenu.setVisible(true);

    }

    @Override
    public void runChooseCatalog(boolean linkcatMode, CMEType cmeType) {
        jMapCompute.setLinkcatMode(linkcatMode);
        jMapCompute.setCmeType(cmeType);
        //seriesClick.getData().clear();
        System.out.println("*** runChooseCatalog");
        // Launch compute Catalog
        getCatalogData();
    }

    @Override
    public void runChooseMap(String pSource, String pInst) {
        //worker.updateImage(source, type);
        source.setValue(pSource);
        type.setValue(pInst);
        resetZoomFields();
        //clearFitSeries();
        fluxList.getTimedFluxes().clear();
        getRadioData();
    }

    @Override
    public void runSetZoom() {
        zoomSelection = true;
    }

    @Override
    public void runResetZoom() {
        resizeXAxis(minX, maxX);
        resetZoomFields();
    }

    /**
     * get interpolated value in between Ya/Yb range
     *
     * @param pXa min X
     * @param pXb max X
     * @param pX value to interpolate
     * @param pYa min Y
     * @param pYb max Y
     * @return value in Y scale
     */
    private static double linearInterpol(double pXa, double pXb, double pX,
            double pYa, double pYb) {
        double lValueDest = pYa + (pX - pXa) * (pYb - pYa) / (pXb - pXa);
        return lValueDest;
    }

    @Override
    public void runMannInSituLine() {
        radioModel.setDensityModel(RadioRadialModel.eDensityModel.MANN);
        computeInSitu(radioModel.getDate(), radioModel.getSpeed());
    }

    @Override
    public void runLeblancInSituLine() {
        radioModel.setDensityModel(RadioRadialModel.eDensityModel.LEBLANC);
        computeInSitu(radioModel.getDate(), radioModel.getSpeed());
    }

    /**
     * Compute and draw freq/time line on spectro from start date and speed.
     * Start point at sun surface (1 RSun)
     *
     * @param pStartDate Cme start date
     * @param pCmeSpeed Cme speed in km/s
     */
    private void computeInSitu(Date pStartDate, double pCmeSpeed) {
        computeInSitu(pStartDate, pCmeSpeed, 1);
    }

    /**
     * Compute and draw freq/time line on spectro from start date and speed.
     *
     * @param pStartDate Cme start date
     * @param pCmeSpeed Cme speed in km/s
     * @param pRStart First point distance from center in RSun (start at 1)
     */
    private void computeInSitu(Date pStartDate, double pCmeSpeed, double pRStart) {
        //log.debug("*** computeInSitu " + pStartDate + " " + pCmeSpeed);
        if (pStartDate == null) {
            return;
        }
        try {
            seriesFit.getData().clear();
            int lNbPoints = 120;
            int lTimeStep = 30; //30s base step
            double lRsun = 9.9634e5; // Solar radius in km

            //get CME start and speed
            double lStartTime = pStartDate.getTime() / 1000d; //time in seconds
            double[] lTimes = new double[lNbPoints]; // in s
            double[] lRArr = new double[lNbPoints]; // in rsun
            double[] lNe = new double[lNbPoints];
            double[] lFreqDerived = new double[lNbPoints];

            double lTimeProg = 0; //make it progress
            for (int i = 0; i < lRArr.length; i++) {
                lTimes[i] = lStartTime + lTimeProg * lTimeStep;
                lRArr[i] = pRStart + pCmeSpeed * (lTimes[i] - lStartTime) / lRsun;
                //log.debug("*** computed " + new Date((long) (lTimes[i] * 1000)) + " " + lRArr[i] + "  -  " + (lTimes[i] - lStartTime));
                //lTimeProg += 0.1 * lTimeProg;
                lTimeProg += 0.1 + 0.1 * lTimeProg;
            }
            //from radius values found, compute frequencies
            double[][] lMannModel = getRMannData();
            double[] lRMann = Arrays.stream(lMannModel).mapToDouble(row -> row[0]).toArray();

            //comvert ne to freq
            if (eDensityModel.LEBLANC.equals(radioModel.getDensityModel())) {
                for (int i = 0; i < lRArr.length; i++) {
                    lNe[i] = 3.3e5 * Math.pow(lRArr[i], -2) + 4.1e6 * Math.pow(lRArr[i], -4) + 8.0e7 * Math.pow(lRArr[i], -6);
                    lFreqDerived[i] = Math.sqrt(lNe[i]) * 8.98e3;
                }
                //log.debug("**** converted with leblanc algo");
            }
            if (eDensityModel.MANN.equals(radioModel.getDensityModel())) {
                double[] lNeMann = Arrays.stream(lMannModel).mapToDouble(row -> row[1]).toArray();
                double[] lFreqData = Arrays.stream(lNeMann).map(lNeArr -> Math.sqrt(lNeArr) * (8.98e3)).toArray();
                lFreqDerived = RadioBurstAnalysis.interpolate(lRMann, lFreqData, lRArr);
                //log.debug("**** converted with mann algo");
            }
            //double[] lFreqData = Arrays.stream(lNeMann).map(f -> Math.pow(f / (8.98e3), 2)).toArray();

            //add line on spectro
            for (int i = 0; i < lRArr.length; i++) {
                //System.out.println("*** computed "+lTimes[i]+" "+lRArr[i]+"  -  "+(lTimes[i]-lStartTime)+" "+lFreqDerived[i]);
                seriesFit.getData().add(new XYChart.Data<Number, Number>((long) lTimes[i] * 1000l, lFreqDerived[i]));
            }

        } catch (IOException ex) {
            java.util.logging.Logger.getLogger(RadioRadialComponent.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * compute spectro image and colorMap from data
     */
    private void computeSpectraImg() {
        log.info("computeSpectroImg: Begin");
        //recompute colormap
        log.info("computeSpectroImg fluxList min/max: " + fluxList.getMinValue() + "/" + fluxList.getMaxValue());
        double lFluxMin = spectroColorMapAxis.getLowerBound();
        double lFluxMax = spectroColorMapAxis.getUpperBound();
        //fixed color table:
//        double lFluxMin = 1e2;
//        double lFluxMax = 1e6;

        colorTable = new UcarColorTable(
                Math.log10(lFluxMin), Math.log10(lFluxMax),
                UcarColorTableType.rainbow);
        //compute image
        //BufferedImage lBi = null;
        spectroImg.setImage(null);
        spectroColorMapImg.setImage(null);

        if (fluxList.getTimedFluxes() == null || fluxList.getTimedFluxes().isEmpty()) {
            return;
        }

        int lImgW = fluxList.getTimedFluxes().size();
        int lImgH = 256;

        double lMinNrj = Math.log10(fluxList.getEnergyList().get(0)[0]);
        double lMaxNrj = Math.log10(fluxList.getEnergyList().get(fluxList.getEnergyList().size() - 1)[1]);
        yAxis.setUpperBound(fluxList.getEnergyList().get(0)[0]);
        yAxis.setLowerBound(fluxList.getEnergyList().get(fluxList.getEnergyList().size() - 1)[1]);
        //System.out.println("******** minmax y " + lMinNrj + " " + lMaxNrj + " *******");
        double lMinTime = xAxis.getLowerBound();
        double lMaxTime = xAxis.getUpperBound();

        if (fluxList.getTimedFluxes() != null && !fluxList.getTimedFluxes().isEmpty()) {
            //System.out.println("min-max="+colorTable.getMin() + " "+colorTable.getMax());
            BufferedImage lBi = new BufferedImage(lImgW, lImgH, BufferedImage.TYPE_INT_ARGB);
            //build raster
            int y = 0;
            int x = 0;
            for (x = 0; x < fluxList.getTimedFluxes().size(); x++) {
                Spectro lRecord = fluxList.getTimedFluxes().get(x);
                //System.out.println("*** Time spectro " + x + ": " + lRecord.getEventTime().getTimeInMillis() + " Min=" + lMinTime);

                Spectro lRecordNext = lRecord;
                if (x < fluxList.getTimedFluxes().size() - 1) {
                    lRecordNext = fluxList.getTimedFluxes().get(x + 1);
                }
                if (lRecord.getEventTime().getTimeInMillis() < lMinTime || lRecordNext.getEventTime().getTimeInMillis() > lMaxTime) {
                    //System.out.println("Data outside min and max time, skip record.");
                    continue;
                }

                y = lBi.getHeight() - 1;
                int xMin = (int) linearInterpol(lMinTime, lMaxTime, lRecord.getEventTime().getTimeInMillis(), 0, lImgW);
                int xMax = (int) linearInterpol(lMinTime, lMaxTime, lRecordNext.getEventTime().getTimeInMillis(), 0, lImgW);
                //log.info("Chunck from "+xMin+" to "+xMax+" at "+x);
                int lFluxIdx = 0;
                //System.out.println("Getting values from fluxList: "+lRecord.getFlux());
                for (Float lVal : lRecord.getFlux()) {
                    double lValNrjMin = Math.log10(fluxList.getEnergyList().get(lFluxIdx)[0]);
                    double lValNrjMax = Math.log10(fluxList.getEnergyList().get(lFluxIdx)[1]);
                    //log.info("Interpol Y from "+lValNrjMin+" between "+lMinNrj+" and "+lMaxNrj);
                    int yMin = (int) linearInterpol(lMinNrj, lMaxNrj, lValNrjMin, 0, lImgH);
                    int yMax = (int) linearInterpol(lMinNrj, lMaxNrj, lValNrjMax, 0, lImgH);
                    //log.info("******* compute spectro yMin/yMax "+yMin+"/"+yMax+" lVal="+lVal);
                    java.awt.Color lCol = colorTable.getColor(Math.log10(lVal));
                    if (lVal == 0.0f) {
                        lCol = new java.awt.Color(1, 1, 1, 0);//set 0 to transparent
                    } else {
                        //log.info("set color "+lCol.getRGB()+" at "+x+"/"+y+" for value "+lVal+" min/max: "+colorTable.getMin()+"/"+colorTable.getMax());
                    }
                    //fill square with value
                    for (int i = xMin; i < xMax; i++) {
                        for (int j = yMin; j < yMax; j++) {
                            lBi.setRGB(i, j, lCol.getRGB());
                        }
                    }
                    y--;
                    lFluxIdx++;
                }
            }

            log.info("computeSpectroImg: drew " + x + " chunks");
            //display image
            spectroImg.setImage(SwingFXUtils.toFXImage(lBi, null));

            //Compute colormap
            int lWidth = 1;
            int lHeight = 50;
            double lMin = colorTable.getMin();
            double lMax = colorTable.getMax();
            BufferedImage lBiCm = new BufferedImage(lWidth, lHeight, BufferedImage.TYPE_INT_ARGB);
            for (int i = 0; i < lHeight; i++) {
                double lVal = lMin + ((lHeight - i - 1) / (double) lHeight) * (lMax - lMin);
                //log.info("cbar value: "+i+"=>"+lVal);
                java.awt.Color lC = colorTable.getColor(lVal);
                lBiCm.setRGB(0, i, lC.getRGB());
            }
            spectroColorMapImg.setImage(SwingFXUtils.toFXImage(lBiCm, null));
            //setSpectroColorMapLegend();
        }
    }

    private void resetZoomFields() {
        selectedT1 = 0;
        isT1Selected = false;
        zoomSelection = false;
    }

    private void resizeXAxis(double tMin, double tMax) {
        xAxis.setLowerBound(tMin);
        xAxis.setUpperBound(tMax);
        xAxis.setTickUnit((tMax - tMin) / FluxPlotModel.NB_TICS);
        //generate image with new values
        computeSpectraImg();
    }

    /**
     * Change axes min max and recompute spectro
     *
     * @param pMin
     * @param pMax
     */
    private void setMinMaxSpectraColor(double pMin, double pMax) {
        spectroColorMapAxis.setLowerBound(pMin);
        spectroColorMapAxis.setUpperBound(pMax);
        computeSpectraImg();
    }

    public RadioRadialModel getRadioModel() {
        return radioModel;
    }

    /**
     * ***********************************************************************
     ************************* add click actions *****************************
     * ***********************************************************************
     */
    /**
     * Remove all series but first (click points) and second (fit line)
     */
    private void clearCatalogSeries() {
        if (radioChart.getData().size() > 2) {
            radioChart.getData().remove(2, radioChart.getData().size());
        }
    }

    /**
     * Add a catalogue series from a CME fit points
     */
    public void addCatalogSeries(List<FitPoint> fit) {
        Series<Number, Number> lSeriesCatalog = new Series<Number, Number>();
        if (fit != null) {
            //XYChart.Series<Number, Number> series = new XYChart.Series<Number, Number>();
            for (FitPoint fitPoint : fit) {
                lSeriesCatalog.getData().add(new XYChart.Data<Number, Number>(fitPoint.getTime(), fitPoint.getElongation()));
            }
        }
        radioChart.getData().add(lSeriesCatalog);
    }

    /**
     * add point to click series
     *
     * @param p
     */
    private void addClickPoint(FitPoint p) {
        seriesClick.getData().add(new XYChart.Data<Number, Number>(p.getTime(), p.getElongation()));
        log.debug("New point added : (" + p.getTime() + " " + Formatter.getSdf().format(p.getTime()) + "," + p.getElongation() + ")");
        //log.debug("Size = " + seriesClick.getData().size());
    }

    /**
     * clear fit result from panel
     */
    public void clearFitResults() {
        fitSpeedTf.setText("");
        fitTStartTf.setText("");
        seriesClick.getData().clear();
        seriesFit.getData().clear();
        //force data reset for a openjfx(<23) bug
        radioChart.getData().remove(seriesFit);
        radioChart.getData().add(seriesFit);
    }

    public void startClickPoints() {
        // clear click series and blobs
        clearFitResults();
        currFitResult = null;
        //fitResult.setVisible(false);
        //clearBlobsSeries();
        // configure menu items
        removePointItem.setDisable(false);
        savePointsItem.setDisable(false);
        fitPointsItem.setDisable(false);
        // start click point

        setClickPointsInProgress(true);
    }

    public List<FitPoint> getClickPoints() {
        if (!seriesClick.getData().isEmpty()) {
            List<FitPoint> fit = new ArrayList<FitPoint>();
            for (XYChart.Data<Number, Number> item : seriesClick.getData()) {
                FitPoint fitPoint = new FitPoint(item.getXValue().longValue(), item.getYValue().doubleValue());
                fit.add(fitPoint);
            }
            return fit;
        } else {
            return null;
        }
    }

    /**
     * Upload Click points from file
     * http://docs.oracle.com/javafx/2/ui_controls/file-chooser.htm
     */
    public boolean updloadClickPoints() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Clicked points to upload");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
        fileChooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("All files", "*.*"),
                new FileChooser.ExtensionFilter("TXT", "*.txt"),
                new FileChooser.ExtensionFilter("ASCII", "*.asc")
        );
        File file = fileChooser.showOpenDialog(RootController.stage);
        if (file != null) {
            try {
                //log.debug("DEBUG JMapComponent:updloadClickPoints - Choosen file: " + file.getAbsolutePath());
                List<FitPoint> clickFilePoints = ClickPointsFileHandler.readClickPointsFile(file);
                // All is ok, enable fit
                startClickPoints();
                addClickSeries(clickFilePoints);

            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * Update click series from a click points
     */
    public void addClickSeries(List<FitPoint> fit) {
        if (fit != null) {
            seriesClick.getData().clear();
            for (FitPoint fitPoint : fit) {
                seriesClick.getData().add(new XYChart.Data<Number, Number>(fitPoint.getTime(), fitPoint.getElongation()));
            }
        }
    }

    /**
     * Save Click points from file
     * http://docs.oracle.com/javafx/2/ui_controls/file-chooser.htm
     */
    public void saveClickPoints() {
        // save clicked points
        if (this.getClickPoints() != null) {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Choose file name");
            fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
            fileChooser.getExtensionFilters().addAll(
                    new FileChooser.ExtensionFilter("All files", "*.*"),
                    new FileChooser.ExtensionFilter("TXT", "*.txt"),
                    new FileChooser.ExtensionFilter("ASCII", "*.asc")
            );
            //        if (userDir != null) {
            //            fileChooser.setInitialDirectory(userDir);
            //        }
            File file = fileChooser.showSaveDialog(RootController.stage);
            if (file != null) {
                if (!file.getName().contains(".")) {
                    file = new File(file.getAbsolutePath() + ".txt");
                }
                ClickPointsFileHandler.writeClickPointsFile(this.getClickPoints(), file);
            }
//	        userDir = fileChooser.getInitialDirectory();
        }
    }

    public void setClickPointsInProgress(boolean isClickPointsInProgress) {
        this.isClickPointsInProgress = isClickPointsInProgress;
        if (isClickPointsInProgress) {
            super.setCursor(Cursor.CROSSHAIR);
        } else {
            super.setCursor(null);
        }
    }

    /**
     * ***********************************************************************
     * ********************** Specific menu actions **************************
     * ***********************************************************************
     */
    @Override
    public void runRemoveLastPoint() {
        int position = seriesClick.getData().size() - 1;
        if (position >= 0) {
            //log.debug("Remove last point at position : " + position);
            seriesClick.getData().remove(position);
        }
    }

    double[][] getRMannData() throws IOException {
        return RadioBurstAnalysis.loadData(getClass().getResourceAsStream("Mann_et_al_2023_model.txt"));
    }

    /**
     * Compute fit from clicked points
     */
    @Override
    public void runLeblancFitPoints() {

        if (seriesClick.getData().size() <= 2) {
            DialogBox.warn(" You must create at least 3 points to fit. ");
            return;
        }

        //System.out.println("**** "+lRmanFile.toString()+" "+lRmanFile.toURI().toString());
        double[] Rvec = new double[rMann.length];
        System.arraycopy(rMann, 0, Rvec, 0, rMann.length);
        double[] lNe_Leblanc = new double[Rvec.length];
        for (int i = 0; i < Rvec.length; i++) {
            lNe_Leblanc[i] = 3.3e5 * Math.pow(Rvec[i], -2) + 4.1e6 * Math.pow(Rvec[i], -4) + 8.0e7 * Math.pow(Rvec[i], -6);
        }
        radioModel.setDensityModel(RadioRadialModel.eDensityModel.LEBLANC);
        computeFitPoints(rMann, lNe_Leblanc, "Leblanc 1998");
    }

    /**
     * Compute fit from clicked points
     */
    @Override
    public void runMannFitPoints() {

        if (seriesClick.getData().size() <= 2) {
            DialogBox.warn(" You must create at least 3 points to fit. ");
            return;
        }

        radioModel.setDensityModel(RadioRadialModel.eDensityModel.MANN);
        computeFitPoints(rMann, neMann, "Mann 2023");
    }

    public void computeFitPoints(double[] pR, double[] pNe, String pFitType) {

        double[] lR = Arrays.copyOf(pR, pR.length);
        double[] lNe = Arrays.copyOf(pNe, pNe.length);

        setClickPointsInProgress(false);
        removePointItem.setDisable(true);
        fitPointsItem.setDisable(true);

        List<FitPoint> lPts = getClickPoints();
        double[] lTimes = new double[lPts.size()];
        double[] lFreqs = new double[lPts.size()];
        //log.info("**** Computing Fit from " + lPts.size() + " points.");
        int i = 0;
        double lTStart = 0;
        double lTEnd = 0;
        for (FitPoint lFp : lPts) {
            if (i == 0) {
                lTStart = lFp.getTime() / 1000d;
            }
            if (i == lPts.size() - 1) {
                lTEnd = lFp.getTime() / 1000d;
            }
            lTimes[i] = lFp.getTime() / 1000d - lTStart;
            lFreqs[i++] = lFp.getElongation();
        }

        // Interpolation to find radius so flip to have croissant order on Ne
        RadioBurstAnalysis.flipArray(lNe);
        RadioBurstAnalysis.flipArray(lR);
        // from electron density (in cm^-3) to frequency in Hz (NRL Plasma formulary)
        // f = 8.98*10^3*sqrt(Ne);
        // but here we do the inverted operation assuming fundamental (f has to be in Hz)
        double[] lNeData = Arrays.stream(lFreqs).map(f -> Math.pow(f / (8.98e3), 2)).toArray();

        double[] lRDerived = RadioBurstAnalysis.interpolate(lNe, lR, lNeData);
        double confidenceLevel = 0.95; // 95% Confidence Interval
        RegressionResult lResult = linearRegression(lTimes, lRDerived, confidenceLevel);
        // Convert slope to velocity
        double lRsun = 9.9634e5; // Solar radius in km
        double lVelocity_typeII = lResult.slope * lRsun;

        String lLogStr = "";

        lLogStr += String.format("Slope (b1): %.4f%n", lResult.slope);
        lLogStr += String.format("Intercept (b0): %.4f%n", lResult.intercept);
        lLogStr += String.format("Standard Error: %.4f%n", lResult.se);
        lLogStr += String.format("Standard Error of Slope: %.4f%n", lResult.seSlope);
        lLogStr += String.format("Standard Error of Intercept: %.4f%n", lResult.seIntercept);
        lLogStr += String.format("95%% CI for Slope: [%.4f, %.4f]%n", lResult.ciSlopeLower, lResult.ciSlopeUpper);
        lLogStr += String.format("95%% CI for Intercept: [%.4f, %.4f]%n", lResult.ciInterceptLower, lResult.ciInterceptUpper);
        lLogStr += String.format("95%% CI for Intercept: [%.4f, %.4f]%n", lResult.ciInterceptLower, lResult.ciInterceptUpper);
        lLogStr += String.format("T_start: %.6f [units of tt_typeII_minus_t1]%n", lTStart);
        lLogStr += String.format("En  grandeurs physiques --------------------------------------%n");
        lLogStr += String.format("Velocity : %.2f km/s%n", lVelocity_typeII);
        lLogStr += String.format("Standard Error of velocity : %.4f%n", lResult.seSlope * lRsun);
        lLogStr += String.format("95%% CI for velocity : [%.4f, %.4f]%n", lResult.ciSlopeLower * lRsun, lResult.ciSlopeUpper * lRsun);
        log.debug(lLogStr);
        fitSpeedTf.setText(String.format("%.2f km/s", lVelocity_typeII));
        fitTStartTf.setText(Formatter.getSdf().format(lTStart * 1000));
        //fitErrorTf.setText(String.format("%.4f", lResult.seSlope * Rsun));

        //build fitPoints in radius
        List<FitPoint> lRClickPts = new ArrayList<>();
        i = 0;
        for (FitPoint lFp : lPts) {
            lRClickPts.add(new FitPoint(lFp.getTime(), lRDerived[i++]));
        }
        List<FitPoint> lRFitPts = new ArrayList<>();
        lRFitPts.add(new FitPoint((long) (lTStart * 1000), lResult.slope + lResult.intercept));
        lRFitPts.add(new FitPoint((long) (lTEnd * 1000), lResult.slope * (lTEnd - lTStart) + lResult.intercept));

        fitParameterComponent.setFitParam(source.get(), pFitType, new Date((long) (lTStart * 1000)), lRFitPts, lRClickPts,
                lVelocity_typeII, lResult.seSlope * lRsun, new double[]{lResult.ciSlopeLower * lRsun, lResult.ciSlopeUpper * lRsun});

        //draw back fit line on spectro from fit
        //radioModel.setSpeed(lVelocity_typeII);
        //radioModel.setCmeDate(new Date((long)(lTStart*1000)));
        computeInSitu(new Date((long) (lTStart * 1000)), lVelocity_typeII, lRDerived[0]);

    }

    @Override
    public void runClickPoints() {
        startClickPoints();
        fitParameterComponent.reset();
    }

    @Override
    public void runClickPointsFile() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Clicked points to upload");
        fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
        fileChooser.getExtensionFilters().addAll(
                new FileChooser.ExtensionFilter("All files", "*.*"),
                new FileChooser.ExtensionFilter("TXT", "*.txt"),
                new FileChooser.ExtensionFilter("ASCII", "*.asc")
        );
        File file = fileChooser.showOpenDialog(RootController.stage);
        if (file != null) {
            try {
                //log.debug("DEBUG JMapComponent:updloadClickPoints - Choosen file: " + file.getAbsolutePath());
                List<FitPoint> clickFilePoints = ClickPointsFileHandler.readClickPointsFile(file);
                // All is ok, enable fit
                startClickPoints();
                addClickSeries(clickFilePoints);

            } catch (Exception e) {
                e.printStackTrace();
                //return false;
            }
            //return true;
        }
        //return false;
    }

    @Override
    public void runSavePoints() {
        // save clicked points
        if (this.getClickPoints() != null) {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("Choose file name");
            fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
            fileChooser.getExtensionFilters().addAll(
                    new FileChooser.ExtensionFilter("All files", "*.*"),
                    new FileChooser.ExtensionFilter("TXT", "*.txt"),
                    new FileChooser.ExtensionFilter("ASCII", "*.asc")
            );
            //        if (userDir != null) {
            //            fileChooser.setInitialDirectory(userDir);
            //        }
            File file = fileChooser.showSaveDialog(RootController.stage);
            if (file != null) {
                if (!file.getName().contains(".")) {
                    file = new File(file.getAbsolutePath() + ".txt");
                }
                ClickPointsFileHandler.writeClickPointsFile(this.getClickPoints(), file);
            }
//	        userDir = fileChooser.getInitialDirectory();
        }
    }

    /**
     * ************************************************
     ********* End click menu trigge'rs ***************
     * ************************************************
     */
    /**
     * Draw all flux series of a flux type Version compatible since Java 7
     */
    private void getRadioData() {
        final Task<Object> lGetFlux = this.getData();
        lGetFlux.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) {
                    if (fluxList.getEnergyList().isEmpty()) {
                        log.info("Got empty radio spectro = " + fluxList.getTimedFluxes().size());
                    } else {
                        log.info("Got new radio spectro data: " + fluxList.getEnergyList().size());
                        //computeSpectraImg();
                        //setColor => redraw spectro
                        //if data is wind, set values in m²/Hz
                        if ("WIND".equals(source.getValue())) {
                            setMinMaxSpectraColor(1e-16, 1e-14);
                            spectroColorMapAxis.setLabel("Power spectral density ( V^2/Hz )");
                        } else {
                            setMinMaxSpectraColor(1e2, 1e6);
                            spectroColorMapAxis.setLabel("Power spectral density ( SFU )");
                        }
                        //yAxis.setUpperBound(fluxList.getEnergyList().get(0)[0]);
                        //yAxis.setLowerBound(fluxList.getEnergyList().get(fluxList.getEnergyList().size() - 1)[1]);
                        //loadMenu();
                    }
                    endCursor();

                } else if (newState == Worker.State.FAILED || newState == Worker.State.CANCELLED) {
                    //fluxPlotModel.setValid(false);
                    endCursor(true);
                    log.info("get flux failed - " + lGetFlux.getException().getMessage());
                    lGetFlux.getException().printStackTrace();
                }
            }
        });

        new Thread(lGetFlux).start();
        waitCursor();
    }

    /**
     * Get all energies between start and end dates
     */
    private Task<Object> getData() {
        return new Task<Object>() {
            @Override
            protected Object call() throws Exception {
                //fluxList.clear();
                if (fluxList.getTimedFluxes().isEmpty()) {
                    log.debug("Call fetchSpectro web service for start " + source);
                    fluxList = ServiceFactory.getSpectroService()
                            .fetchRadioSpectro(radioModel.getStartDate(), radioModel.getEndDate(), source.getValue());
                    //fluxList.add(flux);
                    log.debug("Call fetchSpectro web service returned");
                }
                return true;
            }
        };
    }

    /**
     * Compute fit from clicked points
     */
    public List<Fit> convertCatalogFitPoints(List<Fit> pFitsFromCatalog) {

        double lRsun = 9.9634e5; // Solar radius in km
        List<Fit> lFreqList = new ArrayList<>();
        //System.out.println("*** Compute " + pFitsFromCatalog.size() + " Fit(s)");

        for (Fit lFit : pFitsFromCatalog) {
            //System.out.println("*** Compute " + lFit.getFitPoints().size() + " Fit points");
            jMapCompute.getJMapPosition().setPositionObsInSitu();
            //double[] lRBlobs = new double[lFit.getFitPoints().size()];
            Fit lFreqFit = new Fit();
            List<FitPoint> lNewFitPoints = new ArrayList<>();
            for (FitPoint lFitPoint : lFit.getFitPoints()) {
                double lProbeRadius = jMapCompute.getJMapPosition().getObsInSitu().getPosition().length();
                double lB = Math.toRadians(lFit.getBeta());
                double lAlp = Math.toRadians(lFitPoint.getElongation());
                double lBlobRadius = lProbeRadius * Math.tan(lAlp)
                        / (Math.sin(lB) + Math.cos(lB) * Math.tan(lAlp));
                double lNeDerived = RadioBurstAnalysis.interpolate(rMann, neMann, 1 + lBlobRadius / lRsun);
                // from electron density (in cm^-3) to frequency in Hz (NRL Plasma formulary)
                // f = 8.98*10^3*sqrt(Ne);
                FitPoint lNewFitPoint = new FitPoint();
                lNewFitPoint.setTime(lFitPoint.getTime());
                lNewFitPoint.setElongation(8.98e3 * Math.sqrt(lNeDerived));
                lNewFitPoints.add(lNewFitPoint);
//                System.out.println("New Radius compute: " + new Date(
//                        lFitPoint.getTime()) + " " + lFitPoint.getElongation() + " "+lFit.getBeta()+ " = " + (1+lBlobRadius/lRsun)
//                        +" / "+lNeDerived+" "+lNewFitPoint.getElongation());
            }
            lFreqFit.setFitPoints(lNewFitPoints);
            lFreqList.add(lFreqFit);
        }

        return lFreqList;
    }

    /**
     * Compute Catalog
     */
    public void getCatalogData() {
        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) {
                    // add computed fit series
                    if (jMapCompute.getFitCatalog() != null) {
                        List<Fit> lFreqFits = convertCatalogFitPoints(jMapCompute.getFitCatalog());
                        clearCatalogSeries();
                        for (Fit fit : lFreqFits) {
                            log.debug("Add fit serie of {} points",fit.getFitPoints());
                            addCatalogSeries(fit.getFitPoints());
                        }
                    }
                    RootController.endCursor();
                    // disable Fit Parameter tab
//                    fitResultTab.setDisable(false);
//                    fitResultTab.setDisable(true);

                } 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  !");
                    RootController.endCursor();
                }
            }
        });
        new Thread(computeCatalogWorker).start();
        RootController.waitCursor();
    }

    private Task<Object> computeCatalogWorker() {
        return new Task<Object>() {
            @Override
            protected Object call() {
                // Load catalog parameters of fits (time, beta, speed) for obsInSitu between t1 and t2

                log.info("Fetch fits CME catalog for " + radioModel.getStartDate() + " " + radioModel.getEndDate()
                        + " " + jMapCompute.isLinkcatMode() + " " + jMapCompute.getCmeType() + " " + source.getValue());
                jMapCompute.getJMapPosition().setObserverInSitu(source.getValue());
                FitCatalog fitCatalog = ServiceFactory.getFitService().
                        fetchFitsCME(radioModel.getStartDate(), radioModel.getEndDate(), jMapCompute.isLinkcatMode(),
                                jMapCompute.getCmeType(), source.getValue());
                // 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
//                    //jMapRadialModel.updateViewFromFit(jMapComponent.getSelectedFit());
//                    // notify change
//                    getHasChanged().set(true);
//                    getHasChanged().set(false);
//                    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;
//            }
//        };
//    }

    /*
     * Getter and setters
     * 
     */
    public void setFitParameterComponent(RadioFitParameterComponent pComponent) {
        this.fitParameterComponent = pComponent;
    }

    private void waitCursor() {
        pane.setDisable(true);
        root.setCursor(Cursor.WAIT);
    }

    private void endCursor() {
        // default = without error
        endCursor(false);
    }

    private void endCursor(boolean withError) {
        pane.setDisable(withError);
        root.setCursor(null);
    }

    public BooleanProperty getHasChanged() {
        return hasChanged;
    }

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

}
