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()));
}
}