Evaluation.java

/*
 * This file is part of Indicators.
 *
 * Indicators is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Indicators is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
 */
package fr.inrae.agroclim.indicators.model;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import fr.inrae.agroclim.indicators.exception.IndicatorsException;
import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
import fr.inrae.agroclim.indicators.model.data.DailyData;
import fr.inrae.agroclim.indicators.model.data.ResourceManager;
import fr.inrae.agroclim.indicators.model.data.Variable;
import fr.inrae.agroclim.indicators.model.data.Variable.Type;
import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
import fr.inrae.agroclim.indicators.model.data.phenology.AnnualStageData;
import fr.inrae.agroclim.indicators.model.data.phenology.PhenologicalResource;
import fr.inrae.agroclim.indicators.model.data.phenology.PhenologyCalculator;
import fr.inrae.agroclim.indicators.model.data.phenology.Stage;
import fr.inrae.agroclim.indicators.model.data.soil.SoilDailyData;
import fr.inrae.agroclim.indicators.model.data.soil.SoilLoaderProxy;
import fr.inrae.agroclim.indicators.model.function.aggregation.JEXLFunction;
import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
import fr.inrae.agroclim.indicators.model.indicator.Indicator;
import fr.inrae.agroclim.indicators.model.indicator.IndicatorCategory;
import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorEvent;
import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
import fr.inrae.agroclim.indicators.model.result.IndicatorResult;
import fr.inrae.agroclim.indicators.model.result.PhaseResult;
import fr.inrae.agroclim.indicators.resources.Messages;
import fr.inrae.agroclim.indicators.util.DateUtils;
import fr.inrae.agroclim.indicators.util.StageUtils;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

/**
 * Indicators evaluations.
 *
 * Last change $Date$
 *
 * @author $Author$
 * @version $Revision$
 */
@EqualsAndHashCode(callSuper = true, of = {"isTranscient", "settings"})
@Log4j2
public final class Evaluation extends CompositeIndicator {
    /**
     * UUID for Serializable.
     */
    private static final long serialVersionUID = 9205643160821888597L;

    /**
     * Add values of child of indicator into indicatorResults.
     *
     * @param indicator indicator to inspect
     * @param indicatorResults list to populate
     */
    private static void fillIndicatorResults(final CompositeIndicator indicator,
            final List<IndicatorResult> indicatorResults) {
        indicator.getIndicators().forEach(ind -> {
            IndicatorCategory cat;
            cat = IndicatorCategory.getByTag(ind.getCategory());
            if (cat == IndicatorCategory.PHENO_PHASES) {
                return;
            }

            final IndicatorResult result = new IndicatorResult();
            result.setIndicatorCategory(cat);
            result.setIndicatorId(ind.getId());
            result.setNormalizedValue(ind.getValue());
            result.setRawValue(ind.getNotNormalizedValue());
            indicatorResults.add(result);

            if (ind instanceof CompositeIndicator compositeIndicator) {
                fillIndicatorResults(compositeIndicator, result.getIndicatorResults());
            }
        });
    }

    /**
     * All resources needed to run the evaluation.
     *
     * Resources filled by Loader's.
     */
    @Getter
    private final ResourceManager resourceManager;

    /**
     * Flag to ignore when climatic data is empty for a phase.
     */
    private boolean ignoreEmptyClimaticData = false;

    /**
     * Flag for state Saved.
     */
    @Getter
    private boolean isTranscient;

    /**
     * Settings from XML.
     */
    @Getter
    private EvaluationSettings settings;

    /**
     * State.
     */
    @Getter
    @Setter
    private transient EvaluationState state;

    /**
     * Constructor.
     */
    public Evaluation() {
        super();
        resourceManager = new ResourceManager();
        if (getAggregationFunction() == null) {
            setAggregationFunction(new JEXLFunction());
        }
        setState(EvaluationState.NEW);
    }

    /**
     * Constructor from CompositeIndicator.
     *
     * @param indicator
     *            indicator
     */
    public Evaluation(final CompositeIndicator indicator) {
        super(indicator);
        resourceManager = new ResourceManager();
        if (getAggregationFunction() == null) {
            setAggregationFunction(new JEXLFunction());
        }
        setState(EvaluationState.NEW);
    }

    /**
     * Constructor for cloning purpose.
     *
     * @param evaluation
     *            evaluation to clone.
     */
    public Evaluation(final Evaluation evaluation) {
        super(evaluation);
        if (getAggregationFunction() == null) {
            setAggregationFunction(new JEXLFunction());
        }
        try {
            resourceManager = evaluation.resourceManager.clone();
        } catch (final CloneNotSupportedException ex) {
            throw new RuntimeException("This should never occur!", ex);
        }
        if (evaluation.settings != null) {
            try {
                settings = evaluation.settings.clone();
            } catch (final CloneNotSupportedException ex) {
                throw new RuntimeException("This should never occur!", ex);
            }
        }
        isTranscient = evaluation.isTranscient;
        state = evaluation.state;
    }

    /**
     * Add the indicator to category PHENO_PHASES or to parent.
     *
     * @param category
     *            PHENO_PHASES or anything else
     * @param parent
     *            parent indicator
     * @param indicator
     *            indicator to onIndicatorAdd
     * @return added indicator
     * @throws CloneNotSupportedException should never occurs as indicator must
     * implement clone()
     */
    public Indicator add(final IndicatorCategory category,
            final CompositeIndicator parent, final Indicator indicator)
                    throws CloneNotSupportedException {
        final Indicator newIndicator = indicator.clone();
        newIndicator.setParent(parent);
        if (!category.equals(IndicatorCategory.PHENO_PHASES)
                && newIndicator instanceof CompositeIndicator) {
            ((CompositeIndicator) newIndicator).clearIndicators();
        }

        if (category.equals(IndicatorCategory.PHENO_PHASES)) {
            // the parent is the evaluation
            final String endStageId = newIndicator.getId();
            final String firstStageId = ((CompositeIndicator) newIndicator)
                    .getIndicators().iterator().next().getId();

            if (!isStagePresent(firstStageId, endStageId)) {
                add(newIndicator);
                fireIndicatorEvent(IndicatorEvent.Type.ADD.event(newIndicator));
            } else {
                LOGGER.warn("Phase ({},{}) already exists for evaluation.",
                        firstStageId, endStageId);
            }
        } else if (!parent.isPresent(newIndicator.getId())) {
            parent.add(newIndicator);
            newIndicator.setComputable(newIndicator
                    .isComputable(resourceManager.getClimaticResource()));
            fireIndicatorEvent(IndicatorEvent.Type.ADD.event(newIndicator));
        } else {
            LOGGER.warn("Indicator {} already exists for {}.",
                    newIndicator.getId(), parent.getId());
            return newIndicator;
        }
        // TODO : CHANGE est-il nécessaire en plus de ADD ?
        fireIndicatorEvent(IndicatorEvent.Type.CHANGE.event(this));

        return newIndicator;
    }

    /**
     * Add the indicator to category PHENO_PHASES or to parent.
     *
     * @param categoryTag
     *            PHENO_PHASES or anything else
     * @param parent
     *            parent indicator
     * @param indicator
     *            indicator to onIndicatorAdd
     * @return added indicator
     * @throws CloneNotSupportedException should never occurs as indicator must
     * implement clone()
     */
    public Indicator add(final String categoryTag,
            final CompositeIndicator parent, final Indicator indicator)
                    throws CloneNotSupportedException {

        final IndicatorCategory category = IndicatorCategory.getByTag(categoryTag);
        if (category == null) {
            throw new RuntimeException("Unknown category: " + categoryTag);
        }

        return add(category, parent, indicator);
    }

    /**
     * Check data before running compute* methods.
     *
     * @param phases phenological phases to check
     * @param climaticResource climatic resource to check
     * @throws IndicatorsException exception
     */
    private void checkBeforeCompute(final List<CompositeIndicator> phases, final ClimaticResource climaticResource)
            throws IndicatorsException {
        if (phases == null) {
            throw new RuntimeException("Phase list is null!");
        }
        if (phases.isEmpty()) {
            throw new RuntimeException("Phase list is empty!");
        }
        if (climaticResource.isEmpty()) {
            throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
        }
        if (resourceManager.getPhenologicalResource().isEmpty()) {
            throw new IndicatorsException(ResourceErrorType.PHENO_EMPTY);
        }
    }

    @Override
    public Evaluation clone() {
        return new Evaluation(this);
    }

    /**
     * Compute indicator results.
     *
     * @return Results of computation by year.
     * @throws IndicatorsException
     *             from Indicator.compute()
     */
    public Map<Integer, EvaluationResult> compute() throws IndicatorsException {
        LOGGER.trace("start computing evaluation \"" + getName() + "\"");
        this.ignoreEmptyClimaticData = false;
        fireIndicatorEvent(IndicatorEvent.Type.COMPUTE_START.event(this));

        final List<CompositeIndicator> phases = getPhases();
        final ClimaticResource climaticResource = resourceManager.getClimaticResource();
        checkBeforeCompute(phases, climaticResource);

        var results = compute(climaticResource, phases);
        fireIndicatorEvent(IndicatorEvent.Type.COMPUTE_SUCCESS.event(this));
        return results;
    }

    private Map<Integer, EvaluationResult> compute(final ClimaticResource climaticResource,
            final List<CompositeIndicator> phases) throws IndicatorsException {
        final Map<Integer, EvaluationResult> results = new LinkedHashMap<>();

        /* Pour chaque phase */
        final List<AnnualStageData> stageDatas = getResourceManager().getPhenologicalResource().getData();
        for (final CompositeIndicator phase : phases) {
            final String phaseId = phase.getId();
            if (phaseId == null) {
                throw new RuntimeException("Id of phase is null!");
            }

            /* Nom du stade de départ de la phase */
            final String startStageName = phase.getFirstIndicator().getName();
            /* Nom du stage de fin de la phase */
            final String endStageName = phase.getName();

            /* Pour chaque année phénologique */
            for (final AnnualStageData stageData : stageDatas) {
                /* Année phénologique */
                final Integer year = stageData.getYear();

                // DOY coded on 2 years: last stage on second year
                int dateYear = year;
                if (stageData.existWinterCrop()) {
                    dateYear -= 1;
                }

                if (!climaticResource.getYears().contains(dateYear)) {
                    continue;
                }

                if (!results.containsKey(year)) {
                    results.put(year, new EvaluationResult());
                }

                /* Valeur du stade de départ de la phase */
                final Integer startStage = stageData.getStageValue(startStageName);

                /* Valeur du stade de fin de la phase */
                final Integer endStage = stageData.getStageValue(endStageName);

                final AnnualPhase annualPhase = new AnnualPhase();
                annualPhase.setHarvestYear(year);
                annualPhase.setEndStage(endStageName);
                annualPhase.setStartStage(startStageName);
                annualPhase.setUid(phaseId);
                final PhaseResult phaseResult = new PhaseResult();
                phaseResult.setAnnualPhase(annualPhase);
                results.get(year).getPhaseResults().add(phaseResult);

                if (startStage == null) {
                    LOGGER.info(String.format("No start stage for %s/%s : %s", startStageName, endStageName,
                            stageData.toString()));
                    continue;
                }

                // Phenology not finished.
                if (endStage == null || endStage == 0) {
                    LOGGER.info(String.format("No end stage for %s/%s : %s", startStageName, endStageName,
                            stageData.toShortString()));
                    continue;
                }
                //-

                final Date endDate = DateUtils.getDate(dateYear, endStage);
                annualPhase.setEnd(endDate);
                final Date startDate = DateUtils.getDate(dateYear, startStage);
                annualPhase.setStart(startDate);

                // Do not compute at all if there are missing stages
                if (!stageData.isComplete()) {
                    LOGGER.info("In {}, missing stages {}, do not compute", year, stageData);
                    continue;
                }

                /* Données climatiques pendant la phase et l'année donnée */
                final ClimaticResource climaticData = climaticResource
                        .getClimaticDataByPhaseAndYear(startDate, endDate);
                if (climaticData.isEmpty()) {
                    if (climaticResource.isEmpty()) {
                        throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
                    }
                    if (ignoreEmptyClimaticData) {
                        continue;
                    }
                    final int yearToSearch = dateYear;
                    final List<ClimaticDailyData> ddataList = climaticResource.getData().stream() //
                            .filter(f -> f.getYear() == yearToSearch) //
                            .collect(Collectors.toList());

                    final ClimaticDailyData startData = ddataList.get(0);
                    final ClimaticDailyData endData = ddataList.get(ddataList.size() - 1);

                    throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY_FOR_PHASE,
                            startStageName, endStageName, startStage, endStage, dateYear, //
                            startData.getYear() + "-" + startData.getMonth() + "-" + startData.getDay(), //
                            endData.getYear() + "-" + endData.getMonth() + "-" + endData.getDay()
                    );
                }
                /* #9451 - En cas de données manquantes, on passe à l'année suivante
                 * Par défaut, le résultat de l'évaluation du couple phase/année vaut NA (null).
                 * Si toutes les données sont présentes, alors le calcul est réalisé.
                 */
                Double value = null;
                if (climaticData.hasCompleteDataInYear(settings.getClimateLoader().getProvidedVariables())) {
                    value = phase.compute(climaticData);
                }
                phaseResult.setNormalizedValue(value);

                fillIndicatorResults(phase, phaseResult.getIndicatorResults());
                phase.fireValueUpdated();
            }
        }

        computeFaisability(results);
        LOGGER.trace("end of computing evaluation \"{}\"", getName());
        return results;
    }

    /**
     * Compute indicator results for each step of provided climatic data.
     *
     * @return Results of computation by year, for each step in climatic data:
     * Climatic date ⮕ (year, {@link EvaluationResult}).
     * @throws IndicatorsException
     *             from Indicator.compute()
     */
    public Map<LocalDate, Map<Integer, EvaluationResult>> computeEachDate() throws IndicatorsException {
        this.ignoreEmptyClimaticData = true;
        final List<CompositeIndicator> phases = getPhases();
        final ClimaticResource climaticResource = resourceManager.getClimaticResource();
        checkBeforeCompute(phases, climaticResource);

        final List<ClimaticDailyData> dailyData = climaticResource.getData();
        // first, create Maps
        final Map<LocalDate, Map<Integer, EvaluationResult>> allResults = new LinkedHashMap<>();
        dailyData.stream().map(DailyData::getLocalDate).forEach(date -> allResults.put(date, new LinkedHashMap<>()));
        // then compute
        final ClimaticResource resource = new ClimaticResource();
        resource.setMissingVariables(climaticResource.getMissingVariables());
        for (int i = 0; i < dailyData.size(); i++) {
            final List<ClimaticDailyData> data = dailyData.subList(0, i);
            resource.setData(data);
            final Map<Integer, EvaluationResult> results = compute(resource, phases);
            final LocalDate date = dailyData.get(i).getLocalDate();
            allResults.put(date, results);
        }
        return allResults;
    }

    /**
     * Agrégation des phases : calcul du climatic faisability.
     *
     * valeur pour année n = agrégation(valeur phase 1, année n; valeur phase 2,
     * année n...) ;
     *
     *
     * Pour chaque année, Pour chaque phase, valeurs.onIndicatorAdd(valeur phase
     * p, année n); Fin pour aggregation(valeurs); Fin pour;
     *
     * @param results Results of computation by year.
     * @throws IndicatorsException raised by AggregationFunction.aggregate()
     */
    private void computeFaisability(final Map<Integer, EvaluationResult> results) throws IndicatorsException {
        LOGGER.traceEntry();
        if (getType() == EvaluationType.WITHOUT_AGGREGATION) {
            return;
        }
        if (getPhases().size() == 1) {
            // if only 1 phase, no need to aggregate
            // evaluation value = value of the phase
            results.values().forEach(result ->
                    result.setNormalizedValue(result.getPhaseResults().get(0).getNormalizedValue()));
            return;
        } else if (getAggregationFunction().getExpression() == null) {
            throw new IllegalStateException("An evaluation with more than 1 "
                    + "phase must have a defined expression for aggregation!");
        }

        for (final Map.Entry<Integer, EvaluationResult> entry : results.entrySet()) {
            final EvaluationResult evaluationResult = entry.getValue();
            final int year = entry.getKey();
            if (evaluationResult == null) {
                LOGGER.warn(
                        "Strange, null value for EvaluationResult for year {}",
                        year);
                continue;
            }
            if (evaluationResult.getPhaseResults().isEmpty()) {
                LOGGER.warn("Strange, empty results for phases for year {}",
                        year);
                continue;
            }
            final Map<String, Double> values = new HashMap<>();
            boolean failedPhase = false;
            for (final PhaseResult phase : evaluationResult.getPhaseResults()) {
                if (phase.getNormalizedValue() == null) {
                    failedPhase = true;
                    break;
                }
                values.put(phase.getEncodedPhaseId(),
                        phase.getNormalizedValue());
            }
            if (failedPhase) {
                evaluationResult.setNormalizedValue(0.);
                continue;
            }
            final Double normalizedValue = getAggregationFunction().aggregate(values);
            evaluationResult.setNormalizedValue(normalizedValue);
        }
    }

    /**
     * This implementation takes into account if no phases are defined.
     *
     * @return true if at least one of the composed indicators is climatic
     */
    @Override
    public boolean containsClimaticIndicator() {
        boolean contains = true;
        for (final CompositeIndicator phase : getPhases()) {
            if (!phase.containsClimaticIndicator()) {
                /* Ne contient pas d'indicateur climatique */
                fireIndicatorEvent(
                        IndicatorEvent.Type.CLIMATIC_MISSING.event(phase));
                contains = false;
            }
        }

        if (getPhases().isEmpty()) {
            LOGGER.trace("Evaluation {} does not contain any phase!",
                    getName());
            fireIndicatorEvent(IndicatorEvent.Type.PHASE_MISSING.event(this));
        }
        return contains;
    }

    /**
     * @param fire fire events while checking
     * @return true if at least one of the composed indicators is climatic
     */
    public boolean containsClimaticIndicator(final boolean fire) {
        if (fire) {
            return containsClimaticIndicator();
        }
        boolean contains = true;
        for (final CompositeIndicator phase : getPhases()) {
            if (!phase.containsClimaticIndicator()) {
                /* Ne contient pas d'indicateur climatique */
                contains = false;
            }
        }

        return contains;
    }

    /**
     * @return climaticResource filled with data from climateLoader.
     */
    public ClimaticResource getClimaticResource() {
        return resourceManager.getClimaticResource();
    }

    @Override
    public String getId() {
        return "root-evaluation";
    }

    /**
     * An evaluation does not have any parent.
     *
     * @return null
     */
    @Override
    public Indicator getParent() {
        return null;
    }

    /**
     * @return children composite indicator with category PHENO_PHASES.
     */
    public List<CompositeIndicator> getPhases() {
        final List<CompositeIndicator> phases = new ArrayList<>();
        for (final Indicator indicator : getIndicators()) {
            if (indicator instanceof CompositeIndicator compositeIndicator && compositeIndicator.isPhase()) {
                phases.add(compositeIndicator);
            }
        }
        return phases;
    }

    /**
     * @return distinct stages of phases
     */
    public List<String> getStages() {
        final List<String> stages = new ArrayList<>();
        for (final CompositeIndicator phase : getPhases()) {
            if (phase == null) {
                throw new RuntimeException("phase in getPhases() must not be null!");
            }
            if (phase.getFirstIndicator() != null) {
                final String startStage = phase.getFirstIndicator().getName();
                if (startStage == null) {
                    throw new RuntimeException("Name of first indicator in phase must not be null!");
                }
                if (!stages.contains(startStage)) {
                    stages.add(startStage);
                }
            }
            final String endStage = phase.getName();
            if (endStage == null) {
                throw new RuntimeException("Name of phase must not be null!");
            }
            if (!stages.contains(endStage)) {
                stages.add(endStage);
            }
        }
        Collections.sort(stages);
        return stages;
    }

    /**
     * @return phenological stages (4 by year) for soil calculator
     */
    private List<Date> getStagesForSoil() {
        LOGGER.traceEntry();

        PhenologyCalculator soilPhenoCalc = settings.getSoilPhenologyCalculator();
        if (soilPhenoCalc != null) {
            final List<AnnualStageData> dates = soilPhenoCalc.load();
            return PhenologicalResource.asDates(
                    StageUtils.sanitizeStagesForSoil(
                            dates,
                            soilPhenoCalc.getSowingDate()));
        }

        // test 4 stages
        final List<AnnualStageData> data = settings.getPhenologyLoader().load();
        final int nbStages = data.get(0).getStages().size();
        if (nbStages == Stage.FOUR) {
            return PhenologicalResource.asDates(data);
        }
        throw new RuntimeException(Messages.format(nbStages, "warning.soilcalculator.4stages", nbStages));
    }

    /**
     * Variables from indicators and models.
     *
     * @return Variables used to compute data.
     */
    @Override
    public Set<Variable> getVariables() {
        final Set<Variable> variables = new HashSet<>(super.getVariables());
        if (settings == null) {
            LOGGER.error("Evaluation.settings is null!");
            return variables;
        }
        if (settings.getPhenologyLoader() != null) {
            if (settings.getPhenologyLoader().getVariables() != null) {
                variables.addAll(settings.getPhenologyLoader().getVariables());
            } else {
                LOGGER.warn("No variable in PhenologyLoader().getVariables())");
            }
        }
        if (settings.getSoilLoader() != null) {
            variables.addAll(settings.getSoilLoader().getVariables());
        }
        variables.remove(null);
        return variables;
    }

    /**
     * Fill climaticResource with data from climateLoader.
     */
    private void initializeClimaticResource() {
        LOGGER.traceEntry();
        if (settings == null) {
            throw new RuntimeException("settings should not be null!");
        }
        if (settings.getClimateLoader() == null) {
            throw new RuntimeException(
                    "settings.getClimateLoader() should not be null!");
        }
        settings.getClimateLoader().addDataLoadingListener(this);
        settings.getClimateLoader().setTimeScale(settings.getTimescale());
        final List<ClimaticDailyData> data = settings.getClimateLoader().load();
        resourceManager.getClimaticResource().setData(data);
        resourceManager.getClimaticResource().setMissingVariables(
                settings.getClimateLoader().getMissingVariables());
    }

    /**
     * Fill phenologicalResource with data from phenologyLoader.
     */
    private void initializePhenologicalResource() {
        LOGGER.trace("start");
        if (settings == null) {
            throw new RuntimeException("settings should not be null!");
        }
        if (settings.getPhenologyLoader() == null) {
            throw new RuntimeException("settings.getPhenologyLoader() should not be null!");
        }
        if (settings.getPhenologyLoader().getCalculator() != null) {
            List<ClimaticDailyData> climaticData = settings.getPhenologyLoader().getCalculator().getClimaticDailyData();
            if (climaticData == null || climaticData.isEmpty()) {
                climaticData = resourceManager.getClimaticResource().getData();
                settings.getPhenologyLoader().getCalculator().setClimaticDailyData(climaticData);
            }
        }
        final List<AnnualStageData> data = settings.getPhenologyLoader().load();
        LOGGER.trace("{} stages", data.size());
        resourceManager.getPhenologicalResource().setData(data);
        LOGGER.trace("{} stages", resourceManager.getPhenologicalResource().getData().size());
        if (settings.getPhenologyLoader().getFile() != null) {
            resourceManager.getPhenologicalResource().setUserHeader(
                    settings.getPhenologyLoader().getFile().getHeaders());
        }
        LOGGER.trace("end");
    }

    /**
     * Load resources.
     */
    public void initializeResources() {
        initializeResources(false, false);
    }

    /**
     * Load climatic, phenological and soil resources.
     *
     * @param climatic initialize climatic resources, not depending on needed
     * variables
     * @param soil initialize soil resources, not depending on needed variables
     */
    public void initializeResources(final boolean climatic, final boolean soil) {
        LOGGER.trace("start");
        // Set all needed variables to ResourceManager in order to check
        // consistency.
        resourceManager.setVariables(getVariables());
        LOGGER.trace(getVariables());
        // init climate resource
        if (climatic || resourceManager.hasClimaticVariables()) {
            LOGGER.trace("hasClimaticVariables!");
            initializeClimaticResource();
        }
        // init phenology before init soil which needs 4 stages
        initializePhenologicalResource();
        // init soil resources
        // only if climate file does not provide all soil variables
        final Set<Variable> neededSoilVariables = getVariables().stream()
                .filter(v -> v.getType() == Type.SOIL)
                .collect(Collectors.toSet());
        if (soil || !neededSoilVariables.isEmpty()) {
            final Set<Variable> providedSoilVariables = settings.getClimateLoader().getProvidedVariables().stream()
                    .filter(v -> v.getType() == Type.SOIL)
                    .collect(Collectors.toSet());
            final boolean compute = !providedSoilVariables.containsAll(neededSoilVariables);
            initializeSoilResource(compute);
        }
    }

    /**
     * Get soil data from soil loader.
     * @param compute compute soil data using phenological model if soil data are not provided
     */
    private void initializeSoilResource(final boolean compute) {
        if (compute) {
            if (settings == null) {
                throw new RuntimeException("settings should not be null!");
            }
            final SoilLoaderProxy soilLoader = settings.getSoilLoader();
            if (soilLoader == null) {
                throw new RuntimeException(Messages.get("warning.soilloader.missing"));
            }
            soilLoader.addDataLoadingListener(this);
            final List<ClimaticDailyData> data = resourceManager.getClimaticResource().getData();
            if (settings.getSoilPhenologyCalculator() != null) {
                final List<ClimaticDailyData> filled = new ArrayList<>();
                // duplicate first year in case of multi-year crop
                int nbOfYears = settings.getSoilPhenologyCalculator().getNbOfYears();
                if (nbOfYears > 1) {
                    int firstYear = data.get(0).getYear();
                    for (int i = nbOfYears - 1; i > 0; i--) {
                        int year = firstYear - i;
                        int nbOfDays = DateUtils.nbOfDays(year);
                        for (int d = 0; d < nbOfDays; d++) {
                            ClimaticDailyData aData = new ClimaticDailyData(data.get(d));
                            LocalDate date = DateUtils.asLocalDate(aData.getDate()).minusYears(i);
                            aData.setDay(date.getDayOfMonth());
                            aData.setMonth(date.getMonthValue());
                            aData.setYear(date.getYear());
                            filled.add(aData);
                        }
                    }
                }
                filled.addAll(data);
                settings.getSoilPhenologyCalculator().setClimaticDailyData(filled);
            }
            soilLoader.setClimaticDailyData(data);
            soilLoader.setStages(getStagesForSoil());
            final Iterator<SoilDailyData> soilDataIterator = soilLoader.load().iterator();
            // Do not keep the list of soil data,
            // fill ClimaticDailyData with soil data
            for (final ClimaticDailyData aClimaticData : data) {
                final SoilDailyData aSoilData = soilDataIterator.next();
                aClimaticData.setValue(Variable.WATER_RESERVE, aSoilData.getWaterReserve());
                aClimaticData.setValue(Variable.SOILWATERCONTENT, aSoilData.getSwc());
            }
        }
        final List<String> missingVariables = resourceManager.getClimaticResource().getMissingVariables();
        missingVariables.remove(Variable.WATER_RESERVE.getName());
        missingVariables.remove(Variable.SOILWATERCONTENT.getName());
        resourceManager.getClimaticResource().setMissingVariables(missingVariables);
    }

    /**
     * Ensure aggregation expression is set (when needed) and valid for the
     * phases.
     *
     * @return check
     */
    public boolean isAggregationValid() {
        if (getType() == EvaluationType.WITHOUT_AGGREGATION || getPhases().size() < 2) {
            return true;
        }
        final Map<String, Double> values = new HashMap<>();
        getPhases().forEach(phase -> values.put(phase.getId(), 1.));
        try {
            getAggregationFunction().aggregate(values);
        } catch (final IndicatorsException ex) {
            LOGGER.info("Invalid aggregation: {}", ex.getLocalizedMessage());
            return false;
        }
        return true;
    }

    /**
     * @return the evaluation settings has no file path
     */
    public boolean isNew() {
        return getSettings().getFilePath() == null;
    }

    /**
     * @param fire fire events while checking
     * @return the evaluation is not fully defined or with errors
     */
    public boolean isOnErrorOrIncomplete(final boolean fire) {
        final boolean hasClimaticIndicator = containsClimaticIndicator(fire);
        final boolean isToAggregate = isAggregationMissing(fire);
        final boolean isComputable = isComputable();

        return !hasClimaticIndicator || isToAggregate || !isComputable;
    }

    /**
     * Check if period matches the stage list.
     *
     * @param firstId
     *            stage id of period start
     * @param endId
     *            stage id of period end
     * @return presence
     */
    protected boolean isStagePresent(final String firstId, final String endId) {
        LOGGER.trace("firstId: {}, endId: {}", firstId, endId);
        boolean result = false;
        for (final Indicator i : getIndicators()) {
            final Indicator firstIndicator = ((CompositeIndicator) i).getIndicators()
                    .get(0);
            LOGGER.trace("indicator id={}", i.getId());
            LOGGER.trace("first child indicator id={}", firstIndicator.getId());
            if (i.getId().equals(endId)
                    && firstIndicator.getId().equals(firstId)) {
                result = true;
                break;
            }
        }
        return result;
    }

    /**
     * Set parameters (id and attributes) for the indicator and its criteria
     * from knowledge defined in settings.
     */
    public void setParametersFromKnowledge() {
        if (getSettings() == null) {
            throw new IllegalStateException("Settings are not set!");
        }
        if (getSettings().getKnowledge() == null) {
            throw new IllegalStateException(
                    "Knowledge is not set in settings!");
        }
        setParametersFromKnowledge(getSettings().getKnowledge());
    }

    /**
     * @param value
     *            settings from XML
     */
    public void setSettings(final EvaluationSettings value) {
        Objects.requireNonNull(value);
        settings = value;
        if (settings.getEvaluation().getAggregationFunction() != null) {
            setAggregationFunction(settings.getEvaluation()
                    .getAggregationFunction());
        } else {
            setAggregationFunction(new JEXLFunction());
        }
        setName("en", settings.getName());
        setTimescale(settings.getTimescale());
        resourceManager.setVariables(this.getVariables());
    }

    /**
     * Set Transcient evaluation value (fire indicator event).
     * @param value
     *            Flag for state Saved.
     */
    public void setTranscient(final boolean value) {
        LOGGER.traceEntry();
        this.setTranscient(value, false);
    }
    /**
     * Set Transcient evaluation value with possibility to disable firing indicator event.<br>
     * Default method is {@link #setTranscient(boolean)}
     * @param value
     * @param fromSave flag if is from save method ({@code false} : default value)
     */
    public void setTranscient(final boolean value, final boolean fromSave) {
        LOGGER.traceEntry();
        if (isTranscient == value) {
            return;
        }
        this.isTranscient = value;
        if (!fromSave) {
            fireIndicatorEvent(IndicatorEvent.Type.CHANGE.event(this));
        }
    }

    /**
     * @param type evaluation type
     */
    public void setType(@NonNull final EvaluationType type) {
        Objects.requireNonNull(settings);
        settings.setType(type);
    }

    /**
     * @return Structured string representation.
     */
    public String toStringTree() {
        final StringBuilder sb = new StringBuilder();
        getIndicators().forEach(phase -> sb.append(phase.toStringTree("")).append("\n"));
        return sb.toString();
    }

    /**
     * Validate evaluation.
     */
    public void validate() {
        state.onValidate(this);
    }

}