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);
}
}