diff --git a/client/src/main/java/client/scenes/nutrition/NutritionPieChartCtrl.java b/client/src/main/java/client/scenes/nutrition/NutritionPieChartCtrl.java new file mode 100644 index 0000000..a90a677 --- /dev/null +++ b/client/src/main/java/client/scenes/nutrition/NutritionPieChartCtrl.java @@ -0,0 +1,234 @@ +package client.scenes.nutrition; + +import client.utils.LocaleAware; +import client.utils.LocaleManager; +import com.google.inject.Inject; +import commons.FormalIngredient; +import commons.Ingredient; +import commons.Recipe; +import commons.VagueIngredient; +import javafx.fxml.FXML; +import javafx.scene.chart.PieChart; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +public class NutritionPieChartCtrl implements LocaleAware { + /** + * Nutrition info for a recipe or an ingredient. + * + * @param protein The protein this recipe/ingredient has + * @param fat The fat this recipe/ingredient has + * @param carbs The carbs this recipe/ingredient has + */ + private record CompleteNutritionInfo( + double protein, + double fat, + double carbs + ) { + /** + * Create a new {@link CompleteNutritionInfo} with zeroed values. + * + * @return A {@link CompleteNutritionInfo} with zeroed values. + */ + static CompleteNutritionInfo zero() { + return new CompleteNutritionInfo(0.0, 0.0, 0.0); + } + + /** + * Check whether this instance has all values set to zero. + * + * @return Whether all values (protein, carbs, fat) are zero. + */ + public boolean isZero() { + return this.protein() == 0.0 + && this.carbs() == 0.0 + && this.fat() == 0.0; + } + + /** + * Scale this object by an amount. Multiplies all values by amount. + * + * @param amount The amount to scale it by. + * @return The newly scaled nutrition info. + */ + public CompleteNutritionInfo scaled(double amount) { + return new CompleteNutritionInfo( + this.protein() * amount, + this.fat() * amount, + this.carbs() * amount + ); + } + + /** + * Add another nutrition info object to this object. + * + * @param rhs The nutrition info object to add. + * @return A new nutrition info object with the sum of both objects' nutrients. + */ + public CompleteNutritionInfo add(CompleteNutritionInfo rhs) { + return new CompleteNutritionInfo( + this.protein() + rhs.protein(), + this.fat() + rhs.fat(), + this.carbs() + rhs.carbs() + ); + } + } + + private final LocaleManager localeManager; + private final Logger logger = Logger.getLogger(NutritionPieChartCtrl.class.getName()); + + @FXML + PieChart pieChart; + + private Recipe recipe; + + @Inject + NutritionPieChartCtrl( + LocaleManager manager + ) { + this.localeManager = manager; + } + + /** + * Get the data for this pie chart based on the current recipe. + *

+ * This accumulates all the nutrients (properly scaled) from all ingredients in this recipe + * and returns a summary with data points. + *

+ *

+ * The list will be of length 3 if any nutrients are added, or 0 if all the nutrients + * sum up to be 0. + *

+ * @return The data for this pie chart, as a labeled list of data. + */ + private List getPieChartData() { + CompleteNutritionInfo info = this.recipe + .getIngredients() + .stream() + .map(ri -> { + Ingredient i = ri.getIngredient(); + logger.info("Mapping ingredient " + i.toString()); + + switch (ri) { + case FormalIngredient fi -> { + return new CompleteNutritionInfo( + i.getProteinPer100g(), + i.getFatPer100g(), + i.getCarbsPer100g() + ) + .scaled(fi.getBaseAmount()); + } + case VagueIngredient vi -> { + return new CompleteNutritionInfo( + i.getProteinPer100g(), + i.getFatPer100g(), + i.getCarbsPer100g() + ) + .scaled(vi.getBaseAmount()); + } + default -> { + return null; + } + } + }) + .filter(Objects::nonNull) + .reduce(CompleteNutritionInfo::add) + .orElseGet(CompleteNutritionInfo::zero); + + this.logger.info( "Updated data: " + info.toString() ); + + if (info.isZero()) { + return List.of(); + } + + return List.of( + new PieChart.Data( + this.getLocaleString("menu.nutrition.protein"), + info.protein() + ), + new PieChart.Data( + this.getLocaleString("menu.nutrition.fat"), + info.fat() + ), + new PieChart.Data( + this.getLocaleString("menu.nutrition.carbs"), + info.carbs() + ) + ); + } + + /** + * Set the current recipe to be displayed in the pie chart. + * The pie chart will be refreshed. + *

+ * The pie chart will disappear if all the nutrients in the recipe + * are zero. + *

+ * @param recipe The recipe to display. + */ + public void setRecipe(Recipe recipe) { + this.recipe = recipe; + this.refresh(); + } + + @Override + public void updateText() { + List data = this.pieChart.getData(); + if (data.isEmpty()) return; + final int EXPECTED_DATA_SIZE = 3; + if (data.size() != EXPECTED_DATA_SIZE) return; + + data.get(0).setName(this.getLocaleString("menu.nutrition.protein")); + data.get(1).setName(this.getLocaleString("menu.nutrition.fat")); + final int TWO = 2; + data.get(TWO).setName(this.getLocaleString("menu.nutrition.carbs")); + } + + @Override + public LocaleManager getLocaleManager() { + return this.localeManager; + } + + /** + * Refresh the data in this pie chart. + */ + @SuppressWarnings("checkstyle:MagicNumber") + public void refresh() { + if (this.recipe == null) { + this.pieChart.setVisible(false); + logger.info("Refreshing pie chart with no recipe"); + return; + } + + logger.info("Refreshing pie chart with recipe"); + this.pieChart.setVisible(true); + if (this.pieChart.getData().isEmpty()) { + this.pieChart.getData().setAll( + this.getPieChartData() + ); + } else { + List fresh = this.getPieChartData(); + List data = this.pieChart.getData(); + + if (fresh.isEmpty()) { + data.clear(); + return; + } + + data.get(0).setPieValue(fresh.get(0).getPieValue()); + data.get(1).setPieValue(fresh.get(1).getPieValue()); + final int TWO = 2; + data.get(TWO).setPieValue(fresh.get(TWO).getPieValue()); + } + } + + public void initializeComponents() { + final double START_ANGLE = 60.0; + + this.pieChart.setClockwise(true); + this.pieChart.setStartAngle(START_ANGLE); + this.refresh(); + } +} diff --git a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java index cacccb9..37c8d50 100644 --- a/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java +++ b/client/src/main/java/client/scenes/recipe/RecipeDetailCtrl.java @@ -2,6 +2,7 @@ package client.scenes.recipe; import client.exception.UpdateException; import client.scenes.FoodpalApplicationCtrl; +import client.scenes.nutrition.NutritionPieChartCtrl; import client.service.ShoppingListService; import client.utils.Config; import client.utils.ConfigService; @@ -106,6 +107,9 @@ public class RecipeDetailCtrl implements LocaleAware { @FXML private ComboBox langSelector; + @FXML + private NutritionPieChartCtrl pieChartController; + private ListView getParentRecipeList() { return this.appCtrl.recipeList; } @@ -185,11 +189,14 @@ public class RecipeDetailCtrl implements LocaleAware { // expose the scaled view to list controllers this.ingredientListController.refetchFromRecipe(this.recipeView.getScaled()); this.stepListController.refetchFromRecipe(this.recipeView.getScaled()); + this.pieChartController.setRecipe(recipe); return; } this.ingredientListController.refetchFromRecipe(recipe); this.stepListController.refetchFromRecipe(recipe); + + this.pieChartController.setRecipe(recipe); } /** diff --git a/client/src/main/resources/client/scenes/nutrition/NutritionPieChart.fxml b/client/src/main/resources/client/scenes/nutrition/NutritionPieChart.fxml new file mode 100644 index 0000000..d63531b --- /dev/null +++ b/client/src/main/resources/client/scenes/nutrition/NutritionPieChart.fxml @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml b/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml index 602b827..355486c 100644 --- a/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml +++ b/client/src/main/resources/client/scenes/recipe/RecipeDetailView.fxml @@ -42,6 +42,11 @@ -