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.indicator;
18  
19  import java.util.ArrayList;
20  import java.util.Comparator;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.stream.Collectors;
27  
28  import fr.inrae.agroclim.indicators.exception.IndicatorsException;
29  import fr.inrae.agroclim.indicators.exception.type.ComputationErrorType;
30  import fr.inrae.agroclim.indicators.model.Evaluation;
31  import fr.inrae.agroclim.indicators.model.EvaluationType;
32  import fr.inrae.agroclim.indicators.model.Knowledge;
33  import fr.inrae.agroclim.indicators.model.LocalizedString;
34  import fr.inrae.agroclim.indicators.model.Parameter;
35  import fr.inrae.agroclim.indicators.model.data.DailyData;
36  import fr.inrae.agroclim.indicators.model.data.Data;
37  import fr.inrae.agroclim.indicators.model.data.DataLoadingListener;
38  import fr.inrae.agroclim.indicators.model.data.DataLoadingListener.DataFile;
39  import fr.inrae.agroclim.indicators.model.data.DataLoadingListenerHandler;
40  import fr.inrae.agroclim.indicators.model.data.HasDataLoadingListener;
41  import fr.inrae.agroclim.indicators.model.data.Resource;
42  import fr.inrae.agroclim.indicators.model.data.Variable;
43  import fr.inrae.agroclim.indicators.model.function.aggregation.AggregationFunction;
44  import fr.inrae.agroclim.indicators.model.function.aggregation.JEXLFunction;
45  import fr.inrae.agroclim.indicators.model.function.listener.AggregationFunctionListener;
46  import fr.inrae.agroclim.indicators.model.function.normalization.Exponential;
47  import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorEvent;
48  import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorListener;
49  import fr.inrae.agroclim.indicators.util.Doublet;
50  import jakarta.xml.bind.annotation.XmlElement;
51  import jakarta.xml.bind.annotation.XmlRootElement;
52  import jakarta.xml.bind.annotation.XmlTransient;
53  import jakarta.xml.bind.annotation.XmlType;
54  import lombok.EqualsAndHashCode;
55  import lombok.Getter;
56  import lombok.Setter;
57  import lombok.extern.log4j.Log4j2;
58  
59  /**
60   * Composite indicator has list of indicators.
61   *
62   * Last date $Date$
63   *
64   * @author $Author$
65   * @version $Revision$
66   */
67  @XmlRootElement
68  @XmlType(propOrder = {"tag", "aggregationFunction", "indicators"})
69  @EqualsAndHashCode(
70          callSuper = true,
71          of = {"aggregationFunction", "indicators", "tag"}
72          )
73  @Log4j2
74  public class CompositeIndicator extends Indicator
75  implements DataLoadingListener, Detailable, HasDataLoadingListener, Comparable<Indicator> {
76  
77      /**
78       * UUID for Serializable.
79       */
80      private static final long serialVersionUID = 6030595237342422003L;
81      /**
82       * @param start id of start stage (eg.: s0)
83       * @param end id of end stage (eg.: s1)
84       * @return phase as a CompositeIndicator
85       */
86      public static CompositeIndicator createPhase(final String start,
87              final String end) {
88          final String langCode = "en";
89          final Indicator startStage = new CompositeIndicator();
90          startStage.setId("pheno_" + start);
91          startStage.setName(langCode, start);
92          startStage.setIndicatorCategory(IndicatorCategory.PHENO_PHASES);
93          final CompositeIndicator phase = new CompositeIndicator();
94          phase.setId(start + end);
95          phase.setName(langCode, end);
96          phase.setIndicatorCategory(IndicatorCategory.PHENO_PHASES);
97          phase.add(startStage);
98          // by default, set fake aggregation
99          final JEXLFunction jexl = new JEXLFunction();
100         jexl.setExpression("0.0d");
101         phase.setAggregationFunction(jexl);
102         return phase;
103     }
104 
105     /**
106      * Function to aggregate values from indicator list.
107      */
108     @XmlElement
109     @Getter
110     @Setter
111     private AggregationFunction aggregationFunction;
112 
113     /**
114      * Handler for data loading listeners.
115      */
116     @XmlTransient
117     private final DataLoadingListenerHandler dataLoadingListenerHandler;
118 
119     /**
120      * Indicator list componing the composite indicator.
121      */
122     @XmlElement(name = "indicator")
123     @Getter
124     private List<Indicator> indicators;
125 
126     /**
127      * Tag of phase.
128      */
129     @XmlElement
130     @Getter
131     @Setter
132     private String tag;
133 
134     /**
135      * Constructor.
136      */
137     public CompositeIndicator() {
138         super();
139         indicators = new ArrayList<>();
140         dataLoadingListenerHandler = new DataLoadingListenerHandler(
141                 getListeners());
142     }
143 
144 
145     /**
146      * Constructor.
147      *
148      * @param c indicator to clone
149      */
150     public CompositeIndicator(final CompositeIndicator c) {
151         this();
152         setId(c.getId());
153         try {
154             if (c.getNames() != null) {
155                 setNames(new ArrayList<>());
156                 for (final LocalizedString name : c.getNames()) {
157                     getNames().add(name.clone());
158                 }
159             }
160             if (c.getNormalizationFunction() != null) {
161                 setNormalizationFunction(c.getNormalizationFunction().clone());
162             } else {
163                 setNormalizationFunction(new Exponential());
164             }
165         } catch (final CloneNotSupportedException ex) {
166             LOGGER.catching(ex);
167         }
168         this.setCategory(c.getCategory());
169         this.setParent(c.getParent());
170         final ArrayList<Indicator> newIndicatorlist = new ArrayList<>();
171         c.getIndicators().forEach(i -> {
172             try {
173                 newIndicatorlist.add(i.clone());
174             } catch (final CloneNotSupportedException ex) {
175                 LOGGER.fatal("should never occurs as "
176                         + "indicator must implement clone()", ex);
177             }
178         });
179         this.setIndicators(newIndicatorlist);
180         this.setAggregationFunction(c.getAggregationFunction());
181         this.setNormalizationFunction(c.getNormalizationFunction());
182         this.setValue(c.getValue());
183         this.tag = c.tag;
184         this.setColor(c.getColor());
185         this.setNotNormalizedValue(c.getNotNormalizedValue());
186     }
187 
188     /**
189      * @param i
190      *            phase to add.
191      */
192     public final void add(final Indicator i) {
193         i.setParent(this);
194         indicators.add(i);
195         if (getAggregationFunction() != null) {
196             return;
197         }
198         /* Si il ne s'agit pas de l'évaluation */
199         if (isAggregationNeeded()) {
200             setAggregationFunction(new JEXLFunction());
201             fireAggregationFunctionUpdated();
202         }
203     }
204 
205     @Override
206     public final void addDataLoadingListener(final DataLoadingListener l) {
207         dataLoadingListenerHandler.addDataLoadingListener(l);
208     }
209 
210     @Override
211     public final void addDataLoadingListeners(final DataLoadingListener[] ls) {
212         dataLoadingListenerHandler.addDataLoadingListeners(ls);
213     }
214 
215     /**
216      * @param listener
217      *            listener for aggregation function changes
218      */
219     public final void addFunctionListener(final AggregationFunctionListener listener) {
220         getListeners().add(AggregationFunctionListener.class, listener);
221     }
222 
223     /**
224      * Remove all agregation function listeners.
225      */
226     public final void clearFunctionListener() {
227         for (final AggregationFunctionListener listener
228                 : getAggregationFunctionListeners()) {
229             getListeners().remove(AggregationFunctionListener.class, listener);
230         }
231     }
232 
233     /**
234      * Clear phase list.
235      */
236     public final void clearIndicators() {
237         indicators.clear();
238     }
239 
240     @Override
241     @SuppressWarnings("checkstyle:DesignForExtension")
242     public CompositeIndicator clone() {
243         return new CompositeIndicator(this);
244     }
245 
246     @Override
247     public final int compareTo(final Indicator o) {
248         final Comparator<Indicator> comparator
249         = Comparator.comparing(Indicator::getName,
250                 Comparator.nullsFirst(Comparator.naturalOrder()));
251         return comparator.compare(this, o);
252     }
253 
254     @Override
255     public final Double compute(final Resource<? extends DailyData> climResource) throws IndicatorsException {
256         if (climResource.getYears().isEmpty()) {
257             throw new RuntimeException(
258                     String.format(
259                             "No years in ClimaticResource (%d dailyData)!",
260                             climResource.getData().size()));
261         }
262         final Map<String, Double> results = new HashMap<>();
263         double valueAfterAggregation = 0;
264         Double valueAfterNormalization;
265 
266         for (final Indicator indicator : indicators) {
267             // isPhase() ?
268             if (IndicatorCategory.PHENO_PHASES.getTag().equals(
269                     indicator.getCategory())) {
270                 // On ignore l'indicateur s'il s'agit du stade final de la phase
271                 indicator.setValue(null);
272                 indicator.setNotNormalizedValue(null);
273                 continue;
274             }
275 
276             try {
277                 final Double value = indicator.compute(climResource);
278                 results.put(indicator.getId(), value);
279             } catch (final IndicatorsException e) {
280                 throw new IndicatorsException(ComputationErrorType.COMPOSITE_COMPUTATION, e, indicator.getId());
281             }
282         }
283         if (isAggregationNeeded()) {
284             if (aggregationFunction != null) {
285                 valueAfterAggregation = aggregationFunction.aggregate(results);
286             } else {
287                 LOGGER.error("No aggregation function defined for {} in evaluation of type {}", getId(), getType());
288             }
289         } else if (EvaluationType.WITHOUT_AGGREGATION != getType()) {
290             if (results.keySet().isEmpty()) {
291                 LOGGER.trace("No result for indicator {}", getId());
292             } else {
293                 valueAfterAggregation = results.values().iterator().next();
294             }
295         }
296 
297         // no normalization for simple evaluation
298         if (EvaluationType.WITHOUT_AGGREGATION != getType() && getNormalizationFunction() != null) {
299             if (getCategory() == null) {
300                 if (!getCategory().equals(
301                         IndicatorCategory.CLIMATIC_EFFECTS.getTag())) {
302                     valueAfterNormalization = getNormalizationFunction()
303                             .normalize(valueAfterAggregation);
304                 } else {
305                     valueAfterNormalization = valueAfterAggregation;
306                 }
307             } else {
308                 valueAfterNormalization = getNormalizationFunction().normalize(valueAfterAggregation);
309             }
310         } else {
311             valueAfterNormalization = valueAfterAggregation;
312         }
313         setNotNormalizedValue(valueAfterAggregation);
314         setValue(valueAfterNormalization);
315         return valueAfterNormalization;
316     }
317 
318     /**
319      * @return true if at least one of the composed indicators is climatic
320      */
321     @SuppressWarnings("checkstyle:DesignForExtension")
322     public boolean containsClimaticIndicator() {
323         boolean contains = false;
324         for (final Indicator indicator : getIndicators()) {
325             final String cat = indicator.getCategory();
326             if (indicator instanceof final CompositeIndicator compositeIndicator
327                     && !IndicatorCategory.PHENO_PHASES.getTag().equals(cat)) {
328                 contains = compositeIndicator.containsClimaticIndicator();
329                 if (!contains) {
330                     break;
331                 }
332             } else if (IndicatorCategory.INDICATORS.getTag().equals(cat)) {
333                 contains = true;
334                 break;
335             }
336         }
337         return contains;
338     }
339 
340     /**
341      * This implementation raises functionAdded event to the
342      * AggregationFunctionListener of the composite indicator.
343      */
344     public final void fireAggregationFunctionUpdated() {
345         for (final AggregationFunctionListener a
346                 : getAggregationFunctionListeners()) {
347             a.onFunctionAdded(this);
348         }
349     }
350 
351     @Override
352     public final void fireDataLoadingAddEvent(final Data data) {
353         dataLoadingListenerHandler.fireDataLoadingAddEvent(data);
354     }
355 
356     @Override
357     public final void fireDataLoadingEndEvent(final String text) {
358         dataLoadingListenerHandler.fireDataLoadingEndEvent(text);
359     }
360 
361     @Override
362     public final void fireDataLoadingStartEvent(final String text) {
363         dataLoadingListenerHandler.fireDataLoadingStartEvent(text);
364     }
365 
366     @Override
367     public void fireDataSetEvent(final DataFile dataFile) {
368         // do nothing
369     }
370 
371     @Override
372     public final void fireValueUpdated() {
373         getIndicators().forEach(Indicator::fireValueUpdated);
374         for (final IndicatorListener l
375                 : getListeners().getListeners(IndicatorListener.class)) {
376             l.onIndicatorEvent(
377                     IndicatorEvent.Type.UPDATED_VALUE.event(this));
378         }
379     }
380 
381     /**
382      * @return listeners for aggregation function
383      */
384     private AggregationFunctionListener[] getAggregationFunctionListeners() {
385         return getListeners().getListeners(AggregationFunctionListener.class);
386     }
387 
388     @Override
389     public final DataLoadingListener[] getDataLoadingListeners() {
390         return dataLoadingListenerHandler.getDataLoadingListeners();
391     }
392 
393     /**
394      * @return The first indicator of the list.
395      */
396     public final Indicator getFirstIndicator() {
397         if (getIndicators() == null || getIndicators().isEmpty()) {
398             return null;
399         }
400         return getIndicators().iterator().next();
401     }
402 
403     @Override
404     public final List<Doublet<Parameter, Number>> getParameterDefaults() {
405         final List<Doublet<Parameter, Number>> val = new ArrayList<>();
406         indicators.forEach(i -> val.addAll(i.getParameterDefaults()));
407         return val;
408     }
409 
410     @Override
411     public final List<Parameter> getParameters() {
412         return indicators.stream()
413                 .flatMap(i -> i.getParameters().stream())
414                 .distinct()
415                 .collect(Collectors.toList());
416     }
417 
418     @Override
419     public final Map<String, Double> getParametersValues() {
420         final Map<String, Double> val = new HashMap<>();
421         indicators.forEach(indicator ->
422         indicator.getParametersValues().forEach((id, value) -> {
423             if (!val.containsKey(id)) {
424                 val.put(id, value);
425             }
426         })
427                 );
428         return val;
429     }
430 
431     /**
432      * @return Evaluation type.
433      */
434     @Override
435     public final EvaluationType getType() {
436         Evaluation evaluation = null;
437         if (this instanceof final Evaluation eval) {
438             evaluation = eval;
439         } else {
440             if (getParent() == null) {
441                 return null;
442             }
443             CompositeIndicator p = (CompositeIndicator) getParent();
444             while (p != null) {
445                 if (p instanceof final Evaluation eval) {
446                     evaluation = eval;
447                     break;
448                 }
449                 p = (CompositeIndicator) p.getParent();
450             }
451         }
452         if (evaluation == null) {
453             return null;
454         }
455         if (evaluation.getSettings() == null) {
456             return null;
457         }
458         return evaluation.getSettings().getType();
459     }
460 
461     @Override
462     @SuppressWarnings("checkstyle:DesignForExtension")
463     public Set<Variable> getVariables() {
464         final Set<Variable> variables = new HashSet<>();
465         getIndicators().forEach(indicator -> variables.addAll(indicator.getVariables()));
466         return variables;
467     }
468 
469     /**
470      * Set parent of indicators.
471      */
472     public void initializeParent() {
473         getIndicators().stream().map(ind -> {
474             ind.setParent(this);
475             return ind;
476         })
477         .filter(CompositeIndicator.class::isInstance)
478         .forEach(ind -> ((CompositeIndicator) ind).initializeParent());
479     }
480 
481     /**
482      * Detect if aggregation function is needed but missing.
483      *
484      * @param fire fire events while checking
485      * @return true if aggregation function is needed but missing
486      */
487     public final boolean isAggregationMissing(final boolean fire) {
488         if (getType() == EvaluationType.WITHOUT_AGGREGATION) {
489             return false;
490         }
491         boolean isMissing = false;
492         final AggregationFunction aggregation = getAggregationFunction();
493         if (isAggregationNeeded()
494                 && (aggregation == null || !aggregation.isValid())) {
495             if (fire) {
496                 fireIndicatorEvent(IndicatorEvent.Type.AGGREGATION_MISSING
497                         .event(this));
498             }
499             isMissing = true;
500         }
501 
502         for (final Indicator indicator : getIndicators()) {
503             if (indicator instanceof final CompositeIndicator compositeIndicator
504                     && compositeIndicator.isAggregationMissing(fire)) {
505                 isMissing = true;
506             }
507         }
508         return isMissing;
509     }
510 
511     /**
512      * @return if aggregation is needed, according to category and number of
513      * composed indicators
514      */
515     private boolean isAggregationNeeded() {
516         if (getType() == EvaluationType.WITHOUT_AGGREGATION) {
517             return false;
518         }
519         final int minimum;
520         // evaluation
521         if (getCategory() == null || !isPhase()) {
522             minimum = 1;
523         } else {
524             /*
525              * Cas d'une phase phénologique : le 1er indicateur correspond au
526              * stade phénologique de fin
527              */
528             minimum = 2;
529         }
530         return indicators.size() > minimum;
531     }
532 
533     @Override
534     public final boolean isComputable() {
535         boolean isComputable = true;
536 
537         for (final Indicator indicator : getIndicators()) {
538             if (!indicator.isComputable()) {
539                 isComputable = false;
540                 fireIndicatorEvent(
541                         IndicatorEvent.Type.NOT_COMPUTABLE.event(indicator));
542             }
543         }
544 
545         return isComputable;
546     }
547 
548     @Override
549     public final boolean isComputable(
550             final Resource<? extends DailyData> data) {
551         return true;
552     }
553 
554     /**
555      * Phases are categorized as CULTURAL_PRACTICES or
556      * ECOPHYSIOLOGICAL_PROCESSES in GETARI after edition.
557      *
558      * @return if it is a phase.
559      */
560     public final boolean isPhase() {
561         return IndicatorCategory.PHENO_PHASES.equals(getIndicatorCategory())
562                 || getTag() != null && getTag().startsWith("pheno-");
563     }
564 
565     /**
566      * @param id
567      *            id of indicator to check
568      * @return indicator of id is present
569      */
570     public final boolean isPresent(final String id) {
571         boolean result = false;
572         for (final Indicator i : indicators) {
573             if (i.getId() == null) {
574                 throw new RuntimeException("Indicator id is null for " + i);
575             }
576             if (i.getId().equals(id)) {
577                 result = true;
578                 break;
579             }
580         }
581         return result;
582     }
583 
584     @Override
585     public final void onDataLoadingAdd(final Data data) {
586         fireDataLoadingAddEvent(data);
587     }
588 
589     @Override
590     public final void onDataLoadingEnd(final String text) {
591         fireDataLoadingEndEvent(text);
592     }
593 
594     @Override
595     public final void onDataLoadingStart(final String text) {
596         fireDataLoadingStartEvent(text);
597     }
598 
599     @Override
600     public void onFileSet(final DataFile dataFile) {
601         // do nothing
602     }
603 
604     /**
605      * @param i
606      *            indicator to remove
607      * @return if this list contained the specified indicator
608      */
609     public final boolean remove(final Indicator i) {
610         boolean result;
611         result = indicators.remove(i);
612         // Pour une phase, si il s'agit du second appel (result = false), on ne fait rien
613         if (!result && i.getIndicatorCategory() == IndicatorCategory.PHENO_PHASES) {
614             return false;
615         }
616         if (!isAggregationNeeded() && aggregationFunction != null) {
617             setAggregationFunction(null);
618             fireAggregationFunctionUpdated();
619         }
620         fireIndicatorEvent(IndicatorEvent.Type.REMOVE.event(i));
621         isAggregationMissing(true);
622         if (!containsClimaticIndicator()) {
623             /* Ne contient pas d'indicateur climatique */
624             fireIndicatorEvent(
625                     IndicatorEvent.Type.CLIMATIC_MISSING.event(this));
626         }
627         return result;
628     }
629 
630     @Override
631     public final void removeParameter(final Parameter param) {
632         if (getIndicators() != null) {
633             getIndicators().forEach(i -> i.removeParameter(param));
634         }
635     }
636 
637     /**
638      * @param children children indicators
639      */
640     public final void setIndicators(final List<Indicator> children) {
641         this.indicators = new ArrayList<>(children);
642     }
643 
644     @Override
645     public final void setParametersFromKnowledge(final Knowledge knowledge) {
646         getIndicators().forEach(i -> i.setParametersFromKnowledge(knowledge));
647     }
648 
649     @Override
650     public final void setParametersValues(final Map<String, Double> values) {
651         getIndicators().forEach(i -> i.setParametersValues(values));
652     }
653 
654     @Override
655     public final String toStringTree(final String indent) {
656         final StringBuilder sb = new StringBuilder();
657         sb.append(toStringTreeBase(indent));
658 
659         if (aggregationFunction != null) {
660             sb.append(indent).append("  aggregation: ")
661             .append(aggregationFunction.toString()).append("\n");
662         }
663         getIndicators().forEach(indicator -> {
664             sb.append(indent).append("  indicator:\n");
665             sb.append(indicator.toStringTree(indent + "  "));
666         });
667         return sb.toString();
668     }
669 
670 }