PotentialSowingDaysFrequency.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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import fr.inrae.agroclim.indicators.exception.IndicatorsException;
import fr.inrae.agroclim.indicators.model.Knowledge;
import fr.inrae.agroclim.indicators.model.Parameter;
import fr.inrae.agroclim.indicators.model.data.DailyData;
import fr.inrae.agroclim.indicators.model.data.Resource;
import fr.inrae.agroclim.indicators.model.data.Variable;
import fr.inrae.agroclim.indicators.model.data.climate.ClimaticDailyData;
import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
import fr.inrae.agroclim.indicators.util.Doublet;
import jakarta.xml.bind.annotation.XmlType;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

/**
 * % days matching sowing condition/period.
 *
 * Fréquence de jours où le semis est possible durant sow_period selon des
 * conditions sur la température et la teneur en eau du sol après le semis
 * (faisabilité écophysiologique) et selon la teneur en eau du sol le jour du
 * semis (faisabilité technique du semis).
 *
 * @author jcufi
 */
@XmlType(propOrder = {"nbDays", "rainThreshold",
        "soilWaterContentAtFieldCapacity", "soilWaterContentThreshold",
        "tmeanThreshold", "tminThreshold"})
@Log4j2
public final class PotentialSowingDaysFrequency extends SimpleIndicator implements Detailable {

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

    /**
     * Number of consecutive days after sowing during which conditions are
     * observed for sowing decision.
     */
    @Getter
    @Setter
    private int nbDays;

    /**
     * Maximal rain sum for emergence.
     */
    @Getter
    @Setter
    private double rainThreshold;

    /**
     * Maximal soil water content for emergence.
     */
    @Getter
    @Setter
    private double soilWaterContentAtFieldCapacity;

    /**
     * Minimum soil water content for emergence.
     */
    @Getter
    @Setter
    private double soilWaterContentThreshold;

    /**
     * Mean temperature for sowing (°C).
     */
    @Getter
    @Setter
    private double tmeanThreshold;

    /**
     * Minimal temperature for sowing (°C).
     */
    @Getter
    @Setter
    private double tminThreshold;

    /**
     * Constructor.
     */
    public PotentialSowingDaysFrequency() {
        super();
    }

    /**
     * @param index index of first day
     * @param res climatic data
     * @return sum of rain > rainThreshold
     */
    private boolean checkSumOfRain(final int index,
            final ClimaticResource res) {
        final List<ClimaticDailyData> data = res.getData();
        double sum = 0;
        for (int i = index; i <= index + nbDays; i++) {
            sum += data.get(i).getValue(Variable.RAIN);
        }
        return sum >= rainThreshold;
    }

    /**
     * @param index index of first day
     * @param res climatic data
     * @return tmin <= tminThreshold && tmean <= tmeanThreshold
     */
    private boolean checkTminTmoy(final int index, final ClimaticResource res) {
        final List<ClimaticDailyData> data = res.getData();
        for (int i = index; i <= index + nbDays; i++) {
            final double tmin = data.get(i).getValue(Variable.TMIN);
            final double tmean = data.get(i).getValue(Variable.TMEAN);
            if (!(tmin > tminThreshold && tmean > tmeanThreshold)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public PotentialSowingDaysFrequency clone()
            throws CloneNotSupportedException {
        PotentialSowingDaysFrequency clone;
        clone = (PotentialSowingDaysFrequency) super.clone();
        clone.soilWaterContentThreshold = soilWaterContentThreshold;
        clone.soilWaterContentAtFieldCapacity = soilWaterContentAtFieldCapacity;
        clone.nbDays = nbDays;
        clone.tmeanThreshold = tmeanThreshold;
        clone.tminThreshold = tminThreshold;
        clone.rainThreshold = rainThreshold;
        return clone;
    }

    @Override
    public double computeSingleValue(final Resource<? extends DailyData> resource) throws IndicatorsException {
        Objects.requireNonNull(resource, "Resource must not be null!");
        if (!(resource instanceof ClimaticResource)) {
            throw new IllegalArgumentException(
                    getClass().getName()
                    + ".computeSingleValue() handles "
                    + "only ClimaticResource, not "
                    + resource.getClass().getName());
        }
        final ClimaticResource res = (ClimaticResource) resource;
        int cpt = 0;
        final List<ClimaticDailyData> data = res.getData();
        if (data.size() >= nbDays) {
            for (int index = 0; index < res.getData().size(); index++) {
                final double soilWaterContent = data.get(index)
                        .getValue(Variable.SOILWATERCONTENT);
                if (soilWaterContent >= soilWaterContentThreshold
                        && soilWaterContent < soilWaterContentAtFieldCapacity
                        && index + nbDays < data.size()
                        && checkTminTmoy(index, res)
                        && checkSumOfRain(index, res)) {
                    cpt += 1;
                }
            }
        }
        final double percent = 100.;
        return cpt * percent / data.size();
    }

    private Map<String, Number> getAttributeValues() {
        return Map.of(//
                "soilWaterContentThreshold", soilWaterContentThreshold, //
                "nbDays", nbDays, //
                "rainThreshold", rainThreshold, //
                "soilWaterContentAtFieldCapacity", soilWaterContentAtFieldCapacity, //
                "tmeanThreshold", tmeanThreshold, //
                "tminThreshold", tminThreshold
        );
    }

    @Override
    public List<Doublet<Parameter, Number>> getParameterDefaults() {
        if (getParameters() == null) {
            return List.of();
        }
        final List<Doublet<Parameter, Number>> val = new ArrayList<>();
        getAttributeValues().forEach((k, v) -> {
            final Optional<Parameter> found = getParameters().stream() //
                .filter(a -> k.equals(a.getAttribute())) //
                .findFirst();
            if (found.isPresent()) {
                val.add(Doublet.of(found.get(), v));
            }
        });
        return val;
    }

    /**
     * @return parameters (id and attribute) for the indicator.
     */
    @Override
    public List<Parameter> getParameters() {
        if (super.getParameters() != null) {
            return super.getParameters();
        } else {
            return List.of();
        }
    }

    @Override
    public Map<String, Double> getParametersValues() {
        final Map<String, Double> val = new HashMap<>();
        // if no substitution is defined
        if (getParameters() == null) {
            return val;
        }
        final Map<String, Number> attributeValues = getAttributeValues();
        for (final Parameter param : getParameters()) {
            if (param == null) {
                continue;
            }
            final Number value = attributeValues.get(param.getId());
            if (value != null) {
                val.put(param.getId(), value.doubleValue());
            }
        }
        return val;
    }

    @Override
    public Set<Variable> getVariables() {
        final Set<Variable> variables = new HashSet<>();
        variables.add(Variable.RAIN);
        variables.add(Variable.SOILWATERCONTENT);
        variables.add(Variable.TMEAN);
        variables.add(Variable.TMIN);
        return variables;
    }

    @Override
    public boolean isComputable(final Resource<? extends DailyData> resource) {
        if (resource == null) {
            throw new IllegalArgumentException("resource must no be null!");
        }
        if (!(resource instanceof ClimaticResource)) {
            throw new IllegalArgumentException(getClass().getName()
                    + " handles only ClimaticResource, not "
                    + resource.getClass().getName());
        }
        return true;
    }

    @Override
    public void setParametersFromKnowledge(final Knowledge knowledge) {
        final Indicator indicator = knowledge.getIndicator(getId());
        setParameters(indicator.getParameters());
    }

    @Override
    public void setParametersValues(final Map<String, Double> values) {
        // if no substitution is defined
        if (getParameters() == null) {
            return;
        }
        getParameters().forEach(param -> {
            final String id = param.getId();
            if (values.containsKey(id)) {
                switch (param.getAttribute()) {
                case "soilWaterContentThreshold":
                    soilWaterContentThreshold = values.get(id);
                    break;
                case "soilWaterContentAtFieldCapacity":
                    soilWaterContentAtFieldCapacity = values.get(id);
                    break;
                case "nbDays":
                    if (values.get(id) != null) {
                        nbDays = values.get(id).intValue();
                    }
                    break;
                case "tmeanThreshold":
                    tmeanThreshold = values.get(id);
                    break;
                case "tminThreshold":
                    tminThreshold = values.get(id);
                    break;
                case "rainThreshold":
                    rainThreshold = values.get(id);
                    break;
                default:
                    break;
                }
            }
        });
    }

    @Override
    public String toStringTree(final String indent) {
        return toStringTreeBase(indent);
    }

    @Override
    public void removeParameter(final Parameter param) {
        if (getParameters() != null) {
            getParameters().remove(param);
        }
    }

}