Indicator.java

/*
 * This file is part of Indicators.
 *
 * Indicators is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Indicators is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indicators. If not, see <https://www.gnu.org/licenses/>.
 */
package fr.inrae.agroclim.indicators.model.indicator;

import java.io.Serializable;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;

import javax.swing.event.EventListenerList;

import fr.inrae.agroclim.indicators.model.Computable;
import fr.inrae.agroclim.indicators.model.EvaluationType;
import fr.inrae.agroclim.indicators.model.HasParameters;
import fr.inrae.agroclim.indicators.model.Knowledge;
import fr.inrae.agroclim.indicators.model.LocalizedString;
import fr.inrae.agroclim.indicators.model.Nameable;
import fr.inrae.agroclim.indicators.model.Note;
import fr.inrae.agroclim.indicators.model.Parameter;
import fr.inrae.agroclim.indicators.model.Quantifiable;
import fr.inrae.agroclim.indicators.model.TimeScale;
import fr.inrae.agroclim.indicators.model.Unit;
import fr.inrae.agroclim.indicators.model.data.UseVariables;
import fr.inrae.agroclim.indicators.model.function.normalization.NormalizationFunction;
import fr.inrae.agroclim.indicators.model.indicator.listener.HasIndicatorListener;
import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorEvent;
import fr.inrae.agroclim.indicators.model.indicator.listener.IndicatorListener;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlElementWrapper;
import jakarta.xml.bind.annotation.XmlIDREF;
import jakarta.xml.bind.annotation.XmlSeeAlso;
import jakarta.xml.bind.annotation.XmlTransient;
import jakarta.xml.bind.annotation.XmlType;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

/**
 * Ancestor of all indicators.
 *
 * Last changed : $Date$
 *
 * @author jucufi
 * @author $Author$
 * @version $Revision$
 */
@EqualsAndHashCode(
        callSuper = false,
        of = {"category", "id", "timescale", "parent"}
        )
@Log4j2
@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({SimpleIndicator.class, CompositeIndicator.class})
@XmlType(propOrder = {"descriptions", "names", "id", "category", "color",
        "timescale", "normalizationFunction", "parameters", "notes", "unit"})
public abstract class Indicator implements Cloneable,
Computable, HasIndicatorListener, HasParameters, Nameable, Quantifiable,
Serializable, UseVariables {

    /**
     * UUID for Serializable.
     */
    private static final long serialVersionUID = 6030595237342422007L;

    /**
     * Tag of IndicatorCategory.
     *
     * Category (level) of indicator (evaluation ⮕ simple climatic indicator).
     */
    @XmlElement
    @Getter
    @Setter
    private String category;

    /**
     * Color for the whole phase in the Getari graph.
     */
    @XmlElement
    @Getter
    @Setter
    private String color;

    /**
     * Localized descriptions.
     */
    @XmlElement(name = "description")
    @Getter
    @Setter
    private List<LocalizedString> descriptions;

    /**
     * String ID.
     */
    @XmlElement(required = true)
    @Getter
    @Setter
    private String id;

    /**
     * If the indicator is computable with the resource (needed variable,
     * instance properties, ...).
     */
    @XmlTransient
    @Getter
    @Setter
    private boolean isComputable = true;

    /**
     * List for all listeners.
     */
    @XmlTransient
    @Getter(AccessLevel.PROTECTED)
    private final EventListenerList listeners = new EventListenerList();

    /**
     * Localized names.
     */
    @XmlElement(name = "name")
    @Getter(AccessLevel.PUBLIC)
    @Setter
    private List<LocalizedString> names;

    /**
     * Function to normalize between 0 and 1.
     */
    @XmlElement
    @Getter
    @Setter
    private NormalizationFunction normalizationFunction;

    /**
     * Raw value before applying normalization function.
     */
    @XmlTransient
    @Getter
    @Setter
    private Double notNormalizedValue;

    /**
     * Parameters for indicator.
     */
    @XmlElementWrapper(name = "parameters")
    @XmlElement(name = "parameter")
    @Getter
    @Setter
    private List<Parameter> parameters;

    /**
     * Parent indicator (composite or phase or evaluation).
     */
    @XmlTransient
    @Getter
    @Setter
    private Indicator parent;

    /**
     * Timescale of climatic data.
     */
    @Getter
    @Setter
    private TimeScale timescale = TimeScale.DAILY;

    /**
     * Normalized value.
     */
    @XmlTransient
    @Getter
    @Setter
    private Double value;

    /**
     * References of notes indicator.
     */
    @XmlElement(name = "note")
    @Getter
    @Setter
    @XmlIDREF
    private List<Note> notes;

    /**
     * Measurement unit of the indicator, defined for the case of climatic variables with common units.
     *
     * Only set for the indicators provided in Knowledge.
     */
    @XmlElement(name = "unit")
    @Getter
    @Setter
    @XmlIDREF
    private Unit unit;

    /**
     * Constructor.
     */
    protected Indicator() {
    }

    @Override
    public final void addIndicatorListener(final IndicatorListener listener) {
        if (getIndicatorListeners() != null) {
            for (final IndicatorListener l : getIndicatorListeners()) {
                if (listener.equals(l)) {
                    return;
                }
            }
        }
        listeners.add(IndicatorListener.class, listener);
        if (this instanceof CompositeIndicator compositeIndicator) {
            compositeIndicator.getIndicators()
            .forEach(i -> i.addIndicatorListener(listener));
        }
    }

    @Override
    @SuppressWarnings("checkstyle:DesignForExtension")
    public Indicator clone() throws CloneNotSupportedException {
        final Indicator clone = (Indicator) super.clone();
        clone.category = category;
        clone.color = color;
        if (descriptions != null) {
            clone.descriptions = new ArrayList<>();
            for (final LocalizedString description : descriptions) {
                clone.descriptions.add(description.clone());
            }
        }
        clone.id = id;
        // Do not clone indicatorValueListener
        clone.isComputable = isComputable;
        if (names != null) {
            clone.names = new ArrayList<>();
            for (final LocalizedString name : names) {
                clone.names.add(name.clone());
            }
        }
        if (normalizationFunction != null) {
            clone.normalizationFunction = normalizationFunction.clone();
        }
        clone.notNormalizedValue = notNormalizedValue;
        if (parameters != null) {
            clone.parameters = new ArrayList<>();
            for (final Parameter param : parameters) {
                clone.parameters.add(param.clone());
            }
        }
        // do not clone parent or loop on clone...
        clone.parent = parent;
        clone.value = value;
        return clone;
    }

    @Override
    public final void fireIndicatorEvent(final IndicatorEvent event) {
        LOGGER.traceEntry("{} : {} {}", getId(), event.getSource().getId(),
                event.getAssociatedType());
        if (getIndicatorListeners() == null
                || getIndicatorListeners().length == 0) {
            if (getParent() != null) {
                getParent().fireIndicatorEvent(event);
            }
            return;
        }
        for (final IndicatorListener listener : getIndicatorListeners()) {
            listener.onIndicatorEvent(event);
        }
    }

    /**
     * Notify listeners that indicator value has changed.
     */
    public abstract void fireValueUpdated();

    /**
     * @param languageCode lang code
     * @return description for the lang
     */
    public final String getDescription(@NonNull final String languageCode) {
        return LocalizedString.getString(descriptions, languageCode);
    }

    /**
     * @return indicator category according to tag
     */
    public final IndicatorCategory getIndicatorCategory() {
        return IndicatorCategory.getByTag(category);
    }

    @Override
    public final IndicatorListener[] getIndicatorListeners() {
        return listeners.getListeners(IndicatorListener.class);
    }

    @Override
    public final String getName() {
        final String langCode = Locale.getDefault().getLanguage();
        return getName(langCode);
    }

    @Override
    public final String getName(final Locale locale) {
        final String langCode = locale.getLanguage();
        return getName(langCode);
    }

    /**
     * @param languageCode lang code
     * @return name for the lang
     */
    public final String getName(@NonNull final String languageCode) {
        return LocalizedString.getString(names, languageCode);
    }

    /**
     * @return XML path of the indicator
     */
    public final String getPath() {
        if (parent != null) {
            return parent.getPath() + "/" + id;
        }
        return id;
    }

    /**
     * @param languageCode lang code
     * @return description for the lang where parameters are replaced and
     * formatted
     */
    public final String getPrettyDescription(
            @NonNull final String languageCode) {
        String description = getDescription(languageCode);
        final Locale locale = Locale.forLanguageTag(languageCode);
        final NumberFormat nf = NumberFormat.getInstance(locale);
        for (final Map.Entry<String, Double> entry : getParametersValues()
                .entrySet()) {
            description = description.replace(
                    "{" + entry.getKey() + "}",
                    nf.format(entry.getValue())
                    );
        }
        return description;
    }

    /**
     * @return Evaluation type.
     */
    public abstract EvaluationType getType();

    /**
     * @param indicatorCategory indicator category
     */
    public final void setIndicatorCategory(
            @NonNull final IndicatorCategory indicatorCategory) {
        category = indicatorCategory.getTag();
    }

    /**
     * @param languageCode lang code
     * @param name for the lang
     */
    public final void setName(
            @NonNull final String languageCode, final String name) {
        if (names == null) {
            names = new ArrayList<>();
        }
        for (final LocalizedString string : names) {
            if (Objects.equals(string.getLang(), languageCode)) {
                string.setValue(name);
                return;
            }
        }
        final LocalizedString string = new LocalizedString();
        string.setLang(languageCode);
        string.setValue(name);
        names.add(string);
    }

    /**
     * Set parameters (id and attributes) for the indicator and its criteria
     * from knowledge.
     *
     * @param knowledge reference definition of indicators
     */
    public abstract void setParametersFromKnowledge(Knowledge knowledge);

    @Override
    public final String toString() {
        return getName();
    }

    /**
     * @param indent indentation string
     * @return Structured string representation.
     */
    public abstract String toStringTree(String indent);

    /**
     * @param indent indentation string
     * @return Structured string representation.
     */
    protected final String toStringTreeBase(final String indent) {
        final StringBuilder sb = new StringBuilder();
        sb.append(indent).append("  id: ").append(getId()).append("\n");
        sb.append(indent).append("  name: ").append(getName()).append("\n");
        sb.append(indent).append("  class: ").append(getClass().getName())
        .append("\n");
        sb.append(indent).append("  category: ").append(category).append("\n");
        return sb.toString();
    }
}