View Javadoc
1   /**
2    * This file is part of Indicators.
3    *
4    * Indicators is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * Indicators is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU General Public License for more details.
13   *
14   * You should have received a copy of the GNU General Public License
15   * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
16   */
17  package fr.inrae.agroclim.indicators.model;
18  
19  import java.time.LocalDate;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.Date;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.LinkedHashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.Set;
31  import java.util.stream.Collectors;
32  
33  import fr.inrae.agroclim.indicators.exception.IndicatorsException;
34  import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
35  import fr.inrae.agroclim.indicators.model.data.DailyData;
36  import fr.inrae.agroclim.indicators.model.data.ResourceManager;
37  import fr.inrae.agroclim.indicators.model.data.Variable;
38  import fr.inrae.agroclim.indicators.model.data.Variable.Type;
39  import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
40  import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
41  import fr.inrae.agroclim.indicators.model.data.phenology.AnnualStageData;
42  import fr.inrae.agroclim.indicators.model.data.phenology.PhenologicalResource;
43  import fr.inrae.agroclim.indicators.model.data.phenology.PhenologyCalculator;
44  import fr.inrae.agroclim.indicators.model.data.phenology.Stage;
45  import fr.inrae.agroclim.indicators.model.data.soil.SoilDailyData;
46  import fr.inrae.agroclim.indicators.model.data.soil.SoilLoaderProxy;
47  import fr.inrae.agroclim.indicators.model.function.aggregation.JEXLFunction;
48  import fr.inrae.agroclim.indicators.model.indicator.CompositeIndicator;
49  import fr.inrae.agroclim.indicators.model.indicator.Indicator;
50  import fr.inrae.agroclim.indicators.model.indicator.IndicatorCategory;
51  import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorEvent;
52  import fr.inrae.agroclim.indicators.model.result.EvaluationResult;
53  import fr.inrae.agroclim.indicators.model.result.IndicatorResult;
54  import fr.inrae.agroclim.indicators.model.result.PhaseResult;
55  import fr.inrae.agroclim.indicators.resources.Messages;
56  import fr.inrae.agroclim.indicators.util.DateUtils;
57  import fr.inrae.agroclim.indicators.util.StageUtils;
58  import lombok.EqualsAndHashCode;
59  import lombok.Getter;
60  import lombok.NonNull;
61  import lombok.Setter;
62  import lombok.extern.log4j.Log4j2;
63  
64  /**
65   * Indicators evaluations.
66   *
67   * Last change $Date$
68   *
69   * @author $Author$
70   * @version $Revision$
71   */
72  @EqualsAndHashCode(callSuper = true, of = {"isTranscient", "settings"})
73  @Log4j2
74  public final class Evaluation extends CompositeIndicator {
75      /**
76       * UUID for Serializable.
77       */
78      private static final long serialVersionUID = 9205643160821888597L;
79  
80      /**
81       * Add values of child of indicator into indicatorResults.
82       *
83       * @param indicator indicator to inspect
84       * @param indicatorResults list to populate
85       */
86      private static void fillIndicatorResults(final CompositeIndicator indicator,
87              final List<IndicatorResult> indicatorResults) {
88          indicator.getIndicators().forEach(ind -> {
89              IndicatorCategory cat;
90              cat = IndicatorCategory.getByTag(ind.getCategory());
91              if (cat == IndicatorCategory.PHENO_PHASES) {
92                  return;
93              }
94  
95              final IndicatorResult result = new IndicatorResult();
96              result.setIndicatorCategory(cat);
97              result.setIndicatorId(ind.getId());
98              result.setNormalizedValue(ind.getValue());
99              result.setRawValue(ind.getNotNormalizedValue());
100             indicatorResults.add(result);
101 
102             if (ind instanceof CompositeIndicator compositeIndicator) {
103                 fillIndicatorResults(compositeIndicator, result.getIndicatorResults());
104             }
105         });
106     }
107 
108     /**
109      * All resources needed to run the evaluation.
110      *
111      * Resources filled by Loader's.
112      */
113     @Getter
114     private final ResourceManager resourceManager;
115 
116     /**
117      * Flag to ignore when climatic data is empty for a phase.
118      */
119     private boolean ignoreEmptyClimaticData = false;
120 
121     /**
122      * Flag for state Saved.
123      */
124     @Getter
125     private boolean isTranscient;
126 
127     /**
128      * Settings from XML.
129      */
130     @Getter
131     private EvaluationSettings settings;
132 
133     /**
134      * State.
135      */
136     @Getter
137     @Setter
138     private transient EvaluationState state;
139 
140     /**
141      * Constructor.
142      */
143     public Evaluation() {
144         super();
145         resourceManager = new ResourceManager();
146         if (getAggregationFunction() == null) {
147             setAggregationFunction(new JEXLFunction());
148         }
149         setState(EvaluationState.NEW);
150     }
151 
152     /**
153      * Constructor from CompositeIndicator.
154      *
155      * @param indicator
156      *            indicator
157      */
158     public Evaluation(final CompositeIndicator indicator) {
159         super(indicator);
160         resourceManager = new ResourceManager();
161         if (getAggregationFunction() == null) {
162             setAggregationFunction(new JEXLFunction());
163         }
164         setState(EvaluationState.NEW);
165     }
166 
167     /**
168      * Constructor for cloning purpose.
169      *
170      * @param evaluation
171      *            evaluation to clone.
172      */
173     public Evaluation(final Evaluation evaluation) {
174         super(evaluation);
175         if (getAggregationFunction() == null) {
176             setAggregationFunction(new JEXLFunction());
177         }
178         try {
179             resourceManager = evaluation.resourceManager.clone();
180         } catch (final CloneNotSupportedException ex) {
181             throw new RuntimeException("This should never occur!", ex);
182         }
183         if (evaluation.settings != null) {
184             try {
185                 settings = evaluation.settings.clone();
186             } catch (final CloneNotSupportedException ex) {
187                 throw new RuntimeException("This should never occur!", ex);
188             }
189         }
190         isTranscient = evaluation.isTranscient;
191         state = evaluation.state;
192     }
193 
194     /**
195      * Add the indicator to category PHENO_PHASES or to parent.
196      *
197      * @param category
198      *            PHENO_PHASES or anything else
199      * @param parent
200      *            parent indicator
201      * @param indicator
202      *            indicator to onIndicatorAdd
203      * @return added indicator
204      * @throws CloneNotSupportedException should never occurs as indicator must
205      * implement clone()
206      */
207     public Indicator add(final IndicatorCategory category,
208             final CompositeIndicator parent, final Indicator indicator)
209                     throws CloneNotSupportedException {
210         final Indicator newIndicator = indicator.clone();
211         newIndicator.setParent(parent);
212         if (!category.equals(IndicatorCategory.PHENO_PHASES)
213                 && newIndicator instanceof CompositeIndicator) {
214             ((CompositeIndicator) newIndicator).clearIndicators();
215         }
216 
217         if (category.equals(IndicatorCategory.PHENO_PHASES)) {
218             // the parent is the evaluation
219             final String endStageId = newIndicator.getId();
220             final String firstStageId = ((CompositeIndicator) newIndicator)
221                     .getIndicators().iterator().next().getId();
222 
223             if (!isStagePresent(firstStageId, endStageId)) {
224                 add(newIndicator);
225                 fireIndicatorEvent(IndicatorEvent.Type.ADD.event(newIndicator));
226             } else {
227                 LOGGER.warn("Phase ({},{}) already exists for evaluation.",
228                         firstStageId, endStageId);
229             }
230         } else if (!parent.isPresent(newIndicator.getId())) {
231             parent.add(newIndicator);
232             newIndicator.setComputable(newIndicator
233                     .isComputable(resourceManager.getClimaticResource()));
234             fireIndicatorEvent(IndicatorEvent.Type.ADD.event(newIndicator));
235         } else {
236             LOGGER.warn("Indicator {} already exists for {}.",
237                     newIndicator.getId(), parent.getId());
238             return newIndicator;
239         }
240         // TODO : CHANGE est-il nécessaire en plus de ADD ?
241         fireIndicatorEvent(IndicatorEvent.Type.CHANGE.event(this));
242 
243         return newIndicator;
244     }
245 
246     /**
247      * Add the indicator to category PHENO_PHASES or to parent.
248      *
249      * @param categoryTag
250      *            PHENO_PHASES or anything else
251      * @param parent
252      *            parent indicator
253      * @param indicator
254      *            indicator to onIndicatorAdd
255      * @return added indicator
256      * @throws CloneNotSupportedException should never occurs as indicator must
257      * implement clone()
258      */
259     public Indicator add(final String categoryTag,
260             final CompositeIndicator parent, final Indicator indicator)
261                     throws CloneNotSupportedException {
262 
263         final IndicatorCategory category = IndicatorCategory.getByTag(categoryTag);
264         if (category == null) {
265             throw new RuntimeException("Unknown category: " + categoryTag);
266         }
267 
268         return add(category, parent, indicator);
269     }
270 
271     /**
272      * Check data before running compute* methods.
273      *
274      * @param phases phenological phases to check
275      * @param climaticResource climatic resource to check
276      * @throws IndicatorsException exception
277      */
278     private void checkBeforeCompute(final List<CompositeIndicator> phases, final ClimaticResource climaticResource)
279             throws IndicatorsException {
280         if (phases == null) {
281             throw new RuntimeException("Phase list is null!");
282         }
283         if (phases.isEmpty()) {
284             throw new RuntimeException("Phase list is empty!");
285         }
286         if (climaticResource.isEmpty()) {
287             throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
288         }
289         if (resourceManager.getPhenologicalResource().isEmpty()) {
290             throw new IndicatorsException(ResourceErrorType.PHENO_EMPTY);
291         }
292     }
293 
294     @Override
295     public Evaluation clone() {
296         return new Evaluation(this);
297     }
298 
299     /**
300      * Compute indicator results.
301      *
302      * @return Results of computation by year.
303      * @throws IndicatorsException
304      *             from Indicator.compute()
305      */
306     public Map<Integer, EvaluationResult> compute() throws IndicatorsException {
307         LOGGER.trace("start computing evaluation \"" + getName() + "\"");
308         this.ignoreEmptyClimaticData = false;
309         fireIndicatorEvent(IndicatorEvent.Type.COMPUTE_START.event(this));
310 
311         final List<CompositeIndicator> phases = getPhases();
312         final ClimaticResource climaticResource = resourceManager.getClimaticResource();
313         checkBeforeCompute(phases, climaticResource);
314 
315         var results = compute(climaticResource, phases);
316         fireIndicatorEvent(IndicatorEvent.Type.COMPUTE_SUCCESS.event(this));
317         return results;
318     }
319 
320     private Map<Integer, EvaluationResult> compute(final ClimaticResource climaticResource,
321             final List<CompositeIndicator> phases) throws IndicatorsException {
322         final Map<Integer, EvaluationResult> results = new LinkedHashMap<>();
323 
324         /* Pour chaque phase */
325         final List<AnnualStageData> stageDatas = getResourceManager().getPhenologicalResource().getData();
326         for (final CompositeIndicator phase : phases) {
327             final String phaseId = phase.getId();
328             if (phaseId == null) {
329                 throw new RuntimeException("Id of phase is null!");
330             }
331 
332             /* Nom du stade de départ de la phase */
333             final String startStageName = phase.getFirstIndicator().getName();
334             /* Nom du stage de fin de la phase */
335             final String endStageName = phase.getName();
336 
337             /* Pour chaque année phénologique */
338             for (final AnnualStageData stageData : stageDatas) {
339                 /* Année phénologique */
340                 final Integer year = stageData.getYear();
341 
342                 // DOY coded on 2 years: last stage on second year
343                 int dateYear = year;
344                 if (stageData.existWinterCrop()) {
345                     dateYear -= 1;
346                 }
347 
348                 if (!climaticResource.getYears().contains(dateYear)) {
349                     continue;
350                 }
351 
352                 if (!results.containsKey(year)) {
353                     results.put(year, new EvaluationResult());
354                 }
355 
356                 /* Valeur du stade de départ de la phase */
357                 final Integer startStage = stageData.getStageValue(startStageName);
358 
359                 /* Valeur du stade de fin de la phase */
360                 final Integer endStage = stageData.getStageValue(endStageName);
361 
362                 final AnnualPhase annualPhase = new AnnualPhase();
363                 annualPhase.setHarvestYear(year);
364                 annualPhase.setEndStage(endStageName);
365                 annualPhase.setStartStage(startStageName);
366                 annualPhase.setUid(phaseId);
367                 final PhaseResult phaseResult = new PhaseResult();
368                 phaseResult.setAnnualPhase(annualPhase);
369                 results.get(year).getPhaseResults().add(phaseResult);
370 
371                 if (startStage == null) {
372                     LOGGER.info(String.format("No start stage for %s/%s : %s", startStageName, endStageName,
373                             stageData.toString()));
374                     continue;
375                 }
376 
377                 // Phenology not finished.
378                 if (endStage == null || endStage == 0) {
379                     LOGGER.info(String.format("No end stage for %s/%s : %s", startStageName, endStageName,
380                             stageData.toShortString()));
381                     continue;
382                 }
383                 //-
384 
385                 final Date endDate = DateUtils.getDate(dateYear, endStage);
386                 annualPhase.setEnd(endDate);
387                 final Date startDate = DateUtils.getDate(dateYear, startStage);
388                 annualPhase.setStart(startDate);
389 
390                 // Do not compute at all if there are missing stages
391                 if (!stageData.isComplete()) {
392                     LOGGER.info("In {}, missing stages {}, do not compute", year, stageData);
393                     continue;
394                 }
395 
396                 /* Données climatiques pendant la phase et l'année donnée */
397                 final ClimaticResource climaticData = climaticResource
398                         .getClimaticDataByPhaseAndYear(startDate, endDate);
399                 if (climaticData.isEmpty()) {
400                     if (climaticResource.isEmpty()) {
401                         throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY);
402                     }
403                     if (ignoreEmptyClimaticData) {
404                         continue;
405                     }
406                     final int yearToSearch = dateYear;
407                     final List<ClimaticDailyData> ddataList = climaticResource.getData().stream() //
408                             .filter(f -> f.getYear() == yearToSearch) //
409                             .collect(Collectors.toList());
410 
411                     final ClimaticDailyData startData = ddataList.get(0);
412                     final ClimaticDailyData endData = ddataList.get(ddataList.size() - 1);
413 
414                     throw new IndicatorsException(ResourceErrorType.CLIMATE_EMPTY_FOR_PHASE,
415                             startStageName, endStageName, startStage, endStage, dateYear, //
416                             startData.getYear() + "-" + startData.getMonth() + "-" + startData.getDay(), //
417                             endData.getYear() + "-" + endData.getMonth() + "-" + endData.getDay()
418                     );
419                 }
420                 /* #9451 - En cas de données manquantes, on passe à l'année suivante
421                  * Par défaut, le résultat de l'évaluation du couple phase/année vaut NA (null).
422                  * Si toutes les données sont présentes, alors le calcul est réalisé.
423                  */
424                 Double value = null;
425                 if (climaticData.hasCompleteDataInYear(settings.getClimateLoader().getProvidedVariables())) {
426                     value = phase.compute(climaticData);
427                 }
428                 phaseResult.setNormalizedValue(value);
429 
430                 fillIndicatorResults(phase, phaseResult.getIndicatorResults());
431                 phase.fireValueUpdated();
432             }
433         }
434 
435         computeFaisability(results);
436         LOGGER.trace("end of computing evaluation \"{}\"", getName());
437         return results;
438     }
439 
440     /**
441      * Compute indicator results for each step of provided climatic data.
442      *
443      * @return Results of computation by year, for each step in climatic data:
444      * Climatic date ⮕ (year, {@link EvalutationResult}).
445      * @throws IndicatorsException
446      *             from Indicator.compute()
447      */
448     public Map<LocalDate, Map<Integer, EvaluationResult>> computeEachDate() throws IndicatorsException {
449         this.ignoreEmptyClimaticData = true;
450         final List<CompositeIndicator> phases = getPhases();
451         final ClimaticResource climaticResource = resourceManager.getClimaticResource();
452         checkBeforeCompute(phases, climaticResource);
453 
454         final List<ClimaticDailyData> dailyData = climaticResource.getData();
455         // first, create Maps
456         final Map<LocalDate, Map<Integer, EvaluationResult>> allResults = new LinkedHashMap<>();
457         dailyData.stream().map(DailyData::getLocalDate).forEach(date -> allResults.put(date, new LinkedHashMap<>()));
458         // then compute
459         final ClimaticResource resource = new ClimaticResource();
460         resource.setMissingVariables(climaticResource.getMissingVariables());
461         for (int i = 0; i < dailyData.size(); i++) {
462             final List<ClimaticDailyData> data = dailyData.subList(0, i);
463             resource.setData(data);
464             final Map<Integer, EvaluationResult> results = compute(resource, phases);
465             final LocalDate date = dailyData.get(i).getLocalDate();
466             allResults.put(date, results);
467         }
468         return allResults;
469     }
470 
471     /**
472      * Agrégation des phases : calcul du climatic faisability.
473      *
474      * valeur pour année n = agrégation(valeur phase 1, année n; valeur phase 2,
475      * année n...) ;
476      *
477      *
478      * Pour chaque année, Pour chaque phase, valeurs.onIndicatorAdd(valeur phase
479      * p, année n); Fin pour aggregation(valeurs); Fin pour;
480      *
481      * @param results Results of computation by year.
482      * @throws IndicatorsException raised by AggregationFunction.aggregate()
483      */
484     private void computeFaisability(final Map<Integer, EvaluationResult> results) throws IndicatorsException {
485         LOGGER.traceEntry();
486         if (getType() == EvaluationType.WITHOUT_AGGREGATION) {
487             return;
488         }
489         if (getPhases().size() == 1) {
490             // if only 1 phase, no need to aggregate
491             // evaluation value = value of the phase
492             results.values().forEach(result ->
493                     result.setNormalizedValue(result.getPhaseResults().get(0).getNormalizedValue()));
494             return;
495         } else if (getAggregationFunction().getExpression() == null) {
496             throw new IllegalStateException("An evaluation with more than 1 "
497                     + "phase must have a defined expression for aggregation!");
498         }
499 
500         for (final Map.Entry<Integer, EvaluationResult> entry : results.entrySet()) {
501             final EvaluationResult evaluationResult = entry.getValue();
502             final int year = entry.getKey();
503             if (evaluationResult == null) {
504                 LOGGER.warn(
505                         "Strange, null value for EvaluationResult for year {}",
506                         year);
507                 continue;
508             }
509             if (evaluationResult.getPhaseResults().isEmpty()) {
510                 LOGGER.warn("Strange, empty results for phases for year {}",
511                         year);
512                 continue;
513             }
514             final Map<String, Double> values = new HashMap<>();
515             boolean failedPhase = false;
516             for (final PhaseResult phase : evaluationResult.getPhaseResults()) {
517                 if (phase.getNormalizedValue() == null) {
518                     failedPhase = true;
519                     break;
520                 }
521                 values.put(phase.getEncodedPhaseId(),
522                         phase.getNormalizedValue());
523             }
524             if (failedPhase) {
525                 evaluationResult.setNormalizedValue(0.);
526                 continue;
527             }
528             final Double normalizedValue = getAggregationFunction().aggregate(values);
529             evaluationResult.setNormalizedValue(normalizedValue);
530         }
531     }
532 
533     /**
534      * This implementation takes into account if no phases are defined.
535      *
536      * @return true if at least one of the composed indicators is climatic
537      */
538     @Override
539     public boolean containsClimaticIndicator() {
540         boolean contains = true;
541         for (final CompositeIndicator phase : getPhases()) {
542             if (!phase.containsClimaticIndicator()) {
543                 /* Ne contient pas d'indicateur climatique */
544                 fireIndicatorEvent(
545                         IndicatorEvent.Type.CLIMATIC_MISSING.event(phase));
546                 contains = false;
547             }
548         }
549 
550         if (getPhases().isEmpty()) {
551             LOGGER.trace("Evaluation {} does not contain any phase!",
552                     getName());
553             fireIndicatorEvent(IndicatorEvent.Type.PHASE_MISSING.event(this));
554         }
555         return contains;
556     }
557 
558     /**
559      * @param fire fire events while checking
560      * @return true if at least one of the composed indicators is climatic
561      */
562     public boolean containsClimaticIndicator(final boolean fire) {
563         if (fire) {
564             return containsClimaticIndicator();
565         }
566         boolean contains = true;
567         for (final CompositeIndicator phase : getPhases()) {
568             if (!phase.containsClimaticIndicator()) {
569                 /* Ne contient pas d'indicateur climatique */
570                 contains = false;
571             }
572         }
573 
574         return contains;
575     }
576 
577     /**
578      * @return climaticResource filled with data from climateLoader.
579      */
580     public ClimaticResource getClimaticResource() {
581         return resourceManager.getClimaticResource();
582     }
583 
584     @Override
585     public String getId() {
586         return "root-evaluation";
587     }
588 
589     /**
590      * An evaluation does not have any parent.
591      *
592      * @return null
593      */
594     @Override
595     public Indicator getParent() {
596         return null;
597     }
598 
599     /**
600      * @return children composite indicator with category PHENO_PHASES.
601      */
602     public List<CompositeIndicator> getPhases() {
603         final List<CompositeIndicator> phases = new ArrayList<>();
604         for (final Indicator indicator : getIndicators()) {
605             if (indicator instanceof CompositeIndicator compositeIndicator && compositeIndicator.isPhase()) {
606                 phases.add(compositeIndicator);
607             }
608         }
609         return phases;
610     }
611 
612     /**
613      * @return distinct stages of phases
614      */
615     public List<String> getStages() {
616         final List<String> stages = new ArrayList<>();
617         for (final CompositeIndicator phase : getPhases()) {
618             if (phase == null) {
619                 throw new RuntimeException("phase in getPhases() must not be null!");
620             }
621             if (phase.getFirstIndicator() != null) {
622                 final String startStage = phase.getFirstIndicator().getName();
623                 if (startStage == null) {
624                     throw new RuntimeException("Name of first indicator in phase must not be null!");
625                 }
626                 if (!stages.contains(startStage)) {
627                     stages.add(startStage);
628                 }
629             }
630             final String endStage = phase.getName();
631             if (endStage == null) {
632                 throw new RuntimeException("Name of phase must not be null!");
633             }
634             if (!stages.contains(endStage)) {
635                 stages.add(endStage);
636             }
637         }
638         Collections.sort(stages);
639         return stages;
640     }
641 
642     /**
643      * @return phenological stages (4 by year) for soil calculator
644      */
645     private List<Date> getStagesForSoil() {
646         LOGGER.traceEntry();
647 
648         PhenologyCalculator soilPhenoCalc = settings.getSoilPhenologyCalculator();
649         if (soilPhenoCalc != null) {
650             final List<AnnualStageData> dates = soilPhenoCalc.load();
651             return PhenologicalResource.asDates(
652                     StageUtils.sanitizeStagesForSoil(
653                             dates,
654                             soilPhenoCalc.getSowingDate()));
655         }
656 
657         // test 4 stages
658         final List<AnnualStageData> data = settings.getPhenologyLoader().load();
659         final int nbStages = data.get(0).getStages().size();
660         if (nbStages == Stage.FOUR) {
661             return PhenologicalResource.asDates(data);
662         }
663         throw new RuntimeException(Messages.format(nbStages, "warning.soilcalculator.4stages", nbStages));
664     }
665 
666     /**
667      * Variables from indicators and models.
668      *
669      * @return Variables used to compute data.
670      */
671     @Override
672     public Set<Variable> getVariables() {
673         final Set<Variable> variables = new HashSet<>(super.getVariables());
674         if (settings == null) {
675             LOGGER.error("Evaluation.settings is null!");
676             return variables;
677         }
678         if (settings.getPhenologyLoader() != null) {
679             if (settings.getPhenologyLoader().getVariables() != null) {
680                 variables.addAll(settings.getPhenologyLoader().getVariables());
681             } else {
682                 LOGGER.warn("No variable in PhenologyLoader().getVariables())");
683             }
684         }
685         if (settings.getSoilLoader() != null) {
686             variables.addAll(settings.getSoilLoader().getVariables());
687         }
688         variables.remove(null);
689         return variables;
690     }
691 
692     /**
693      * Fill climaticResource with data from climateLoader.
694      */
695     private void initializeClimaticResource() {
696         LOGGER.traceEntry();
697         if (settings == null) {
698             throw new RuntimeException("settings should not be null!");
699         }
700         if (settings.getClimateLoader() == null) {
701             throw new RuntimeException(
702                     "settings.getClimateLoader() should not be null!");
703         }
704         settings.getClimateLoader().addDataLoadingListener(this);
705         settings.getClimateLoader().setTimeScale(settings.getTimescale());
706         final List<ClimaticDailyData> data = settings.getClimateLoader().load();
707         resourceManager.getClimaticResource().setData(data);
708         resourceManager.getClimaticResource().setMissingVariables(
709                 settings.getClimateLoader().getMissingVariables());
710     }
711 
712     /**
713      * Fill phenologicalResource with data from phenologyLoader.
714      */
715     private void initializePhenologicalResource() {
716         LOGGER.trace("start");
717         if (settings == null) {
718             throw new RuntimeException("settings should not be null!");
719         }
720         if (settings.getPhenologyLoader() == null) {
721             throw new RuntimeException("settings.getPhenologyLoader() should not be null!");
722         }
723         if (settings.getPhenologyLoader().getCalculator() != null) {
724             List<ClimaticDailyData> climaticData = settings.getPhenologyLoader().getCalculator().getClimaticDailyData();
725             if (climaticData == null || climaticData.isEmpty()) {
726                 climaticData = resourceManager.getClimaticResource().getData();
727                 settings.getPhenologyLoader().getCalculator().setClimaticDailyData(climaticData);
728             }
729         }
730         final List<AnnualStageData> data = settings.getPhenologyLoader().load();
731         LOGGER.trace("{} stages", data.size());
732         resourceManager.getPhenologicalResource().setData(data);
733         LOGGER.trace("{} stages", resourceManager.getPhenologicalResource().getData().size());
734         if (settings.getPhenologyLoader().getFile() != null) {
735             resourceManager.getPhenologicalResource().setUserHeader(
736                     settings.getPhenologyLoader().getFile().getHeaders());
737         }
738         LOGGER.trace("end");
739     }
740 
741     /**
742      * Load resources.
743      */
744     public void initializeResources() {
745         initializeResources(false, false);
746     }
747 
748     /**
749      * Load climatic, phenological and soil resources.
750      *
751      * @param climatic intialize climatic resources, not depending on needed
752      * variables
753      * @param soil intialize soil resources, not depending on needed variables
754      */
755     public void initializeResources(final boolean climatic, final boolean soil) {
756         LOGGER.trace("start");
757         // Set all needed variables to ResourceManager in order to check
758         // consistency.
759         resourceManager.setVariables(getVariables());
760         LOGGER.trace(getVariables());
761         // init climate resource
762         if (climatic || resourceManager.hasClimaticVariables()) {
763             LOGGER.trace("hasClimaticVariables!");
764             initializeClimaticResource();
765         }
766         // init phenology before init soil which needs 4 stages
767         initializePhenologicalResource();
768         // init soil resources
769         // only if climate file does not provide all soil variables
770         final Set<Variable> neededSoilVariables = getVariables().stream()
771                 .filter(v -> v.getType() == Type.SOIL)
772                 .collect(Collectors.toSet());
773         if (soil || !neededSoilVariables.isEmpty()) {
774             final Set<Variable> providedSoilVariables = settings.getClimateLoader().getProvidedVariables().stream()
775                     .filter(v -> v.getType() == Type.SOIL)
776                     .collect(Collectors.toSet());
777             final boolean compute = !providedSoilVariables.containsAll(neededSoilVariables);
778             initializeSoilResource(compute);
779         }
780     }
781 
782     /**
783      * Get soil data from soil loader.
784      * @param compute compute soil data using phenological model if soil data are not provided
785      */
786     private void initializeSoilResource(final boolean compute) {
787         if (compute) {
788             if (settings == null) {
789                 throw new RuntimeException("settings should not be null!");
790             }
791             final SoilLoaderProxy soilLoader = settings.getSoilLoader();
792             if (soilLoader == null) {
793                 throw new RuntimeException(Messages.get("warning.soilloader.missing"));
794             }
795             soilLoader.addDataLoadingListener(this);
796             final List<ClimaticDailyData> data = resourceManager.getClimaticResource().getData();
797             if (settings.getSoilPhenologyCalculator() != null) {
798                 final List<ClimaticDailyData> filled = new ArrayList<>();
799                 // duplicate first year in case of multi-year crop
800                 int nbOfYears = settings.getSoilPhenologyCalculator().getNbOfYears();
801                 if (nbOfYears > 1) {
802                     int firstYear = data.get(0).getYear();
803                     for (int i = nbOfYears - 1; i > 0; i--) {
804                         int year = firstYear - i;
805                         int nbOfDays = DateUtils.nbOfDays(year);
806                         for (int d = 0; d < nbOfDays; d++) {
807                             ClimaticDailyData aData = new ClimaticDailyData(data.get(d));
808                             LocalDate date = DateUtils.asLocalDate(aData.getDate()).minusYears(i);
809                             aData.setDay(date.getDayOfMonth());
810                             aData.setMonth(date.getMonthValue());
811                             aData.setYear(date.getYear());
812                             filled.add(aData);
813                         }
814                     }
815                 }
816                 filled.addAll(data);
817                 settings.getSoilPhenologyCalculator().setClimaticDailyData(filled);
818             }
819             soilLoader.setClimaticDailyData(data);
820             soilLoader.setStages(getStagesForSoil());
821             final Iterator<SoilDailyData> soilDataIterator = soilLoader.load().iterator();
822             // Do not keep the list of soil data,
823             // fill ClimaticDailyData with soil data
824             for (final ClimaticDailyData aClimaticData : data) {
825                 final SoilDailyData aSoilData = soilDataIterator.next();
826                 aClimaticData.setValue(Variable.WATER_RESERVE, aSoilData.getWaterReserve());
827                 aClimaticData.setValue(Variable.SOILWATERCONTENT, aSoilData.getSwc());
828             }
829         }
830         final List<String> missingVariables = resourceManager.getClimaticResource().getMissingVariables();
831         missingVariables.remove(Variable.WATER_RESERVE.getName());
832         missingVariables.remove(Variable.SOILWATERCONTENT.getName());
833         resourceManager.getClimaticResource().setMissingVariables(missingVariables);
834     }
835 
836     /**
837      * Ensure aggregation expression is set (when needed) and valid for the
838      * phases.
839      *
840      * @return check
841      */
842     public boolean isAggregationValid() {
843         if (getType() == EvaluationType.WITHOUT_AGGREGATION || getPhases().size() < 2) {
844             return true;
845         }
846         final Map<String, Double> values = new HashMap<>();
847         getPhases().forEach(phase -> values.put(phase.getId(), 1.));
848         try {
849             getAggregationFunction().aggregate(values);
850         } catch (final IndicatorsException ex) {
851             LOGGER.info("Invalid aggregation: {}", ex.getLocalizedMessage());
852             return false;
853         }
854         return true;
855     }
856 
857     /**
858      * @return the evaluation settings has no file path
859      */
860     public boolean isNew() {
861         return getSettings().getFilePath() == null;
862     }
863 
864     /**
865      * @param fire fire events while checking
866      * @return the evaluation is not fully defined or with errors
867      */
868     public boolean isOnErrorOrIncomplete(final boolean fire) {
869         final boolean hasClimaticIndicator = containsClimaticIndicator(fire);
870         final boolean isToAggregate = isAggregationMissing(fire);
871         final boolean isComputable = isComputable();
872 
873         return !hasClimaticIndicator || isToAggregate || !isComputable;
874     }
875 
876     /**
877      * Check if period matches the stage list.
878      *
879      * @param firstId
880      *            stage id of period start
881      * @param endId
882      *            stage id of period end
883      * @return presence
884      */
885     protected boolean isStagePresent(final String firstId, final String endId) {
886         LOGGER.trace("firstId: {}, endId: {}", firstId, endId);
887         boolean result = false;
888         for (final Indicator i : getIndicators()) {
889             final Indicator firstIndicator = ((CompositeIndicator) i).getIndicators()
890                     .get(0);
891             LOGGER.trace("indicator id={}", i.getId());
892             LOGGER.trace("first child indicator id={}", firstIndicator.getId());
893             if (i.getId().equals(endId)
894                     && firstIndicator.getId().equals(firstId)) {
895                 result = true;
896                 break;
897             }
898         }
899         return result;
900     }
901 
902     /**
903      * Set parameters (id and attributes) for the indicator and its criteria
904      * from knowledge defined in settings.
905      */
906     public void setParametersFromKnowledge() {
907         if (getSettings() == null) {
908             throw new IllegalStateException("Settings are not set!");
909         }
910         if (getSettings().getKnowledge() == null) {
911             throw new IllegalStateException(
912                     "Knowledge is not set in settings!");
913         }
914         setParametersFromKnowledge(getSettings().getKnowledge());
915     }
916 
917     /**
918      * @param value
919      *            settings from XML
920      */
921     public void setSettings(final EvaluationSettings value) {
922         Objects.requireNonNull(value);
923         settings = value;
924         if (settings.getEvaluation().getAggregationFunction() != null) {
925             setAggregationFunction(settings.getEvaluation()
926                     .getAggregationFunction());
927         } else {
928             setAggregationFunction(new JEXLFunction());
929         }
930         setName("en", settings.getName());
931         setTimescale(settings.getTimescale());
932         resourceManager.setVariables(this.getVariables());
933     }
934 
935     /**
936      * Set Transcient evaluation value (fire indicator event).
937      * @param value
938      *            Flag for state Saved.
939      */
940     public void setTranscient(final boolean value) {
941         LOGGER.traceEntry();
942         this.setTranscient(value, false);
943     }
944     /**
945      * Set Transcient evaluation value with possibility to disable firing indicator event.<br>
946      * Default method is {@link #setTranscient(boolean)}
947      * @param value
948      * @param fromSave flag if is from save method ({@code false} : default value)
949      */
950     public void setTranscient(final boolean value, final boolean fromSave) {
951         LOGGER.traceEntry();
952         if (isTranscient == value) {
953             return;
954         }
955         this.isTranscient = value;
956         if (!fromSave) {
957             fireIndicatorEvent(IndicatorEvent.Type.CHANGE.event(this));
958         }
959     }
960 
961     /**
962      * @param type evaluation type
963      */
964     public void setType(@NonNull final EvaluationType type) {
965         Objects.requireNonNull(settings);
966         settings.setType(type);
967     }
968 
969     /**
970      * @return Structured string representation.
971      */
972     public String toStringTree() {
973         final StringBuilder sb = new StringBuilder();
974         getIndicators().forEach(phase -> sb.append(phase.toStringTree("")).append("\n"));
975         return sb.toString();
976     }
977 
978     /**
979      * Validate evaluation.
980      */
981     public void validate() {
982         state.onValidate(this);
983     }
984 
985 }