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