ResourceManager.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.data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import fr.inrae.agroclim.indicators.exception.ErrorMessage;
import fr.inrae.agroclim.indicators.exception.type.ResourceErrorType;
import fr.inrae.agroclim.indicators.model.TimeScale;
import fr.inrae.agroclim.indicators.model.data.Variable.Type;
import fr.inrae.agroclim.indicators.model.data.climate.ClimaticResource;
import fr.inrae.agroclim.indicators.model.data.phenology.PhenologicalResource;
import fr.inrae.agroclim.indicators.util.DateUtils;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;

/**
 * The resource manager holds all climate, phenology and soilResource data to
 * compute indicators.
 *
 * Its responsibility is data storage and checking data consistency.
 *
 * Last changed : $Date: 2023-03-16 17:36:45 +0100 (jeu. 16 mars 2023) $
 *
 * @author $Author: omaury $
 * @version $Revision: 644 $
 */
@Log4j2
public final class ResourceManager implements Serializable, Cloneable {

    /**
     * UUID for Serializable.
     */
    private static final long serialVersionUID = 5645729254868967485L;
    /**
     * @param errors errors into add message
     * @param errorI18nKey Message key from .property resource.
     */
    private static void addErrorMessage(
            final Map<ResourceErrorType, ErrorMessage> errors,
            final ResourceErrorType errorI18nKey) {
        errors.put(errorI18nKey.getParent(), new ErrorMessage(
                "fr.inrae.agroclim.indicators.resources.messages",
                errorI18nKey, null));
    }

    /**
     * Storage of climatic daily data.
     */
    @Getter
    private final ClimaticResource climaticResource;

    /**
     * Number of development years for the crop.
     */
    @Setter
    @NonNull
    private Integer cropDevelopmentYears = 1;

    /**
     * Storage of phenological daily data.
     */
    @Getter
    private final PhenologicalResource phenologicalResource;

    /**
     * Time scale of evaluation.
     */
    @Setter
    private TimeScale timeScale;

    /**
     * Variable used to compute indicator.
     */
    @Setter
    private Set<Variable> variables;

    /**
     * Constructor.
     */
    public ResourceManager() {
        climaticResource = new ClimaticResource();
        phenologicalResource = new PhenologicalResource();
    }

    /**
     * Constructor for cloning.
     *
     * @param climatic
     *            climatic resource
     * @param phenological
     *            phenological resource
     */
    public ResourceManager(final ClimaticResource climatic,
            final PhenologicalResource phenological) {
        climaticResource = climatic;
        phenologicalResource = phenological;
    }

    @Override
    public ResourceManager clone() throws CloneNotSupportedException {
        return new ResourceManager(climaticResource.clone(), phenologicalResource.clone());
    }

    /**
     * @return consistency errors
     */
    public Map<ResourceErrorType, ErrorMessage> getConsistencyErrors() {
        final Map<ResourceErrorType, ErrorMessage> errors = new EnumMap<>(ResourceErrorType.class);
        // variables not set ?
        if (variables == null) {
            addErrorMessage(errors, ResourceErrorType.VARIABLES_MISSING);
            return errors;
        }
        if (variables.isEmpty()) {
            addErrorMessage(errors, ResourceErrorType.VARIABLES_EMPTY);
            return errors;
        }
        // empty climate
        final List<Integer> climaticYears = climaticResource.getYears();
        if (hasClimaticVariables() && climaticResource.getData().isEmpty()) {
            addErrorMessage(errors, ResourceErrorType.CLIMATE_EMPTY);
        } else {
            // missing days or hours
            final int nbClimatic = climaticResource.getData().size();
            int nb = 0;
            nb = climaticYears.stream()
                    .map(DateUtils::nbOfDays)
                    .reduce(nb, Integer::sum);
            if (timeScale == TimeScale.HOURLY) {
                nb = nb * DateUtils.NB_OF_HOURS_IN_DAY;
            }
            if (nbClimatic != nb) {
                addErrorMessage(errors, ResourceErrorType.CLIMATE_SIZE_WRONG);
            }
        }
        // empty phenology
        if (phenologicalResource.isEmpty()) {
            addErrorMessage(errors, ResourceErrorType.PHENO_EMPTY);
        }
        if (!errors.isEmpty()) {
            return errors;
        }
        // period
        final List<Integer> phenoYears = phenologicalResource.getYears();
        if (hasClimaticVariables() && climaticYears.isEmpty()) {
            addErrorMessage(errors, ResourceErrorType.CLIMATE_YEARS_EMPTY);
        }
        if (phenoYears.isEmpty()) {
            addErrorMessage(errors, ResourceErrorType.PHENO_YEARS_EMPTY);
        }
        if (!errors.isEmpty()) {
            return errors;
        }
        if (cropDevelopmentYears == null) {
            addErrorMessage(errors, ResourceErrorType.RESOURCES_CROPDEVELOPMENT_YEARS);
            return errors;
        }
        // Phenology data drives evaluation, so
        // - it's allowed to have more climate years that pheno years
        // - each pheno year must match a climate year
        final List<Integer> expectedClimateYears = new ArrayList<>(phenoYears);
        Collections.sort(expectedClimateYears);
        for (int i = 1; i < cropDevelopmentYears; i++) {
            LOGGER.trace("First year {} will not have phenological stages.", expectedClimateYears.get(0));
            expectedClimateYears.remove(0);
        }
        if (hasClimaticVariables()
                && !climaticYears.containsAll(expectedClimateYears)) {
            final Collection<Serializable> theMissing = new ArrayList<>(expectedClimateYears);
            theMissing.removeAll(climaticYears);
            final ErrorMessage error = new ErrorMessage(
                    "fr.inrae.agroclim.indicators.resources.messages",
                    ResourceErrorType.CLIMATE_YEARS_MISSING,
                    theMissing);
            errors.put(ResourceErrorType.CLIMATE_YEARS_MISSING.getParent(), error);
        }
        if (!errors.isEmpty()) {
            return errors;
        }
        // number of data (days)
        if (hasClimaticVariables() && hasSoilVariables()) {
            int nbDays = 0;
            for (final int year : climaticYears) {
                nbDays += DateUtils.nbOfDays(year);
            }
            final int nbSoil = climaticResource.getData().size();
            if (nbSoil != nbDays) {
                addErrorMessage(errors, ResourceErrorType.SOIL_SIZE_WRONG);
            }
        }
        if (errors.isEmpty()) {
            return null;
        } else {
            return errors;
        }
    }

    /**
     * @return at least one of the variables used to compute indicator is a
     *         climatic variable
     */
    public boolean hasClimaticVariables() {
        Objects.requireNonNull(variables, "variables is not set!");
        return variables.stream().anyMatch(variable -> variable != null && Type.CLIMATIC.equals(variable.getType()));
    }

    /**
     * @return at least one of the variables used to compute indicator is a soil
     *         variable
     */
    public boolean hasSoilVariables() {
        Objects.requireNonNull(variables, "variables is not set!");
        return variables.stream().anyMatch(variable -> variable != null && Type.SOIL.equals(variable.getType()));
    }

}