diff --git a/build.gradle b/build.gradle index 88a7a2b..e789fc4 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'application' id 'org.openjfx.javafxplugin' version '0.0.13' id 'org.beryx.jlink' version '2.25.0' + id 'jacoco' } group 'ch.zhaw.gartenverwaltung' @@ -25,7 +26,7 @@ tasks.withType(JavaCompile) { application { mainModule = 'ch.zhaw.gartenverwaltung' - mainClass = 'ch.zhaw.gartenverwaltung.HelloApplication' + mainClass = 'ch.zhaw.gartenverwaltung.Main' } javafx { @@ -41,10 +42,21 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4' testImplementation 'org.mockito:mockito-core:4.3.+' + implementation 'com.sun.mail:javax.mail:1.6.2' + } test { useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test +} + +jacoco { + toolVersion = "0.8.8" } jlink { @@ -57,4 +69,4 @@ jlink { jlinkZip { group = 'distribution' -} \ No newline at end of file +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/Config.java b/src/main/java/ch/zhaw/gartenverwaltung/Config.java deleted file mode 100644 index 2fa0117..0000000 --- a/src/main/java/ch/zhaw/gartenverwaltung/Config.java +++ /dev/null @@ -1,19 +0,0 @@ -package ch.zhaw.gartenverwaltung; - -import ch.zhaw.gartenverwaltung.types.HardinessZone; - -public class Config { - private static HardinessZone currentHardinessZone; - - static { - currentHardinessZone = HardinessZone.ZONE_8A; - } - - public static HardinessZone getCurrentHardinessZone() { - return currentHardinessZone; - } - - public static void setCurrentHardinessZone(HardinessZone currentHardinessZone) { - Config.currentHardinessZone = currentHardinessZone; - } -} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java b/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java index 6e5d792..b8c6f5b 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java @@ -1,31 +1,58 @@ package ch.zhaw.gartenverwaltung; -import ch.zhaw.gartenverwaltung.gardenplan.Gardenplanmodel; +import ch.zhaw.gartenverwaltung.bootstrap.AppLoader; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.io.TaskList; +import ch.zhaw.gartenverwaltung.models.Garden; import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.plantList.PlantListModel; -import ch.zhaw.gartenverwaltung.taskList.TaskListModel; +import ch.zhaw.gartenverwaltung.models.GardenSchedule; +import ch.zhaw.gartenverwaltung.models.PlantNotFoundException; import ch.zhaw.gartenverwaltung.types.Crop; import ch.zhaw.gartenverwaltung.types.Pest; import ch.zhaw.gartenverwaltung.types.Plant; import ch.zhaw.gartenverwaltung.types.Task; +import javafx.application.Platform; +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.Label; +import javafx.scene.control.*; +import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; import javafx.stage.Stage; import java.io.IOException; import java.util.List; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; +/** + * Controller class for the CropDetail.fxml file + */ public class CropDetailController { - private Crop crop = null; - private final PlantListModel plantListModel = new PlantListModel(); - private final TaskListModel taskListModel = new TaskListModel(); - private final Gardenplanmodel gardenplanmodel = new Gardenplanmodel(taskListModel); + private Crop crop; + + @Inject + private PlantList plantList; + @Inject + private GardenSchedule gardenSchedule; + @Inject + private Garden garden; + + @Inject + AppLoader appLoader; + + private static final Logger LOG = Logger.getLogger(CropDetailController.class.getName()); + private final ListProperty taskListProperty = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final ListProperty pestListProperty = new SimpleListProperty<>(FXCollections.observableArrayList()); @FXML private ImageView imageView; @@ -42,91 +69,361 @@ public class CropDetailController { @FXML private Label description_label; - @FXML - private VBox growthPhases_vbox; - - @FXML - private Label location_label; - @FXML private Label light_label; - @FXML - private Button location_button; - - @FXML - private VBox pests_vbox; - @FXML private Label soil_label; @FXML private Label spacing_label; - public CropDetailController() throws IOException { - } + @FXML + private Button addTask_button; @FXML - void editTaskList(ActionEvent event) { - - } + private ListView taskList_listView; @FXML - void goBack(ActionEvent event) { + private ListView pests_listView; + + @FXML + void addTask() throws IOException, HardinessZoneNotSetException { + createTaskDialog(true, null); + } + + /** + * close Window + */ + @FXML + void goBack() { Stage stage = (Stage) imageView.getScene().getWindow(); stage.close(); } + /** + * open dialog to set area + */ @FXML - void setArea(ActionEvent event) { - + void setArea() throws IOException { + openTextFieldDialog(); } - @FXML - void setLocation(ActionEvent event) { - - } - - public void setPlantFromCrop(Crop crop) throws HardinessZoneNotSetException, IOException { + /** + * set labels and image from selected {@link Crop} + * set icons for buttons + * @param crop {@link Crop} which will be displayed + * @throws PlantNotFoundException exception + */ + public void setPlantFromCrop(Crop crop) throws PlantNotFoundException { this.crop = crop; - Plant plant = plantListModel.getFilteredPlantListById(Config.getCurrentHardinessZone(), crop.getPlantId()).get(0); - cropName_label.setText(plant.name()); - description_label.setText(plant.description()); - light_label.setText(String.valueOf(plant.light())); - soil_label.setText(plant.soil()); - spacing_label.setText(plant.spacing()); - if (plant.image() != null) { - imageView.setImage(plant.image()); + try { + Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()) + .orElseThrow(PlantNotFoundException::new); + + cropName_label.setText(plant.name()); + description_label.setText(plant.description()); + light_label.setText(String.valueOf(plant.light())); + soil_label.setText(plant.soil()); + spacing_label.setText(plant.spacing()); + if (plant.image() != null) { + imageView.setImage(plant.image()); + } + area_label.setText(String.valueOf(crop.getArea())); + + initializeTaskListProperty(crop); + + TaskList.TaskListObserver taskListObserver = newTaskList -> { + Platform.runLater(() -> { + taskListProperty.clear(); + try { + taskListProperty.addAll(gardenSchedule.getTaskListForCrop(crop.getCropId().get())); + } catch (IOException e) { + e.printStackTrace(); + } + }); + }; + gardenSchedule.setTaskListObserver(taskListObserver); + + taskList_listView.itemsProperty().bind(taskListProperty); + + pestListProperty.addAll(plant.pests()); + pests_listView.itemsProperty().bind(pestListProperty); + + + } catch (HardinessZoneNotSetException | IOException e) { + throw new PlantNotFoundException(); } - area_label.setText(""); - location_label.setText(""); - createTaskLists(crop); - createPestList(plant); + setIconToButton(addTask_button, "addIcon.png"); + setIconToButton(area_button, "areaIcon.png"); + setCellFactoryPests(); + setCellFactoryTasks(); } - private void createTaskLists(Crop crop) throws IOException { - List taskList = taskListModel.getTaskListForCrop(crop.getCropId().get()); - for (Task task : taskList) { - Label label = new Label(task.getDescription()); - growthPhases_vbox.getChildren().add(label); + /** + * cell Factory for TaskListView + */ + private void setCellFactoryTasks() { + taskList_listView.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(Task task, boolean empty) { + super.updateItem(task, empty); + + if (empty || task == null) { + setText(null); + setGraphic(null); + } else { + setText(""); + setGraphic(createTaskHBox(task)); + } + } + }); + } + + /** + * cell Factory for PestListView + */ + private void setCellFactoryPests() { + pests_listView.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(Pest pest, boolean empty) { + super.updateItem(pest, empty); + + if (empty || pest == null) { + setText(null); + setGraphic(null); + } else { + setText(""); + setGraphic(createPestHBox(pest)); + } + } + }); + } + + /** + * initialize task list + * @param crop {@link Crop} that is selected + */ + private void initializeTaskListProperty(Crop crop) { + crop.getCropId().ifPresent(id -> { + List taskList; + try { + taskList = gardenSchedule.getTaskListForCrop(id); + taskListProperty.clear(); + taskListProperty.addAll(taskList); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Could not get task list for crop", e.getCause()); + } + }); + } + + /** + * Creates a {@link HBox} for the given {@link Task}. + * @param task {@link Task} which is selected + * @return {@link HBox} that was created + */ + private HBox createTaskHBox(Task task) { + HBox hBox = new HBox(10); + Label taskName = new Label(task.getName()+": "); + taskName.setMinWidth(100); + taskName.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + taskName.setStyle("-fx-font-weight: bold"); + Label taskDescription = new Label(task.getDescription()); + taskDescription.setWrapText(true); + taskDescription.setMaxSize(600, Double.MAX_VALUE); + Pane puffer = new Pane(); + HBox.setHgrow(puffer, Priority.ALWAYS); + + + Button edit = new Button(); + Button delete = new Button(); + edit.getStyleClass().add("button-class"); + delete.getStyleClass().add("button-class"); + HBox.setHgrow(edit, Priority.NEVER); + HBox.setHgrow(delete, Priority.NEVER); + setIconToButton(edit, "editIcon.png"); + setIconToButton(delete, "deleteIcon.png"); + edit.setOnAction(getEditTaskEvent(task)); + delete.setOnAction(deleteTask(task)); + + hBox.getChildren().addAll(taskName, taskDescription, puffer, edit, delete); + return hBox; + } + + /** + * Creates a {@link HBox} for the given {@link Pest}. + * @param pest {@link Pest} which is selected + * @return {@link HBox} that was created + */ + private HBox createPestHBox(Pest pest) { + Label label = new Label(pest.name() + ": "); + label.setStyle("-fx-font-weight: bold"); + HBox hBox = new HBox(); + hBox.fillHeightProperty(); + Label description = new Label(pest.description()); + description.setAlignment(Pos.TOP_LEFT); + description.setWrapText(true); + description.setMaxWidth(800); + hBox.getChildren().addAll(label, description); + return hBox; + } + + /** + * adds icon to button + * @param button the button which get the icon + * @param iconFileName file name of icon + */ + private void setIconToButton(Button button, String iconFileName) { + Image img = new Image(String.valueOf(getClass().getResource("icons/" + iconFileName))); + ImageView imageView = new ImageView(img); + imageView.setFitHeight(20); + imageView.setPreserveRatio(true); + button.setGraphic(imageView); + } + + /** + * opens dialog of {@link Task} edit. + * @param task {@link Task} + * @return {@link EventHandler} for the case of editing a {@link Task} + */ + private EventHandler getEditTaskEvent(Task task) { + return (event) -> { + try { + createTaskDialog(false, task); + } catch (IOException | HardinessZoneNotSetException e) { + e.printStackTrace(); + } + }; + } + + /** + * opens alert of {@link Task} deletion. + * @param task {@link Task} + * @return {@link EventHandler} for the case of deleting a {@link Task} + */ + private EventHandler deleteTask(Task task) { + return (event) -> { + showDeleteTask(task); + }; + } + + /** + * opens a dialog to create a new Task or edit the given Task + * @param newTask boolean if it is a new Task + * @param givenTask {@link Task} which was selected + * @throws IOException Exception + * @throws HardinessZoneNotSetException Exception + */ + private void createTaskDialog(boolean newTask, Task givenTask) throws IOException, HardinessZoneNotSetException { + Dialog dialog = new Dialog<>(); + dialog.setTitle("Set Task"); + dialog.setHeaderText("Add/Edit Task:"); + dialog.setResizable(false); + + DialogPane dialogPane = dialog.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + ButtonType saveTask; + if(newTask) { + saveTask = new ButtonType("Add", ButtonBar.ButtonData.OK_DONE); + } else { + saveTask = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); + } + dialogPane.getButtonTypes().addAll(saveTask, ButtonType.CANCEL); + + if (appLoader.loadPaneToDialog("TaskFormular.fxml", dialogPane) instanceof TaskFormularController controller) { + controller.setCorp(this.crop); + controller.initSaveButton((Button) dialogPane.lookupButton(saveTask)); + if (!newTask) { + controller.setTaskValue(givenTask); + } + dialog.setResultConverter(button -> button.equals(saveTask) ? controller.returnResult(this.crop) : null); + + dialog.showAndWait() + .ifPresent(task -> { + if (newTask) { + try { + gardenSchedule.addTask(task); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + try { + gardenSchedule.addTask(givenTask.updateTask(task)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + } + + } + + /** + * opens TextField Dialog to enter the plant area. + * @throws IOException Exception + */ + private void openTextFieldDialog() throws IOException { + Dialog dialog = new Dialog<>(); + dialog.setTitle("set Text Area"); + dialog.setHeaderText("set Text Area"); + dialog.setResizable(false); + + DialogPane dialogPane = dialog.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + ButtonType save = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(save, ButtonType.CANCEL); + + if (appLoader.loadPaneToDialog("TextFieldFormular.fxml", dialogPane) instanceof TextFieldFormularController controller) { + controller.setDescription_label("Text Area"); + controller.setValueTextArea(area_label.getText()); + controller.initSaveButton((Button) dialogPane.lookupButton(save)); + + dialog.setResultConverter(button -> button.equals(save) ? controller.getValue() : null); + + dialog.showAndWait() + .ifPresent(string -> { + try { + garden.updateCrop(this.crop.withArea(Double.parseDouble(string))); + } catch (IOException e) { + e.printStackTrace(); + } + area_label.setText(string); + }); } } - private void createPestList(Plant plant) { - List pests = plant.pests(); - for (Pest pest : pests) { - Label label = new Label(pest.name() + ":"); - label.setStyle("-fx-font-weight: bold"); - HBox hBox = new HBox(); - hBox.fillHeightProperty(); - Label label1 = new Label(pest.description()); - label1.setAlignment(Pos.TOP_LEFT); - label1.setWrapText(true); - label1.setMaxWidth(600); - label1.setMaxHeight(100); - Button button = new Button("Get Counter Measures"); - hBox.getChildren().addAll(label1, button); - pests_vbox.getChildren().addAll(label, hBox); - } + /** + * Alert to delete Task. + * @param task {@link Task} which is being deleted + */ + private void showDeleteTask(Task task) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Delete " + task.getName()); + alert.setHeaderText("Are you sure want to delete this Task?"); + + DialogPane dialogPane = alert.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + alert.showAndWait() + .ifPresent(buttonType -> { + if (buttonType == ButtonType.OK) { + try { + gardenSchedule.removeTask(task); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Could not remove crop.", e); + } + } + }); } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/HelloApplication.java b/src/main/java/ch/zhaw/gartenverwaltung/HelloApplication.java deleted file mode 100644 index fd31017..0000000 --- a/src/main/java/ch/zhaw/gartenverwaltung/HelloApplication.java +++ /dev/null @@ -1,23 +0,0 @@ -package ch.zhaw.gartenverwaltung; - -import javafx.application.Application; -import javafx.fxml.FXMLLoader; -import javafx.scene.Scene; -import javafx.stage.Stage; - -import java.io.IOException; - -public class HelloApplication extends Application { - @Override - public void start(Stage stage) throws IOException { - FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("MainFXML.fxml")); - Scene scene = new Scene(fxmlLoader.load()); - stage.setTitle("Gartenverwaltung"); - stage.setScene(scene); - stage.show(); - } - - public static void main(String[] args) { - launch(); - } -} \ No newline at end of file diff --git a/src/main/java/ch/zhaw/gartenverwaltung/HelloController.java b/src/main/java/ch/zhaw/gartenverwaltung/HelloController.java deleted file mode 100644 index 7e952ac..0000000 --- a/src/main/java/ch/zhaw/gartenverwaltung/HelloController.java +++ /dev/null @@ -1,14 +0,0 @@ -package ch.zhaw.gartenverwaltung; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; - -public class HelloController { - @FXML - private Label welcomeText; - - @FXML - protected void onHelloButtonClick() { - welcomeText.setText("Welcome to JavaFX Application!"); - } -} \ No newline at end of file diff --git a/src/main/java/ch/zhaw/gartenverwaltung/HomeController.java b/src/main/java/ch/zhaw/gartenverwaltung/HomeController.java index 7df4661..cccaa3a 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/HomeController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/HomeController.java @@ -1,5 +1,55 @@ package ch.zhaw.gartenverwaltung; -public class HomeController -{ +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Controller class for the Home.fxml file + */ +public class HomeController implements Initializable { + + @FXML + private ImageView imageViewDavid; + + @FXML + private ImageView imageViewElias; + + @FXML + private ImageView imageViewGian; + + @FXML + private ImageView imageViewPhilippe; + + @FXML + private ImageView imageViewRoman; + + @Override + public void initialize(URL location, ResourceBundle resources) { + setImages(imageViewDavid, ""); + setImages(imageViewElias, ""); + setImages(imageViewGian, ""); + setImages(imageViewRoman, ""); + setImages(imageViewPhilippe, ""); + } + + /** + * set image to image view + * @param imageView the imageView to update + * @param photoName the file name of the photo + */ + private void setImages(ImageView imageView, String photoName) { + Image img; + if (photoName.equals("")) { + img = new Image(String.valueOf(getClass().getResource("icons/userIcon.png"))); + } else { + img = new Image(String.valueOf(getClass().getResource("icons/" + photoName))); + } + imageView.setImage(img); + } } + diff --git a/src/main/java/ch/zhaw/gartenverwaltung/Main.java b/src/main/java/ch/zhaw/gartenverwaltung/Main.java new file mode 100644 index 0000000..65aed31 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/Main.java @@ -0,0 +1,59 @@ +package ch.zhaw.gartenverwaltung; + +import ch.zhaw.gartenverwaltung.bootstrap.AppLoader; +import ch.zhaw.gartenverwaltung.backgroundtasks.BackgroundTasks; +import ch.zhaw.gartenverwaltung.io.CropList; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.io.TaskList; +import ch.zhaw.gartenverwaltung.models.Garden; +import ch.zhaw.gartenverwaltung.types.Crop; +import javafx.application.Application; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.Timer; + +/** + * Main class of the Application + */ +public class Main extends Application { + Timer backGroundTaskTimer = new Timer(); + BackgroundTasks backgroundTasks; + + /** + * Method which is automatically called if Application is starting. It loads the scenes to stage and shows the stage. + * It creates a new Instance of BackgroundTasks and schedules them with a Timer instance to execute them every minute. + * @param stage Stage to show + * @throws IOException If loading Scenes can not access the fxml Resource File + */ + @Override + public void start(Stage stage) throws IOException { + AppLoader appLoader = new AppLoader(); + + appLoader.loadSceneToStage("MainFXML.fxml", stage); + + stage.setTitle("Gartenverwaltung"); + stage.show(); + + backgroundTasks = new BackgroundTasks((TaskList) appLoader.getAppDependency(TaskList.class),(CropList) appLoader.getAppDependency(CropList.class), (PlantList) appLoader.getAppDependency(PlantList.class)); + backGroundTaskTimer.scheduleAtFixedRate(backgroundTasks, 0, 60000); + + } + + /** + * Method which is automatically called when application is stopped. + * It cancels the timer to not execute the background tasks anymore. + */ + @Override + public void stop(){ + backGroundTaskTimer.cancel(); + } + + /** + * The Main method launches the application + * @param args There are no arguments needed. + */ + public static void main(String[] args) { + launch(); + } +} \ No newline at end of file diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java b/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java index 032bdb8..e351902 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java @@ -1,28 +1,34 @@ package ch.zhaw.gartenverwaltung; -import javafx.event.ActionEvent; +import ch.zhaw.gartenverwaltung.bootstrap.AfterInject; +import ch.zhaw.gartenverwaltung.bootstrap.AppLoader; +import ch.zhaw.gartenverwaltung.bootstrap.ChangeViewEvent; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; +import javafx.event.EventHandler; import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.control.Button; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.Pane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; import java.io.IOException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; -import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; -public class MainFXMLController implements Initializable { - /** - * Caching the panes - */ - private final Map panes = new HashMap<>(); +/** + * Controller class for the MainFXML.fxml file + */ +public class MainFXMLController { private static final Logger LOG = Logger.getLogger(MainFXMLController.class.getName()); + @Inject + AppLoader appLoader; + @FXML private Button home_button; @@ -30,36 +36,93 @@ public class MainFXMLController implements Initializable { private AnchorPane mainPane; @FXML - private Button myPlants_button; + private Button myGarden_button; @FXML private Button mySchedule_button; @FXML - private Button plants_button; + private Button settings_button; @FXML - void goToHome(ActionEvent event) throws IOException { - loadPane("Home.fxml"); - styleChangeButton(home_button); + private Button tutorial_button; + + private final Stage tutorialModal = new Stage(); + + + /** + * go to home pane + */ + @FXML + void goToHome() { + showPaneAsMainView("Home.fxml"); } + /** + * go to my garden pane + */ @FXML - void goToMyPlants(ActionEvent event) throws IOException { - loadPane("MyPlants.fxml"); - styleChangeButton(myPlants_button); + void goToMyPlants() { + showPaneAsMainView("MyGarden.fxml"); } + /** + * go to the schedule pane + */ @FXML - void goToMySchedule(ActionEvent event) throws IOException { - loadPane("MySchedule.fxml"); - styleChangeButton(mySchedule_button); + void goToMySchedule() { + showPaneAsMainView("MySchedule.fxml"); } + /** + * open dialog of the settings + * @throws IOException exception + */ @FXML - void goToPlants(ActionEvent event) throws IOException { - loadPane("Plants.fxml"); - styleChangeButton(plants_button); + public void openSettings() throws IOException { + Dialog dialog = new Dialog<>(); + dialog.setTitle("Settings"); + dialog.setHeaderText("Settings"); + dialog.setResizable(false); + + DialogPane dialogPane = dialog.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + ButtonType saveSettings = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(saveSettings, ButtonType.CANCEL); + + if (appLoader.loadPaneToDialog("Settings.fxml", dialogPane) instanceof SettingsController controller) { + + dialog.showAndWait() + .ifPresent(button -> { + if (button.equals(saveSettings)) { + controller.saveSettings(); + } + }); + } + } + + /** + * Show the tutorial window + */ + public void showTutorial() { + if (!tutorialModal.isShowing()) { + if (tutorialModal.getScene() == null) { + try { + appLoader.loadSceneToStage("Tutorial.fxml", tutorialModal); + tutorialModal.initModality(Modality.NONE); + tutorialModal.setResizable(false); + tutorialModal.sizeToScene(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Could not load Tutorial"); + } + } + tutorialModal.show(); + } + tutorialModal.requestFocus(); } /** @@ -67,43 +130,70 @@ public class MainFXMLController implements Initializable { * set HGrow and VGrow to parent AnchorPane. * Sends MainController to other Controllers. * @param fxmlFile string of fxml file - * @throws IOException exception when file does not exist */ - public void loadPane(String fxmlFile) throws IOException { + public void showPaneAsMainView(String fxmlFile) { + try { + Pane anchorPane = appLoader.loadPane(fxmlFile); + mainPane.getChildren().setAll(anchorPane); + anchorPane.prefWidthProperty().bind(mainPane.widthProperty()); + anchorPane.prefHeightProperty().bind(mainPane.heightProperty()); - AnchorPane anchorPane = panes.get(fxmlFile); - if (anchorPane == null) { - FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(HelloApplication.class.getResource(fxmlFile))); - anchorPane = loader.load(); - panes.put(fxmlFile, anchorPane); - - if(fxmlFile.equals("MyPlants.fxml")) { - MyPlantsController myPlantsController = loader.getController(); - myPlantsController.getMainController(this); - } + anchorPane.removeEventHandler(ChangeViewEvent.CHANGE_MAIN_VIEW, changeMainViewHandler); + anchorPane.addEventHandler(ChangeViewEvent.CHANGE_MAIN_VIEW, changeMainViewHandler); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Could not load pane.", e); } - mainPane.getChildren().setAll(anchorPane); - anchorPane.prefWidthProperty().bind(mainPane.widthProperty()); - anchorPane.prefHeightProperty().bind(mainPane.heightProperty()); - } - private void styleChangeButton(Button button) { - //ToDo changeStyle of the menu buttons + private final EventHandler changeMainViewHandler = (ChangeViewEvent event) -> showPaneAsMainView(event.view()); + + /** + * preload all menu bar panes + * @throws IOException exception + */ + private void preloadPanes() throws IOException { + appLoader.loadAndCacheFxml("MyGarden.fxml"); + appLoader.loadAndCacheFxml("MySchedule.fxml"); + appLoader.loadAndCacheFxml("Plants.fxml"); } /** * loads the default FXML File * {@inheritDoc} */ - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { + @AfterInject + @SuppressWarnings("unused") + public void init() { try { - loadPane("Home.fxml"); - styleChangeButton(home_button); + preloadPanes(); + showPaneAsMainView("MyGarden.fxml"); } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to load FXML-Pane!", e); } + mainPane.getScene().getWindow().setOnCloseRequest(this::closeWindowHandler); + setIconToButton(home_button, "homeIcon.png"); + setIconToButton(settings_button, "settingsIcon.png"); + tutorial_button.visibleProperty().bind(Settings.getInstance().getShowTutorialProperty()); + } + + /** + * close Tutorial Window + * @param windowEvent event + */ + private void closeWindowHandler(WindowEvent windowEvent) { + tutorialModal.close(); + } + + /** + * adds icon to given button + * @param button the button which get the icon + * @param iconFileName file name of icon + */ + private void setIconToButton(Button button, String iconFileName) { + Image img = new Image(String.valueOf(getClass().getResource("icons/" + iconFileName))); + ImageView imageView = new ImageView(img); + imageView.setFitHeight(20); + imageView.setPreserveRatio(true); + button.setGraphic(imageView); } } - diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java b/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java new file mode 100644 index 0000000..21da05a --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/MyGardenController.java @@ -0,0 +1,226 @@ +package ch.zhaw.gartenverwaltung; + +import ch.zhaw.gartenverwaltung.bootstrap.AfterInject; +import ch.zhaw.gartenverwaltung.bootstrap.AppLoader; +import ch.zhaw.gartenverwaltung.bootstrap.ChangeViewEvent; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.models.Garden; +import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; +import ch.zhaw.gartenverwaltung.models.PlantNotFoundException; +import ch.zhaw.gartenverwaltung.types.Crop; +import ch.zhaw.gartenverwaltung.types.Plant; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import java.io.IOException; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Controller class for the MyGarden.fxml file + */ +public class MyGardenController { + private static final Logger LOG = Logger.getLogger(MyGardenController.class.getName()); + @Inject + AppLoader appLoader; + @Inject + private Garden garden; + @Inject + private PlantList plantList; + + @FXML + public AnchorPane myGardenRoot; + @FXML + private Button addPlant_button; + @FXML + private ListView myGarden_listView; + + /** + * initialize crop list + * add listener for crop list + * set icon for button + */ + @AfterInject + @SuppressWarnings("unused") + public void init() { + setIconToButton(addPlant_button, "addIcon.png"); + myGarden_listView.itemsProperty().bind(garden.getPlantedCrops()); + setCellFactory(); + } + + /** + * redirect to plant fxml file + */ + @FXML + void addPlant() { + myGardenRoot.fireEvent(new ChangeViewEvent(ChangeViewEvent.CHANGE_MAIN_VIEW, "Plants.fxml")); + } + + /** + * set cell factory to load {@link HBox} as list view content + */ + private void setCellFactory() { + myGarden_listView.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(Crop crop, boolean empty) { + super.updateItem(crop, empty); + + if (empty || crop == null) { + setText(null); + setGraphic(null); + } else { + try { + setText(""); + setGraphic(createHBoxForListView(crop)); + } catch (HardinessZoneNotSetException | IOException e) { + LOG.log(Level.WARNING, "Could not get plant for Cell", e); + } + } + } + }); + } + + /** + * Creates and returns HBox of the crop + * @param crop {@link Crop} which is selected + * @return {@link HBox} of the {@link Crop} + * @throws HardinessZoneNotSetException exception + * @throws IOException exception + */ + private HBox createHBoxForListView(Crop crop) throws HardinessZoneNotSetException, IOException { + Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).get(); + HBox hBox = new HBox(10); + ImageView imageView = new ImageView(); + imageView.setPreserveRatio(false); + imageView.setFitHeight(100); + imageView.setFitWidth(100); + imageView.maxHeight(100); + if (plant.image() != null) { + imageView.setImage(plant.image()); + } + hBox.setMinHeight(100); + Label label = new Label(plant.name()); + label.setMinWidth(100); + + VBox vbox = new VBox(10); + HBox startDateHBox = new HBox(10); + Label startDateDescription = new Label("Start Date:"); + startDateDescription.setMinWidth(75); + Label startDate = new Label(crop.getStartDate().toString()); + startDateHBox.getChildren().addAll(startDateDescription, startDate); + HBox endDateHBox = new HBox(10); + Label endDateDescription = new Label("End Date:"); + endDateDescription.setMinWidth(75); + Label endDate = new Label(crop.getStartDate().plusDays(plant.timeToHarvest(0)).toString()); + endDateHBox.getChildren().addAll(endDateDescription, endDate); + vbox.getChildren().addAll(startDateHBox, endDateHBox); + + vbox.setMaxWidth(2000); + HBox.setHgrow(vbox, Priority.ALWAYS); + + Button details = new Button(); + Button delete = new Button(); + details.getStyleClass().add("button-class"); + delete.getStyleClass().add("button-class"); + + setIconToButton(details, "detailsIcon.png"); + setIconToButton(delete, "deleteIcon.png"); + details.setOnAction(getGoToCropDetailEvent(crop)); + delete.setOnAction(getDeleteCropEvent(crop)); + + hBox.getChildren().addAll(imageView, label, vbox, details, delete); + return hBox; + } + + /** + * adds icon to button + * @param button the button which get the icon + * @param iconFileName file name of icon + */ + private void setIconToButton(Button button, String iconFileName) { + Image img = new Image(String.valueOf(getClass().getResource("icons/" + iconFileName))); + ImageView imageView = new ImageView(img); + imageView.setFitHeight(20); + imageView.setPreserveRatio(true); + button.setGraphic(imageView); + } + + /** + * open detail window of the selected {@link Crop} + * @param crop {@link Crop} which is selected + * @return {@link EventHandler} for button + */ + private EventHandler getGoToCropDetailEvent(Crop crop) { + return (event) -> { + try { + Stage stage = new Stage(); + if (appLoader.loadSceneToStage("CropDetail.fxml", stage) instanceof CropDetailController controller) { + controller.setPlantFromCrop(crop); + } + stage.initModality(Modality.APPLICATION_MODAL); + stage.setResizable(true); + stage.showAndWait(); + } catch (IOException | PlantNotFoundException e) { + LOG.log(Level.SEVERE, "Could not load plant details.", e); + } + }; + } + + /** + * open alert for deleting the selected {@link Crop} + * @param crop {@link Crop} which is selected + * @return {@link EventHandler} for button + */ + private EventHandler getDeleteCropEvent(Crop crop) { + return (event) -> { + try { + showConfirmation(crop); + } catch (IOException | HardinessZoneNotSetException e) { + e.printStackTrace(); + } + }; + } + + /** + * Alert to confirm that the crop can be deleted. + * @param crop {@link Crop} which is selected + * @throws IOException exception + * @throws HardinessZoneNotSetException exception + */ + private void showConfirmation(Crop crop) throws IOException, HardinessZoneNotSetException { + Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).get(); + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Delete " + plant.name()); + DialogPane dialogPane = alert.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + alert.setHeaderText("Are you sure want to delete this Crop?"); + alert.setContentText("Deleting this crop will remove all associated tasks from your schedule."); + + alert.showAndWait() + .ifPresent(buttonType -> { + if (buttonType == ButtonType.OK) { + try { + garden.removeCrop(crop); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Could not remove crop.", e); + } + } + }); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MyPlantsController.java b/src/main/java/ch/zhaw/gartenverwaltung/MyPlantsController.java deleted file mode 100644 index 3d9d82c..0000000 --- a/src/main/java/ch/zhaw/gartenverwaltung/MyPlantsController.java +++ /dev/null @@ -1,160 +0,0 @@ -package ch.zhaw.gartenverwaltung; - -import ch.zhaw.gartenverwaltung.gardenplan.Gardenplanmodel; -import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.plantList.PlantListModel; -import ch.zhaw.gartenverwaltung.taskList.TaskListModel; -import ch.zhaw.gartenverwaltung.types.Crop; -import ch.zhaw.gartenverwaltung.types.Plant; -import javafx.event.ActionEvent; -import javafx.event.EventHandler; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Label; -import javafx.scene.image.ImageView; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; -import javafx.stage.Modality; -import javafx.stage.Stage; - -import java.io.IOException; -import java.net.URL; -import java.util.*; - -public class MyPlantsController implements Initializable { - MainFXMLController mainController; - private final TaskListModel taskListModel = new TaskListModel(); - private final Gardenplanmodel gardenplanmodel = new Gardenplanmodel(taskListModel); - private final PlantListModel plantListModel = new PlantListModel(); - - @FXML - private VBox myPlants_vbox; - - public MyPlantsController() throws IOException { - } - - @FXML - void addPlant(ActionEvent event) throws IOException { - mainController.loadPane("Plants.fxml"); - } - - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { - //ToDo update, when new crops are added - try { - loadCropList(); - } catch (HardinessZoneNotSetException | IOException e) { - e.printStackTrace(); - } - } - - private void loadCropList() throws HardinessZoneNotSetException, IOException { - List cropList = new LinkedList<>(); - try { - cropList = getCropList(); - } catch (IOException e) { - e.printStackTrace(); - } - createPlantView(cropList); - } - - private void createPlantView(List crops) throws HardinessZoneNotSetException, IOException { - myPlants_vbox.getChildren().clear(); - for(Crop crop : crops) { - HBox hBox = createPlantView(crop); - myPlants_vbox.getChildren().add(hBox); - } - } - - public void getMainController(MainFXMLController controller) { - mainController = controller; - } - - private List getCropList() throws IOException { - List cropList; - cropList = gardenplanmodel.getCrops(); - return cropList; - } - - private HBox createPlantView(Crop crop) throws HardinessZoneNotSetException, IOException { - //ToDo add better design - Plant plant = plantListModel.getFilteredPlantListById(Config.getCurrentHardinessZone(), crop.getPlantId()).get(0); - HBox hBox = new HBox(10); - ImageView imageView = new ImageView(); - imageView.setPreserveRatio(false); - imageView.setFitHeight(100); - imageView.setFitWidth(100); - imageView.maxHeight(100); - if (plant.image() != null) { - imageView.setImage(plant.image()); - } - hBox.setMinHeight(100); - Label label = new Label(plant.name()); - label.setMaxWidth(2000); - HBox.setHgrow(label, Priority.ALWAYS); - Button details = new Button("Details"); - Button delete = new Button("delete"); - details.setOnAction(getGoToCropDetailEvent(crop)); - delete.setOnAction(getDeleteCropEvent(crop)); - hBox.getChildren().addAll(imageView, label, details, delete); - return hBox; - } - - private EventHandler getGoToCropDetailEvent(Crop crop) { - EventHandler event = new EventHandler() { - @Override - public void handle(ActionEvent event) { - Parent root; - FXMLLoader fxmlLoader = new FXMLLoader(Objects.requireNonNull(getClass().getResource("CropDetail.fxml"))); - try { - root = fxmlLoader.load(); - CropDetailController controller = fxmlLoader.getController(); - controller.setPlantFromCrop(crop); - Stage stage = new Stage(); - stage.setScene(new Scene(root)); - stage.initModality(Modality.APPLICATION_MODAL); - stage.setResizable(true); - stage.showAndWait(); - } catch (IOException | HardinessZoneNotSetException e) { - e.printStackTrace(); - } - } - }; - return event; - } - - private EventHandler getDeleteCropEvent(Crop crop) { - EventHandler event = new EventHandler() { - @Override - public void handle(ActionEvent event) { - try { - showConfirmation(crop); - } catch (IOException | HardinessZoneNotSetException e) { - e.printStackTrace(); - } - } - }; - return event; - } - - private void showConfirmation(Crop crop) throws IOException, HardinessZoneNotSetException { - Alert alert = new Alert(Alert.AlertType.CONFIRMATION); - alert.setTitle("Delete Crop"); - alert.setHeaderText("Are you sure want to delete this Crop?"); - alert.setContentText("placeholder"); - - Optional option = alert.showAndWait(); - - if (option.get() == ButtonType.OK) { - gardenplanmodel.removeCrop(crop); - loadCropList(); - } - } -} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java b/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java index a73052c..df31cb9 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/MyScheduleController.java @@ -1,80 +1,50 @@ package ch.zhaw.gartenverwaltung; -import ch.zhaw.gartenverwaltung.gardenplan.Gardenplanmodel; +import ch.zhaw.gartenverwaltung.bootstrap.AfterInject; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.io.TaskList; +import ch.zhaw.gartenverwaltung.models.Garden; import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.plantList.PlantListModel; -import ch.zhaw.gartenverwaltung.taskList.TaskListModel; +import ch.zhaw.gartenverwaltung.models.GardenSchedule; import ch.zhaw.gartenverwaltung.types.Crop; +import ch.zhaw.gartenverwaltung.types.Plant; import ch.zhaw.gartenverwaltung.types.Task; +import javafx.application.Platform; import javafx.beans.property.ListProperty; -import javafx.beans.property.SimpleListProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import java.io.IOException; -import java.net.URL; import java.time.LocalDate; -import java.util.LinkedList; import java.util.List; -import java.util.ResourceBundle; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Controller class for the MySchedule.fxml file + */ +public class MyScheduleController { + private static final Logger LOG = Logger.getLogger(MyScheduleController.class.getName()); -public class MyScheduleController implements Initializable { private Crop selectedCrop = null; - private final TaskListModel taskListModel = new TaskListModel(); - private final Gardenplanmodel gardenplanmodel = new Gardenplanmodel(taskListModel); - private final PlantListModel plantListModel = new PlantListModel(); - private final ListProperty cropListProperty = new SimpleListProperty<>(FXCollections.observableArrayList()); + @Inject + private GardenSchedule gardenSchedule; + @Inject + private Garden garden; + @Inject + private PlantList plantList; @FXML - private Label day1_label; - - @FXML - private Pane day1_pane; - - @FXML - private Label day2_label; - - @FXML - private Pane day2_pane; - - @FXML - private Label day3_label; - - @FXML - private Pane day3_pane; - - @FXML - private Label day4_label; - - @FXML - private Pane day4_pane; - - @FXML - private Label day5_label; - - @FXML - private Pane day5_pane; - - @FXML - private Label day6_label; - - @FXML - private Pane day6_pane; - - @FXML - private Label day7_label; - - @FXML - private Pane day7_pane; + private ListView> week_listView; @FXML private Label information_label; @@ -82,57 +52,58 @@ public class MyScheduleController implements Initializable { @FXML private ListView scheduledPlants_listview; - public MyScheduleController() throws IOException { + @FXML + private void showAllTasks(ActionEvent actionEvent) throws IOException { + gardenSchedule.getTasksUpcomingWeek(); + scheduledPlants_listview.getSelectionModel().clearSelection(); } - @Override - public void initialize(URL location, ResourceBundle resources) { - List cropList; - try { - cropList = gardenplanmodel.getCrops(); - cropListProperty.addAll(cropList); - } catch (IOException e) { - e.printStackTrace(); - } - setCellFactoryListView(); - scheduledPlants_listview.itemsProperty().bind(cropListProperty); + @AfterInject + @SuppressWarnings("unused") + public void init() throws IOException { + setCellFactoryCropListView(); + setCellFactoryTaskListView(); + scheduledPlants_listview.itemsProperty().bind(garden.getPlantedCrops()); + ListProperty> taskListProperty = gardenSchedule.getWeeklyTaskListProperty(); + week_listView.itemsProperty().bind(taskListProperty); lookForSelectedListEntries(); - setDayLabels(); information_label.setText(""); - try { - loadTaskList(); - } catch (IOException e) { - e.printStackTrace(); - } + + gardenSchedule.getTasksUpcomingWeek(); + TaskList.TaskListObserver taskListObserver = newTaskList -> { + Platform.runLater(() -> { + try { + gardenSchedule.getTasksUpcomingWeek(); + scheduledPlants_listview.getSelectionModel().clearSelection(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + }; + gardenSchedule.setTaskListObserver(taskListObserver); } + /** + * sort scheduler to selected crop + */ private void lookForSelectedListEntries() { - scheduledPlants_listview.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Crop oldValue, Crop newValue) { - selectedCrop = newValue; - try { - loadTaskList(); - } catch (IOException e) { - e.printStackTrace(); - } + scheduledPlants_listview.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + selectedCrop = newValue; + try { + loadTaskList(); + } catch (IOException e) { + e.printStackTrace(); } }); } - private void setDayLabels() { - LocalDate today = LocalDate.now(); - day1_label.setText(today.getDayOfWeek().toString()); - day2_label.setText(today.plusDays(1).getDayOfWeek().toString()); - day3_label.setText(today.plusDays(2).getDayOfWeek().toString()); - day4_label.setText(today.plusDays(3).getDayOfWeek().toString()); - day5_label.setText(today.plusDays(4).getDayOfWeek().toString()); - day6_label.setText(today.plusDays(5).getDayOfWeek().toString()); - day7_label.setText(today.plusDays(6).getDayOfWeek().toString()); - } - - private void setCellFactoryListView() { - scheduledPlants_listview.setCellFactory(param -> new ListCell() { + /** + * set cellFactory for the crops. + */ + private void setCellFactoryCropListView() { + MultipleSelectionModel selectionModel = scheduledPlants_listview.getSelectionModel(); + selectionModel.setSelectionMode(SelectionMode.MULTIPLE); + scheduledPlants_listview.setCellFactory(param -> new ListCell<>() { @Override protected void updateItem(Crop crop, boolean empty) { super.updateItem(crop, empty); @@ -141,42 +112,120 @@ public class MyScheduleController implements Initializable { setText(null); } else { try { - setText(plantListModel.getFilteredPlantListById(Config.getCurrentHardinessZone(), crop.getPlantId()).get(0).name()); + String text = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()) + .map(Plant::name) + .orElse(""); + setText(text); } catch (HardinessZoneNotSetException | IOException e) { - e.printStackTrace(); + LOG.log(Level.WARNING, "Could not get plant for Cell", e); } } } }); } + /** + * set CallFactory for the given Tasks + */ + private void setCellFactoryTaskListView() { + week_listView.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(List taskList, boolean empty) { + super.updateItem(taskList, empty); + + if (empty || taskList == null) { + setText(null); + setGraphic(null); + } else { + setText(""); + setGraphic(weekTaskVBox(taskList, this.getIndex())); + } + } + }); + } + + /** + * update task list + * @throws IOException exception + */ private void loadTaskList() throws IOException { - List> taskLists = new LinkedList<>(); + List> taskLists; if (selectedCrop != null) { - taskLists = taskListModel.getTasksUpcomingWeekForCrop(selectedCrop.getCropId().get()); + gardenSchedule.getTasksUpcomingWeekForCrop(selectedCrop.getCropId().get()); } else { - taskLists = taskListModel.getTasksUpcomingWeek(); - } - if (!taskLists.isEmpty()) { - viewTaskListOfDay(day1_pane, taskLists.get(0)); - viewTaskListOfDay(day2_pane, taskLists.get(1)); - viewTaskListOfDay(day3_pane, taskLists.get(2)); - viewTaskListOfDay(day4_pane, taskLists.get(3)); - viewTaskListOfDay(day5_pane, taskLists.get(4)); - viewTaskListOfDay(day6_pane, taskLists.get(5)); - viewTaskListOfDay(day7_pane, taskLists.get(6)); + gardenSchedule.getTasksUpcomingWeek(); } } - private void viewTaskListOfDay(Pane pane, List tasks) { - //ToDo update pane with task list - VBox vBox = new VBox(); + /** + * Create a {@link VBox} of the given TaskList. + * @param tasks List of {@link Task}s + * @param dayIndex index of the day + * @return {@link VBox} of the given Task of the day + */ + private VBox weekTaskVBox(List tasks, int dayIndex) { + VBox vBox = new VBox(10); + LocalDate today = LocalDate.now(); + Label weekDay = new Label(today.plusDays(dayIndex).getDayOfWeek().toString()); + weekDay.setStyle("-fx-font-weight: bold; -fx-underline: true"); + vBox.getChildren().add(weekDay); for (Task task : tasks) { - Label label = new Label(task.getDescription()); - vBox.getChildren().add(label); + HBox hBox = new HBox(10); + Label taskName = new Label(task.getName() + ":"); + taskName.setStyle("-fx-font-weight: bold"); + taskName.setMinWidth(100); + taskName.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); + hBox.getChildren().addAll(taskName); + + HBox hBoxDescription = new HBox(); + Label taskDescription = new Label(task.getDescription()); + taskDescription.setWrapText(true); + taskDescription.setMaxSize(600, Double.MAX_VALUE); + Pane puffer = new Pane(); + HBox.setHgrow(puffer, Priority.ALWAYS); + Button button = new Button("Task completed!"); + button.getStyleClass().add("button-class"); + + button.setOnAction(new EventHandler() { + @Override + public void handle(ActionEvent event) { + showConfirmation(task); + } + }); + HBox.setHgrow(button, Priority.NEVER); + hBoxDescription.getChildren().addAll(taskDescription, puffer, button); + vBox.getChildren().addAll(hBox, hBoxDescription); } - pane.getChildren().add(vBox); + return vBox; } + /** + * Alert to confirm that task has been completed. + * @param task {@link Task} which is selected + */ + private void showConfirmation(Task task) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Task Completed?"); + alert.setHeaderText("Are you sure you have completed this task?"); + alert.setContentText("Confirming that you have completed the task will remove it from the schedule."); + + DialogPane dialogPane = alert.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + alert.showAndWait() + .ifPresent(buttonType -> { + if (buttonType == ButtonType.OK) { + task.done(); + try { + loadTaskList(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java b/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java index c8ff333..d29d097 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/PlantsController.java @@ -1,44 +1,55 @@ package ch.zhaw.gartenverwaltung; +import ch.zhaw.gartenverwaltung.bootstrap.AfterInject; +import ch.zhaw.gartenverwaltung.bootstrap.AppLoader; +import ch.zhaw.gartenverwaltung.bootstrap.ChangeViewEvent; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.plantList.PlantListModel; +import ch.zhaw.gartenverwaltung.models.Garden; +import ch.zhaw.gartenverwaltung.models.PlantListModel; +import ch.zhaw.gartenverwaltung.models.PlantNotFoundException; import ch.zhaw.gartenverwaltung.types.HardinessZone; import ch.zhaw.gartenverwaltung.types.Plant; import ch.zhaw.gartenverwaltung.types.Seasons; import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; -import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.fxml.Initializable; import javafx.geometry.Insets; -import javafx.scene.Parent; -import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.VBox; -import javafx.stage.Modality; -import javafx.stage.Stage; import java.io.IOException; -import java.net.URL; +import java.time.LocalDate; import java.util.List; import java.util.Objects; -import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; -public class PlantsController implements Initializable { +/** + * Controller class for the Plants.fxml file + */ +public class PlantsController { private static final Logger LOG = Logger.getLogger(PlantsController.class.getName()); - private final PlantListModel plantListModel = new PlantListModel(); + + @Inject + private PlantListModel plantListModel; + @Inject + private AppLoader appLoader; + @Inject + private Garden garden; + private Plant selectedPlant = null; private final HardinessZone DEFAULT_HARDINESS_ZONE = HardinessZone.ZONE_8A; - // TODO: move to model private final ListProperty plantListProperty = new SimpleListProperty<>(FXCollections.observableArrayList()); + @FXML + public AnchorPane plantsRoot; + @FXML private VBox seasons; @@ -62,20 +73,38 @@ public class PlantsController implements Initializable { /** * open new window to select sow or harvest day to save the crop - * @param event event */ @FXML - void selectSowDate(ActionEvent event) throws IOException { - Parent root; - FXMLLoader fxmlLoader = new FXMLLoader(Objects.requireNonNull(getClass().getResource("SelectSowDay.fxml"))); - root = fxmlLoader.load(); - SelectSowDayController controller = fxmlLoader.getController(); - controller.getSelectedPlant(selectedPlant); - Stage stage = new Stage(); - stage.setScene(new Scene(root)); - stage.initModality(Modality.APPLICATION_MODAL); - stage.setResizable(false); - stage.showAndWait(); + void selectSowDate() throws IOException { + Dialog dateSelection = new Dialog<>(); + dateSelection.setTitle("Select Date"); + dateSelection.setHeaderText(String.format("Select Harvest/Sow Date for %s:", selectedPlant.name())); + dateSelection.setResizable(false); + + DialogPane dialogPane = dateSelection.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + ButtonType sowButton = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(sowButton, ButtonType.CANCEL); + + if (appLoader.loadPaneToDialog("SelectSowDay.fxml", dialogPane) instanceof SelectSowDayController controller) { + controller.initSaveButton((Button) dialogPane.lookupButton(sowButton)); + controller.setSelectedPlant(selectedPlant); + dateSelection.setResultConverter(button -> button.equals(sowButton) ? controller.retrieveResult() : null); + + dateSelection.showAndWait() + .ifPresent(date -> { + try { + garden.plantAsCrop(selectedPlant, date); + } catch (IOException | HardinessZoneNotSetException | PlantNotFoundException e) { + LOG.log(Level.SEVERE, "Couldn't save Crop", e); + } + plantsRoot.fireEvent(new ChangeViewEvent(ChangeViewEvent.CHANGE_MAIN_VIEW, "MyGarden.fxml")); + }); + } } /** @@ -85,8 +114,9 @@ public class PlantsController implements Initializable { * create event listener for selected list entry and search by query * {@inheritDoc} */ - @Override - public void initialize(URL url, ResourceBundle resourceBundle) { + @AfterInject + @SuppressWarnings("unused") + public void init() { setListCellFactory(); fillPlantListWithHardinessZone(); list_plants.itemsProperty().bind(plantListProperty); @@ -97,6 +127,7 @@ public class PlantsController implements Initializable { createFilterSeasons(); createFilterHardinessZone(); lookForSelectedListEntry(); + try { viewFilteredListBySearch(); } catch (HardinessZoneNotSetException e) { @@ -107,7 +138,7 @@ public class PlantsController implements Initializable { } /** - * set text of list view to plant name + * set text of list view to plant name */ private void setListCellFactory() { list_plants.setCellFactory(param -> new ListCell<>() { @@ -127,9 +158,10 @@ public class PlantsController implements Initializable { /** * get plant list according to param season and hardiness zone * fill list view with plant list + * * @param season enum of seasons * @throws HardinessZoneNotSetException throws exception - * @throws IOException throws exception + * @throws IOException throws exception */ private void viewFilteredListBySeason(Seasons season) throws HardinessZoneNotSetException, IOException { clearListView(); @@ -139,8 +171,9 @@ public class PlantsController implements Initializable { /** * get plant list filtered by search plant entry and hardiness zone * fill list view with plant list + * * @throws HardinessZoneNotSetException throws exception when no hardiness zone is defined - * @throws IOException throws exception + * @throws IOException throws exception */ private void viewFilteredListBySearch() throws HardinessZoneNotSetException, IOException { search_plants.textProperty().addListener((observable, oldValue, newValue) -> { @@ -185,7 +218,7 @@ public class PlantsController implements Initializable { for (HardinessZone zone : HardinessZone.values()) { RadioButton radioButton = new RadioButton(zone.name()); radioButton.setToggleGroup(hardinessGroup); - radioButton.setPadding(new Insets(0,0,10,0)); + radioButton.setPadding(new Insets(0, 0, 10, 0)); if (zone.equals(DEFAULT_HARDINESS_ZONE)) { radioButton.setSelected(true); } @@ -207,12 +240,12 @@ public class PlantsController implements Initializable { for (Seasons season : Seasons.values()) { RadioButton radioButton = new RadioButton(season.getName()); radioButton.setToggleGroup(seasonGroup); - radioButton.setPadding(new Insets(0,0,10,0)); - if (season.equals(Seasons.AllSEASONS)) { + radioButton.setPadding(new Insets(0, 0, 10, 0)); + if (season.equals(Seasons.ALLSEASONS)) { radioButton.setSelected(true); } radioButton.selectedProperty().addListener((observable, oldValue, newValue) -> { - if (season.equals(Seasons.AllSEASONS)) { + if (season.equals(Seasons.ALLSEASONS)) { fillPlantListWithHardinessZone(); } else { try { @@ -241,12 +274,12 @@ public class PlantsController implements Initializable { Image img = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png"))); img_plant.setImage(img); list_plants.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - if(newValue != null) { + if (newValue != null) { selectedPlant = newValue; - description_plant.setText(selectedPlant.description()); + description_plant.setText(getPlantDescription()); selectSowDay_button.setDisable(false); Image img1; - if(selectedPlant.image() != null) { + if (selectedPlant.image() != null) { img1 = selectedPlant.image(); } else { img1 = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png"))); @@ -262,6 +295,20 @@ public class PlantsController implements Initializable { }); } + /** + * creates {@link String} of the plant information. + * @return return {@link Plant} description + */ + private String getPlantDescription() { + StringBuilder sb = new StringBuilder(); + sb.append("Name: ").append(selectedPlant.name()) + .append("\nDescription:\n").append(selectedPlant.description()) + .append("\nLight Level: ").append(selectedPlant.light()) + .append("\nSoil: ").append(selectedPlant.soil()) + .append("\nSpacing: ").append(selectedPlant.spacing()); + return sb.toString(); + } + /** * clears the ListView of entries diff --git a/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java b/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java index c5445e1..a26246c 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/SelectSowDayController.java @@ -1,174 +1,107 @@ package ch.zhaw.gartenverwaltung; -import ch.zhaw.gartenverwaltung.gardenplan.Gardenplanmodel; -import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.taskList.PlantNotFoundException; -import ch.zhaw.gartenverwaltung.taskList.TaskListModel; import ch.zhaw.gartenverwaltung.types.GrowthPhaseType; import ch.zhaw.gartenverwaltung.types.Plant; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.fxml.Initializable; import javafx.scene.control.*; -import javafx.stage.Stage; import javafx.util.Callback; -import java.io.IOException; -import java.net.URL; import java.time.LocalDate; -import java.util.List; -import java.util.ResourceBundle; -public class SelectSowDayController implements Initializable { - private Plant selectedPlant = null; - private final TaskListModel taskListModel = new TaskListModel(); - private final Gardenplanmodel gardenplanmodel = new Gardenplanmodel(taskListModel); +/** + * Controller class for the SelectSowDay.fxml file + * Gets opened with a dialog. + */ +public class SelectSowDayController { + private Plant selectedPlant; @FXML private DatePicker datepicker; @FXML - private Label popup_label; - - @FXML - private Button save_button; - + private RadioButton harvest_radio; @FXML private RadioButton sow_radio; - - public SelectSowDayController() throws IOException {} + @FXML + public ToggleGroup phase_group; /** - * close the date selector window - * @param event event + * if sow date radio button was selected return sow date + * if sow date was not selected get sow from harvest day and return sow date + * @return {@link LocalDate} of the sow date */ - @FXML - void cancel(ActionEvent event) { - closeWindow(); - } - - /** - * get sow date from datePicker or calculate sow date from harvest date - * save selected plant and sow date - * @param event event - */ - @FXML - void save(ActionEvent event) throws HardinessZoneNotSetException, IOException, PlantNotFoundException { - LocalDate sowDate; - if (sow_radio.isSelected()) { - sowDate = datepicker.getValue(); - } else { - //ToDo method to get current lifecycle group in plant - sowDate = selectedPlant.sowDateFromHarvestDate(datepicker.getValue(), 0); + public LocalDate retrieveResult() { + LocalDate sowDate = datepicker.getValue(); + if (harvest_radio.isSelected()) { + sowDate = selectedPlant.sowDateFromHarvestDate(sowDate); } - gardenplanmodel.plantAsCrop(selectedPlant, sowDate); - closeWindow(); + return sowDate; } /** - * save the plant which will be planted and update label + * Set the {@link Plant} for which a date should be selected. + * * @param plant Plant */ - public void getSelectedPlant(Plant plant) { + public void setSelectedPlant(Plant plant) { selectedPlant = plant; - popup_label.setText("Select Harvest/Sow Date for" + selectedPlant.name()); } /** * add listener and set default values - * {@inheritDoc} - * @param location location - * @param resources resources */ - @Override - public void initialize(URL location, ResourceBundle resources) { + @FXML + public void initialize() { clearDatePickerEntries(); - Callback dayCellFactory= getDayCellFactory(); + Callback dayCellFactory = getDayCellFactory(); datepicker.setDayCellFactory(dayCellFactory); - datepicker.getEditor().setEditable(false); + datepicker.setEditable(false); - enableDisableSaveButton(); + sow_radio.setUserData(GrowthPhaseType.SOW); + harvest_radio.setUserData(GrowthPhaseType.HARVEST); + } + + /** + * Disable save button when date picker is empty + * @param saveButton {@link Button} to be disabled + */ + public void initSaveButton(Button saveButton) { + saveButton.disableProperty().bind(datepicker.valueProperty().isNull()); } /** * clear date picker editor when radio button is changed */ private void clearDatePickerEntries() { - sow_radio.selectedProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean isNowSelected) { - datepicker.getEditor().clear(); - } - }); + harvest_radio.selectedProperty().addListener((observable, oldValue, isNowSelected) -> datepicker.setValue(null)); } /** * date picker disable/enable dates according to selected plant: sow or harvest day + * * @return cellFactory of datePicker */ private Callback getDayCellFactory() { - final Callback dayCellFactory = new Callback() { + return (datePicker) -> new DateCell() { + private final LocalDate today = LocalDate.now(); @Override - public DateCell call(final DatePicker datePicker) { - return new DateCell() { - @Override - public void updateItem(LocalDate item, boolean empty) { - super.updateItem(item, empty); - setDisable(true); - setStyle("-fx-background-color: #ffc0cb;"); - List dates; - LocalDate today = LocalDate.now(); - if (sow_radio.isSelected()) { - dates = selectedPlant.getDateListOfGrowthPhase(GrowthPhaseType.SOW); - } else { - dates = selectedPlant.getDateListOfGrowthPhase(GrowthPhaseType.HARVEST); - } - for (LocalDate date : dates) { - if (item.getMonth() == date.getMonth() - && item.getDayOfMonth() == date.getDayOfMonth() - && item.compareTo(today) > 0) { - setDisable(false); - setStyle("-fx-background-color: #32CD32;"); - } - } - if ((!sow_radio.isSelected() && selectedPlant.sowDateFromHarvestDate(item, 0).compareTo(today) < 0)) { - setDisable(true); - setStyle("-fx-background-color: #ffc0cb;"); - } + public void updateItem(LocalDate item, boolean empty) { + super.updateItem(item, empty); + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + + if (item.compareTo(today) > 0 && (!harvest_radio.isSelected() || selectedPlant.sowDateFromHarvestDate(item).compareTo(today) >= 0)) { + GrowthPhaseType selectedPhase = (GrowthPhaseType) phase_group.getSelectedToggle().getUserData(); + + if (selectedPlant.isDateInPhase(item, selectedPhase)) { + setDisable(false); + setStyle("-fx-background-color: #32CD32;"); } - }; - } - }; - return dayCellFactory; - } - - /** - * close date picker window - */ - private void closeWindow() { - Stage stage = (Stage) save_button.getScene().getWindow(); - stage.close(); - } - - /** - * disable save button, when there is no date selected in date picker - */ - private void enableDisableSaveButton() { - save_button.setDisable(true); - datepicker.getEditor().textProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, String oldValue, String newValue) { - if (newValue == null || newValue.equals("")) { - save_button.setDisable(true); - } else { - save_button.setDisable(false); } } - }); + }; } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/Settings.java b/src/main/java/ch/zhaw/gartenverwaltung/Settings.java new file mode 100644 index 0000000..491db5d --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/Settings.java @@ -0,0 +1,121 @@ +package ch.zhaw.gartenverwaltung; + +import ch.zhaw.gartenverwaltung.backgroundtasks.email.SmtpCredentials; +import ch.zhaw.gartenverwaltung.types.HardinessZone; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import java.util.List; +import java.util.Objects; + +/** + * Singleton Class to store default Settings and User Settings + */ +public class Settings { + /* + * The Class instance to use everywhere + */ + + private static final Settings instance; + /** + * The current Hardiness zone initialized as default value Zone_8A + */ + private HardinessZone currentHardinessZone = HardinessZone.ZONE_8A; + /** + * Setting to show or hide the Tutorial in Main Menu + */ + private final BooleanProperty showTutorial = new SimpleBooleanProperty(false); + /** + * The SMTP Credentials which are used to send E-Mail Notifications + * The following Google Account is created for Testing: + * E-Mail Address: pm3.hs22.it21b.win.team1@gmail.com + * Account Password: Gartenverwaltung.PM3.2022 + * SMTP Password: bisefhhjtrrhtoqr + */ + private SmtpCredentials smtpCredentials = new SmtpCredentials("imap.gmail.com", "587", "pm3.hs22.it21b.win.team1@gmail.com", "pm3.hs22.it21b.win.team1@gmail.com", "bisefhhjtrrhtoqr", true); + /** + * List of Receivers for E-Mailnotifications. Multiple E-Mailadresses can be separated by ";" + * Following Public E-mail address was used for Testing: pm3.hs22.it21b.win.team1@mailinator.com + * The E-Mail inbox of this address can be found here: https://www.mailinator.com/v4/public/inboxes.jsp?to=pm3.hs22.it21b.win.team1 + */ + private String mailNotificationReceivers = "pm3.hs22.it21b.win.team1@mailinator.com"; + /** + * String Template to create E-Mail Subject for Notifications + * Variables: Task Name + */ + private String mailNotificationSubjectTemplate = "Task %s is due!"; + /** + * String Template to create E-Mail Text for Notifications + * Variables: Task Name, plant Name, nextExecution, Task description + */ + private String mailNotificationTextTemplate = "Dear user\nYour gardentask %s for plant %s is due at %tF. Don't forget to confirm in your application if the task is done.\nTask description:\n%s"; + + /** + * Location of the garden can be set by user (Used for Weather Service) + */ + private String location = ""; + + /* + Create Instance of Settings + */ + static { + instance = new Settings(); + } + + public static Settings getInstance() { + return Settings.instance; + } + + /** + * Private constructor to prevent Classes from creating more instances + */ + private Settings() {} + + public HardinessZone getCurrentHardinessZone() { + return currentHardinessZone; + } + + /** + * Method to set the current Hardiness Zone. If no Hardiness Zone is given the default zone (ZONE_8A) will be used. + * @param currentHardinessZone the new Hardiness Zone + */ + public void setCurrentHardinessZone(HardinessZone currentHardinessZone) { + this.currentHardinessZone = Objects.requireNonNullElse(currentHardinessZone, HardinessZone.ZONE_8A); + } + + public void setShowTutorial (boolean showTutorial) { + this.showTutorial.setValue(showTutorial); + } + + public BooleanProperty getShowTutorialProperty() { + return this.showTutorial; + } + + public boolean getShowTutorial() { + return this.showTutorial.get(); + } + + public void setLocation(String location) { + this.location = location; + } + + public String getLocation() { + return this.location; + } + + public SmtpCredentials getSmtpCredentials() { + return smtpCredentials; + } + + public String getMailNotificationReceivers() { + return mailNotificationReceivers; + } + + public String getMailNotificationSubjectTemplate() { + return mailNotificationSubjectTemplate; + } + + public String getMailNotificationTextTemplate() { + return mailNotificationTextTemplate; + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/SettingsController.java b/src/main/java/ch/zhaw/gartenverwaltung/SettingsController.java new file mode 100644 index 0000000..db076db --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/SettingsController.java @@ -0,0 +1,116 @@ +package ch.zhaw.gartenverwaltung; + +import ch.zhaw.gartenverwaltung.bootstrap.AppLoader; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; +import ch.zhaw.gartenverwaltung.types.HardinessZone; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +import java.io.IOException; +import java.net.URL; +import java.util.Objects; +import java.util.ResourceBundle; + +/** + * Controller class for the Settings.fxml file + */ +public class SettingsController implements Initializable { + Settings settings = Settings.getInstance(); + + @Inject + AppLoader appLoader; + @FXML + private ComboBox selectHardinessZone_comboBox; + + @FXML + private CheckBox showTutorial_checkBox; + + @FXML + private Button location_button; + + @FXML + private Label location_label; + + /** + * open dialog to set location + * @param event event + * @throws IOException exception + */ + @FXML + void setLocation(ActionEvent event) throws IOException { + openTextFieldDialog(); + } + + /** + * save selected values to {@link Settings} + */ + public void saveSettings() { + settings.setShowTutorial(showTutorial_checkBox.isSelected()); + settings.setCurrentHardinessZone(selectHardinessZone_comboBox.getValue()); + } + + /** + * save default values form {@link Settings} + * @param location location + * @param resources resources + */ + @Override + public void initialize(URL location, ResourceBundle resources) { + showTutorial_checkBox.setSelected(settings.getShowTutorial()); + selectHardinessZone_comboBox.getItems().addAll(HardinessZone.values()); + selectHardinessZone_comboBox.setValue(settings.getCurrentHardinessZone()); + setIconToButton(location_button, "locationIcon.png"); + location_label.setText(settings.getLocation()); + } + + /** + * adds icon to button + * @param button the button which get the icon + * @param iconFileName file name of icon + */ + private void setIconToButton(Button button, String iconFileName) { + Image img = new Image(String.valueOf(getClass().getResource("icons/" + iconFileName))); + ImageView imageView = new ImageView(img); + imageView.setFitHeight(20); + imageView.setPreserveRatio(true); + button.setGraphic(imageView); + } + + /** + * opens Dialog to set exception + * @throws IOException exception + */ + private void openTextFieldDialog() throws IOException { + Dialog dialog = new Dialog<>(); + dialog.setTitle("Set Location of your Garden"); + dialog.setHeaderText("set Location of your Garden!"); + dialog.setResizable(false); + + DialogPane dialogPane = dialog.getDialogPane(); + + dialogPane.getStylesheets().add( + Objects.requireNonNull(getClass().getResource("bootstrap/dialogStyle.css")).toExternalForm()); + dialogPane.getStyleClass().add("myDialog"); + + ButtonType save = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(save, ButtonType.CANCEL); + + if (appLoader.loadPaneToDialog("TextFieldFormular.fxml", dialogPane) instanceof TextFieldFormularController controller) { + controller.setDescription_label("Set"); + controller.setValueTextArea(settings.getLocation()); + controller.initSaveButton((Button) dialogPane.lookupButton(save)); + + dialog.setResultConverter(button -> button.equals(save) ? controller.getValue() : null); + + dialog.showAndWait() + .ifPresent(string -> { + settings.setLocation(string); + location_label.setText(settings.getLocation()); + }); + } + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java b/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java new file mode 100644 index 0000000..791da18 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java @@ -0,0 +1,190 @@ +package ch.zhaw.gartenverwaltung; + +import ch.zhaw.gartenverwaltung.bootstrap.AfterInject; +import ch.zhaw.gartenverwaltung.bootstrap.Inject; +import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.models.Garden; +import ch.zhaw.gartenverwaltung.models.GardenSchedule; +import ch.zhaw.gartenverwaltung.types.Crop; +import ch.zhaw.gartenverwaltung.types.Plant; +import ch.zhaw.gartenverwaltung.types.Task; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.util.Callback; + +import java.io.IOException; +import java.net.URL; +import java.time.LocalDate; +import java.util.ResourceBundle; + +/** + * Controller class for the TaskFormular.fxml file + */ +public class TaskFormularController implements Initializable { + private Crop crop; + private Plant plant; + private Task task = null; + + @Inject + private GardenSchedule gardenSchedule; + @Inject + private Garden garden; + @Inject + private PlantList plantList; + + @FXML + private TextArea description_area; + + @FXML + private DatePicker end_datePicker; + + @FXML + private TextField interval_field; + + @FXML + private DatePicker start_datePicker; + + @FXML + private TextField taskName_field; + + @AfterInject + @SuppressWarnings("unused") + + /** + * returns the edited or added {@link Task} + * @param crop {@link Crop} which was selected + * @return {@link Task} which was edited or added + */ + public Task returnResult(Crop crop) { + int interval = 0; + if (interval_field.getText() != null && !(interval_field.getText().isEmpty() || interval_field.getText().equals(""))) { + interval = Integer.parseInt(interval_field.getText()); + } + Task task = new Task(taskName_field.getText(), description_area.getText(), + start_datePicker.getValue(), end_datePicker.getValue(), + interval, crop.getCropId().get()); + if (this.task != null) return this.task.updateTask(task); + return task; + } + + /** + * set selected crop and get the plant from the crop. + * @param crop {@link Crop} which was selected + * @throws HardinessZoneNotSetException exception + * @throws IOException exception + */ + public void setCorp(Crop crop) throws HardinessZoneNotSetException, IOException { + this.crop = crop; + this.plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).get(); + } + + /** + * set the values of task into the labels and datePicker. + * @param task {@link Task} which was given + */ + public void setTaskValue(Task task) { + this.task = task; + taskName_field.setText(task.getName()); + description_area.setText(task.getDescription()); + start_datePicker.setValue(task.getStartDate()); + end_datePicker.setValue(task.getEndDate().orElse(null)); + if(task.getInterval().orElse(null)!=null) { + interval_field.setText(task.getInterval().get().toString()); + } else { + interval_field.setText(null); + } + } + + /** + * dayCellFactory of the start date + * @return {@link Callback} of the dayCellFactory + */ + private Callback getDayCellFactoryStartDate() { + + return (datePicker) -> new DateCell() { + private final LocalDate today = LocalDate.now(); + + @Override + public void updateItem(LocalDate item, boolean empty) { + super.updateItem(item, empty); + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + + if (item.compareTo(today) > 0 && item.compareTo(crop.getStartDate()) > 0 && item.compareTo(crop.getStartDate().plusDays(plant.timeToHarvest(0))) < 0) { + setDisable(false); + setStyle("-fx-background-color: #32CD32;"); + } + + if (end_datePicker.getValue() != null && item.compareTo(end_datePicker.getValue()) > 0) { + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + } + } + }; + } + + /** + * dayCellFactory of the end date + * @return {@link Callback} of the dayCellFactory + */ + private Callback getDayCellFactoryEndDate() { + + return (datePicker) -> new DateCell() { + private final LocalDate today = LocalDate.now(); + + @Override + public void updateItem(LocalDate item, boolean empty) { + super.updateItem(item, empty); + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + + if (item.compareTo(today) > 0 && item.compareTo(crop.getStartDate()) > 0 && item.compareTo(crop.getStartDate().plusDays(plant.timeToHarvest(0))) < 0) { + setDisable(false); + setStyle("-fx-background-color: #32CD32;"); + } + + if (start_datePicker.getValue() != null && item.compareTo(start_datePicker.getValue()) < 0) { + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + } + } + }; + } + + /** + * disable button until condition meet. + * @param button {@link Button} which was given + */ + public void initSaveButton(Button button) { + interval_field.textProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null && !newValue.matches("\\d*")) { + interval_field.setText(newValue.replaceAll("[^\\d]", "")); + } + }); + + button.disableProperty().bind(start_datePicker.valueProperty().isNull() + .or(taskName_field.textProperty().isEmpty()) + .or(description_area.textProperty().isEmpty())); + } + + /** + * initialize dayCellFactories + * @param location + * The location used to resolve relative paths for the root object, or + * {@code null} if the location is not known. + * + * @param resources + * The resources used to localize the root object, or {@code null} if + * the root object was not localized. + */ + @Override + public void initialize(URL location, ResourceBundle resources) { + start_datePicker.setDayCellFactory(getDayCellFactoryStartDate()); + start_datePicker.setEditable(false); + + end_datePicker.setDayCellFactory(getDayCellFactoryEndDate()); + end_datePicker.setEditable(false); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/TextFieldFormularController.java b/src/main/java/ch/zhaw/gartenverwaltung/TextFieldFormularController.java new file mode 100644 index 0000000..f045ae5 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/TextFieldFormularController.java @@ -0,0 +1,60 @@ +package ch.zhaw.gartenverwaltung; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; + +/** + * Controller class for the TexFieldFormular.fxml file + */ +public class TextFieldFormularController { + + @FXML + private Label description_label; + + @FXML + private TextField text_area; + + + /** + * set description label + * @param string string of the description + */ + public void setDescription_label(String string) { + description_label.setText(string); + } + + /** + * set text area value + * @param string string of text area value + */ + public void setValueTextArea(String string) { + text_area.setText(string); + } + + /** + * return value of text area + * @return string of the tex area + */ + public String getValue() { + return text_area.getText(); + } + + /** + * Disable Button until condition meet + * @param button {@link Button} which is gets dissabled + */ + public void initSaveButton(Button button) { + text_area.textProperty().addListener((observable, oldValue, newValue) -> { + if (newValue.matches("\\d*\\.?\\d*")) { + text_area.setText(newValue); + } else { + text_area.setText(oldValue); + } + }); + + button.disableProperty().bind(text_area.textProperty().isEmpty()); + } + +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java b/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java new file mode 100644 index 0000000..7c57197 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java @@ -0,0 +1,93 @@ +package ch.zhaw.gartenverwaltung; + + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import java.io.InputStream; + +/** + * Controller class for the Tutorial.fxml file + */ +public class TutorialController { + + @FXML + public Button previousPageButton; + @FXML + public Button nextPageButton; + @FXML + public StackPane tourPages; + public ImageView imgAddNewPlant; + public ImageView imgTaskList; + public ImageView imgSelectDate; + public ImageView imgAddTaskButton; + public ImageView imgDetailDeleteButtons; + + private int page = 0; + + @FXML + public void initialize() { + switchViews(); + setButtonAbilities(); + + setImageView(imgAddNewPlant, "add-new-plant.png"); + setImageView(imgSelectDate, "select-sow-harvest.png"); + setImageView(imgDetailDeleteButtons, "details-delete.png"); + setImageView(imgTaskList, "schedule.png"); + setImageView(imgAddTaskButton, "add-task.png"); + } + + /** + * update the given image view with screenshot or placeholder image. + * @param imageView the image view to update + * @param fileName the file name of the source + */ + private void setImageView(ImageView imageView, String fileName) { + InputStream is = PlantsController.class.getResourceAsStream("screenshots/" + fileName); + Image image; + if (is != null) { + image = new Image(String.valueOf(PlantsController.class.getResource("screenshots/" + fileName))); + } else { + image = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png"))); + } + imageView.setImage(image); + } + + public void viewNextPage() { + page++; + switchViews(); + setButtonAbilities(); + } + public void viewPreviousPage() { + page--; + switchViews(); + setButtonAbilities(); + } + + /** + * disable next or close button according to the location of button + */ + private void setButtonAbilities() { + previousPageButton.setDisable(page <= 0); + nextPageButton.setDisable(page >= tourPages.getChildren().size() - 1); + } + + /** + * switch to next view + */ + private void switchViews() { + tourPages.getChildren().forEach(node -> node.setOpacity(0)); + tourPages.getChildren().get(page).setOpacity(1); + } + + /** + * close Tutorial + */ + public void closeTutorial() { + Stage root = (Stage) tourPages.getScene().getWindow(); + root.close(); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/BackgroundTasks.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/BackgroundTasks.java new file mode 100644 index 0000000..e0071a0 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/BackgroundTasks.java @@ -0,0 +1,74 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks; + +import ch.zhaw.gartenverwaltung.CropDetailController; +import ch.zhaw.gartenverwaltung.backgroundtasks.weather.WeatherGradenTaskPlanner; +import ch.zhaw.gartenverwaltung.io.CropList; +import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.io.TaskList; +import ch.zhaw.gartenverwaltung.models.PlantNotFoundException; +import ch.zhaw.gartenverwaltung.types.Task; + +import javax.mail.MessagingException; +import java.io.IOException; +import java.time.LocalDate; +import java.util.List; +import java.util.TimerTask; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class with tasks which must be executed periodic and not triggered by user. Method run must be called in a fix interval. + */ +public class BackgroundTasks extends TimerTask { + private static final Logger LOG = Logger.getLogger(CropDetailController.class.getName()); + private final TaskList taskList; + private final Notifier notifier; + private final WeatherGradenTaskPlanner weatherGardenTaskPlaner; + + public BackgroundTasks(TaskList taskList, CropList cropList, PlantList plantList) { + this.taskList = taskList; + notifier = new Notifier(taskList, plantList, cropList); + weatherGardenTaskPlaner = new WeatherGradenTaskPlanner(taskList, plantList, cropList); + } + + /** + * Changes the field "nextExecution" of all tasks if it's in the past to the actual date as long as they are not done. + * @throws IOException if the taskList can't be read. + */ + private void movePastTasks() throws IOException { + List taskList = this.taskList.getTaskList(LocalDate.MIN.plusDays(1), LocalDate.now().minusDays(1)); + for (Task task : taskList) { + if (!task.isDone()) { + task.setNextExecution(LocalDate.now()); + this.taskList.saveTask(task); + } + } + } + + /** + * Method to call if Tasks should be executed. It calls all Background tasks after each other. + */ + @Override + public void run() { + try { + movePastTasks(); + } catch (IOException e) { + e.printStackTrace(); + LOG.log(Level.WARNING, "Could not execute Background Task: move past Tasks ", e.getCause()); + } + try { + weatherGardenTaskPlaner.refreshTasks(); + } catch (IOException | HardinessZoneNotSetException | PlantNotFoundException e) { + e.printStackTrace(); + LOG.log(Level.WARNING, "Could not execute Background Task: Refresh Tasks by WeatherGardenTaskPlaner ", e.getCause()); + } + try { + notifier.sendNotifications(); + } catch (IOException | MessagingException | HardinessZoneNotSetException e) { + e.printStackTrace(); + LOG.log(Level.WARNING, "Could not execute Background Task: send Notification for due Tasks", e.getCause()); + } + } +} + diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/Notifier.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/Notifier.java new file mode 100644 index 0000000..c28a4b8 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/Notifier.java @@ -0,0 +1,66 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks; + +import ch.zhaw.gartenverwaltung.Settings; +import ch.zhaw.gartenverwaltung.backgroundtasks.email.EMailSender; +import ch.zhaw.gartenverwaltung.io.CropList; +import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.io.TaskList; +import ch.zhaw.gartenverwaltung.types.Crop; +import ch.zhaw.gartenverwaltung.types.Task; + +import javax.mail.MessagingException; +import java.io.IOException; +import java.time.LocalDate; + +/** + * Class to send Notifications to the user + */ +public class Notifier { + private final TaskList taskList; + private final CropList cropList; + private final PlantList plantList; + private final EMailSender eMailSender = new EMailSender(); + + public Notifier(TaskList taskList, PlantList plantList, CropList cropList) { + this.taskList = taskList; + this.cropList = cropList; + this.plantList = plantList; + } + + /** + * Method to send a Notification to the user for e specific Task + * @param task The task to use for notification + * @throws IOException if tasklist can not be read + * @throws MessagingException if E-Mail can not be sent for any reason + * @throws HardinessZoneNotSetException if hardiness zone is not set in plant list. + */ + private void sendNotification(Task task) throws IOException, MessagingException, HardinessZoneNotSetException { + String plantName = "unkown plant"; + if(cropList.getCropById((task.getCropId())).isPresent()){ + Crop crop = cropList.getCropById(task.getCropId()).get(); + if(plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).isPresent()) { + plantName = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).get().name(); + } + } + String messageSubject = String.format(Settings.getInstance().getMailNotificationSubjectTemplate(), task.getName()); + String messageText = String.format(Settings.getInstance().getMailNotificationTextTemplate(), task.getName(), plantName, task.getNextExecution(), task.getDescription()); + eMailSender.sendMails(Settings.getInstance().getMailNotificationReceivers(), messageSubject, messageText); + } + + /** + * Sends a notification to the user for each task which is due and not done every day as long as it's not done. + * @throws IOException if tasklist can not be read + * @throws MessagingException if E-Mail can not be sent for any reason + * @throws HardinessZoneNotSetException if hardiness zone is not set in plant list. + */ + public void sendNotifications() throws IOException, MessagingException, HardinessZoneNotSetException { + for (Task task : taskList.getTaskList(LocalDate.MIN, LocalDate.MAX)) { + if (task.getNextNotification() != null && task.getNextNotification().isBefore(LocalDate.now().minusDays(1))) { + sendNotification(task); + task.setNextNotification(LocalDate.now().plusDays(1)); + taskList.saveTask(task); + } + } + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/email/EMailSender.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/email/EMailSender.java new file mode 100644 index 0000000..d77f918 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/email/EMailSender.java @@ -0,0 +1,44 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks.email; + +import ch.zhaw.gartenverwaltung.Settings; + +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Class to send E-Mails + */ +public class EMailSender { + + /** + * Method to send E-Mail to one or multiple recipients + * @param recipients recipients E-Mail addresses separated by ";" + * @param subject Subject of the E-Mail + * @param text E-Mail message Text as rear text or html + * @throws MessagingException If sending the E-Mail fails for any reason + */ + public void sendMails(String recipients, String subject, String text) throws MessagingException { + printMail(recipients, subject, text); + MimeMessage message = new MimeMessage(Settings.getInstance().getSmtpCredentials().getSession()); + message.addHeader("Content-type", "text/HTML; charset=UTF-8"); + message.addHeader("format", "flowed"); + message.addHeader("Content-Transfer-Encoding", "8bit"); + message.setFrom(new InternetAddress(Settings.getInstance().getSmtpCredentials().fromAddress())); + message.setReplyTo(InternetAddress.parse(Settings.getInstance().getSmtpCredentials().fromAddress(), false)); + message.setSubject(subject); + message.setText(text); + message.setSentDate(new Date()); + message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipients, false)); + Transport.send(message); + } + + private void printMail(String receiver, String subject, String text){ + System.out.printf("\nSending E-Mail:\nTo: %s\nSubject: %s\nMessage:\n%s\n", receiver, subject, text); + } + +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/email/SmtpCredentials.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/email/SmtpCredentials.java new file mode 100644 index 0000000..b2b8737 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/email/SmtpCredentials.java @@ -0,0 +1,54 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks.email; + +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import java.net.Authenticator; +import java.util.Properties; + +/** + * Class to store SMTP Credentials to send E-Mails and create + * corresponding Session Object + */ +public record SmtpCredentials( + String host, + String port, + String fromAddress, + String username, + String password, + boolean startTLS + ) { + + /** + * Creates a Properties Object with SMTP Server Information + * @return the created Properties Object + */ + private Properties getProperties() { + Properties properties = new Properties(); + properties.put("mail.smtp.host", host); + properties.put("mail.smtp.port", port); + properties.put("mail.smtp.auth", "true"); + properties.put("mail.smtp.starttls.enable", startTLS ? "true" : "false"); + return properties; + } + + /** + * Creates a javax.mail.Authenticator Object with username and password for SMTP Server + * @return the created javax.mail.Authenticator Object + */ + private javax.mail.Authenticator getAuthenticator() { + return new javax.mail.Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }; + } + + /** + * Method to get the Session Instance with the given SMTP Credentials + * @return the Session Instance + */ + public Session getSession() { + return Session.getInstance(getProperties(), getAuthenticator()); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/SevereWeather.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/SevereWeather.java new file mode 100644 index 0000000..a52ffc4 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/SevereWeather.java @@ -0,0 +1,8 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks.weather; + +/** + * Enum of possible Weather events + */ +public enum SevereWeather { + FROST,SNOW,HAIL,NO_SEVERE_WEATHER,RAIN +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/WeatherGradenTaskPlanner.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/WeatherGradenTaskPlanner.java new file mode 100644 index 0000000..555e88b --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/WeatherGradenTaskPlanner.java @@ -0,0 +1,162 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks.weather; + +import ch.zhaw.gartenverwaltung.io.CropList; +import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; +import ch.zhaw.gartenverwaltung.io.PlantList; +import ch.zhaw.gartenverwaltung.io.TaskList; +import ch.zhaw.gartenverwaltung.models.PlantNotFoundException; +import ch.zhaw.gartenverwaltung.types.*; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +/** + * The WeatherGardenTaskPlanner creates Tasks based on weather events and the rain amount from the last days + */ +public class WeatherGradenTaskPlanner { + private final TaskList taskList; + private final PlantList plantList; + private final CropList cropList; + WeatherService weatherService; + private final LocalDate dateSevereWeather = LocalDate.now(); + + public WeatherGradenTaskPlanner(TaskList taskList, PlantList plantList, CropList cropList) { + this.taskList = taskList; + this.plantList = plantList; + this.cropList = cropList; + weatherService = new WeatherService(); + } + + /** + * Method to refresh watering tasks and severe weather tasks + * @throws IOException If the database cannot be accessed + * @throws HardinessZoneNotSetException If the hardiness zone is not available + * @throws PlantNotFoundException if the plant is not available for the watering task + */ + public void refreshTasks() throws IOException, HardinessZoneNotSetException, PlantNotFoundException { + getSevereWeatherEvents(); + getRainAmount(); + } + + private void getSevereWeatherEvents() throws IOException { + SevereWeather actualWeather = weatherService.causeSevereWeather(1); + + List actualCrops = cropList.getCrops(); + for (Crop crop : actualCrops) { + + List actualCropTasks = taskList.getTaskForCrop(crop.getCropId().orElse(-1L)); + + if (SevereWeather.HAIL.equals(actualWeather)) { + createPreHailTask(crop, actualCropTasks); + } else if (SevereWeather.FROST.equals(actualWeather)) { + createPreFrostTask(crop, actualCropTasks); + } else if (SevereWeather.SNOW.equals(actualWeather)) { + createPreSnowTask(crop, actualCropTasks); + } + } + } + + private void getRainAmount() throws IOException, HardinessZoneNotSetException, PlantNotFoundException { + int rainAmount = weatherService.causeRainAmount(3); + adjustWateringTask(rainAmount); + } + + /** + * Method to create a PreHailTask and saves it in the tasklist + * @throws IOException If the database cannot be accessed + */ + private void createPreHailTask(Crop crop, List actualTasksForCrop) throws IOException { + Task preHailTask = new Task("Hail", + "During a summer Thunderstorm it could hail heavily. THe Hail could damage the crops. To prevent damage cover the plants with a strong tarpaulin", + dateSevereWeather,dateSevereWeather.plusDays(1L),crop.getCropId().orElse(-1L)); + + List hailTasks = actualTasksForCrop.stream().filter(task -> task.getName().equals("Hail")).toList(); + + if(isNoSevereWeatherTaskAtDate(preHailTask, hailTasks) && hailTasks.isEmpty()){ + taskList.saveTask(preHailTask); + } + } + + /** + * Method to create a PreFrosttask and saves it in the tasklist + * @throws IOException If the database cannot be accessed + */ + private void createPreFrostTask(Crop crop, List actualTasksForCrop) throws IOException { + Task preFrostTask = new Task("Frost", + "The temperatur falls below zero degrees, cover especially the root with wool", + dateSevereWeather,dateSevereWeather.plusDays(1L),crop.getCropId().orElse(-1L)); + + List frostTasks = actualTasksForCrop.stream().filter(task -> task.getName().equals("Frost")).toList(); + + if(isNoSevereWeatherTaskAtDate(preFrostTask, frostTasks) && frostTasks.isEmpty()){ + taskList.saveTask(preFrostTask); + } + } + + /** + * Method to create a PreSnowTask and saves it in the tasklist + * @throws IOException If the database cannot be accessed + */ + private void createPreSnowTask(Crop crop, List actualTasksForCrop) throws IOException { + Task preSnowTask = new Task("Snow", + "The weather brings little snowfall. Cover your crops", + dateSevereWeather, dateSevereWeather.plusDays(1L), crop.getCropId().orElse(-1L)); + + List snowTasklist = actualTasksForCrop.stream().filter(task -> task.getName().equals("Snow")).toList(); + + if(isNoSevereWeatherTaskAtDate(preSnowTask, snowTasklist) && snowTasklist.isEmpty()){ + taskList.saveTask(preSnowTask); + } + } + + /** + * Method to create a PreSnowTask and saves it in the tasklist + * @param preSevereWeatherTask the Task which would be added if there is not already + * the same Type of severe weather task + * @param severeWeatherTasks List of severe weather tasks from e specific severe weather + * @return true If there is not already a severe weather task of the same type of preSevereWeatherTask + * task at the date of the preSevereWeatherTask + */ + private boolean isNoSevereWeatherTaskAtDate(Task preSevereWeatherTask, List severeWeatherTasks) { + List severeWeatherTasksAtDate = new ArrayList<>(); + for (Task task : severeWeatherTasks) { + if (task.getStartDate() == preSevereWeatherTask.getStartDate()) { + severeWeatherTasksAtDate.add(task); + } + } + return severeWeatherTasksAtDate.isEmpty(); + } + + /** + * Method to adjust the water plant tasks + * @param rainAmount Amount of rain from the last 7 days + * @throws IOException If the database cannot be accessed + * @throws HardinessZoneNotSetException If the hardiness zone is not available + * @throws PlantNotFoundException if the plant is not available for the watering task + */ + private void adjustWateringTask(int rainAmount) throws HardinessZoneNotSetException, IOException, PlantNotFoundException { + + for (Crop crop : cropList.getCrops()) { + Plant plant = plantList.getPlantById(HardinessZone.ZONE_8A,crop.getPlantId()).orElseThrow(PlantNotFoundException::new); + + if(plant.wateringCycle().litersPerSqM() < rainAmount){ + adjustNextExecutionOfWateringTasks(taskList.getTaskList(LocalDate.now(), LocalDate.now().plusDays(7))); + } + } + + } + + /** + * Method to set next execution date of the water plant tasks + * @param cropTaskList List with tasks from crops + */ + private void adjustNextExecutionOfWateringTasks(List cropTaskList){ + for(Task task : cropTaskList){ + if(task.getName().equals("water plant")){ + task.setNextExecution(task.getNextExecution().plusDays(task.getInterval().orElse(1))); + } + } + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/WeatherService.java b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/WeatherService.java new file mode 100644 index 0000000..0bbd051 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/backgroundtasks/weather/WeatherService.java @@ -0,0 +1,41 @@ +package ch.zhaw.gartenverwaltung.backgroundtasks.weather; +/** + * The WeatherService is a class to cause weather events for the WeatherGardenTaskPlanner + */ +public class WeatherService { + private static final int NO_RAIN = 0; + private static final int LITTLE_RAIN = 15; + private static final int RAIN = 25; + private static final int HEAVY_RAIN = 50; + + /** + * Method to simmulate a Weather Service for testing + * @param randomWeather random int. Range: 1-3 + * @return the selected SevereWeather + */ + public SevereWeather causeSevereWeather(int randomWeather) { + return switch (randomWeather) { + case 1 -> SevereWeather.HAIL; + case 2 -> SevereWeather.SNOW; + case 3 -> SevereWeather.FROST; + default -> null; + }; + + } + + /** + * Method to simulate a Weather Service for testing + * @param randomRainAmount random int. Range: 1-4 + * @return the selected rain amount + */ + public int causeRainAmount(int randomRainAmount) { + return switch (randomRainAmount) { + case 1 -> NO_RAIN; + case 2 -> LITTLE_RAIN; + case 3 -> RAIN; + case 4 -> HEAVY_RAIN; + default -> -1; + }; + + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AfterInject.java b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AfterInject.java new file mode 100644 index 0000000..23772bd --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AfterInject.java @@ -0,0 +1,14 @@ +package ch.zhaw.gartenverwaltung.bootstrap; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a method to be executed after all dependencies annotated with {@link Inject} + * have been injected. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AfterInject { } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java new file mode 100644 index 0000000..b6c8d6a --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java @@ -0,0 +1,167 @@ +package ch.zhaw.gartenverwaltung.bootstrap; + +import ch.zhaw.gartenverwaltung.CropDetailController; +import ch.zhaw.gartenverwaltung.Main; +import ch.zhaw.gartenverwaltung.io.*; +import ch.zhaw.gartenverwaltung.models.Garden; +import ch.zhaw.gartenverwaltung.models.GardenSchedule; +import ch.zhaw.gartenverwaltung.models.PlantListModel; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.DialogPane; +import javafx.scene.layout.Pane; +import javafx.stage.Stage; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Class responsible for bootstrapping the application wide dependencies + * and injecting them into JavaFX Controllers. + */ +public class AppLoader { + private static final Logger LOG = Logger.getLogger(CropDetailController.class.getName()); + /** + * Caching the panes + */ + private final Map panes = new HashMap<>(); + + /** + * Application-wide dependencies + */ + private final Map dependencies = new HashMap<>(); + + public AppLoader() throws IOException { + PlantList plantList = new JsonPlantList(); + CropList cropList = new JsonCropList(); + TaskList taskList = new JsonTaskList(); + GardenSchedule gardenSchedule = new GardenSchedule(taskList, plantList); + + dependencies.put(PlantList.class.getSimpleName(), plantList); + dependencies.put(CropList.class.getSimpleName(), cropList); + dependencies.put(TaskList.class.getSimpleName(), taskList); + + dependencies.put(PlantListModel.class.getSimpleName(), new PlantListModel(plantList)); + + dependencies.put(GardenSchedule.class.getSimpleName(), gardenSchedule); + dependencies.put(Garden.class.getSimpleName(), new Garden(gardenSchedule, cropList)); + dependencies.put(AppLoader.class.getSimpleName(), this); + } + + /** + * Loads and returns a {@link Pane} (cached). + * + * @param fxmlFile The file name to be loaded + * @return The loaded Pane + * @throws IOException if the file could not be loaded + */ + public Pane loadPane(String fxmlFile) throws IOException { + Pane pane = panes.get(fxmlFile); + if (pane == null) { + loadAndCacheFxml(fxmlFile); + pane = panes.get(fxmlFile); + } + return pane; + } + + /** + * Loads the given fxml-file from resources (no caching) and creates a new {@link Scene}, + * which is then appended to the given {@link Stage}. + * Performs dependency-injection. + * + * @param fxmlFile The file name to be loaded + * @param appendee The {@link Stage} to which the new {@link Scene} is appended. + * @return The controller of the loaded scene. + * @throws IOException if the file could not be loaded + */ + public Object loadSceneToStage(String fxmlFile, Stage appendee) throws IOException { + FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource(fxmlFile))); + Pane root = loader.load(); + Scene scene = new Scene(root); + String css = Objects.requireNonNull(this.getClass().getResource("styles.css")).toExternalForm(); + appendee.setScene(scene); + scene.getStylesheets().add(css); + Object controller = loader.getController(); + annotationInject(controller); + return controller; + } + + /** + * Loads the given fxml-file from resources (no caching) and appendeds it's + * contents to the given {@link DialogPane}. + * Performs dependency-injection. + * + * @param fxmlFile The file name to be loaded + * @param appendee The {@link DialogPane} to which the FXML contents are to be appended. + * @return The controller of the loaded scene. + * @throws IOException if the file could not be loaded + */ + public Object loadPaneToDialog(String fxmlFile, DialogPane appendee) throws IOException { + FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource(fxmlFile))); + appendee.setContent(loader.load()); + Object controller = loader.getController(); + annotationInject(controller); + return controller; + } + + /** + * Loads the given fxml-file from resources and caches the pane. + * Performs dependency-injection. + * + * @param fxmlFile The file name to be loaded + * @throws IOException if the file could not be loaded + */ + public void loadAndCacheFxml(String fxmlFile) throws IOException { + FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource(fxmlFile))); + Pane pane = loader.load(); + panes.put(fxmlFile, pane); + annotationInject(loader.getController()); + } + + /** + * Injects the applications dependencies into the given object's fields annotated with {@link Inject}. + * Afterwards, all methods on the objects annotated with {@link AfterInject} are executed. + * (Success of the injections is not guaranteed!) + * + * @param controller The class containing the injectable fields + */ + public void annotationInject(Object controller) { + Arrays.stream(controller.getClass().getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Inject.class)) + .forEach(field -> { + field.setAccessible(true); + try { + field.set(controller, getAppDependency(field.getType())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + field.setAccessible(false); + }); + + Arrays.stream(controller.getClass().getMethods()) + .filter(method -> method.isAnnotationPresent(AfterInject.class) && method.getParameterCount() == 0) + .forEach(afterInjectMethod -> { + try { + afterInjectMethod.invoke(controller); + } catch (IllegalAccessException | InvocationTargetException e) { + LOG.log(Level.SEVERE, "Could not invoke afterInjectMethod", e.getCause()); + e.printStackTrace(); + } + }); + } + + /** + * Method to get any AppDependency + * @param type Class of Dependency + * @return the App dependency + */ + public Object getAppDependency(Class type) { + return dependencies.get(type.getSimpleName()); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/ChangeViewEvent.java b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/ChangeViewEvent.java new file mode 100644 index 0000000..f8242d3 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/ChangeViewEvent.java @@ -0,0 +1,28 @@ +package ch.zhaw.gartenverwaltung.bootstrap; + +import javafx.event.Event; +import javafx.event.EventType; + +/** + * Represents an event that should lead to a view being changed. + */ +public class ChangeViewEvent extends Event { + private final String view; + + public static final EventType CHANGE_MAIN_VIEW = new EventType<>("CHANGE_MAIN_VIEW"); + + /** + * Creates an Event that should lead to the main view being changed. + * + * @param eventType The {@link EventType} specifying which view should be changed. + * @param view The filename of the View to be changed to + */ + public ChangeViewEvent(EventType eventType, String view) { + super(eventType); + this.view = view; + } + + public String view() { + return view; + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/Inject.java b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/Inject.java new file mode 100644 index 0000000..803844f --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/Inject.java @@ -0,0 +1,13 @@ +package ch.zhaw.gartenverwaltung.bootstrap; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotates a Field to be injected from the application-dependencies. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface Inject { } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java b/src/main/java/ch/zhaw/gartenverwaltung/io/CropList.java similarity index 89% rename from src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java rename to src/main/java/ch/zhaw/gartenverwaltung/io/CropList.java index 38d0598..0805c99 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/GardenPlan.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/CropList.java @@ -6,7 +6,11 @@ import java.io.IOException; import java.util.List; import java.util.Optional; -public interface GardenPlan { +/** + * Represents a List of {@link Crop}s. + * The interface specifies the operations to add/update and remove entries. + */ +public interface CropList { /** * Yields a list of all {@link Crop}s in the database. * diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/HardinessZoneNotSetException.java b/src/main/java/ch/zhaw/gartenverwaltung/io/HardinessZoneNotSetException.java index cbb7015..1c730ad 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/HardinessZoneNotSetException.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/HardinessZoneNotSetException.java @@ -1,5 +1,8 @@ package ch.zhaw.gartenverwaltung.io; +/** + * Exceptionm which is thrown if Hardiness zone is not set in plant list. + */ public class HardinessZoneNotSetException extends Exception { public HardinessZoneNotSetException() { super("HardinessZone must be set to retrieve plants!"); diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java b/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java index 5842f0d..49fd868 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/IdProvider.java @@ -1,10 +1,18 @@ package ch.zhaw.gartenverwaltung.io; +/** + * Provides an sequential ID starting from the given initial Value. + */ public class IdProvider { private long currentId; public IdProvider(long initialValue) { currentId = initialValue; } + + /** + * Yields the next ID in the sequence. + * @return The next ID + */ public long incrementAndGet() { return ++currentId; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java b/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java index 9c3b8dc..2547ff1 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/InvalidJsonException.java @@ -2,6 +2,9 @@ package ch.zhaw.gartenverwaltung.io; import java.io.IOException; +/** + * Excption which is thrown if a JSON file has a invalid File format. + */ class InvalidJsonException extends IOException { public InvalidJsonException(String reason) { super(reason); diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonCropList.java similarity index 92% rename from src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java rename to src/main/java/ch/zhaw/gartenverwaltung/io/JsonCropList.java index cfe2663..aa30391 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonGardenPlan.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonCropList.java @@ -19,7 +19,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; -public class JsonGardenPlan implements GardenPlan { +/** + * Implements the {@link CropList} interface for reading and writing {@link Crop} objects + * from and to a local JSON file. + * The reads are cached to minimize file-io operations. + */ +public class JsonCropList implements CropList { private final URL dataSource; private IdProvider idProvider; @@ -40,9 +45,9 @@ public class JsonGardenPlan implements GardenPlan { } /** - * Default constructor + * Default constructor. Uses a file from the app resources. */ - public JsonGardenPlan() { + public JsonCropList() { this.dataSource = getClass().getResource("user-crops.json"); } @@ -50,9 +55,10 @@ public class JsonGardenPlan implements GardenPlan { * Constructor to use a specified {@link URL} as a {@link #dataSource} * @param dataSource A {@link URL} to the file to be used as a data source */ - public JsonGardenPlan(URL dataSource) { + public JsonCropList(URL dataSource) { this.dataSource = dataSource; } + /** * {@inheritDoc} */ diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabase.java b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonPlantList.java similarity index 82% rename from src/main/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabase.java rename to src/main/java/ch/zhaw/gartenverwaltung/io/JsonPlantList.java index f09d90e..ec46d67 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonPlantDatabase.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonPlantList.java @@ -20,12 +20,12 @@ import java.util.Map; import java.util.Optional; /** - * Implements the {@link PlantDatabase} interface for loading {@link Plant} objects - * from a JSON file. + * Implements the {@link PlantList} interface for reading {@link Plant} objects + * from a local JSON file. * The reads are cached to minimize file-io operations. */ -public class JsonPlantDatabase implements PlantDatabase { - private final URL dataSource = getClass().getResource("plantdb.json"); +public class JsonPlantList implements PlantList { + private final URL dataSource; private HardinessZone currentZone; private Map plantMap = Collections.emptyMap(); @@ -44,12 +44,27 @@ public class JsonPlantDatabase implements PlantDatabase { imageModule.addDeserializer(Image.class, new PlantImageDeserializer()); } + /** + * Default constructor. Uses a file from the app resources. + */ + public JsonPlantList() { + this.dataSource = getClass().getResource("plantdb.json"); + } + + /** + * Constructor to use a specified {@link URL} as a {@link #dataSource} + * @param dataSource A {@link URL} to the file to be used as a data source + */ + public JsonPlantList(URL dataSource) { + this.dataSource = dataSource; + } + /** * If no data is currently loaded, or the specified zone differs * from the {@link #currentZone}, data is loaded from {@link #dataSource}. * In any case, the values of {@link #plantMap} are returned. * - * @see PlantDatabase#getPlantList(HardinessZone) + * @see PlantList#getPlantList(HardinessZone) */ @Override public List getPlantList(HardinessZone zone) throws IOException, HardinessZoneNotSetException { @@ -60,7 +75,7 @@ public class JsonPlantDatabase implements PlantDatabase { } /** - * @see PlantDatabase#getPlantById(long) + * @see PlantList#getPlantById(HardinessZone, long) */ @Override public Optional getPlantById(HardinessZone zone, long id) throws HardinessZoneNotSetException, IOException { diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskDatabase.java b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java similarity index 69% rename from src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskDatabase.java rename to src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java index be0d2ab..869783c 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskDatabase.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java @@ -1,6 +1,5 @@ package ch.zhaw.gartenverwaltung.io; -import ch.zhaw.gartenverwaltung.types.Crop; import ch.zhaw.gartenverwaltung.types.Task; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; @@ -14,22 +13,24 @@ import java.net.URISyntaxException; import java.net.URL; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** - * Implements the {@link TaskDatabase} interface for loading and writing {@link Task} objects - * from and to a JSON file. + * Implements the {@link TaskList} interface for reading and writing {@link Task} objects + * from and to a local JSON file. * The reads are cached to minimize file-io operations. */ -public class JsonTaskDatabase implements TaskDatabase{ +public class JsonTaskList implements TaskList { IdProvider idProvider; - private final URL dataSource = getClass().getResource("taskdb.json"); + private final URL dataSource; private final static String INVALID_DATASOURCE_MSG = "Invalid datasource specified!"; private Map taskMap = Collections.emptyMap(); + private final List subscribers = new ArrayList<>(); /** * Creating constant objects required to deserialize the {@link LocalDate} classes @@ -44,18 +45,33 @@ public class JsonTaskDatabase implements TaskDatabase{ timeModule.addSerializer(LocalDate.class, dateSerializer); } + /** + * Default constructor. Uses a file from the app resources. + */ + public JsonTaskList() { + this.dataSource = getClass().getResource("taskdb.json"); + } + + /** + * Constructor to use a specified {@link URL} as a {@link #dataSource} + * @param dataSource A {@link URL} to the file to be used as a data source + */ + public JsonTaskList(URL dataSource) { + this.dataSource = dataSource; + } + /** * If no data is currently loaded, data is loaded from {@link #dataSource}. * In any case, the values of {@link #taskMap} are returned. * - * @see TaskDatabase#getTaskList(LocalDate, LocalDate) + * @see TaskList#getTaskList(LocalDate, LocalDate) */ @Override - public List getTaskList(LocalDate start, LocalDate end) throws IOException{ + public synchronized List getTaskList(LocalDate start, LocalDate end) throws IOException{ if(taskMap.isEmpty()) { loadTaskListFromFile(); } - return taskMap.values().stream().filter(task -> task.isInTimePeriode(start, end)).toList(); + return taskMap.values().stream().filter(task -> task.isInTimePeriod(start, end)).toList(); } /** @@ -64,7 +80,7 @@ public class JsonTaskDatabase implements TaskDatabase{ * @return List of Tasks for given Crop */ @Override - public List getTaskForCrop(long cropId) throws IOException { + public synchronized List getTaskForCrop(long cropId) throws IOException { if(taskMap.isEmpty()) { loadTaskListFromFile(); } @@ -80,7 +96,9 @@ public class JsonTaskDatabase implements TaskDatabase{ if(taskMap.isEmpty()) { loadTaskListFromFile(); } - taskMap.values().removeIf(task -> task.getCropId() == cropId); + taskMap.entrySet().removeIf(entry -> entry.getValue().getCropId() == cropId); + writeTaskListToFile(); + notifySubscribers(); } /** @@ -90,17 +108,18 @@ public class JsonTaskDatabase implements TaskDatabase{ * it to the {@link #taskMap}. In any case, the {@link #taskMap} is written * to the {@link #dataSource}. * - * @see TaskDatabase#saveTask(Task) + * @see TaskList#saveTask(Task) */ @Override - public void saveTask(Task task) throws IOException { + public synchronized void saveTask(Task task) throws IOException { if(taskMap.isEmpty()) { loadTaskListFromFile(); } - if(task.getId() == 0) { - task.withId(idProvider.incrementAndGet()); - } + long id = task.getId().orElse(idProvider.incrementAndGet()); + + taskMap.put(id, task.withId(id)); writeTaskListToFile(); + notifySubscribers(); } /** @@ -108,17 +127,37 @@ public class JsonTaskDatabase implements TaskDatabase{ * If the {@link Task}s id is found in the {@link #taskMap}, the Task is removed * from the {@link #taskMap}. Then the Task are written to the {@link #dataSource}. * - * @see TaskDatabase#removeTask(Task) + * @see TaskList#removeTask(Task) */ @Override public void removeTask(Task task) throws IOException { if(taskMap.isEmpty()) { loadTaskListFromFile(); } - if(taskMap.containsKey(task.getId())){ - taskMap.remove(task.getId()); + Long taskId = task.getId().orElseThrow(IOException::new); + if(taskMap.containsKey(taskId)){ + taskMap.remove(taskId); writeTaskListToFile(); } + notifySubscribers(); + } + + /** + * {@inheritDoc} + * @param observer The change handler + */ + @Override + public void subscribe(TaskListObserver observer) { + subscribers.add(observer); + } + + /** + * Calls the change handler method on all registered observers. + */ + private void notifySubscribers() throws IOException { + for (TaskListObserver subscriber : subscribers) { + subscriber.onChange(taskMap.values().stream().toList()); + } } /** @@ -156,7 +195,7 @@ public class JsonTaskDatabase implements TaskDatabase{ taskMap = result.stream() .collect(HashMap::new, - (res, task) -> res.put(task.getId(), task), + (res, task) -> res.put(task.getId().orElse(0L), task), (existing, replacement) -> {}); } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/PlantDatabase.java b/src/main/java/ch/zhaw/gartenverwaltung/io/PlantList.java similarity index 95% rename from src/main/java/ch/zhaw/gartenverwaltung/io/PlantDatabase.java rename to src/main/java/ch/zhaw/gartenverwaltung/io/PlantList.java index 455791e..f5c4dc9 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/PlantDatabase.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/PlantList.java @@ -8,10 +8,10 @@ import java.util.List; import java.util.Optional; /** - * A database of {@link Plant}s. + * A List of {@link Plant}s. * The interface specifies the minimal required operations. */ -public interface PlantDatabase { +public interface PlantList { /** * Yields a list of all {@link Plant}s in the database with only data relevant to the specfied {@link HardinessZone} * diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/TaskDatabase.java b/src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java similarity index 72% rename from src/main/java/ch/zhaw/gartenverwaltung/io/TaskDatabase.java rename to src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java index 02bef7a..e03878c 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/TaskDatabase.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java @@ -1,20 +1,16 @@ package ch.zhaw.gartenverwaltung.io; -import ch.zhaw.gartenverwaltung.types.Crop; -import ch.zhaw.gartenverwaltung.types.HardinessZone; -import ch.zhaw.gartenverwaltung.types.Plant; import ch.zhaw.gartenverwaltung.types.Task; import java.io.IOException; import java.time.LocalDate; -import java.util.Date; import java.util.List; /** - * A database of {@link Task}s. - * The interface specifies the minimal required operations. + * Represents a List of {@link Task}s. + * The interface specifies the operations to add/update and remove entries. */ -public interface TaskDatabase { +public interface TaskList { /** * Yields a list of all {@link Task}s in the database with the start and end date of a period of time. * @@ -55,4 +51,22 @@ public interface TaskDatabase { * @throws IOException If the database cannot be accessed */ void removeTask(Task task) throws IOException; + + /** + * Registers an observer to be notified of Changes in the TaskList + * @param observer The change handler + */ + void subscribe(TaskListObserver observer); + + /** + * Specifies an observer for a TaskList + */ + @FunctionalInterface + interface TaskListObserver { + /** + * Method which will be called when changes occur. + * @param newTaskList The new values + */ + void onChange(List newTaskList) throws IOException; + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/json/GrowthPhaseTypeDeserializer.java b/src/main/java/ch/zhaw/gartenverwaltung/json/GrowthPhaseTypeDeserializer.java index 35956a1..9da78da 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/json/GrowthPhaseTypeDeserializer.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/json/GrowthPhaseTypeDeserializer.java @@ -7,6 +7,9 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; +/** + * Used by the Jackson Library to deserialize a String to a {@link GrowthPhaseType} + */ public class GrowthPhaseTypeDeserializer extends StdDeserializer { public GrowthPhaseTypeDeserializer(Class vc) { super(vc); diff --git a/src/main/java/ch/zhaw/gartenverwaltung/json/HardinessZoneDeserializer.java b/src/main/java/ch/zhaw/gartenverwaltung/json/HardinessZoneDeserializer.java index 2a8ec1b..8170436 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/json/HardinessZoneDeserializer.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/json/HardinessZoneDeserializer.java @@ -7,6 +7,9 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import java.io.IOException; +/** + * Used by the Jackson Library to deserialize a String to a {@link HardinessZone} + */ public class HardinessZoneDeserializer extends StdDeserializer { public HardinessZoneDeserializer(Class vc) { super(vc); diff --git a/src/main/java/ch/zhaw/gartenverwaltung/json/PlantImageDeserializer.java b/src/main/java/ch/zhaw/gartenverwaltung/json/PlantImageDeserializer.java index 14da20c..f0b818b 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/json/PlantImageDeserializer.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/json/PlantImageDeserializer.java @@ -1,6 +1,6 @@ package ch.zhaw.gartenverwaltung.json; -import ch.zhaw.gartenverwaltung.io.PlantDatabase; +import ch.zhaw.gartenverwaltung.io.PlantList; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; @@ -13,18 +13,17 @@ import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; +/** + * Used by the Jackson Library to deserialize a String to a {@link Image} + */ public class PlantImageDeserializer extends JsonDeserializer { @Override public Image deserialize(JsonParser parser, DeserializationContext context) throws IOException { Image result = null; - URL imageUrl = PlantDatabase.class.getResource(String.format("images/%s", parser.getText())); - if (imageUrl != null) { - try (InputStream is = new FileInputStream(new File(imageUrl.toURI()))) { - result = new Image(is); - } catch (IllegalArgumentException | URISyntaxException e) { - throw new IOException(String.format("Cannot find Image \"%s\"\n", imageUrl.getFile())); - } + InputStream is = PlantList.class.getResourceAsStream(String.format("images/%s", parser.getText())); + if (is != null) { + result = new Image(is); } return result; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/gardenplan/Gardenplanmodel.java b/src/main/java/ch/zhaw/gartenverwaltung/models/Garden.java similarity index 54% rename from src/main/java/ch/zhaw/gartenverwaltung/gardenplan/Gardenplanmodel.java rename to src/main/java/ch/zhaw/gartenverwaltung/models/Garden.java index 00ebd44..7f37fbf 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/gardenplan/Gardenplanmodel.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/models/Garden.java @@ -1,41 +1,41 @@ -package ch.zhaw.gartenverwaltung.gardenplan; +package ch.zhaw.gartenverwaltung.models; -import ch.zhaw.gartenverwaltung.io.GardenPlan; +import ch.zhaw.gartenverwaltung.io.CropList; import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.io.JsonGardenPlan; -import ch.zhaw.gartenverwaltung.taskList.PlantNotFoundException; -import ch.zhaw.gartenverwaltung.taskList.TaskListModel; import ch.zhaw.gartenverwaltung.types.Crop; import ch.zhaw.gartenverwaltung.types.Plant; import ch.zhaw.gartenverwaltung.types.Task; +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; import java.io.IOException; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; /** * The Gardenplan model manages the crops in the gardenplan. */ -public class Gardenplanmodel { - private GardenPlan gardenPlan; - private List cropList; - private TaskListModel taskListModel; - private Object IllegalArgumentException; +public class Garden { + private final CropList cropList; + private final ListProperty plantedCrops = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final GardenSchedule gardenSchedule; /** * Constructor of Gardenplan model * - * @param taskListModel holds a reference to the task list object. + * @param gardenSchedule holds a reference to the task list object. */ - public Gardenplanmodel(TaskListModel taskListModel) throws IOException { - this.taskListModel = taskListModel; - gardenPlan = new JsonGardenPlan(); - cropList = new ArrayList<>(); - cropList = gardenPlan.getCrops(); + public Garden(GardenSchedule gardenSchedule, CropList cropList) throws IOException { + this.gardenSchedule = gardenSchedule; + this.cropList = cropList; + plantedCrops.addAll(cropList.getCrops()); + } + + public ListProperty getPlantedCrops() { + return plantedCrops; } /** @@ -50,11 +50,10 @@ public class Gardenplanmodel { */ public void plantAsCrop(Plant plant, LocalDate plantingDate) throws IOException, HardinessZoneNotSetException, PlantNotFoundException { Crop crop = new Crop(plant.id(), plantingDate); - //TODO Add Area to Plant - //crop.withArea(0); - gardenPlan.saveCrop(crop); - taskListModel.planTasksForCrop(crop); - cropList = gardenPlan.getCrops(); + cropList.saveCrop(crop); + gardenSchedule.planTasksForCrop(crop); + plantedCrops.clear(); + plantedCrops.addAll(cropList.getCrops()); } /** @@ -64,20 +63,31 @@ public class Gardenplanmodel { * @throws IOException If the database cannot be accessed */ public void removeCrop(Crop crop) throws IOException { - gardenPlan.removeCrop(crop); - taskListModel.removeTasksForCrop(crop.getCropId().orElseThrow(IllegalArgumentException::new)); - cropList = gardenPlan.getCrops(); + cropList.removeCrop(crop); + gardenSchedule.removeTasksForCrop(crop.getCropId().orElseThrow(IllegalArgumentException::new)); + plantedCrops.clear(); + plantedCrops.addAll(cropList.getCrops()); } + + /** + * Updates the {@link Crop} from the file and the cache + * + * @param crop The crop which is being updated + * @throws IOException If the database cannot be accessed + */ + public void updateCrop(Crop crop) throws IOException { + cropList.saveCrop(crop); + plantedCrops.clear(); + plantedCrops.addAll(cropList.getCrops()); + } + /** * Returns a list of {@link Crop}s which are currently in the gardenplan. * * @throws IOException If the database cannot be accessed */ public List getCrops() throws IOException { - if(!cropList.isEmpty()){ - cropList = gardenPlan.getCrops(); - } - return cropList; + return cropList.getCrops(); } /** @@ -87,6 +97,6 @@ public class Gardenplanmodel { * @throws IOException If the database cannot be accessed */ public Optional getCrop(Long cropId) throws IOException { - return gardenPlan.getCropById(cropId); + return cropList.getCropById(cropId); } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/taskList/TaskListModel.java b/src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java similarity index 61% rename from src/main/java/ch/zhaw/gartenverwaltung/taskList/TaskListModel.java rename to src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java index af0ca00..343a09f 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/taskList/TaskListModel.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java @@ -1,37 +1,49 @@ -package ch.zhaw.gartenverwaltung.taskList; +package ch.zhaw.gartenverwaltung.models; -import ch.zhaw.gartenverwaltung.Config; +import ch.zhaw.gartenverwaltung.Settings; import ch.zhaw.gartenverwaltung.io.*; import ch.zhaw.gartenverwaltung.types.*; +import javafx.application.Platform; +import javafx.beans.property.ListProperty; +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; import java.io.IOException; import java.time.LocalDate; import java.util.ArrayList; import java.util.Comparator; import java.util.List; -import java.util.function.Predicate; import java.util.stream.Collectors; -public class TaskListModel { - private TaskDatabase taskDatabase; - private PlantDatabase plantDatabase; +public class GardenSchedule { + private final TaskList taskList; + private final PlantList plantList; /** * Comparators to create sorted Task List */ static final Comparator sortByStartDate = Comparator.comparing(Task::getStartDate); - - public TaskListModel(){ - taskDatabase = new JsonTaskDatabase(); - plantDatabase = new JsonPlantDatabase(); - } + static final Comparator sortByNextExecution = Comparator.comparing(Task::getNextExecution); + private final ListProperty> weeklyTaskListProperty = new SimpleListProperty<>(FXCollections.observableArrayList()); /** * Constructor to create Database Objects. */ - public TaskListModel(TaskDatabase taskDatabase, PlantDatabase plantDatabase) { - this.taskDatabase = taskDatabase; - this.plantDatabase = plantDatabase; + public GardenSchedule(TaskList taskList, PlantList plantList) throws IOException { + this.taskList = taskList; + this.plantList = plantList; + } + + public ListProperty> getWeeklyTaskListProperty() { + return weeklyTaskListProperty; + } + + /** + * subscribe task list observer to get notifications + * @param observer the task list which will be ovserved + */ + public void setTaskListObserver(TaskList.TaskListObserver observer) { + taskList.subscribe(observer); } /** @@ -40,7 +52,7 @@ public class TaskListModel { * @throws IOException If the database cannot be accessed */ public void addTask(Task task) throws IOException { - taskDatabase.saveTask(task); + taskList.saveTask(task); } /** @@ -51,8 +63,13 @@ public class TaskListModel { * @throws IOException If the database cannot be accessed */ public void planTasksForCrop(Crop crop) throws PlantNotFoundException, HardinessZoneNotSetException, IOException { - Plant plant = plantDatabase.getPlantById(Config.getCurrentHardinessZone(), crop.getPlantId()).orElseThrow(PlantNotFoundException::new); - for (GrowthPhase growthPhase : plant.lifecycle()) { + Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).orElseThrow(PlantNotFoundException::new); + int growPhaseGroup = plant.getGrowphaseGroupForDate(crop.getStartDate()); + addTask(new Task("watering Task", "pour water over the plant circa : "+ plant.wateringCycle().litersPerSqM() +" per square meter", + crop.getStartDate(), crop.getStartDate().plusDays(plant.timeToHarvest(0)), + plant.wateringCycle().interval(), crop.getCropId().orElse(0L))); + + for (GrowthPhase growthPhase : plant.lifecycleForGroup(growPhaseGroup)) { for (TaskTemplate taskTemplate : growthPhase.taskTemplates()) { addTask(taskTemplate.generateTask(crop.getStartDate(), crop.getCropId().orElse(0L))); } @@ -65,7 +82,7 @@ public class TaskListModel { * @throws IOException If the database cannot be accessed */ public void removeTasksForCrop(long cropId) throws IOException { - taskDatabase.removeTasksForCrop(cropId); + taskList.removeTasksForCrop(cropId); } /** @@ -74,7 +91,7 @@ public class TaskListModel { * @throws IOException If the database cannot be accessed */ public void removeTask(Task task) throws IOException { - taskDatabase.removeTask(task); + taskList.removeTask(task); } private List filterListByCrop(List taskList, Long cropId) { @@ -141,26 +158,40 @@ public class TaskListModel { * @throws IOException If the database cannot be accessed */ public List> getTasksUpcomingWeek() throws IOException { + final int listLength = 7; + List weekTasks = taskList.getTaskList(LocalDate.now(), LocalDate.now().plusDays(listLength - 1)); List> dayTaskList = new ArrayList<>(); - for(int i = 0; i < 7; i++) { + for(int i = 0; i < listLength; i++) { LocalDate date = LocalDate.now().plusDays(i); - dayTaskList.add(taskDatabase.getTaskList(date, date)); + dayTaskList.add(new ArrayList<>()); + final int finalI = i; + weekTasks.forEach(task -> { + if(task.getNextExecution() != null) { + LocalDate checkDate = task.getNextExecution(); + do { + if (date.equals(task.getNextExecution()) || (date.equals(checkDate) && !date.isAfter(task.getEndDate().orElse(LocalDate.MIN)))) { + dayTaskList.get(finalI).add(task); + break; + } + checkDate = checkDate.plusDays(task.getInterval().orElse(0)); + } while (!(task.getInterval().orElse(0) == 0) && checkDate.isBefore(LocalDate.now().plusDays(listLength))); + } + }); } + weeklyTaskListProperty.clear(); + weeklyTaskListProperty.addAll(dayTaskList); return dayTaskList; } /** * Method to get an List of 7 Tasklists for the next 7 days. (Filtered Index 0 is Tasklist for Today. - * @return List with length 7 (List>) * @throws IOException If the database cannot be accessed */ - public List> getTasksUpcomingWeekForCrop(Long cropId) throws IOException { - List> dayTaskList = new ArrayList<>(); - for(int i = 0; i < 7; i++) { - LocalDate date = LocalDate.now().plusDays(i); - dayTaskList.add(filterListByCrop(taskDatabase.getTaskList(date, date), cropId)); - } - return dayTaskList; + public void getTasksUpcomingWeekForCrop(Long cropId) throws IOException { + List> dayTaskList = getTasksUpcomingWeek(); + dayTaskList.forEach(taskList -> taskList.removeIf(task -> task.getCropId() != cropId)); + weeklyTaskListProperty.clear(); + weeklyTaskListProperty.addAll(dayTaskList); } /** @@ -171,7 +202,7 @@ public class TaskListModel { * @throws IOException If the database cannot be accessed */ public List getFilteredTaskList(LocalDate start, LocalDate end) throws IOException { - return getSortedTaskList(taskDatabase.getTaskList(start, end), sortByStartDate); + return getSortedTaskList(taskList.getTaskList(start, end), sortByNextExecution); } /** @@ -181,6 +212,6 @@ public class TaskListModel { * @return a sorted coppy of the given Tasklist */ private List getSortedTaskList(List taskList, Comparator comparator) { - return taskList.stream().sorted(comparator).collect(Collectors.toList()); + return taskList.stream().filter(task -> task.getNextExecution() != null).sorted(comparator).collect(Collectors.toList()); } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/plantList/PlantListModel.java b/src/main/java/ch/zhaw/gartenverwaltung/models/PlantListModel.java similarity index 90% rename from src/main/java/ch/zhaw/gartenverwaltung/plantList/PlantListModel.java rename to src/main/java/ch/zhaw/gartenverwaltung/models/PlantListModel.java index ffccb30..33627c9 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/plantList/PlantListModel.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/models/PlantListModel.java @@ -1,8 +1,8 @@ -package ch.zhaw.gartenverwaltung.plantList; +package ch.zhaw.gartenverwaltung.models; +import ch.zhaw.gartenverwaltung.Settings; import ch.zhaw.gartenverwaltung.io.HardinessZoneNotSetException; -import ch.zhaw.gartenverwaltung.io.JsonPlantDatabase; -import ch.zhaw.gartenverwaltung.io.PlantDatabase; +import ch.zhaw.gartenverwaltung.io.PlantList; import ch.zhaw.gartenverwaltung.types.GrowthPhaseType; import ch.zhaw.gartenverwaltung.types.HardinessZone; import ch.zhaw.gartenverwaltung.types.Plant; @@ -16,8 +16,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; public class PlantListModel { - private PlantDatabase plantDatabase; - private HardinessZone currentZone; + private final PlantList plantList; /** * Comparators to create sorted Plant List @@ -28,26 +27,21 @@ public class PlantListModel { /** * Constructor to create Database Object. */ - public PlantListModel() { - plantDatabase = new JsonPlantDatabase(); - setDefaultZone(); - } - - public PlantListModel(PlantDatabase plantDatabase) { - this.plantDatabase = plantDatabase; + public PlantListModel(PlantList plantList) { + this.plantList = plantList; setDefaultZone(); } private void setDefaultZone() { - currentZone = HardinessZone.ZONE_8A; // TODO: get Default Zone from Config + Settings.getInstance().setCurrentHardinessZone(null); } public void setCurrentZone(HardinessZone currentZone) { - this.currentZone = currentZone; + Settings.getInstance().setCurrentHardinessZone(currentZone); } public HardinessZone getCurrentZone() { - return currentZone; + return Settings.getInstance().getCurrentHardinessZone(); } /** @@ -72,7 +66,7 @@ public class PlantListModel { */ public List getSortedPlantList(HardinessZone zone, Comparator comparator) throws HardinessZoneNotSetException, IOException { setCurrentZone(zone); - return plantDatabase.getPlantList(zone).stream().sorted(comparator).collect(Collectors.toList()); + return plantList.getPlantList(zone).stream().sorted(comparator).collect(Collectors.toList()); } /** @@ -101,7 +95,7 @@ public class PlantListModel { public List getFilteredPlantListById(HardinessZone zone, Long id) throws HardinessZoneNotSetException, IOException { setCurrentZone(zone); List plantList = new ArrayList<>(); - plantDatabase.getPlantById(zone, id).ifPresent(plantList::add); + this.plantList.getPlantById(zone, id).ifPresent(plantList::add); return plantList; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/taskList/PlantNotFoundException.java b/src/main/java/ch/zhaw/gartenverwaltung/models/PlantNotFoundException.java similarity index 55% rename from src/main/java/ch/zhaw/gartenverwaltung/taskList/PlantNotFoundException.java rename to src/main/java/ch/zhaw/gartenverwaltung/models/PlantNotFoundException.java index 149e1ef..c180005 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/taskList/PlantNotFoundException.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/models/PlantNotFoundException.java @@ -1,5 +1,8 @@ -package ch.zhaw.gartenverwaltung.taskList; +package ch.zhaw.gartenverwaltung.models; +/** + * Exception which is thrown if there is no plant in plant Database with the given id. + */ public class PlantNotFoundException extends Exception { public PlantNotFoundException() { super("The selected Plant was not found in Database!"); diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java index e8f539f..96a78e6 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Crop.java @@ -4,6 +4,9 @@ import java.time.LocalDate; import java.util.Objects; import java.util.Optional; +/** + * Represents a crop, meaning a specific plant growing at a specific time. + */ public class Crop { private Long cropId = null; private final long plantId; @@ -37,7 +40,6 @@ public class Crop { public Optional getCropId() { return Optional.ofNullable(cropId); } - public long getPlantId() { return plantId; } public LocalDate getStartDate() { return startDate; } public double getArea() { return area; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhase.java b/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhase.java index 9348f2b..c5c80cd 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhase.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhase.java @@ -7,12 +7,23 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.time.MonthDay; import java.util.List; - +/** + * Represents a growth phase of a plant. + * Plants go through several phases during their life (sowing, germinating, growing, ..., harvest). + * These phases are characterized by what kinds of tasks need to be executed by the gardener to stay alive. + * This class represents one such phase. + * + * @param startDate The earliest date on which this phase can start + * @param endDate The latest date on which this phase can start + * @param group Which group this phase belongs to (if the growth phase can occur multiple times a year, default: 0) + * @param type What {@link GrowthPhaseType} this represents + * @param zone The hardiness zone for which this growth phase is valid + * @param taskTemplates The (undated) tasks required to be performed by the gardener + */ public record GrowthPhase( MonthDay startDate, MonthDay endDate, int group, - WateringCycle wateringCycle, @JsonDeserialize(using = GrowthPhaseTypeDeserializer.class) GrowthPhaseType type, @JsonDeserialize(using = HardinessZoneDeserializer.class) HardinessZone zone, List taskTemplates) { diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhaseType.java b/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhaseType.java index 96609cc..4a3ef38 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhaseType.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/GrowthPhaseType.java @@ -1,5 +1,9 @@ package ch.zhaw.gartenverwaltung.types; +/** + * Enumerates the different possible types of {@link GrowthPhase}. + * (Subject to later expansion) + */ public enum GrowthPhaseType { SOW, PLANT, REPLANT, HARVEST } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Pest.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Pest.java index cfbcb81..a1dbc2d 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Pest.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Pest.java @@ -1,4 +1,11 @@ package ch.zhaw.gartenverwaltung.types; +/** + * Represents a pest or pathogen which may afflict a plant. + * + * @param name The name of the pest + * @param description A description of the pest + * @param measures Measures that can be taken against the pest. + */ public record Pest(String name, String description, String measures) { } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java index 9cb3d08..d1a0905 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java @@ -3,12 +3,26 @@ package ch.zhaw.gartenverwaltung.types; import javafx.scene.image.Image; import java.time.LocalDate; -import java.util.LinkedList; +import java.time.MonthDay; import java.util.List; import java.util.stream.Collectors; import static java.time.temporal.ChronoUnit.DAYS; +/** + * Represents a plant + * + * @param id A unique identifier + * @param name The name of the plant + * @param description A description of the plant + * @param image An image representing the plant + * @param spacing The amount of space needed between individual plants of this type + * @param light The amount of light preferred by the plant (h/d) + * @param soil The type of soil required for the plant + * @param pests {@link Pest}s that may afflict the plant + * @param wateringCycle The {@link WateringCycle} required by the plant + * @param lifecycle A list of {@link GrowthPhase}s constituting the plants lifecycle + */ public record Plant( long id, String name, @@ -18,6 +32,7 @@ public record Plant( int light, String soil, List pests, + WateringCycle wateringCycle, List lifecycle) { /** @@ -29,9 +44,10 @@ public record Plant( } /** - * get all growthPhases of lifecycle group - * @param group lifecycle group - * @return list of growthPhases + * Get all {@link GrowthPhase}s of a lifecycle group + * + * @param group The lifecycle group + * @return A list of {@link GrowthPhase}s */ public List lifecycleForGroup(int group) { return lifecycle.stream() @@ -40,66 +56,95 @@ public record Plant( } /** - * get sow date from given harvest day from lifecycle group - * @param harvestDate date of the harvest - * @param group lifecycle group - * @return LocaleDate of sow date + * Given a {@link LocalDate}, determines which lifecycle group contains it. + * + * @param date The date to look for + * @return Which lifecycle group the date is in */ - public LocalDate sowDateFromHarvestDate(LocalDate harvestDate, int group) { - return harvestDate.minusDays(timeToHarvest(group)); + public int getGrowphaseGroupForDate(LocalDate date) { + for(GrowthPhase growthPhase : lifecycle){ + MonthDay plantingDate = MonthDay.of(date.getMonth().getValue(), date.getDayOfMonth()); + if(plantingDate.isAfter(growthPhase.startDate()) && plantingDate.isBefore(growthPhase.endDate())){ + return growthPhase.group(); + } + } + return 0; } /** - * calculate the days between sow and harvest day for lifecycle group + * Get sow date from given harvest date from lifecycle group + * + * @param harvestDate date of the harvest + * @return {@link LocalDate} of sow date + */ + public LocalDate sowDateFromHarvestDate(LocalDate harvestDate) { + return harvestDate.minusDays(timeToHarvest(lifecycleGroupFromHarvestDate(harvestDate))); + } + + /** + * Calculate the number of days between sow and harvest date for lifecycle group + * * @param group the lifecycle group * @return Integer number of dates between sow and harvest day */ public int timeToHarvest(int group) { List activeLifecycle = lifecycleForGroup(group); GrowthPhase sow = activeLifecycle.stream() - .filter(growthPhase -> !growthPhase.type().equals(GrowthPhaseType.SOW)) + .filter(growthPhase -> growthPhase.type().equals(GrowthPhaseType.SOW)) .findFirst() .orElseThrow(); GrowthPhase harvest = activeLifecycle.stream() - .filter(growthPhase -> !growthPhase.type().equals(GrowthPhaseType.HARVEST)) + .filter(growthPhase -> growthPhase.type().equals(GrowthPhaseType.HARVEST)) .findFirst() .orElseThrow(); int currentYear = LocalDate.now().getYear(); - return (int) DAYS.between(harvest.startDate().atYear(currentYear), sow.startDate().atYear(currentYear)); + return (int) DAYS.between(sow.startDate().atYear(currentYear),harvest.startDate().atYear(currentYear)); } /** - * filter out the given growthPhase out of the lifecycle - * create list of dates for this growthPhase and return it - * @param growthPhase the wanted growthPhase - * @return a list of dates of the current year + * Given a harvest date, determines which lifecycle group it belongs to. + * + * @param harvestDate The harvest date + * @return Which lifecycle group the harvest date is in */ - public List getDateListOfGrowthPhase(GrowthPhaseType growthPhase) { - List dates = new LinkedList<>(); - for (GrowthPhase growth : lifecycle) { - if (growth.type().equals(growthPhase)) { - dates = addDatesFromMonthDay(growth); - } - } - return dates; + public int lifecycleGroupFromHarvestDate(LocalDate harvestDate) { + return lifecycle.stream() + .filter(growthPhase -> growthPhase.type().equals(GrowthPhaseType.HARVEST) && + dateInRange(harvestDate, growthPhase.startDate(), growthPhase.endDate())) + .map(GrowthPhase::group) + .findFirst() + .orElse(0); } /** - * transform monthDay value of the given growthPhase to localDate - * return a list of dates from start to end of growth phase - * @param growthPhase the current growthPhase - * @return a list of dates of the current year + * Checks if the given {@link LocalDate} is within a {@link GrowthPhase} of the given {@link GrowthPhaseType} + * + * @param date The date to check. + * @param phase The {@link GrowthPhaseType} to match against + * @return Whether the date is within the given {@link GrowthPhaseType} */ - private List addDatesFromMonthDay(GrowthPhase growthPhase) { - List dates = new LinkedList<>(); - LocalDate today = LocalDate.now(); - LocalDate start = growthPhase.startDate().atYear(today.getYear()); - LocalDate end = growthPhase.endDate().atYear(today.getYear()); - while (!start.isAfter(end)) { - dates.add(start); - start = start.plusDays(1); - } - return dates; + public boolean isDateInPhase(LocalDate date, GrowthPhaseType phase) { + return lifecycle.stream() + .filter(growthPhase -> growthPhase.type().equals(phase)) + .anyMatch(growthPhase -> dateInRange(date, growthPhase.startDate(), growthPhase.endDate())); + } + + /** + * Checks if the given {@link LocalDate} is within the given {@link MonthDay} range. + * (regardless of year) + * + * @param subject The date to check + * @param min The start of the date-range + * @param max The end of the date-range + * @return Whether the subject is within the range. + */ + private boolean dateInRange(LocalDate subject, MonthDay min, MonthDay max) { + return subject.getMonth().compareTo(min.getMonth()) >= 0 && + subject.getMonth().compareTo(max.getMonth()) <= 0 && + // if the day is less than the minimum day, the minimum month must not be equal + (subject.getDayOfMonth() >= min.getDayOfMonth() || !subject.getMonth().equals(min.getMonth())) && + // if the day is greater than the maximum day, the maximum month must not be equal + (subject.getDayOfMonth() <= max.getDayOfMonth() || !subject.getMonth().equals(max.getMonth())); } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Seasons.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Seasons.java index 2a2d2d0..311c078 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Seasons.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Seasons.java @@ -2,8 +2,12 @@ package ch.zhaw.gartenverwaltung.types; import java.time.MonthDay; +/** + * Describes the 4 Seasons in terms of {@link java.time.LocalDate}s + * Also describes "All Seasons" as the full year. + */ public enum Seasons { - AllSEASONS("--01-01", "--12-31", "All Seasons"), + ALLSEASONS("--01-01", "--12-31", "All Seasons"), SPRING("--03-01", "--05-30", "Spring"), SUMMER("--06-01", "--08-30", "Summer"), AUTUMN("--09-01", "--11-30", "Autumn"), diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java index 16de6c3..9316095 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java @@ -1,43 +1,86 @@ package ch.zhaw.gartenverwaltung.types; +import com.fasterxml.jackson.annotation.JsonIgnore; import java.time.LocalDate; import java.util.Optional; /** - * Models a Task + * Represents a Task * May be created using the builder pattern. */ public class Task { - private long id; - private final String name; - private final String description; - private final LocalDate startDate; + private Long id; + private String name; + private String description; + private LocalDate startDate; private Integer interval; private LocalDate endDate; + private LocalDate nextExecution; + private LocalDate nextNotification; private long cropId; /** - * default constructor + * Default constructor * (used by Json deserializer) */ public Task(){ name= ""; description= ""; startDate = LocalDate.now(); + endDate = startDate; + nextExecution = startDate; } + /** + * Constructor for a non-repeating task related to a {@link Crop} + * + * @param name The name of the task + * @param description A description of the task + * @param startDate The start date of the task + * @param cropId The id of the crop to which the task belongs + */ public Task(String name, String description, LocalDate startDate, long cropId) { this.name = name; this.description = description; this.startDate = startDate; + this.endDate = startDate; + nextExecution = startDate; this.cropId = cropId; } + /** + * Constructor for weather events + * + * @param name The name of the task + * @param description A description of the task + * @param startDate The start date of the task + * @param endDate The maximum date on which the task should be executed + */ + public Task(String name, String description, LocalDate startDate, LocalDate endDate, long cropId) { + this.name = name; + this.description = description; + this.startDate = startDate; + nextExecution = startDate; + this.endDate = endDate; + this.cropId = cropId; + } + + /** + * Full constructor (without id) + * + * @param name The name of the task + * @param description A description of the task + * @param startDate The start date of the task + * @param endDate The maximum date on which the task should be executed + * @param interval The number of days between executions + * @param cropId The id of the crop to which the task belongs + */ public Task(String name, String description, LocalDate startDate, LocalDate endDate, int interval, long cropId) { this.name = name; this.description = description; this.startDate = startDate; + nextExecution = startDate; this.endDate = endDate; this.interval = interval; this.cropId = cropId; @@ -57,12 +100,47 @@ public class Task { return this; } - public boolean isInTimePeriode(LocalDate searchStartDate, LocalDate searchEndDate){ - return startDate.isAfter(searchStartDate) && startDate.isBefore(searchEndDate); + /** + * Checks if the Task is within a specific date range. + * + * @param searchStartDate The minimum date + * @param searchEndDate The maximum date + * @return Whether the Task is within the given range + */ + public boolean isInTimePeriod(LocalDate searchStartDate, LocalDate searchEndDate) { + return (endDate.isAfter(searchStartDate) && startDate.isBefore(searchEndDate)) || ((nextExecution != null && nextExecution.isBefore(searchEndDate.plusDays(1)) && nextExecution.isAfter(searchStartDate.minusDays(1)))); + } + + /** + * Marks a specific execution of a Task as done. + */ + public void done(){ + if(interval != null && interval != 0 && !nextExecution.plusDays(interval).isAfter(endDate)){ + nextExecution = nextExecution.plusDays(interval); + nextNotification = nextExecution; + } else { + nextExecution = null; + nextNotification = null; + } + } + + @JsonIgnore + public boolean isDone(){ + return nextExecution == null; + } + + public void setNextExecution(LocalDate nextExecution) { + this.nextExecution = nextExecution; + } + + public void setNextNotification(LocalDate nextNotification) { + this.nextNotification = nextNotification; } // Getters - public long getId() { return id; } + public LocalDate getNextNotification() { return nextNotification; } + public LocalDate getNextExecution() { return nextExecution; } + public Optional getId() { return Optional.ofNullable(id); } public String getName() { return name; } public String getDescription() { return description; } public LocalDate getStartDate() { return startDate; } @@ -74,4 +152,20 @@ public class Task { public Optional getEndDate() { return Optional.ofNullable(endDate); } + + /** + * Updates the fields of this Task using the values of the given Task + * + * @param task The task whose fields to copy + * @return This task with the fields already updated + */ + public Task updateTask(Task task) { + this.name = task.getName(); + this.description = task.getDescription(); + this.startDate = task.getStartDate(); + this.endDate = task.getEndDate().orElse(null); + this.interval = task.getInterval().orElse(0); + this.cropId = task.getCropId(); + return this; + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java b/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java index 8ae7862..d79b454 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.time.LocalDate; +/** + * Class which represents a Task if the start and enddate is not known yet. + */ public class TaskTemplate { @JsonProperty private final String name; @@ -16,10 +19,6 @@ public class TaskTemplate { @JsonProperty private Integer interval; - // TODO: reconsider if we need this - @JsonProperty - private boolean isOptional = false; - /** * Default constructor * (Used by deserializer) @@ -34,6 +33,7 @@ public class TaskTemplate { public void setRelativeEndDate(Integer relativeEndDate) { this.relativeEndDate = relativeEndDate; } + public void setInterval(Integer interval) { this.interval = interval; } @@ -44,14 +44,22 @@ public class TaskTemplate { this.relativeStartDate = relativeStartDate; } + /** + * Create a concrete {@link Task} given a concrete start date + * + * @param realStartDate The start date of the {@link GrowthPhase} to which the {@link #relativeStartDate} is relative. + * @param cropId The crop for which the task should be generated. + * @return The created {@link Task} + */ public Task generateTask(LocalDate realStartDate, long cropId) { - Task task = new Task(name, description, realStartDate.plusDays(relativeStartDate), cropId); - if (relativeEndDate != null) { - task.withEndDate(realStartDate.plusDays(relativeEndDate)); + LocalDate endDate = relativeEndDate != null ? realStartDate.plusDays(relativeEndDate) : realStartDate; + + if (interval == null) { + this.interval = 0; } - if (interval != null) { - task.withInterval(interval); - } - return task; + + return new Task(name, description, realStartDate.plusDays(relativeStartDate), cropId) + .withInterval(interval) + .withEndDate(endDate); } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/WateringCycle.java b/src/main/java/ch/zhaw/gartenverwaltung/types/WateringCycle.java index 9d7c7d0..5ac9778 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/WateringCycle.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/WateringCycle.java @@ -1,5 +1,12 @@ package ch.zhaw.gartenverwaltung.types; +/** + * Describes the cycle in which a {@link Plant} should be watered + * + * @param litersPerSqM How many litres of water per square metre of ground + * @param interval The interval (days) + * @param notes Notes on the cycle + */ public record WateringCycle( int litersPerSqM, int interval, diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6900f7f..159fd0b 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -5,10 +5,18 @@ module ch.zhaw.gartenverwaltung { requires com.fasterxml.jackson.datatype.jsr310; requires com.fasterxml.jackson.datatype.jdk8; requires java.logging; + requires java.mail; + opens ch.zhaw.gartenverwaltung to javafx.fxml; opens ch.zhaw.gartenverwaltung.types to com.fasterxml.jackson.databind; exports ch.zhaw.gartenverwaltung; + exports ch.zhaw.gartenverwaltung.io; exports ch.zhaw.gartenverwaltung.types; + exports ch.zhaw.gartenverwaltung.models; exports ch.zhaw.gartenverwaltung.json; + exports ch.zhaw.gartenverwaltung.backgroundtasks; + opens ch.zhaw.gartenverwaltung.backgroundtasks to javafx.fxml; + exports ch.zhaw.gartenverwaltung.backgroundtasks.email; + opens ch.zhaw.gartenverwaltung.backgroundtasks.email to javafx.fxml; } \ No newline at end of file diff --git a/src/main/resources/META-INF/javamail.default.address.map b/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/CropDetail.fxml b/src/main/resources/ch/zhaw/gartenverwaltung/CropDetail.fxml index ed3a476..89ff698 100644 --- a/src/main/resources/ch/zhaw/gartenverwaltung/CropDetail.fxml +++ b/src/main/resources/ch/zhaw/gartenverwaltung/CropDetail.fxml @@ -3,6 +3,7 @@ + @@ -12,39 +13,34 @@ - - + - + - - - - + - - + + + + + diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/MyPlants.fxml b/src/main/resources/ch/zhaw/gartenverwaltung/MyPlants.fxml deleted file mode 100644 index 61b215a..0000000 --- a/src/main/resources/ch/zhaw/gartenverwaltung/MyPlants.fxml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/MySchedule.fxml b/src/main/resources/ch/zhaw/gartenverwaltung/MySchedule.fxml index b778d50..e0f8cd8 100644 --- a/src/main/resources/ch/zhaw/gartenverwaltung/MySchedule.fxml +++ b/src/main/resources/ch/zhaw/gartenverwaltung/MySchedule.fxml @@ -1,74 +1,62 @@ + + - - - - - - + - - + - - + + - - - - - - - - - - - - - - + -