1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
61
62
63
64
65
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
79
80 private static final long serialVersionUID = 6030595237342422003L;
81
82
83
84
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
99 final JEXLFunction jexl = new JEXLFunction();
100 jexl.setExpression("0.0d");
101 phase.setAggregationFunction(jexl);
102 return phase;
103 }
104
105
106
107
108 @XmlElement
109 @Getter
110 @Setter
111 private AggregationFunction aggregationFunction;
112
113
114
115
116 @XmlTransient
117 private final DataLoadingListenerHandler dataLoadingListenerHandler;
118
119
120
121
122 @XmlElement(name = "indicator")
123 @Getter
124 private List<Indicator> indicators;
125
126
127
128
129 @XmlElement
130 @Getter
131 @Setter
132 private String tag;
133
134
135
136
137 public CompositeIndicator() {
138 super();
139 indicators = new ArrayList<>();
140 dataLoadingListenerHandler = new DataLoadingListenerHandler(
141 getListeners());
142 }
143
144
145
146
147
148
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
190
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
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
217
218
219 public final void addFunctionListener(final AggregationFunctionListener listener) {
220 getListeners().add(AggregationFunctionListener.class, listener);
221 }
222
223
224
225
226 public final void clearFunctionListener() {
227 for (final AggregationFunctionListener listener
228 : getAggregationFunctionListeners()) {
229 getListeners().remove(AggregationFunctionListener.class, listener);
230 }
231 }
232
233
234
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
268 if (IndicatorCategory.PHENO_PHASES.getTag().equals(
269 indicator.getCategory())) {
270
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
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
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
342
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
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
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
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
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
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
483
484
485
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
513
514
515 private boolean isAggregationNeeded() {
516 if (getType() == EvaluationType.WITHOUT_AGGREGATION) {
517 return false;
518 }
519 final int minimum;
520
521 if (getCategory() == null || !isPhase()) {
522 minimum = 1;
523 } else {
524
525
526
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
556
557
558
559
560 public final boolean isPhase() {
561 return IndicatorCategory.PHENO_PHASES.equals(getIndicatorCategory())
562 || getTag() != null && getTag().startsWith("pheno-");
563 }
564
565
566
567
568
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
602 }
603
604
605
606
607
608
609 public final boolean remove(final Indicator i) {
610 boolean result;
611 result = indicators.remove(i);
612
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
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
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 }