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