diff --git a/.gitignore b/.gitignore index 5e59b862ba4..81af32269e9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,11 @@ src/main/resources/docs/ /*.iml # Storage/log files -/data/ +/data/exercisebook.json +/data/propertybook.json +/data/regimebook.json +/data/schedulebook.json + /config.json /preferences.json /*.log.* diff --git a/.travis.yml b/.travis.yml index 924a42eb8da..30dd2747044 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,15 @@ language: java + matrix: include: - jdk: openjdk11 +services: + - xvfb + +before_script: + - export DISPLAY=:99.0 + script: >- ./config/travis/run-checks.sh && ./gradlew clean checkstyleMain checkstyleTest test coverage coveralls asciidoctor @@ -17,8 +24,11 @@ deploy: before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + - rm -f $HOME/.gradle/caches/*/fileHashes/fileHashes.bin + - rm -f $HOME/.gradle/caches/*/fileHashes/fileHashes.lock cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ + - $HOME/.m2 diff --git a/README.adoc b/README.adoc index d34211c9341..17a8a5e2dd5 100644 --- a/README.adoc +++ b/README.adoc @@ -1,23 +1,21 @@ -= Address Book (Level 3) += ExerHealth ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level3[image:https://travis-ci.org/se-edu/addressbook-level3.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level3[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level3?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level3/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level3?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level3&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] -https://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] +https://travis-ci.org/AY1920S1-CS2103T-T09-2/main[image:https://travis-ci.org/AY1920S1-CS2103T-T09-2/main.svg?branch=master[Build Status]] +https://coveralls.io/github/AY1920S1-CS2103T-T09-2/main?branch=master[image:https://coveralls.io/repos/github/AY1920S1-CS2103T-T09-2/main/badge.svg?branch=master[Coverage]] +https://app.netlify.com/sites/exerhealth/deploys[image:https://api.netlify.com/api/v1/badges/8b51ce83-a6ec-4994-8014-1c4b30aa3cd6/deploy-status[Netlify Status]] ifdef::env-github[] -image::docs/images/Ui.png[width="600"] +image::docs/images/Ui.png[width="800] endif::[] ifndef::env-github[] -image::images/Ui.png[width="600"] +image::images/Ui.png[width="800"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. +* ExerHealth is a one-stop platform for students who wish to embark on a journey of health and fitness. +* This application has a GUI but is optimised for students who prefer to work with a Command Line Interface(CLI). +* If you can type fast and wish to maintain a healthy lifestyle, Exerhealth is that one app for you! == Site Map @@ -28,9 +26,11 @@ endif::[] * <> == Acknowledgements +* Original source code, AddressBook-Level3, was created by https://se-education.org[SE-EDU initiative]. * Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by _Marco Jakob_. -* Libraries used: https://openjfx.io/[JavaFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/junit-team/junit5[JUnit5] +* Libraries used: https://openjfx.io/[JavaFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/junit-team/junit5[JUnit5], https://github.com/TestFX/TestFX[TestFX], https://github.com/TestFX/Monocle[Monocle] +* Icons made by https://www.flaticon.com/authors/freepik[Freepik] from https://www.flaticon.com[Flaticon] == Licence : link:LICENSE[MIT] diff --git a/build.gradle b/build.gradle index 93029ef8262..d8470f15e04 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ plugins { } // Specifies the entry point of the application -mainClassName = 'seedu.address.Main' +mainClassName = 'seedu.exercise.Main' sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -42,7 +42,8 @@ test { } dependencies { - String jUnitVersion = '5.4.0' + String testFxVersion = '4.0.16-alpha' + String jUnitVersion = '5.5.1' String javaFxVersion = '11' implementation group: 'org.openjfx', name: 'javafx-base', version: javaFxVersion, classifier: 'win' @@ -61,13 +62,19 @@ dependencies { implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' + testCompile group: 'org.assertj', name: 'assertj-core', version: '3.13.2' + testCompile "org.testfx:testfx-junit5:4.0.16-alpha" + + testImplementation group: 'org.testfx', name: 'testfx-core', version: testFxVersion + testRuntimeOnly group: 'org.testfx', name: 'openjfx-monocle', version: 'jdk-11+26' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion + } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'ExerHealth.jar' destinationDir = file("${buildDir}/jar/") } diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index 458e6134f45..bb2e32a0a6c 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,54 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 3 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + +ExerHealth was developed by the https://github.com/AY1920S1-CS2103T-T09-2[ExerHealth] team based on +the AddressBook code base done by the https://se-edu.github.io/docs/Team.html[se-edu] team. + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Gary Lim Yan Peng +image::garylyp.png[width="150", align="left"] +{empty}[https://github.com/garylyp[github]] [<>] -Role: Project Advisor +Role: Team Lead + +Responsibilities: Overall project coordination ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Kwek Kee En +image::kwekke.png[width="150", align="left"] +{empty}[https://github.com/kwekke[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Developer + +Responsibilities: Testing ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Tan Chee Peng +image::t-cheepeng.png[width="150", align="left"] +{empty}[https://github.com/t-cheepeng[github]] [<>] Role: Developer + -Responsibilities: Data +Responsibilities: Integration of project items ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Chai Jie Tung +image::jietung.png[width="150", align="left"] +{empty}[https://github.com/jietung[github]] [<>] Role: Developer + -Responsibilities: Dev Ops + Threading +Responsibilities: Code quality, deliverable and deadlines ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Ho Wei Haw +image::weihaw08.png[width="150", align="left"] +{empty}[https://github.com/weihaw08[github]] [<>] Role: Developer + -Responsibilities: UI +Responsibilities: Quality of various project documents ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 81be279ef6d..69869da0c03 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,6 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level3/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/AY1920S1-CS2103T-T09-2/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` +* *Email us* : You can also reach us at `e0310551 [at] u.nus.edu` diff --git a/docs/DevOps.adoc b/docs/DevOps.adoc index 2aa5a6bc0c1..629a9d6ab3d 100644 --- a/docs/DevOps.adoc +++ b/docs/DevOps.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 3 - Dev Ops += ExerHealth - Dev Ops :site-section: DeveloperGuide :toc: :toc-title: diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 3d65905a853..d86e9843eb8 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 3 - Developer Guide += ExerHealth - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -12,9 +12,9 @@ ifdef::env-github[] :note-caption: :information_source: :warning-caption: :warning: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level3/tree/master +:repoURL: https://github.com/AY1920S1-CS2103T-T09-2/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `Team ExerHealth`      Since: `Sep 2019`      Licence: `MIT` == Setting up @@ -26,7 +26,7 @@ Refer to the guide <>. === Architecture .Architecture Diagram -image::ArchitectureDiagram.png[] +image::ArchitectureDiagram.png[align="center"] The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component. @@ -34,7 +34,7 @@ The *_Architecture Diagram_* given above explains the high-level design of the A The `.puml` files used to create diagrams in this document can be found in the link:{repoURL}/docs/diagrams/[diagrams] folder. Refer to the <> to learn how to create and edit diagrams. -`Main` has two classes called link:{repoURL}/src/main/java/seedu/address/Main.java[`Main`] and link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp`]. It is responsible for, +`Main` has two classes called link:{repoURL}/src/main/java/seedu/exercise/Main.java[`Main`] and link:{repoURL}/src/main/java/seedu/exerecise/MainApp.java[`MainApp`]. It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup method where necessary. @@ -59,15 +59,15 @@ Each of the four components For example, the `Logic` component (see the class diagram given below) defines it's API in the `Logic.java` interface and exposes its functionality using the `LogicManager.java` class. .Class Diagram of the Logic Component -image::LogicClassDiagram.png[] +image::LogicClassDiagram.png[align="center"] [discrete] ==== How the architecture components interact with each other -The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The _Sequence Diagram_ below shows how the components interact with each other for the scenario where the user issues the command `delete t/exercise i/1`. -.Component interactions for `delete 1` command -image::ArchitectureSequenceDiagram.png[] +.Component interactions for `delete t/exercise i/1` command +image::ArchitectureSequenceDiagram.png[align="center", width=85%, scaledwidth=15cm] The sections below give more details of each component. @@ -75,13 +75,13 @@ The sections below give more details of each component. === UI component .Structure of the UI Component -image::UiClassDiagram.png[] +image::UiClassDiagram.png[align="center"] -*API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] +*API* : link:{repoURL}/src/main/java/seedu/exercise/ui/Ui.java[`Ui.java`] -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `ExerciseListPanel`, `ExerciseCard` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. -The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] +The `UI` component uses JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the link:{repoURL}/src/main/java/seedu/exercise/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] The `UI` component, @@ -93,21 +93,21 @@ The `UI` component, [[fig-LogicClassDiagram]] .Structure of the Logic Component -image::LogicClassDiagram.png[] +image::LogicClassDiagram.png[align="center"] *API* : -link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] +link:{repoURL}/src/main/java/seedu/exercise/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `ExerciseBookParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. -. The command execution can affect the `Model` (e.g. adding a person). +. The command execution can affect the `Model` (e.g. adding an exercise/regime). . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. . In addition, the `CommandResult` object can also instruct the `Ui` to perform certain actions, such as displaying help to the user. Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. .Interactions Inside the Logic Component for the `delete 1` Command -image::DeleteSequenceDiagram.png[] +image::DeleteSequenceDiagram.png[align="center"] NOTE: The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. @@ -115,132 +115,542 @@ NOTE: The lifeline for `DeleteCommandParser` should end at the destroy marker (X === Model component .Structure of the Model Component -image::ModelClassDiagram.png[] +image::ModelClassDiagram.png[align="center"] -*API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] +*API* : link:{repoURL}/src/main/java/seedu/exercise/model/Model.java[`Model.java`] -The `Model`, +The `Model` * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores a `PropertyBook` object that represents the custom properties defined by the user. +* stores a `ExerciseBook` object that represents the user's exercises being tracked. +* stores a `ExerciseDatabaseBook` object that represents the database of exercises in ExerHealth. +* stores a `RegimeBook` object that represents the user's regimes. +* stores a `ScheduleBook` object that represents the user's schedules. +* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * does not depend on any of the other three components. -[NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + - + -image:BetterModelClassDiagram.png[] [[Design-Storage]] === Storage component .Structure of the Storage Component -image::StorageClassDiagram.png[] +image::StorageClassDiagram.png[align="center"] -*API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] +*API* : link:{repoURL}/src/main/java/seedu/exercise/storage/Storage.java[`Storage.java`] The `Storage` component, * can save `UserPref` objects in json format and read it back. -* can save the Address Book data in json format and read it back. +* can save the Resource Book data in json format and read it back. +* can save the Property Book data in json format and read it back. +[NOTE] +Resource Book data consists of Exercise Book, Regime Book and Schedule Book data + [[Design-Commons]] === Common classes -Classes used by multiple components are in the `seedu.addressbook.commons` package. +Classes used by multiple components are in the `seedu.exercise.commons` package. == Implementation This section describes some noteworthy details on how certain features are implemented. // tag::undoredo[] -=== [Proposed] Undo/Redo feature -==== Proposed Implementation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. -Additionally, it implements the following operations: +=== Undo/Redo feature + +==== Implementation -* `VersionedAddressBook#commit()` -- Saves the current address book state in its history. -* `VersionedAddressBook#undo()` -- Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` -- Restores a previously undone address book state from its history. +The undo/redo mechanism is facilitated by the `events` package consisting of `EventHistory`, `EventFactory`, +`EventPayload` and the various `Event` classes. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +The `EventHistory` is a singleton class used to store a history of successfully executed commands as `Event` objects. +Instances of `Event` are stored in either the `undoStack` or the `redoStack` depending on the user's course of action. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +The `EventHistory` class has two primary methods namely `undo(Model model)` and `redo(Model model)`: -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +* `eventHistory.undo(model)` -- Undoes the `Event` at the top of the `undoStack`, executes it, and pushes it to the top of the `redoStack` +* `eventHistory.redo(model)` -- Redoes the `Event` at the top of the `redoStack`, executes it, and pushes it to the top of the `undoStack` -image::UndoRedoState0.png[] +These operations are utilised in the `UndoCommand` and `RedoCommand` respectively. -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. +The following steps will describe the steps taken in the execution of an UndoableCommand, and subsequently +the UndoCommand and RedoCommand. -image::UndoRedoState1.png[] +*Step 1:* When an `UndoableCommand` is executed, key information used during the command will be added into a +newly initialized `EventPayload`. +[NOTE] +The `EventPayload` is a wrapper class to store key information about the particular command. +For instance, if an `EditCommand` has been executed, the `EventPayload` will store the `originalExercise` +as well as the `editedExercise`. + +*Step 2:* The `EventFactory` takes in the `UndoableCommand` and generates an `Event` using the `EventPayload` +stored in the `UndoableCommand`. +The `Event` is then added to the undo stack of the `EventHistory`. +[NOTE] +The `EventFactory` checks for the command word of the `UndoableCommand` to decide which specific `Event` object +to generate. It will then obtain the `EventPayload` from the `UndoableCommand` and pass it into the constructor of the +`Event` so that the `Event` captures the key information of the `UndoableCommand`. -Step 3. The user executes `add n/David ...` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. +*Step 3:* To undo the latest `UndoableCommand` the user executes the `UndoCommand` by entering `undo` +into the command box. -image::UndoRedoState2.png[] +*Step 4:* The `UndoCommand` executes `eventHistory.undo(model)`, which prompts the `EventHistory` instance +to pop the next `Event` to undo from the undo stack. Once the `Event` is undone, it will be pushed to the +top of the redo stack. +*Step 5:* To redo the command that has been undone, the user executes the `RedoCommand`. This execution +behaves similarly to step 4, except that the next `Event` is taken from the top of the redo stack and +pushed to the undo stack instead. [NOTE] -If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. +In steps 4 and 5, if any of the respective stack is empty when undo or redo is called, a `CommandException` will be thrown +and an error message will be displayed to indicate there is no undoable or redoable commands. + +The following two Sequence Diagrams show a sample flow of the execution when an `EditCommand`, which is an +`UndoableCommand`, has been executed and subsequently undone. -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +The first diagram below describes the process of storing an `EditEvent` to `EventHistory` during the execution of the +`EditCommand`. The `EventPayload` is only initialized when the `EditCommand` is executed. The `EventPayload` is +subsequently used for the initialization of the `EditEvent`. -image::UndoRedoState3.png[] +.The process of storing an EditEvent to EventHistory +image::UndoableCommandSequenceDiagram.png[align="center"] +The second diagram below describes the process of undoing the executed `EditCommand` using the `UndoCommand`. +When the `UndoCommand` is executed, the `EventHistory` calls the `undo` method of the next `Event` in the undo stack +(i.e. the `EditEvent`). + +.The process of undoing the EditCommand +image::UndoSequenceDiagram.png[align="center"] + +Given below is a Class Diagram to show the associations between Event, Command and Model. It is specifically designed +such that only objects that implement the `Event` and `Command` interface will need to handle the `model` class. +[[undoablecommands]] [NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the undo. +The only commands that implements the `UndoableCommand` are `AddCommand`, `DeleteCommand`, `EditCommand`, +`ClearCommand`, `ScheduleCommand` and `ResolveCommand`. They each stores an `EventPayload` instance. + +.The associations between Event, Command and Model +image::EventClassDiagram.png[align="center", width=85%, scaledwidth=15cm] + +The following Activity Diagram summarizes what happens when a user enters undoable commands, +the undo command and the redo command. + +.The workflow when a user enters an undoable command and performs undo/redo +image::CommitActivityDiagram.png[align="center"] + +==== Design Considerations +===== Aspect: How undo & redo executes + +* **Choice 1: (current choice)** Implements undo and redo of each Command in a separate Event object stored in the EventHistory +** Pros: +*** Uses less memory to store Event objects and payloads as compared to entire copies of the Model object. +*** Open for extensions and close to modifications as the Event interface only contains undo and redo methods, and can +be easily implemented when new Undoable commands are introduced. +** Cons: +*** UndoableCommand objects are forced to depend on EventPayloads when it does not actually use it directly. +(e.g. `DeleteCommand` has to store the exercise being deleted despite using it only once). + +* **Choice 2:** Individual command knows how to undo/redo by itself. +** Pros: +*** Uses less memory to store each command as compared to entire copies of the Model object. +** Cons: +*** Violates Single Responsibility Principle as Commands need to contain specific implementation of the inverse action of itself +and also stores data such as the exercise being deleted in a local field. + +* **Choice 3:** Saves the entire model consisting of the exercise, regime, schedule and suggestion lists. +** Pros: +*** Easy to implement. +** Cons: +*** May have performance issues in terms of memory usage as multiple lists need to be stored +(i.e. Exercise list, Regime list, Schedule list) +*** Unnecessary storage of irrelevant details such as suggestion list. + +===== Aspect: Data structure to support the undo/redo commands + +* **Choice 1 (current choice):** Use a singleton EventHistory to store stacks of Events generated by a EventFactory. +** Pros: +*** Ensures only one instance of EventHistory exists +*** The EventFactory relies on the Factory pattern that helps to reduce coupling between +EventHistory and each individual Event. +** Cons: +*** The Singleton pattern may have a chance of breaking if multiple threads initialize the singleton class +at the same time, creating multiple instances of EventHistory. However, if this problem arises, the instantiation +method can be made "synchronized" to circumvent this issue. + +* **Choice 2:** Use a list to store the history of model objects. +** Pros: +*** Very simple to implement as each step simply requires a deep copy of the model to be created +and stored. +** Cons: +*** Difficult to monitor multiple resource books (e.g. Regime books and Exercise books) as they all +manage different types of resources that can be altered by commands. + +// end::undoredo[] + +==== Design Patterns + +The Undo/Redo feature implementation is based on the Singleton, Command, and Factory design patterns + +* *Singleton* +** To help ensure that only one instance of `EventHistory` exists during the execution of the program +** Allows easier access by the various command classes (i.e. the UndoableCommands, UndoCommand and RedoCommand) + +* *Command* +** Extensions of new `Event` is easy and can be done without significant changes to the existing code + +* *Factory* +** Suitable for the context of taking in a particular Command and returning a corresponding Event +** Reduces coupling between Command classes and Event classes + + +// tag::resolvefeature[] + +=== Resolve feature +==== Rationale +There are multiple times where if the user wishes to schedule a regime, they find themselves in trouble over which kind of exercise regime they can fit into their schedule. The motivation behind this feature is so that users can customise their own schedules to their own liking. The alternative of an auto scheduler will restrict users from having the regime of their liking be scheduled. Instead of forcing users to adhere to some pre-generated resolution, we allow the users to make their own choice and choose their own exercise regime to be scheduled. + +==== Implementation +The resolve feature is used when there is a scheduling conflict that happens within ExerHealth. This feature will alter the state of the program. The state is known by `MainApp` and it is either `State.IN_CONFLICT` or `State.NORMAL`. Only when the state is `State.IN_CONFLICT` will `resolve` commands be allowed. -The following sequence diagram shows how the undo operation works: +For the implementation of the resolve feature, the `ResolveCommand` will hold a `Conflict` object which is then passed into `Model`. The concrete implementation, `ModelManager` then resolves the conflict that is being held there. Each `Conflict` object will hold 1 conflicting `schedule` and 1 `schedule` that was originally scheduled on the date. -image::UndoSequenceDiagram.png[] +Shown below is the class diagram for the implementation of the `Resolve` feature. -NOTE: The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +.Class diagram for Resolve Command +image::ScheduleResolveClassDiagram.png[align="center", width=85%, scaledwidth=15cm] -The `redo` command does the opposite -- it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +With regards to the flow of the program for a scheduling conflict, the steps are laid out below: + +Step 1. User enters a `schedule` command that will cause a scheduling conflict. The `ScheduleCommand` will change `MainApp` state to `State.IN_CONFLICT`. + +[NOTE] +`schedule` can conflict with another `schedule` when the dates from the 2 schedules are the same. The method `model.hasSchedule()` returns `true` if that happens. + +Step 2. A `CommandResult` object is returned to `MainWindow` where the flag `showResolve` is set to `true`. + +Step 3. Upon receipt of the object, `MainWindow` will show the resolve window and the user is required to resolve the conflict. [NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +The `ResolveWindow` will block all inputs to `MainWindow` and only allow `resolve` command to be entered. + +Shown below is the sequence diagram for when a scheduling conflict happens: -Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. +.Sequence diagram when a scheduling conflict happens +image::ScheduleConflict.png[align="center"] -image::UndoRedoState4.png[] +Step 5. When the user is prompted with the `ResolveWindow`, all the conflicting exercises will be shown in one page. The previously `scheduled regime` on the left and the `conflicting regime` on the right. -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. +Step 6. Once the user issue a `resolve` command correctly, the `model` and `storage` of ExerHealth will be updated to reflect the changes. A new regime will be added for the user from the `resolve`. + +[NOTE] +The `ResolveWindow` will only take one valid `resolve` command and `Ui` will close the `ResolveWindow` immediately after the command finishes. The newly made schedule will result in a new `regime` being added to the user's `RegimeList`, so the name of the `regime` in the `resolve` command cannot have any conflicts with current names in `RegimeList`. -image::UndoRedoState5.png[] +Step 7. The `ResolveWindow` then closes upon successful `resolve` and the application continues. -The following activity diagram summarizes what happens when a user executes a new command: +The following activity diagram summarizes what happens when a user enters a `schedule` command: -image::CommitActivityDiagram.png[] +.Activity diagram for when a user enters a `schedule` command +image::ScheduleActivityDiagram.png[align="center", width=85%, scaledwidth=15cm] ==== Design Considerations +===== Aspect: Signalling Schedule Conflict +* **Choice 1 (current choice): ** Using `CommandResult` object +** Pros: +*** Makes use of existing objects in codebase making it easier to implement +*** UI does not have to handle logic when encountering schedule conflicts. It only has to show the `ResolveWindow` and pass the data accordingly. +** Cons: +*** If we have to signal different types of outcomes to the UI, the `CommandResult` class will become bloated. -===== Aspect: How undo & redo executes +* **Choice 2: ** throw `ScheduleException` +** Pros: +*** Easy to implement. `ScheduleCommand` just has to throw an exception and `UI` catches it. +** Cons: +*** `UI's` execute methods will contain multiple `try/catch` which acts like a control flow mechanism which increases code smell. +*** If there is a need to pass down information from executed Commands, an exception is unable to convey any sort of complex information that the `UI` can act on. Thus, encapsulating information in an object will be more open to extension compared to throwing an exception. -* **Alternative 1 (current choice):** Saves the entire address book. -** Pros: Easy to implement. -** Cons: May have performance issues in terms of memory usage. -* **Alternative 2:** Individual command knows how to undo/redo by itself. -** Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). -** Cons: We must ensure that the implementation of each individual command are correct. +// end::resolvefeature[] -===== Aspect: Data structure to support the undo/redo commands +//tag::customfeature[] +=== Custom feature -* **Alternative 1 (current choice):** Use a list to store the history of address book states. -** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. -** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. -* **Alternative 2:** Use `HistoryManager` for undo/redo -** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. -** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. -// end::undoredo[] +==== Rationale +A quick conversation with a few of our friends revealed that there are many properties which they intend to keep track +for exercises. However, it is unlikely that we can implement all of these properties for the exercises as there may +be too much overhead and we can never be certain that we have met all of the users' needs. + +==== Overview +This feature is facilitated by both `PropertyBook` and `CustomProperty`. Whenever a user +adds a newly defined custom property, a `CustomProperty` object will be created which is stored in +`PropertyBook`. Its corresponding prefix and full name will be tracked by `PropertyBook` to avoid +clashes in their uses. + +==== Current Implementation +`CustomProperty` encapsulates a single custom property that the user defines. It contains +information such as name, prefix and parameter type of the custom property. The parameter type is supported by +an enumeration class ``ParameterType`` and is restricted to one of the following 3 types: ``Number``, ``Text``, ``Date``. + +`PropertyBook` serves as a singleton class that helps to manage all of the custom properties that have been +defined by the user. This class acts as an access point for any information relating to the creation or deletion +of custom properties. + +To keep track of the custom properties and its relevant information, the following are used: + +1. `customProperties`: A set containing all of the `CustomProperty` objects that +have been created. + +2. `customPrefixes`: A set containing all of the `Prefix` objects associated with existing custom +properties. + +3. `customFullNames`: A set containing the full names of the existing custom properties. + +4. `defaultPrefixes`: A set containing all of the `Prefix` objects associated with default properties and parameter +types. + +5. `defaultFullNames`: A set containing all of the full names of default properties. + +Custom names and prefixes are separated from its default counterparts to ensure that the default names and prefixes +will always be present when the `PropertyBook` is first initialised. + +To help facilitate `PropertyBook` in its custom properties management, the following main methods are implemented: + +1. `PropertyBook#isPrefixUsed(Prefix)`: Checks if the given prefix has been used by a default or custom property. + +2. `PropertyBook#isFullNameUsed(String)`: Checks if the given name has been used by a default or custom property. + +3. `PropertyBook#isFullNameUsedByCustomProperty(String)`: Checks if the given name has been used by a custom property + +4. `PropertyBook#addCustomProperty(CustomProperty)`: Adds the new custom property. Each time a custom property is +added, the prefix set in `CliSyntax` is also updated. + +5. `PropertyBook#removeCustomProperty(CustomProperty)`: Removes a pre-defined custom property. Its associated prefix +is also removed from the prefix set in `CliSyntax`. + +All of the crucial associations mentioned above are summarised in the next class diagram. + +.Class diagram of the associations of `PropertyBook` and `CustomProperty` +image::CustomClassDiagram.png[align="center", width=85%, scaledwidth=15cm] + +===== Adding Custom Properties +To add a new custom property for the exercises, the user can do it through the command `custom s/PREFIX_NAME f/FULL_NAME +p/PARAMETER_TYPE`. Examples include `custom s/r f/Rating p/Number` and `custom s/ed f/Ending Date p/Date`. + +The following sequence diagram will illustrate how the custom operation works when a custom +property is *successfully added*. + +.Sequence diagram of a successful addition of a custom property +image::EditedCustomAddSequenceDiagram.png[align="center"] + +For further clarity, one can identify the above diagram with the following sequence of steps: + +*Step 1:* User first defines the custom property they wish to add for the exercises. + +*Step 2:* The custom property will be parsed by the app's parser and a new `CustomProperty` +object is created. + +*Step 3:* This `CustomProperty` object will be returned together with a newly created `CustomAddCommand` object. + +*Step 4:* The `execute` method of the `CustomAddCommand` method will be called and the `CustomProperty` object +will be added to `PropertyBook`. + +*Step 5:* Finally, a `CommandResult` object will be created and returned. + +The above steps illustrate the main success scenario. However, not all additions of a custom property will be successful. +The next activity diagram shows the workflow when a new custom property is defined. + +.Activity diagram of the workflow when a new custom property is added +image::CustomAddActivityDiagram.png[align="center", width=85%, scaledwidth=15cm] + +Once a custom property is successfully added into `PropertyBook`, the user can use the prefix of the custom property in +`add` or `edit` command. + +===== Removing Custom Properties +Should a user wish to remove a custom property from all of the exercises, he/she can simply make use of the command +`custom rm/FULL_NAME`. A custom property that has been removed from the ``PropertyBook`` can be re-added back if the user chooses to. +Alternatively, if the user wishes to remove a custom property just from a single exercise, he/she can choose to enter `custom rm/FULL_NAME i/INDEX` instead. + +The next sequence diagram illustrates what happens when a custom property is removed from the `PropertyBook`. If a custom property +is removed from a single exercise instead, only the selected exercise will be updated. + +.Sequence diagram of a successful removal of a custom property from all exercises +image::EditedCustomRemoveSequenceDiagram.png[align="center"] +==== Design Considerations + +===== Aspect: `PropertyBook` design +* **Choice 1 (Current choice)**: Represent `PropertyBook` as a singleton class that will +act as the only access point for the addition and removal of custom properties. +** Pros: Having a singleton helps to provide more utility for methods that rely on the `CustomProperty` objects that have been +created. +** Cons: It makes testing much difficult as the results from the previous test cases are carried over. Furthermore, +it increases coupling across the code base. + +* **Choice 2**: Represent `PropertyBook` as a usual Java object that can be instantiated many times. +** Pros: This reduces coupling and makes testing easier as a new `PropertyBook` object independent of the other tests +can be created for different tests. +** Cons: There could be situations where 2 instances of `PropertyBook` objects are created and the addition of a +custom property is done to only one instance and not in the other. + +After much consideration, Choice 1 was implemented with the following reasons: + +1. `AddCommandParser` and `EditCommandParser` have to gain access to the `CustomProperty` in order to ensure that the values entered +for the custom properties in the add/edit commands are valid. However, as the `ExerciseBookParser` in the original code base only takes in a +`String` as a parameter, there has to be another way of retrieving the custom properties. While we can change the +`ExerciseBookParser` to take in a data structure containing `CustomProperty` objects, this does not seem good as its responsibility +is just to ensure that a predefined command is entered and is passed to the correct command parser.A slightly better choice in this case is to make the data structure holding the `CustomProperty` objects a static variable and parsers that require it can access +it directly. + +2. If the data structure holding the `CustomProperty` object is to be made static, it means that this information is +shared among all of the `PropertyBook` instances if Choice 2 is implemented. Thus, `PropertyBook` +is acting like a singleton and so, a singleton class will be appropriate. + +//end::customfeature[] + +// tag::suggest[] +=== Suggest + +==== Rationale +Beginners now have a plethora of choices, which may overwhelm them when they are deciding on what exercises to do. +Thus, we decided to provide users with sample exercise routines to reduce the inertia of starting this lifestyle change. +On the other hand, regular gym goers may face a repetitive and mundane exercise routine or may want to experiment with different exercises. +As such, to put it briefly, we decided to give users the ability to discover exercises based on the characteristics they are interested in. + +This feature presents a cohesive function that all users can benefit from. +It also makes our application well-rounded so that users can better achieve their fitness goals. + +==== Overview +The sample exercise routines are currently implemented in ExerHealth's database as a hard-coded set of exercises. +More importantly, the `SuggestPossible` command which caters to more experienced gym goers utilises the exercises that the user +has already done, in addition to ExerHealth's database. Hence, we allow users to search for suggestions +based on `Muscle` and `CustomProperty`. + +==== Current Implementation +The `SuggestBasic` command displays a list of exercises from our database to the user. +The `SuggestPossible` command is created by parsing the user's inputs to form a `Predicate` before filtering ExerHealth's database and the user's tracked exercises. + +<<< + +The following activity diagram summarizes what happens when a user enters a `SuggestPossible` command: + +.Activity diagram showing the workflow of a `SuggestPossible` command +image::SuggestActivityDiagram.png[align="center", width=85%, scaledwidth=15cm] + +In detail, when a `SuggestPossible` command is entered, the `Logic` component is responsible for parsing the inputs into a `Predicate`. +The `Predicate` is then used to instantiate a `SuggestPossible` command, and later used to filter a list of `Exercise` when the command is executed. +The interactions between the multiple objects can be captured using a sequence diagram. + +<<< + +The following sequence diagram shows the sequence flow when a user enters a valid `SuggestPossible` command: + +.Sequence diagram of a `SuggestPossibleCommand`. +image::SuggestSequenceDiagram.png[align="center"] + +From the sequence diagram: + +1. When the `LogicManager` receives the `execute` command, it calls the `parseCommand` method of `ExerciseBookParser`. + +2. `ExerciseBookParser` will receive `suggest` as the command type and instantiate `SuggestCommandParser` to further parse the command. + +3. `SuggestCommandParser` will receive `s/possible` as the suggest type and calls the `parsePredicate` method of `ParserUtil` +to parse the user input to create an `ExercisePredicate` object (named `p` in the diagram). + +4. `SuggestCommandParser` will instantiate `SuggestPossibleCommand` with the `ExercisePredicate` as the constructor parameter. + +5. The `SuggestPossibleCommand` object is then returned to `SuggestCommandParser`, followed by `ExerciseBookParser`, and lastly back to `LogicManager` to execute. + +6. `LogicManager` will proceed to `execute` `SuggestPossibleCommand`. + +7. `SuggestPossibleCommand` then calls the `updateSuggestedExerciseList` method in `ModelManager`, passing in the predicate to filter the list of suggest exercises. + +8. `SuggestPossibleCommand` creates a new `CommandResult` to be returned. + +<<< + +In step 3, the process in which the `ExercisePredicate` object is created can be explored deeper. + +.Sequence diagram of how an `ExercisePredicate` is created + +image::SuggestPredicateSequenceDiagram.png[align="center"] + +From the sequence diagram above: + +1. `ParserUtil` creates `ExerciseMusclePredicate` and `ExerciseCustomPropertyPredicate` with the input parameters. + +2. Since there were no CustomProperty tags to filter, `ParserUtil` creates `ExercisePredicate` with only the `musclesPredicate` and the boolean `isStrict`. + +3. The resulting `ExercisePredicate` is then returned to `ParserUtil`, followed by `SuggestCommandParser`. + +A `SuggestPossibleCommand` contains an `ExercisePredicate` object. +An `ExercisePredicate` object contains a `list` of `BasePropertyPredicate`, +where each contains either a `Collection` of `Muscle` or `CustomProperty`. +The diagram below shows the structure of a `ExercisePredicate` object. + +.Class diagram of the classes behind the suggest possible feature + +image::SuggestCommandClassDiagram.png[align="center"] + +Creating classes such as `ExerciseCustomPropertyPredicate` and `ExerciseMusclePredicate` +allows us to conduct better testing because we can compare the `Collection` of `Muscle`/`CustomProperty` between different predicates. + +<<< + +==== Design Considerations +===== Aspect: Implementation of predicate creation +* **Choice 1: ** `SuggestPossibleCommand` to handle the predicates. +** Pros: +*** Easy to implement and understand. The class `SuggestPossibleCommand` contains the parsing and creation of the predicate +all in one place as it stores the tags, and creates the predicate and filters the list of exercises. +** Cons: +*** Violation of Single Responsibility Principle (SRP) as `SuggestPossibleCommand` updates the model and creates the predicate. +* **Choice 2 (current choice): ** Predicate class to handle all predicates. +** Pros: +*** Adheres to SRP and Separation of Concern (SoC). +** Cons: +*** Increases the complexity of the code as more classes are needed, and also increases the lines of code written. +// end::suggest[] + +// tag::statistic[] +=== Statistics + +==== Implementation +Statistics of exercises will be displayed in charts. Supported chart types are Pie Chart, Line Chart and Bar Chart. +StatsFactory will create Statistic using given parameters. +The figure below shows the class diagram of statistics: + +.Class diagram of the classes behind the statistics feature +image::StatisticClassDiagram.png[align="center", width=85%, scaledwidth=15cm] + +The next figure shows the activity diagram when user enter a `stats` command: -// tag::dataencryption[] -=== [Proposed] Data Encryption +.Workflow when a user enters a `stats` command +image::StatisticActivityDiagram.png[align="center", width=85%, scaledwidth=15cm] -_{Explain here how the data encryption feature will be implemented}_ +Given below is an example usage scenario of statistics feature. -// end::dataencryption[] +Step 1: User enters a `stats` command to see statistics of exercises. + +Step 2: `ExerciseBookParser` will receive command from `LogicManager` and pass command to `StatsCommandParser`. + +Step 3: `StatsCommandParse` will parse the command and creates a `StatsCommand`. + +Step 4: `StatsCommand` calls `Model#getExerciseBookData` to get data of all exercises. + +Step 5: `StatsCommand` creates a `StatsFactory` and pass exercises data, chart and category to `StatsFactory`. + +Step 6: `StatsFactory` will then generate `Statistic` and return to `StatsCommand`. + +Step 7: `StatsCommand` then calls `Model#setStatistic` to set the `Statistic` in `Model`. + +Step 8: `StatsCommand` creates a new `CommandResult` and return to `LogicManager`. + +Shown below is the sequence diagram when user enters a valid `stats` command: + +.Sequence diagram of a `stats` command +image::StatisticSequenceDiagram.png[align="center"] + +// end::statistic[] === Logging @@ -279,106 +689,284 @@ Refer to the guide <>. *Target user profile*: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types +* exercises on a regular basis +* actively monitors exercise records +* develops exercise regimes for the future +* prefers desktop apps over other types * can type fast * prefers typing over mouse input * is reasonably comfortable using CLI apps -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +*Value proposition*: + +* provides an integrated platform to track and access past exercise records +* shows more complex data analytics than the statistics a standard tracking app provides +* allows flexible and conflict-free scheduling of planned exercises +* provides exercise suggestions based on past activities [appendix] == User Stories Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` -[width="59%",cols="22%,<23%,<25%,<30%",options="header",] +[width="100%",cols="<10%,<10%,<30%,<30%",options="header",] |======================================================================= |Priority |As a ... |I want to ... |So that I can... -|`* * *` |new user |see usage instructions |refer to instructions when I forget how to use the App -|`* * *` |user |add a new person | +|`* * *` |student who exercises |monitors the types and quantity of the exercises I have completed |remember and have different variations to my exercises -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |athletic student |have a way to store all my exercises and their relative intensities |make reference to past exercises while scheduling future exercises -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |frequent gym-goer |keep track of my rep counts |know how hard I have pushed and how far I am from my target reps -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |student who wants to get stronger |keep track of my the muscles my gym exercise works on |plan what kind of muscle groups I should target to get stronger -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +|`* * *` |amateur at exercising |have the app come up with exercises for me based on my user profile |better plan future regimes based on my previous attempt + +|`* * *` |student who just got into exercising |have some sample training plans |have a starting point for my exercise regime + +|`* * *` |frequent gym-goer with targets |see my progression for every exercise and the date I completed them |see how much I have improved + +|`* * *` |Student who loves visual data |visualise my exercise statistics |understand all my relevant data immediately + +|`* * *` |student who is very busy |have the app detect clashes in my exercising schedules |reschedule some of my exercises somewhere else + +|`* * *` |person who likes customization |add in new attributes for exercises |tailor the app for my personal use + +|`* * *` |careless athletic student |be able to have a way to undo my actions |easily undo my command when I accidentally delete one of my training plans + +|`* * *` |careless athletic student |be able to have a way to redo my actions |simply redo my undone command when I realize I undid an important exercise + +|`* * *` |athletic student who has a fixed training plan |have a way to store this training plan permanently |save some trouble of constantly updating the app whenever I want to begin on that training plan + +|`* *` |student who is impatient |have simple commands |input new entries quickly + +|`* *` |health-conscious student |keep track of my daily calories burnt |monitor my calorie count over a specific duration + +|`* *` |student who wants to get stronger |Know what kind of muscles I have been training for the past month |take note of which muscles I have been focusing for training + +|`* *` |student who wants to track exercises quickly and efficiently |be able to add exercises from history |add the same thing without having to type it all out + +|`* *` |student who wants a balanced exercise regime |have the app auto suggest some forms of exercise |easily find new exercises to do + +|`* *` |athletic student |be able to modify my current training schedule |easily adapt my previous training plans into better plans that can help improve my physique + +|`* *` |athlete who wants to improves |save notes from my previous session |reflect and modify my training regime accordingly to suit my pace + +|`* *` |athletic student who loves to do things my way |be able to define my own command syntax |type the commands for the various features much easily and quickly + +|`*` |athletic student who uses the app often |have an auto-complete or input suggestion feature |easily add in reused exercises conveniently without having to type them out fully again + +|`*` |student who likes to keep things neat |be able to archive my older exercises |be more focused on recent exercises + +|`*` |student who just got into gyming |receive some tips on good gym practices |avoid injuring myself during training -_{More to be added}_ +|`*` |student who just got into sports |Understand the most important tips on good exercise habits |maximise the benefits of my exercises + +|`*` |student who wants to get stronger |be advised on how much increment I should make for each exercise |train progressively + +|`*` |athletic student |be able to keep track of my recovery period |avoid doing harm to my body from excessive training + +|`*` |forgetful student |be reminded of when i have to exercise |set aside time for exercising + +|`*` |frequent gym-goer |be reminded of my exercise schedules |remember to go for my sessions + +|`*` |athletic student |monitor the list of equipment I need for each session |remember what I need for subsequent exercise sessions of the same kind + +|`*` |frequent gym-goer |store my workout music playlist |access my favourite gym workout playlist conveniently when gyming + +|`*` |student with a busy schedule |be able to export my files |to resolve conflicts between my exercise and work schedule + +|`*` |student who is very lazy |be able to mass import all my exercises data from other platforms |save the trouble of inputting an entire list of existing entries one by one + +|`*` |student who uses mobile running apps |import data from other application |avoid the time-consuming process of adding all exercises manually + +|======================================================================= [appendix] == Use Cases -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +(For all use cases below, the *System* is the `ExerHealth` and the *Actor* is the `user`, unless specified otherwise) -[discrete] -=== Use case: Delete person + +[Discrete] +=== UC01: Statistics (bar chart) + +*System:* ExerHealth + +*Actor:* user *MSS* -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User adds multiple exercises to the ExerHealth tracker +2. User requests to see a bar chart of the most frequently +done exercises within a range of date +3. ExerHealth shows user the breakdown of exercises and +their respective frequency for the date range + Use case ends. -*Extensions* -[none] -* 2a. The list is empty. +[Discrete] +=== UC02: Schedule + +*System:* ExerHealth + +*Actor:* user + +*MSS* + +1. User requests for the list of exercises. +2. ExerHealth displays the list of exercises it is tracking. +3. User adds 1 or more exercises to a <> +4. ExerHealth adds the regime to the user's list of regime and display successful addition +5. User <> a regime at a date +6. ExerHealth schedules regime at the date and displays successful scheduling + Use case ends. -* 3a. The given index is invalid. + +*Extensions* +[None] +* 5a. ExerHealth detects more than one regime at the date +[None] +** 5a1. ExerHealth displays resolve window to user +** 5a2. User enters which exercises they wish to schedule at the date from the conflicting regimes +** 5a3. ExerHealth schedules the newly made regime at the date and closes resolve window ++ +Use case ends + + +[Discrete] +=== UC03: Suggest +*System:* Exerhealth + +*Actor:* user + +*MSS* + +1. User asks for suggestions +2. System searches database for previous exercises done +3. System creates a suggestion based on search and request type + -[none] -** 3a1. AddressBook shows an error message. +Use case ends + +[Discrete] +=== UC04: Custom + +*System:* ExerHealth + +*Actor:* user + +*MSS* + +1. User requests to add in a new user-defined <> for exercises +2. ExerHealth adds in the user-defined property for all exercises +3. User adds a new exercise with the newly specified prefix and argument +for the property ++ +Use case ends + +*Extensions* +[None] +* 1a. ExerHealth detects that the <>/full name of the user-defined property is a duplicate +of another property/parameter for add / edit command. +[None] +** 1a1. ExerHealth informs the user that the prefix name/full name of his/her new property is a +duplicate of a current property/parameter for add / edit command. + -Use case resumes at step 2. +Use case ends + +[Discrete] +=== UC05: Undo +*System:* ExerHealth + +*Actor:* user + +*MSS* + +1. User executes an undoable command +2. ExerHealth performs the change +3. User undoes the latest command +4. ExerHealth undoes the latest change ++ +Steps 3-4 can be repeated for as many times as required until there is +no undoable command left to undo ++ +Use case ends + +*Extensions* +[None] +* 3a. The undo history is empty +[None] +** 3a1. ExerHealth informs user that undo is not allowed at this point ++ +Use case ends + + +[Discrete] +=== UC06: Redo +*System:* ExerHealth + +*Actor:* user + +*MSS* + +1. User undoes the latest command +2. ExerHealth undoes the latest change +3. User redoes the latest undoable command that was undone +4. ExerHealth redoes the command again ++ +Steps 3-4 can be repeated for as many times as required until there are no more +undoable command left to redo ++ +Use case ends + +*Extensions* +[None] +* 3a. There is no action to redo as the user has not executed undo before +[None] +** 3a1. ExerHealth informs user that redo is not allowed at this point ++ +Use case ends -_{More to be added}_ [appendix] == Non Functional Requirements . Should work on any <> as long as it has Java `11` or above installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +. Should be able to hold up to 1000 exercises without a noticeable sluggishness in performance for typical usage. . A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +. Should work without requiring an installer. +. Should not depend on a remote server. +. Should be for a single user i.e. (not a multi-user product). -_{More to be added}_ [appendix] == Glossary [[mainstream-os]] Mainstream OS:: -Windows, Linux, Unix, OS-X +Windows, Linux, Unix, OS-X. -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others +[[regime]] Regime:: +A specific set of exercises that are to be done together. For example, +a *Legs* regime at the gym can include multiple exercises such as +squats, hamstring curl and calf raises. -[appendix] -== Product Survey +[[schedule]] Schedule:: +Planning of an exercise on a later day. -*Product Name* +[[property]] Property:: +An attribute of an exercise item. Pre-defined attributes include name, +quantity, units and calories. -Author: ... +[[prefix]] Prefix:: +The term that comes before each parameter in the command. For example, the +prefix in `p/Number` is `p/`. -Pros: +[[prefixName]] Prefix Name:: +The word that comes before `/` in the prefix. For example, the prefix name of `p/` is `p/ -* ... -* ... - -Cons: - -* ... -* ... - -[appendix] == Instructions for Manual Testing Given below are instructions to test the app manually. @@ -392,7 +980,7 @@ These instructions only provide a starting point for testers to work on; testers .. Download the jar file and copy into an empty folder .. Double-click the jar file + - Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + Expected: Shows the GUI with a set of sample exercises. The window size may not be optimum. . Saving window preferences @@ -400,26 +988,186 @@ These instructions only provide a starting point for testers to work on; testers .. Re-launch the app by double-clicking the jar file. + Expected: The most recent window size and location is retained. -_{ more test cases ... }_ +=== Deleting an exercise + +. Deleting an exercise while all exercises are listed + +.. Prerequisites: List all exercises using the `list` command. Multiple exercises in the list. +.. Test case: `delete t/exercise i/1` + + Expected: First exercise is deleted from the list. Details of the deleted exercise shown in the status message. +.. Test case: `delete t/exercise i/0` + + Expected: No exercise is deleted. Error details shown in the status message. Status bar remains the same. +.. Other incorrect delete commands to try: `delete t/exercise`, `delete t/exercise i/x` (where x is larger than the list size) + + Expected: Similar to previous. + +=== Deleting an exercise in regime +. Deleting an exercise in regime while all regimes are listed. + +.. Prerequisites: List all regimes using the `list` command. Regime named `Level 1` has multiple exercises and is in the list. +.. Test case: `delete t/regime n/Level 1 i/1` + + Expected: The first exercise in `Level 1` regime is deleted. +.. Test case: `delete t/regime n/Level 1 i/0` + + Expected: No exercise is deleted in `Level 1` regime. +.. Other incorrect delete commands to try: `delete t/regime`, `delete t/regime i/x` (where x is larger than the list size) + + Expected: Similar to previous. + +=== Deleting a regime +. Deleting a regime while all regimes are listed. + +.. Prerequisites: List all regimes using the `list` command. Regime named `Power set` is in the list. +.. Test case: `delete t/regime n/Power set` + + Expected: The regime named `Power set` is deleted from the list. +.. Test case: `delete t/regime n/power set` + + Expected: `Power set` regime is not deleted as name is case-sensitive. Error details shown in the status message. + +=== Scheduling a regime + +. Scheduling an exercise regime + +.. Prerequisites: Have an exercise regime of name `cardio`. + +.. Test case: `schedule n/cardio d/12/12/2019` with no other schedule on `12/12/2019` + + Expected: Regime `cardio` is now scheduled on `12/12/2019`. Details of schedule should be shown in the center information panel and the left panel should switch to show schedule list. + +.. Test case: `schedule n/cardio d/12/12/2019` with a conflicting schedule on `12/12/2019` + + Expected: Scheduling conflict exist and the resolve window should pop up showing the already scheduled regime on the left panel and the conflicting `cardio` schedule on the right panel. + +=== Resolving scheduling conflict + +. Resolves a scheduling conflict by taking one whole regime + +.. Prerequisites: Resolve window should be shown on scheduling conflict + +.. Test case: `resolve n/conflicting` + + Expected: The conflicting schedule on the right panel should be taken as the resolved schedule and the resolve window should close. The conflicting schedule should now be scheduled on the conflicting date. Details of the schedule is shown on the center information panel. + +. Resolves a scheduling conflict by taking some exercise from both regime + +.. Prerequisites: Resolve window should be shown on scheduling conflict and `new cardio` should not exist in the user's regime list + +.. Test case: `resolve n/new cardio i/1 r/2` + + Expected: A new regime is created called `new cardio` with the exercises from scheduled regime's first index and conflicting regime's second index. Resolve window should close. The newly made regime is now scheduled on conflicting date. Details of the schedule shown on the center information panel. + +=== Suggest +. Suggest basic exercises +.. Test case: `suggest s/basic` + Expected: A list of basic exercises displayed on the +. Suggest possible exercises +.. Prerequisites: There is at least an exercise being tracked or in the database tagged with a `muscle`. + +For example, `add t/exercise n/Run d/03/11/2019 c/200 q/10 u/km m/Legs`. + +... Test case: `suggest s/possible m/Legs` + + Expected: This exercise, along with database's exercises tagged with `Legs` are displayed. +... Test case: `suggest s/possible o/and m/Legs` + + Expected: Similar to previous. +... Test case: `suggest s/possible o/or m/Legs` + + Expected: Similar to previous. + +.. Prerequisites: A `CustomProperty` is created and there are exercises being tracked with `CustomProperty`. + +For example, +1. `custom s/r f/Rating p/Number` + +2. `add t/exercise n/Run d/03/11/2019 c/200 q/10 u/km m/Legs r/8` -=== Deleting a person +3. `add t/exercise n/Bench Press d/05/11/2019 c/150 q/40 u/kg m/Chest r/8` -. Deleting a person while all persons are listed +... Test case: `suggest s/possible o/and m/Chest r/8` + + Expected: The previously added exercise `Bench Press` is displayed. +... Test case: `suggest s/possible o/or m/Chest r/8` + + Expected: The previously added exercises `Bench Press` and `Run` are displayed. In addition, +exercises from database that are tagged `Chest` are also displayed. -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -.. Test case: `delete 1` + - Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. -.. Test case: `delete 0` + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -.. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size) _{give more}_ + +.. Test case: `suggest s/possible m/Chest m/Legs` + + Expected: Error details shown in the status message. +.. Test case: `suggest s/possible o/or` + Expected: Similar to previous. -_{ more test cases ... }_ +=== Statistic +. Display charts and statistic for completed exercises + +.. Test case: `stats t/calories h/barchart s/01/11/2019 e/30/11/2019` + + Expected: The chart in the right panel will be updated to a bar chart. Total and average will be shown below the chart. +.. Test case: `stats t/calories h/piechart s/01/11/2019 e/30/11/2019` + + Expected: The chart in the right panel will be updated to a pie chart. Total and average will be shown below the chart. +.. Test case: `stats t/calories h/linechart s/01/11/2019 e/30/11/2019` + + Expected: The chart in the right panel will be updated to a line chart. Total and average will be shown below the chart. +.. Test case: `stats t/calories h/barchart s/01/01/2019 e/02/02/2019` + + Expected: The chart in the right panel is not updated. Error message shown in the status message. + +=== Adding a custom property + +. Adding a custom property when app is first launched. + +.. Prerequisites: The custom property `Remark` with the prefix name `re` must not be created yet. The following test cases should be tried in order. +.. Test case: `custom s/re f/Remark p/Text` + + Expected: The custom property `Remark` is created for all exercises. The prefix name and full name of the property + will be displayed in the status message. +.. Test case: `custom s/re f/AnotherRemark p/Text` + + Expected: No custom property is created. An error message will be shown, informing the user that the prefix has been used + for an existing parameter in add/edit command. +.. Test case: `custom s/tt f/Remark p/Text` + + Expected: No custom property is created. An error message will be shown, informing the user that the name has been used + by an existing property. + +=== Deleting a custom property + +. Deleting a custom property. + +.. Prerequisites: The custom property `Remark` with the prefix name `re` should have been created. + There should be at least 3 exercises with the custom property `Remark`, preferably + the exercises at indices 1 to 3. The following test cases should be tried in order. +.. Test case: `custom rm/Remark i/1` + + Expected: `Remark` is removed from exercise 1. A message informing the user that `Remark` has been removed from + exercise 1 will be shown. The `Remark` property for exercises 2 and 3 are still present. +.. Test case: `custom rm/Remark` + Expected: `Remark` is removed from the app. A message informing the user that `Remark` has been removed will be shown. + The `Remark` property is removed from all exercises. +.. Test case: `custom rm/Date` + Expected: No custom property is removed. An error message informing the user that `Date` is not used by + a custom property will be shown. + +=== Undo / Redo +. Undoing and redoing an add regime event + +.. Prerequisites: The regime list must not contain a regime with the name "Level 4". There must be at least two + exercises in the exercise list. +.. Test case: + + `add t/regime n/Level 4 i/1 i/2` + + `undo` + + `redo` + + Expected: A new regime called "Level 4" containing two exercises is added to the regime list. + Upon calling `undo`, the regime "Level 4" is deleted from the regime list. + Upon calling `redo`, the regime "Level 4" is added to the regime list again. + +. Undoing and redoing a schedule complete event + +.. Prerequisites: There must be at least one scheduled item in the schedule list. +.. Test case: + + `schedule i/1` + + `undo` + + `redo` + + Expected: The schedule at index 1 of the schedule list should be marked as complete. It will be removed + from the the schedule list and all exercises in it will be added to the exercise list. + Upon calling `undo`, the schedule appears in the schedule list again and the exercises are removed + from the exercise list. + Upon calling `redo`, the schedule will be removed and its exercises will be added back to the exercise list. + +. Attempting to redo after executing a new <> + +.. Test case: + + `add t/exercise n/Stair Climb c/400 q/50 u/flights` + + `undo` + + `add t/exercise n/Simple Walk c/100 q/50 u/steps` + + `redo` + + Expected: Upon calling `redo`, an error message should be displayed saying that there are + no commands to redo. Even though `undo` has been called once, the redo stack + will be cleared once the user enters a new undoable command after `undo`. === Saving data . Dealing with missing/corrupted data files -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ +.. Prerequisites: Must have ran `ExerHealth` at least once and have `exercisebook.json`. -_{ more test cases ... }_ +.. Open up `exercisebook.json` with any text editor and change one of the dates to `//`, representing an invalid date. + + Expected: `ExerHealth` will start with an empty exercise book due to data corruption. Exercise Panel will be empty. diff --git a/docs/Documentation.adoc b/docs/Documentation.adoc index ad90ac87bda..bf39951a2f7 100644 --- a/docs/Documentation.adoc +++ b/docs/Documentation.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 3 - Documentation += ExerHealth - Documentation :site-section: DeveloperGuide :toc: :toc-title: diff --git a/docs/SettingUp.adoc b/docs/SettingUp.adoc index c0659782fab..ae263709c85 100644 --- a/docs/SettingUp.adoc +++ b/docs/SettingUp.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 3 - Setting Up += ExerHealth - Setting Up :site-section: DeveloperGuide :toc: :toc-title: @@ -37,7 +37,7 @@ Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plu == Verifying the setup -. Run the `seedu.address.Main` and try a few commands +. Run the `seedu.exercise.Main` and try a few commands . <> to ensure they all pass. == Configurations to do before writing code @@ -57,9 +57,9 @@ Optionally, you can follow the <> docume === Updating documentation to match your fork -After forking the repo, the documentation will still have the SE-EDU branding and refer to the `se-edu/addressbook-level3` repo. +After forking the repo, the documentation will still have the ExerHealth branding and refer to the `ExerHealth` repo. -If you plan to develop this fork as a separate product (i.e. instead of contributing to `se-edu/addressbook-level3`), you should do the following: +If you plan to develop this fork as a separate product (i.e. instead of contributing to `ExerHealth`), you should do the following: . Configure the <> in link:{repoURL}/build.gradle[`build.gradle`], such as the `site-name`, to suit your own project. @@ -81,4 +81,4 @@ Having both Travis and AppVeyor ensures your App works on both Unix-based platfo === Getting started with coding -When you are ready to start coding, we recommend that you get some sense of the overall design by reading about <>. +When you are ready to start coding, we recommend that you get some sense of the overall design by reading about <>. diff --git a/docs/Testing.adoc b/docs/Testing.adoc index 5767b92912c..2f6b3a37731 100644 --- a/docs/Testing.adoc +++ b/docs/Testing.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 3 - Testing += ExerHealth - Testing :site-section: DeveloperGuide :toc: :toc-title: @@ -32,14 +32,16 @@ See <> for more info on how to run tests using G == Types of tests -We have three types of tests: +We have four types of tests: +. _GUI Unit Tests_ targetting reusable GUI components + +e.g. `seedu.exercise.ui.CommandBoxTest` . _Unit tests_ targeting the lowest level methods/classes. + -e.g. `seedu.address.commons.StringUtilTest` +e.g. `seedu.exercise.commons.StringUtilTest` . _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + -e.g. `seedu.address.storage.StorageManagerTest` +e.g. `seedu.exercise.storage.StorageManagerTest` . Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + -e.g. `seedu.address.logic.LogicManagerTest` +e.g. `seedu.exercise.logic.LogicManagerTest` == Troubleshooting Testing diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 4e5d297a19f..0f06c86991c 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 3 - User Guide += ExerHealth - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,135 +12,933 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level3 +:repoURL: https://github.com/AY1920S1-CS2103T-T09-2/main/tree/master +:baseRepoURL: https://github.com/AY1920S1-CS2103T-T09-2/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `Team ExerHealth` Since: `Sep 2019` Licence: `MIT` == Introduction -AddressBook Level 3 (AB3) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB3 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +Wish to get the summer body that the contestants in your school’s +beauty pageant has? Want to shed some weight that you have gained +from stress-eating? + +Well wait no more! ExerHealth is a one-stop platform for students +who wish to embark on a journey of health and fitness. This application +is optimised for students who prefer to work with a Command Line +Interface (CLI) while still having the benefits of a Graphical User +Interface (GUI). If you can type fast and wish to maintain a healthy +lifestyle, ExerHealth is that one app for you! + +Interested? Jump to <> to get started! Enjoy! == Quick Start . Ensure you have Java `11` or above installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. +. Download the latest `exerhealth.jar` link:{baseRepoURL}/releases[here]. +. Copy the file to the folder you want to use as the home folder for ExerHealth. . Double-click the file to start the app. The GUI should appear in a few seconds. + -image::Ui.png[width="790"] +.The UI of ExerHealth +image::Ui.png[align="center", width=85%, scaledwidth=15cm] + . Type the command in the command box and press kbd:[Enter] to execute it. + e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. . Some example commands you can try: -* *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. -* **`delete`**`3` : deletes the 3rd contact shown in the current list -* *`exit`* : exits the app +* `list t/exercise` : gets all the exercises ExerHealth is tracking. +* `add t/exercise n/Push ups d/01/10/2019 c/123 q/100 u/reps` : adds an exercise named `Push ups` to ExerHealth. +* `exit` : exits the app . Refer to <> for details of each command. +== Important terms + +If this is your first time using our app, fear not! Below are some of the terms that you may commonly encounter when +reading our guide. + +1. Prefix: The term that comes before each parameter. E.g.: the prefix in `t/exercise` is `t/`. + +2. Prefix name: The word in a Prefix. E.g.: the prefix name of `t/` is `t` and the prefix name of `ed/` is `ed`. + +3. List Type: In ExerHealth, there are 4 different types of lists + +* Exercise which will be denoted as `exercise` +* Regime which will be denoted as `regime` +* Schedule which will be denoted as `schedule` +* Suggestion which will be denoted as `suggestion` + [[Features]] == Features ==== *Command Format* -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `stats t/CAT_NAME`, +`CAT_NAME` is a parameter which can be used as `stats t/exercise`. +* Words in `lower_case` are the compulsory keywords that inform ExerHealth about the category of +information being added. e.g. `add t/exercise n/EXERCISE_NAME`, `t/exercise` is a keyword that informs ExerHealth about adding to the exercise tracker. +* Items in square brackets are optional e.g `d/date [m/MUSCLE]` can be used as `d/1/10/2019 m/Legs` +or as `d/1/10/2019`. +* Items with `…`​ after them can be used multiple times including zero times e.g. `[m/MUSCLE]...` +can be used as `{nbsp}` (i.e. 0 times), `m/glutes`, `m/glutes m/chest` etc. +* Parameters can be in any order e.g. if the command specifies `n/NAME d/DATE c/CALORIES`, +`d/DATE c/CALORIES n/NAME` is also acceptable. ==== === Viewing help : `help` +Views the help dialog box which will bring you to this UserGuide. You can expect to see a window as shown below. Format: `help` -=== Adding a person: `add` +.Here's how the help dialog box look like +image::HelpWindow.png[align="center", width=85%, scaledwidth=15cm] -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +Clicking on the `Copy URL` button will copy the link to your clipboard and you can open any browser of your choice to access the UserGuide for `ExerHealth`. -[TIP] -A person can have any number of tags (including 0) + + +=== Adding exercise/regime information: `add` + +[[addexercise]] +==== Adding exercise information + + +Adds the relevant exercise information into ExerHealth. +You can enter the following additional information for each exercise: + +* Calories burnt in kcal +* Quantity of exercises completed +* Units of measure +* Muscle group(s) that are being worked out +* <> that have been added + +Format: `add t/exercise n/EXERCISE_NAME d/DATE c/CALORIES q/QUANTITY u/UNITS [m/MUSCLE]...[CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...` + +**** +* Calories entered have to be less than or equal to 50000. +* Dates that are entered have to be of the form `dd/MM/yyyy`. +* The day of each date must be between 01 and 31 inclusive. +* The month of each date must be between 01 and 12 inclusive. +* If the day of the month entered is within the above range but exceeds the number of days present +in that particular month, the date will be treated as the last day of that particular month e.g. if +`31/02/2019` is entered, it will be treated as `28/02/2019`. +**** Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add t/exercise n/Run d/19/09/2019 c/500 q/2.4 u/km` + +Expected result: + +.The exercise `Run` is added +image::AddRunResult.png[align="center", width=85%, scaledwidth=15cm] + +* `add t/exercise n/Sprinting d/11/11/2019 c/500 q/400 u/metres m/Thighs` + +.The exercise `Sprinting` is added +image::AddExerciseWithMuscle.png[align="center", width=85%, scaledwidth=15cm] -=== Listing all persons : `list` +==== Adding exercises to scheduling regime + -Shows a list of all persons in the address book. + -Format: `list` +Adds exercises from the exercises you have added into a new exercise regime. +If a regime of the same name already exists, exercises will be added to the same scheduling regime. -=== Editing a person : `edit` +Format: `add t/regime n/REGIME_NAME [i/INDEX]...` -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +Example: +* `add t/regime n/power sets i/1 i/2 i/3` + +Adds the 1st, 2nd and 3rd exercise in the list to the regime named `power sets`. + +Expected result: + +.The `power sets` regime is created +image::AddNewRegime.png[align="center", width=85%, scaledwidth=15cm] + +* `add t/regime n/cardio i/4` + +Adds the 4th exercise in the list to the regime named `cardio`. + +Figure below shows regime list with regime named `cardio`: + +.Before adding the 4th exercise into `cardio` +image::RegimeCardioBefore.png[align="center", width=85%, scaledwidth=15cm] + +Expected result after `add t/regime n/cardio i/4` command executed: + +.After adding the 4th exercise into `cardio` +image::RegimeCardioAfter.png[align="center", width=85%, scaledwidth=15cm] + +[[list]] +=== Listing information : `list` + +Retrieves a list of information - Exercise, Regime, Scheduled Regime or Suggestion - from ExerHealth. + +Format: `list t/LIST_TYPE` + +**** +* The `suggestion` list will only be displayed if you have used the <> feature before. **** -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index *must be a positive integer* 1, 2, 3, ... + +Example: + +* `list t/exercise` + +Expected result: + +.The exercise list is displayed +image::ListExerciseResult.png[align="center", width=85%, scaledwidth=15cm] + +* `list t/regime` + +Expected result: + +.The regime list is displayed +image::ListRegimeResult.png[align="center", width=85%, scaledwidth=15cm] + +* `list t/schedule` + +Expected result: + +.The schedule list is displayed +image::ListScheduleResult.png[align="center", width=85%, scaledwidth=15cm] + +=== Selecting an item: `select` + +Selects the item at the given index of the chosen list type. + +Format: `select t/LIST_TYPE i/INDEX` + +Example: `select t/exercise i/3` + +.Exercise number 3 is selected +image::SelectResult.png[align="center", width=85%, scaledwidth=15cm] + +=== Edit exercise information: `edit` + +Edits the relevant exercise information in ExerHealth. +You can edit all the possible information for exercises including <>. + +**** +* The date entered for this command must follow the same requirements as that of <> command. +* Edits the exercise at the specified `INDEX`. The index refers to the index number shown in the displayed exercise list. +The index *must be a positive integer:* 1, 2, 3, ... * At least one of the optional fields must be provided. * Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person's tags by typing `t/` without specifying any tags after it. **** -Examples: +Format: `edit i/INDEX [n/EXERCISE_NAME] [d/DATE] [c/CALORIES] [q/QUANTITY] [u/UNITS] [m/MUSCLE]...[CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...` + +Example: + +* `edit i/2 n/Running d/20/09/2019` + +Edits the name and the date of the 2nd exercise to `Running` and `20/09/2019` respectively. + +[.underline]#Before# + + +.Before editing the 2nd exercise +image::EditExerciseBefore.png[align="center", width=85%, scaledwidth=15cm] + +[.underline]#After# + +Exercise now appears at the bottom as the list is sorted by date. + + +.After editing the 2nd exercise +image::EditExerciseAfter.png[align="center", width=85%, scaledwidth=15cm] + +* `edit i/3 n/Bench press d/04/11/2019 c/240 q/10 u/sets m/Chest` -* `edit 1 p/91234567 e/johndoe@example.com` + -Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` + -Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +Edits the name, date, calories burnt, quantity, units and muscle group trained of the 3rd exercise to `Bench press`, +`22/09/2019`, `240`, `10`, `sets` and `Chest` respectively. -=== Locating persons by name: `find` +[.underline]#Before# + -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +.Before editing the 3rd exercise +image::EditExerciseBefore2.png[align="center", width=85%, scaledwidth=15cm] + +[.underline]#After# + + +.After editing the 3rd exercise +image::EditExerciseAfter2.png[align="center", width=85%, scaledwidth=15cm] + +// tag::delete[] +=== Delete health information : `delete` + +==== Deleting exercise information + +Deleting a certain exercise information from ExerHealth. + +[NOTE] +Deleting an exercise from ExerHealth will not cause it to be deleted from your list of regimes and schedules. If you wish to delete an exercise from a regime, refer to <>. If you wish to complete a scheduled regime, refer to <> + +Format: `delete t/exercise i/INDEX` **** -* The search is case insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +* Deletes the exercise at the specified `INDEX`. +* The index refers to the index number shown in the displayed exercise list. +* The index *must be a positive integer* 1, 2, 3, ... **** Examples: -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +* `delete t/exercise i/2` -// tag::delete[] -=== Deleting a person : `delete` +.Before deleting the 2nd exercise +image::deleteExerciseBefore.png[align="center", width=85%, scaledwidth=15cm] + +Expected result: -Deletes the specified person from the address book. + -Format: `delete INDEX` +.After deleting the 2nd exercise +image::deleteExerciseAfter.png[align="center", width=85%, scaledwidth=15cm] + +[[deleteExerciseFromRegime]] +==== Deleting exercise from scheduling regime + +Deletes exercises from a certain scheduling regime. + +Format: `delete t/regime n/REGIME_NAME [i/INDEX]...` **** -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* 1, 2, 3, ... +* Deletes the exercise at the specified `INDEX`. +* If index is not provided, the command deletes the entire scheduling regime +* The index refers to the index number shown in the displayed regime list. +* The index, if provided, *must be a positive integer* 1, 2, 3, ... **** Examples: -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. +* `delete t/regime n/cardio i/2` + +Deletes the exercise indexed 2 in the cardio regime + +Figure below shows regime list with regime named `cardio` with 4 exercises: + +.The `cardio` regime with 4 exercises +image::RegimeCardioAfter.png[align="center", width=85%, scaledwidth=15cm] + +Expected result after `delete t/regime n/cardio i/2` command executed: + +.The 2nd exercise in `cardio` regime is removed +image::DeleteExerciseFromRegime.png[align="center", width=85%, scaledwidth=15cm] + +* `delete t/regime n/legs` + +Deletes the entire regime named legs + +Figure below shows regime list with regime named `legs`: + +.The `legs` regime before it is deleted +image::DeleteEntireRegimeBefore.png[align="center", width=85%, scaledwidth=15cm] + +Expected result after `delete t/regime n/legs` command executed: + +.The `legs` regime is completely removed +image::DeleteEntireRegimeAfter.png[align="center", width=85%, scaledwidth=15cm] -// end::delete[] === Clearing all entries : `clear` -Clears all entries from the address book. + +Clears all entries from the exercise tracker. + +[TIP] +Exercise tracker must not be empty when the clear command is executed. + Format: `clear` +Example: + +Initially, the exercise tracker is populated with exercises. + +.The populated exercise tracker +image::ClearBefore.png[align="center", width=85%, scaledwidth=15cm] + +After calling the `clear` command, all exercises are removed from the +exercise tracker list. + +.Poof! All of the exercises are removed +image::ClearAfter.png[align="center", width=85%, scaledwidth=15cm] + +// tag::statistic[] +=== Display statistics : `stats` + +Display the statistics of exercises in completed exercise list. + +Format: `stats t/CAT_NAME h/CHART_TYPE [s/START_DATE] [e/END_DATE]` + +**** +* Supported chart types: Pie Chart, Line Chart, Bar Chart +* Supported category: exercise, calories +* If no `START_DATE` and `END_DATE` are provided, the recent 7 days of history will be used. +* If any date is provided, both `START_DATE` and `END_DATE` dates must be there. +* The maximum range between `START_DATE` and `END_DATE` is 31 days. +* Only exercise with same name and unit will be counted as same exercise. +* Exercise will appear as `NAME (UNIT)` in the charts. E.g `Running (km)`. If it is too long (more than 18 characters), it will be formatted to first 10 characters plus last 8 characters. +** For example, exercise `Strength Training (counts)` is more than 18 characters, it will be formatted to `Strength T...(counts)`. +* Total and average `CATEGORY` per day of exercises in the date range is shown below the chart. +**** + +NOTE: Due to space constraint, some labels may not appear. Tooltips can be seen when mouse cursor hovers over the chart. + +Example: + +* `stats t/exercise h/piechart` + +.Image of the pie chart generated +image::PieChart.png[align="center", width=85%, scaledwidth=15cm] + +* `stats t/calories h/linechart` + +.Image of the line chart generated +image::LineChart.png[align="center", width=85%, scaledwidth=15cm] + +* `stats t/calories h/barchart s/31/10/2019 e/06/11/2019` + +.Image of the bar chart generated +image::BarChart.png[align="center", width=85%, scaledwidth=15cm] + +// end::statistic[] + +// tag::undo[] +[[undocommand]] +=== Undo previous command: `undo` + +Undo the previous successful command entered. + +**** +Supported Undoable Commands: add, delete, edit, clear, schedule, resolve + +* `add t/exercise n/Push ups d/1/10/2019 c/123 q/100 u/reps` +* `add t/regime n/Cardio i/1 i/3 i/5` +* `delete t/exercise i/7` +* `edit t/exercise i/3 n/Push Ups c/140 m/Chest` +* `clear` +* `schedule n/Regime Five d/20/11/2019` +* `schedule i/1` +* `resolve n/New Regime i/1 r/2` + +**** + + +[TIP] +If there is no previous command, undo will do nothing. + +Format: `undo` +// end::undo[] + + +// tag::undoredoexample[] +Example: + +* Undoing a `delete t/regime [i/INDEX]...` command that deletes exercise from an existing regime. + + +**Initial**: + + +.Selecting the 3rd regime +image::DeleteExerciseFromRegimeBeforeUndo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 1**: `delete t/regime n/Level 3 i/5` + +Deletes the fifth exercise `Hiking` from the regime `Level 3` + + +.Deleting `Hiking` from our selected regime +image::DeleteExerciseFromRegimeUndo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 2**: `undo` + +Adds the exercise `Hiking` back to the regime `Level 3` + + +.`undo` reverses the deletion! +image::DeleteExerciseFromRegimeUndoOutcome.png[align="center", width=85%, scaledwidth=15cm] +// end::undoredoexample[] + + +* Undoing a <> command and a <> command + + +**Initial**: Schedule list only contains two scheduled regimes + + +.The initial appearance of the schedule list +image::ScheduleRegimeBefore.png[align="center", width=85%, scaledwidth=15cm] + +**Step 1**: `schedule n/Level 3 d/10/11/2019` + +Schedules the regime `Level 3` on `10/11/2019` + + +.`Level 3` scheduled on 10/11/2019 +image::ScheduleRegimeAfter.png[align="center", width=85%, scaledwidth=15cm] + +**Step 2**: `schedule i/1` + +Marks the scheduled regime at index 1, regime `Level 3`, as completed. + + +``` +Exercise list showing the newly completed exercises from the regime "Level 3" +``` + +.The exercises of the completed regime are added into the tracker. +image::ScheduleCompleteAfterExerciseView.png[align="center", width=85%, scaledwidth=15cm] +``` +Schedule list showing the updated schedule without regime "Level 3" +``` + +.The completed regime is removed from the schedule list +image::ScheduleCompleteAfterScheduleView.png[align="center", width=85%, scaledwidth=15cm] + +**Step 3**: `undo` + +Undoes the completion of regime. The regime `Level 3` appears in the schedule list again. + + +.`undo` reverses the removal! +image::ScheduleCompleteUndo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 4**: `undo` + +Undoes the scheduling of regime. The regime `Level 3` is no longer visible in the schedule list. + + +.`undo` also reverses the scheduling! +image::ScheduleRegimeUndo.png[align="center", width=85%, scaledwidth=15cm] + +// tag::redo[] +=== Redo undone command: `redo` + +Redo the previous command that was undone by the user. It can only be executed after +successful executions of the undo command. For a list of Undoable commands, refer +to the <>. + +[TIP] +If the `Undo` command has not been executed after the execution of the last Undoable command, there will be +no command to redo. + +Format: `redo` +// end::redo[] +Example: + +* Redoing a `add t/exercise` command that has just been undone. + + +**Initial**: + + +.Initial appearance of the exercise list +image::AddExerciseBeforeAdding.png[align="center", width=85%, scaledwidth=15cm] + +**Step 1**: `add t/exercise n/Squats d/08/11/2019 c/200 q/150 u/reps` + +Adds a new exercise called `Squats`. + + +.`Squats` is added to the exercise list +image::AddExerciseBeforeUndo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 2**: `undo` + +Undoing the command removes the `Squats` exercise from the list. + + +.`undo` removes the exercise `Squats` +image::AddExerciseBeforeRedo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 3**: `redo` + +Redoing the command returns the `Squats` exercise to the list again. + + +.`redo` adds `Squats` back! +image::AddExerciseAfterRedo.png[align="center", width=85%, scaledwidth=15cm] + +* Redoing a <> command that has just been undone. + + +**Initial**: + +Attempts to schedule `Level 2` regime on the date of `Level 1` regime. + + +.Initial appearance of the schedule list +image::ScheduleRegimeBeforeConflict.png[align="center", width=85%, scaledwidth=15cm] + +**Step 1**: `schedule n/Level 2 d/10/11/2019` + +Resolve conflict window appears due to the conflict in date. +The conflict can be resolved by specifying the index of exercises +to be taken from each regime. + + +.The conflict window opens due to conflicting schedule +image::ScheduleRegimeBeforeResolve.png[align="center", width=85%, scaledwidth=15cm] + +**Step 2**: `resolve n/Level 1 and 2 i/2 r/2 r/3` + +Resolves conflict by creating a new regime `Level 1 and 2`. +The new regime `Level 1 and 2` replaces the initial `Level 1` regime +in the schedule. + + +.The new regime is added to the schedule list +image::ScheduleRegimeBeforeResolveUndo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 3**: `undo` + +Undoing the command reverts the scheduled regime from `Level 1 and 2` to the initial +`Level 1` regime. + + +.`undo` reverses the conflict resolution +image::ScheduleRegimeBeforeResolveRedo.png[align="center", width=85%, scaledwidth=15cm] + +**Step 4**: `redo` + +Redoing the command updates the scheduled regime from `Level 1` to the resolved +`Level 1 and 2` regime. + + +.`redo` returns the new regime that was created after the conflict resolution! +image::ScheduleRegimeAfterResolveRedo.png[align="center", width=85%, scaledwidth=15cm] + + +// tag::scheduleresolve[] + +=== Scheduling exercises: `schedule` +[[scheduleregime]] +==== Schedules a regime + +Schedules an exercise regime for a certain date. If the regime clashes with another scheduled regime, you will be prompted to resolve the conflict via a popup window. Refer to <> for details on resolving scheduling conflicts. + +Format: `schedule n/REGIME_NAME d/DATE` + +**** +* To schedule a regime, `REGIME_NAME` must exist in your regime list. You can use <> command to view the regimes that you currently have. +* The format of `DATE` must be of the form `dd/MM/YYYY`. For example, `12/12/2019` or `01/01/2020`. Even if the number has only one digit, please ensure you include a 0 at the front to ensure that it adheres to the format required. +* You will not be allowed to schedule a regime on a date that falls before the date displayed on your system clock +* You are allowed to schedule a regime of the same name on the same date. The resolve window will pop up for you as per normal. +**** +*Example 1:* + +* `schedule n/cardio d/12/12/2019` + +Schedules the regime called `cardio` on the date `12/12/2019`. If there are no other regimes scheduled on `12/12/2019` then the command is successful. Otherwise, you will be prompted to resolve the scheduling conflict. + +Expected Result: + +.The `cardio` regime is scheduled on 12/12/2019 +image::ScheduleRegimeCardio.png[align="center", width=85%, scaledwidth=15cm] + +Expected Result (if other schedule exist on `12/12/2019`): + +.The result when a scheduling conflict is detected +image::ScheduleRegimeCardioConflict.png[align="center", width=85%, scaledwidth=15cm] + +If the resolve window pops up for you, please refer to <> for details on resolving a scheduling conflict. + +[[schedulecomplete]] +==== Completes a schedule regime + +Once a scheduled regime is completed, you can add that completed schedule to the exercise tracker. The schedule is then deleted from the scheduling list. + +Format: `schedule i/INDEX_OF_SCHEDULE` + + +[TIP] +The `INDEX_OF_SCHEDULE` provided must be a valid index from your schedule list. Please use <> to view the index of the schedule you wish to complete. + +*Example:* + +* `schedule i/2` + +Completes all the exercises that are in the schedule at index `2`. All the exercises scheduled will be added to the exercise list and the schedule at index `2` is deleted. + +Expected Result: + +Before execution of `schedule i/2` + +.Before the execution of the command +image::ScheduleCompleteBefore.png[align="center", width=85%, scaledwidth=15cm] + +In your exercise tracker + +.Exercises from the schedule are added to your tracker +image::ScheduleCompleteExerciseTracker.png[align="center", width=85%, scaledwidth=15cm] + +In your schedule tracker after completing execution of `schedule i/2` + +.The completed regime is removed from your schedule +image::ScheduleCompleteScheduleTracker.png[align="center", width=85%, scaledwidth=15cm] + +// end::scheduleresolve[] + +[[resolve]] +=== Resolving scheduling conflicts: `resolve` +Described in the following two sections are two possible ways to resolve a scheduling conflict in the resolve window that pops up. + +**** +* Once the resolve window pops up, you are not allowed to interact with the main `ExerHealth` window anymore. You are required to resolve the scheduling conflict before continuing to use the application as normal. +* Any *valid* commands that are not `resolve` will not get executed in the resolve window. Only the `resolve` command is allowed in the resolve window. +**** + +[[resolvetakeone]] +==== Taking one of the regimes completely + +Takes the scheduled regime or the conflicting regime completely and discarding the other. Neither regimes will be deleted from the user’s collection. + +Format: `resolve n/SCHEDULED_OR_CONFLICTING` + +[TIP] +You can only enter `n/scheduled` or `n/conflicting` to tell ExerHealth which schedule you wish to take. The scheduled or conflicting regimes are stated at the top of the list of exercises as shown below. + +.An example of the resolve conflict window +image::ResolveScheduledOrConflicting.png[align="center", width=85%, scaledwidth=15cm] + +*Example:* + +* `resolve n/scheduled` + +Takes the already scheduled regime and schedule it at conflicting date. + +*Expected Result:* + +The resolve window and inputting the example command + +.Selecting all the exercises in the scheduled regime +image::ResolveScheduled.png[align="center", width=85%, scaledwidth=15cm] + +Once resolve window closes, the scheduled regime should be taken and scheduled at the correct date. + +.The scheduled regime should appear in your scheduling list +image::ResolveScheduledResult.png[align="center", width=85%, scaledwidth=15cm] + + +==== Taking some exercises from both regimes +Takes some exercises from the scheduled regime and some from the conflicting regime to make a brand new regime. The new regime will be scheduled at the date of conflict. This new regime will also be added to your collection of regimes. + +Format: `resolve n/NEW_REGIME_NAME [i/INDEX_OF_EXERCISE_IN_SCHEDULED] [r/INDEX_OF_EXERCISE_IN_CONFLICTING]` + +**** +* Takes the exercise at the specified indexes of `i/` and `r/` +* `NEW_REGIME_NAME` must be a new name that does not exist in your collection of regimes +* The index refers to the index number shown in the displayed resolve window +* The index *must be a positive integer* 1, 2, 3, ... +* `i/` is for index of the scheduled regime +* `r/` is for index of the conflicting regime +* At least one `i/` or `r/` must exist in your input for this command to succeed. Otherwise, ExerHealth will think you are trying to take one regime completely as stated in the <>. +**** + +*Example:* + +* `resolve n/new cardio i/1 r/4 r/2` + +Expected Result: + +Takes exercise `1` from `scheduled regime` and exercises `2` and `4` from `conflicting regime` and adds them to a new regime called `new cardio` + +.Selecting some exercises from each regime +image::ResolveTakeOneOrOther.png[align="center", width=85%, scaledwidth=15cm] + +The `new cardio` regime is now scheduled at the conflicting date with the new exercises that have been resolved. + +.A newly created regime will appear in your scheduling list +image::ResolveTakeResolved.png[align="center", width=85%, scaledwidth=15cm] + +The `new cardio` regime is now added to your collection of regimes + +.The newly created regime will also appear in your regime list! +image::ResolveRegimeTracker.png[align="center", width=85%, scaledwidth=15cm] + + +// tag::suggest[] + +[[suggestion]] +=== Suggesting ideas: `suggest` + +==== Suggests basic exercises +Recommends exercises from ExerHealth's inbuilt database for beginners. + +Format: `suggest s/basic` + +==== Suggests possible exercises +Suggests exercises matching specified tags. + +===== Based on matching muscle tags +Format: `suggest s/possible o/OPERATION_TYPE [m/MUSCLE]... CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...` + +**** +* You must choose one of the following operation types: `and` / `or` i.e. commands such as `suggest s/possible m/Chest m/Legs` will fail whereas `suggest s/possible o/or m/Chest m/Legs` will succeed. + +* You have to enter at least one property (muscle/custom property) to search for suggestions i.e. commands such as +`suggest s/possible o/and` and `suggest s/possible o/or` will both fail +whereas `suggest s/possible o/or m/Chest` will succeed. + +* The operation type is optional if there is only one tag provided i.e. commands such as +`suggest s/possible o/and m/Chest`, `suggest s/possible o/or m/Chest` and `suggest s/possible m/Chest` +will all achieve the same outcome - +display all the exercises tagged with `Chest` in the exercise tracker and ExerHealth's database. +**** +===== Based on matching custom properties + +Similar to matching muscles tags, you can search for suggestions with matching custom property tags. + +After creating <> and tracking exercises, +you can search for suggestions with those custom properties. + +Example: +Suppose you have created a new custom property and have been tracking a few exercises with said custom property: + +1. `custom s/r f/Rating p/Number` + +2. `add t/exercise n/Run d/03/11/2019 c/200 q/10 u/km m/Legs r/8` + +3. `add t/exercise n/Bench Press d/05/11/2019 c/150 q/40 u/kg m/Chest r/8` + + +Then, the following input will display a list of exercises which are tagged with `Chest` and have a rating of `8`. + +`suggest s/possible o/and m/Chest r/8` + +Thus the command will display only the exercise named "Bench Press". + +<<< + +Expected Result: + +.Exercises with a `Chest` tag and a rating of `8` are shown. +image::suggestPossibleAfter1.png[align="center", width=85%, scaledwidth=15cm] + +The input, `suggest s/possible o/or m/Chest r/8`, however, will display a list of exercises tagged with `Chest` *or* have a rating of `8`. + +Expected Result: + +.Exercises with a `Chest` tag and exercises with a rating of `8` are shown. +image::suggestPossibleAfter2.png[align="center", width=85%, scaledwidth=15cm] + +As shown above, the two previously added exercises, "Bench Press and "Run", are displayed because they each have a rating of `8`. +In addition to the tracked exercises, ExerHealth also displays suggestions in its database. Hence it will display the exercise named "Push Ups" as it has a `Chest` tag. + +<<< + +===== Duplicates + +Sometimes, you may want to track exercises of the same name. Instead of displaying all suggestions of the same name, `suggest` +displays the information of the most recently tracked exercise of that name. +As can be seen below, there are two exercises named "Bench Press". + +.Before entering the new suggest command +image::suggestPossibleBefore3.png[align="center", width=85%, scaledwidth=15cm] + +Expected Result: + +.Only the latest `Bench Press` exercise is displayed +image::suggestPossibleAfter3.png[align="center", width=85%, scaledwidth=15cm] + +As seen from the image above, the information from the "Bench Press" on "06/11/2019" is displayed instead of the one on "05/11/2019" (observe that the calories are different). +// end::suggest[] + +// tag::customfeature[] +[[customproperty]] +=== Custom properties `custom` / `viewcustom` + +==== Adding custom properties: `custom` + +Adds in a custom property which you can define for the exercises. + +Once a new custom property is created, you can simply use the prefix name which you defined for the +property in the `add` and `edit` command to include information for the new property. + +**** +* The prefix name can only contain alphabets and should not contain spaces. +* You must choose exactly one of the following as the parameter type for your custom property: +`Text`, `Number`, `Date`. +* Every word in the full name of each custom property will be changed to Start Case style, where the first letter of each +word is capitalised with the other letters in lower case e.g. `enD DaTe` will be changed to `End Date`. +* The date entered for custom properties with a `DATE` parameter must follow the same requirements as that of <> command. +* The text entered for custom properties with a `TEXT` parameter can contain only alphabets and spaces. +* The number entered for custom properties with a `NUMBER` parameter must be a non-negative integer. +* You need not include the custom properties when adding a new exercise to the app. +**** + +Format: `custom s/PREFIX_NAME f/FULL_NAME p/PARAMETER_TYPE` + +[TIP] +The following names and prefix names have been used for existing add / edit command parameters and properties and so, +cannot be used. +|=========== +|Names used | Prefix names used +|Name | n +|Date | d +|Calories | c +|Quantity | q +|Unit | u +|Muscle | m +|- | t +|- | i +|=========== + +Example: + +* `custom s/r f/Rating p/Number` + +Creates a `Rating` property for each of your exercises. + +Expected Result: + +.Your rating property has been created +image::CustomRating.png[align="center", width=85%, scaledwidth=15cm] + +You can now add a new exercise with `Rating`! + +* `add t/exercise n/Dancing d/07/11/2019 c/400 q/2 u/hours r/5` + +Expected Result: + +.A new exercise with the remark property is added +image::AddCustomRating.png[align="center", width=85%, scaledwidth=15cm] + +==== Removing custom properties: `custom` + +Removes a custom property which you have previously defined either from a single exercise +or from ExerHealth. + +In the second case, you will still be able to add back the deleted custom property if you +wish to. + +**** +* `FULL_NAME` denotes the name of the previously defined custom property. +* The index, if provided, must be a positive integer 1, 2, 3, …​ +**** + +Format: `custom rm/FULL_NAME [i/INDEX]` + +Example: + +* `custom rm/Rating` + +Before the execution, the `Rating` property will be present in exercises that have it. + +.A rating of 5 for exercise 6 +image::AddCustomRating.png[align="center", width=85%, scaledwidth=15cm] + +.A rating of 3 for exercise 7 +image::EditAnotherCustomRating.png[align="center", width=85%, scaledwidth=15cm] + +After the execution, the `Rating` property will be removed from all of the exercises and the app +as illustrated in the next few figures. + +.Rating in exercise 6 is removed +image::DancingRatingRemoved.png[align="center", width=85%, scaledwidth=15cm] + +.Rating in exercise 7 is also removed +image::RunningRatingRemoved.png[align="center", width=85%, scaledwidth=15cm] + +// end::customfeature[] + +* `custom rm/Rating i/6` + +Before the execution, both exercises 6 and 7 have a rating attached to them. + +.A rating of 5 for exercise 6 +image::AddCustomRating.png[align="center", width=85%, scaledwidth=15cm] + +.A rating of 3 for exercise 7 +image::EditAnotherCustomRating.png[align="center", width=85%, scaledwidth=15cm] + +After the execution, only exercise 6 has its rating property removed. + +.The rating for exercise 6 is removed +image::RatingRemovedForSix.png[align="center", width=85%, scaledwidth=15cm] + +.The rating for exercise 7 remains +image::RatingRemainedForSeven.png[align="center", width=85%, scaledwidth=15cm] + +==== Viewing custom properties: `viewcustom` + +Opens up a window for you to view the custom properties you have defined. +The name, prefix and parameter type of all existing custom properties will be shown. + +Note that the window shows the custom properties that are present in the app at the time +that it was opened. Any new addition/deletion will not be reflected unless the `viewcustom` command +is used again. + +Format: `viewcustom` + +Example: + +* `viewcustom` + +Expected Result: + +.A window showing the custom properties you have defined is opened +image::ViewCustom.png[align="center", width=400] + === Exiting the program : `exit` Exits the program. + @@ -148,30 +946,89 @@ Format: `exit` === Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data. + +ExerHealth data are saved in the hard disk automatically after any command that changes the data. + There is no need to save manually. +=== Suggest intensity `[Coming in v2.0]` + +Suggesting the amount of repetitions to complete for exercises. + +=== Remind to do exercises `[coming in v2.0]` + +Reminds you to do exercises. + +=== Autocomplete of commands `[coming in v2.0]` + +Autocompletes the commands while typing. + +=== Importing data `[coming in v2.0]` + +Imports data to update inbuilt database of exercises. + // tag::dataencryption[] === Encrypting data files `[coming in v2.0]` - -_{explain how the user can enable/disable data encryption}_ +Allows you to encrypt your data files for more security. // end::dataencryption[] == FAQ *Q*: How do I transfer my data to another Computer? + -*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous Address Book folder. +*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous ExerHealth folder. + +*Q*: There are times where a `null` message appears in the result display. What should I do? + +.A null message appeared +image::NullMessage.png[align="center", width="400"] + +*A*: Fear not! Simply restart the app and the `null` message will go away. + +*Q*: Exercises with long names are cut off in the info panel. Are there any way for me to view the names of +these exercises? + +*A*: Hover your mouse over the exercise's name and a friendly tooltip will appear to display its full name. +The same thing can also be done for custom properties! + +.The friendly tooltip displaying the name of an exercise +image::LongNamesTooltip.png[align="center", width="400"] + == Command Summary -* *Add* `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` + -e.g. `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` +* *Add exercises* : `add t/exercise n/EXERCISE_NAME d/DATE c/CALORIES q/QUANTITY u/UNITS [m/MUSCLE]... [CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...` + +e.g. `add t/exercise n/Bench press d/19/09/2019 c/500 q/50 u/reps m/Chest` +* *Add regimes* : `add t/regime n/REGIME_NAME [i/INDEX_OF_EXERCISE]...` +e.g. `add t/regime n/Cardio i/1 i/2 i/3` +* *List* : `list t/LIST_TYPE` +e.g. `list t/exercise` +* *Select* : `select t/LIST_TYPE i/INDEX` +e.g. `select t/exercise i/1` +* *Edit exercise* : `edit i/INDEX [n/EXERCISE_NAME] [d/DATE] [c/CALORIES] [q/QUANTITY] [u/UNITS] [m/MUSCLE]... [CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...` +e.g. `edit i/3 n/Bench press d/22/09/2019 c/240 q/10 u/sets m/Chest` +* *Delete exercise* : `delete t/exercise i/INDEX` +e.g. `delete t/exercise i/2` +* *Delete regimes* : `delete t/regime n/REGIME_NAME` +e.g. `delete t/regime n/Cardio` +* *Delete exercise from regime* : `delete t/regime n/REGIME_NAME [i/INDEX_OF_EXERCISE]...` +e.g. `delete t/regime n/Cardio i/1 i/2` * *Clear* : `clear` -* *Delete* : `delete INDEX` + -e.g. `delete 3` -* *Edit* : `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + -e.g. `edit 2 n/James Lee e/jameslee@example.com` -* *Find* : `find KEYWORD [MORE_KEYWORDS]` + -e.g. `find James Jake` -* *List* : `list` * *Help* : `help` +* *Stats* : `stats t/CAT_NAME h/CHART_TYPE [s/START_DATE] [d/END_DATE]` +e.g. `stats t/exercise h/barchart s/20/09/2019 e/27/09/2019` +* *Undo* : `undo` +* *Redo* : `redo` +* *Schedule* : `schedule n/REGIME_NAME d/DATE` +e.g. `schedule n/cardio d/12/12/2019` +* *Complete Schedule* : `schedule i/INDEX_OF_SCHEDULE` +e.g. `schedule i/2` +* *Resolve(Take one regime)* : `resolve n/SCHEDULED_OR_CONFLICTING` +e.g. `resolve n/scheduled`, `resolve n/conflicting` +* *Resolve(Take some exercise from both regime)* : `resolve n/NEW_REGIME_NAME [i/INDEX_OF_SCHEDULED_EXERCISE] [r/INDEX_OF_CONFLICTING_EXERCISE]` +e.g. `resolve n/new cardio i/1 i/3 r/2` +* *Suggest basic* : `suggest s/basic` +* *Suggest possible* : `suggest s/possible [o/OPERATION_TYPE] [m/MUSCLE]... [CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...` +e.g. `suggest s/possible m/Legs`, `suggest s/possible o/and m/Chest m/Leg` +* *Add custom property* : `custom s/PREFIX_NAME f/FULL_NAME p/PARAMETER_TYPE` +e.g. `custom s/ed f/End Date p/Date` +* *Remove custom property* : `custom rm/FULL_NAME [i/INDEX]` +e.g. `custom s/End Date i/1` +* *View custom property* : `viewcustom` +* *Exit* : `exit` diff --git a/docs/diagrams/ArchitectureDiagram.puml b/docs/diagrams/ArchitectureDiagram.puml index d021b3992ed..e93443ec0a0 100644 --- a/docs/diagrams/ArchitectureDiagram.puml +++ b/docs/diagrams/ArchitectureDiagram.puml @@ -13,19 +13,14 @@ Package " "<>{ Class Commons LOGIC_COLOR_T2 Class "Log Center" as Logs UI_COLOR_T2 Class Hidden #FFFFFF - Class HiddenUI #FFFFFF - Class HiddenModel #FFFFFF - } Class "<$user>" as User MODEL_COLOR_T2 Class "<$documents>" as File UI_COLOR_T1 -HiddenUI -up[hidden]-> UI -HiddenModel -left[hidden]-> Model -Main -up-> HiddenUI -Main -left-> HiddenModel +Main -up-> UI +Main -left-> Model UI -> Logic UI -right-> Model Logic -> Storage @@ -38,5 +33,5 @@ Hidden .down.> Commons Storage .right.>File User --> UI -Main --> Hidden +Main -[Hidden]-> Hidden @enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index d1e2ae93675..91b51ce31ec 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,19 +7,19 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "delete t/exercise i/1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("delete t/exercise i/1"") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : deleteExercise(e) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic deactivate model -logic -[LOGIC_COLOR]> storage : saveAddressBook(addressBook) +logic -[LOGIC_COLOR]> storage : saveExerciseBook(exerciseBook) activate storage STORAGE_COLOR storage -[STORAGE_COLOR]> storage : Save to file diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 7790472da52..eaf4558c0c2 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,18 +4,37 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +ReadOnlyResourceBook .down.> "{abstract}\nResource" -UniqueTagList *-right-> "*" Tag -UniquePersonList o-right-> Person +ReadOnlyResourceBook o-right-> "1" UniqueResourceList -Person o-up-> "*" Tag +UniqueResourceList o--> "{abstract}\nResource" + +Exercise -up-|>"{abstract}\nResource" +Regime -up-|>"{abstract}\nResource" +Schedule -up-|>"{abstract}\nResource" + +Exercise *--> "1" Name +Exercise *--> "1" Date +Exercise *--> "1" Calories +Exercise *--> "1" Quantity +Exercise *--> "1" Unit +Exercise *--> "0..*" Muscle +Exercise *--> "0..*" CustomProperty + +CustomProperty *--> "1" Prefix +CustomProperty *--> "1" "<>\nParameterType" + +enum "<>\nParameterType" { + TEXT, + NUMBER, + DATE +} + +Regime *-left-> "1" UniqueResourceList : regimeExercises +Regime *--> "1" Name + +Schedule *-right-> "1" Regime +Schedule *--> "1 " Date -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address @enduml diff --git a/docs/diagrams/CommitActivityDiagram.puml b/docs/diagrams/CommitActivityDiagram.puml index 7f8fe407f89..dafcc998a8f 100644 --- a/docs/diagrams/CommitActivityDiagram.puml +++ b/docs/diagrams/CommitActivityDiagram.puml @@ -1,15 +1,38 @@ @startuml start :User executes command; - -'Since the beta syntax does not support placing the condition outside the -'diamond we place it as the true branch instead. - -if () then ([command commits AddressBook]) - :Purge redunant states; - :Save AddressBook to - addressBookStateList; -else ([else]) +if () then ([is undoableCommand]) + :Executes command; + :Clears redo stack; + :Adds event to undo stack; +else + if () then ([is undo or redo command]) + if () then ([is undo command]) + if () then ([undo stack not empty]) + :Pops event from + undo stack; + : Executes undo + method of event; + : Pushes event to + redo stack; + else([else]) + : Displays error message; + endif + else([is redo command]) + if () then ([redo stack not empty]) + :Pops event from + redo stack; + : Executes redo + method of event; + : Pushes event to + undo stack; + else([else]) + : Displays error message; + endif + endif + else([else]) + :Executes command; + endif endif stop @enduml diff --git a/docs/diagrams/CustomAddActivityDiagram.puml b/docs/diagrams/CustomAddActivityDiagram.puml new file mode 100644 index 00000000000..70be61d1c93 --- /dev/null +++ b/docs/diagrams/CustomAddActivityDiagram.puml @@ -0,0 +1,22 @@ +@startuml +start + +:User defines prefix, full name and +parameter type of the new custom property; + +:Check if prefix, full name and parameter type are valid; + +if () then ([all are valid]) + :Check if prefix and full name are available; + if () then([prefix and full name are available]) + :Add new custom property; + :Updates prefix set in CliSyntax; + else ([else]) + :Inform user that prefix or full name is used; + endif +else ([else]) + :Inform user that input is invalid; +endif + +stop +@enduml diff --git a/docs/diagrams/CustomAddSequenceDiagram.puml b/docs/diagrams/CustomAddSequenceDiagram.puml new file mode 100644 index 00000000000..bf6b58424e4 --- /dev/null +++ b/docs/diagrams/CustomAddSequenceDiagram.puml @@ -0,0 +1,77 @@ +@startuml +!include style.puml +hide footbox + +box "Logic" LOGIC_COLOR_T1 + participant ":LogicManager" as LogicManager LOGIC_COLOR + participant ":ExerciseBookParser" as ExerciseBookParser LOGIC_COLOR + participant ":CustomCommandParser" as CustomCommandParser LOGIC_COLOR + participant ":CustomAddCommand" as CustomAddCommand LOGIC_COLOR + participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box "Model" MODEL_COLOR_T1 + participant "toAdd:CustomProperty" as CustomProperty MODEL_COLOR_T1 + participant ":PropertyBook" as PropertyBook MODEL_COLOR_T1 +end box + +-> LogicManager: execute("custom s/r f/Rating p/Number") +activate LogicManager + +LogicManager -> ExerciseBookParser: parseCommand("custom s/r f/Rating p/Number") +activate ExerciseBookParser + +create CustomCommandParser +ExerciseBookParser -> CustomCommandParser +activate CustomCommandParser + +CustomCommandParser --> ExerciseBookParser +deactivate CustomCommandParser + +ExerciseBookParser -> CustomCommandParser: parse(" s/r f/Rating p/Number") +activate CustomCommandParser + +create CustomProperty +CustomCommandParser -> CustomProperty +activate CustomProperty + +CustomProperty --> CustomCommandParser: toAdd +deactivate CustomProperty + +create CustomAddCommand +CustomCommandParser -> CustomAddCommand: new CustomAddCommand(toAdd) +activate CustomAddCommand + +CustomAddCommand --> CustomCommandParser: cc +deactivate CustomAddCommand + +CustomCommandParser --> ExerciseBookParser: cc +deactivate CustomCommandParser +CustomCommandParser -[hidden]-> ExerciseBookParser +destroy CustomCommandParser + +ExerciseBookParser --> LogicManager: cc +deactivate ExerciseBookParser + +LogicManager -> CustomAddCommand: execute(model) +activate CustomAddCommand + +CustomAddCommand -> PropertyBook: addCustomProperty(toAdd) +activate PropertyBook + +PropertyBook --> CustomAddCommand +deactivate PropertyBook + +create CommandResult +CustomAddCommand -> CommandResult +activate CommandResult + +CommandResult --> CustomAddCommand: result +deactivate CommandResult + +CustomAddCommand --> LogicManager: result +deactivate CustomAddCommand + +<-- LogicManager :result +deactivate LogicManager +@enduml diff --git a/docs/diagrams/CustomClassDiagram.puml b/docs/diagrams/CustomClassDiagram.puml new file mode 100644 index 00000000000..0f6407b0091 --- /dev/null +++ b/docs/diagrams/CustomClassDiagram.puml @@ -0,0 +1,41 @@ +@startuml + +hide CustomProperty methods +hide CliSyntax members +hide ParameterType methods +skinparam classAttributeIconSize 0 + +class CustomProperty { + - String fullName + - Prefix prefix + - ParameterType parameterType +} + +class PropertyBook { + - {static} PropertyBook instance + - Set customProperties + - Set defaultPrefixes + - Set defaultFullNames + - Set customPrefixes + - Set customFullNames + + {static} getInstance() + + isPrefixUsed(Prefix) + + isFullNameUsed(String) + + isFullNameUsedByCustomProperty(String) + + addCustomProperty(CustomProperty) + + removeCustomProperty(String) +} + +enum ParameterType <> { + NUMBER + TEXT + DATE +} + +PropertyBook o-Left-> "*" CustomProperty: tracks > + +PropertyBook <.Left. CliSyntax: updates prefixes > + +CustomProperty -Down-> "1" ParameterType + +@enduml diff --git a/docs/diagrams/CustomRemoveActivityDiagram.puml b/docs/diagrams/CustomRemoveActivityDiagram.puml new file mode 100644 index 00000000000..c183ec64717 --- /dev/null +++ b/docs/diagrams/CustomRemoveActivityDiagram.puml @@ -0,0 +1,29 @@ +@startuml +start + +:User states the name of the custom property to be removed; + +:User provides an index if necessary; + +:Check if the given name is used by a custom property; + +if () then ([given name is used]) + :Check if index is given; + if () then([index is given]) + :Check if index is valid; + if () then([index is valid]) + :Remove custom property from exercise at given index; + else ([else]) + :Inform user that index is invalid; + endif + else ([else]) + :Remove custom property of given name from PropertyBook; + :Update prefix set in CliSyntax; + :Update custom properties of exercises; + endif +else ([else]) + :Inform user that name is not used by custom property; +endif + +stop +@enduml diff --git a/docs/diagrams/CustomRemoveSequenceDiagram.puml b/docs/diagrams/CustomRemoveSequenceDiagram.puml new file mode 100644 index 00000000000..7d12f2e3d31 --- /dev/null +++ b/docs/diagrams/CustomRemoveSequenceDiagram.puml @@ -0,0 +1,85 @@ +@startuml +!include style.puml +hide footbox + +box "Logic" LOGIC_COLOR_T1 + participant ":LogicManager" as LogicManager LOGIC_COLOR + participant ":ExerciseBookParser" as ExerciseBookParser LOGIC_COLOR + participant ":CustomCommandParser" as CustomCommandParser LOGIC_COLOR + participant ":CustomRemoveCommand" as CustomRemoveCommand LOGIC_COLOR + participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box "Model" MODEL_COLOR_T1 + participant ":PropertyBook" as PropertyBook MODEL_COLOR_T1 + participant "Rating:CustomProperty" as CustomProperty MODEL_COLOR_T1 + participant "model:Model" as Model MODEL_COLOR_T1 +end box + +-> LogicManager: execute("custom rm/Rating") +activate LogicManager + +LogicManager -> ExerciseBookParser: parseCommand("custom rm/Rating") +activate ExerciseBookParser + +create CustomCommandParser +ExerciseBookParser -> CustomCommandParser +activate CustomCommandParser + +CustomCommandParser --> ExerciseBookParser +deactivate CustomCommandParser + +ExerciseBookParser -> CustomCommandParser: parse(" rm/Rating") +activate CustomCommandParser + +create CustomRemoveCommand +CustomCommandParser -> CustomRemoveCommand: new CustomRemoveCommand("Rating") +activate CustomRemoveCommand + +CustomRemoveCommand --> CustomCommandParser: cc +deactivate CustomRemoveCommand + +CustomCommandParser --> ExerciseBookParser: cc +deactivate CustomCommandParser +CustomCommandParser -[hidden]-> ExerciseBookParser +destroy CustomCommandParser + +ExerciseBookParser --> LogicManager: cc +deactivate ExerciseBookParser + +LogicManager -> CustomRemoveCommand: execute(model) +activate CustomRemoveCommand + +CustomRemoveCommand -> PropertyBook: removeCustomProperty("Rating") +activate PropertyBook + +PropertyBook -> CustomProperty +activate CustomProperty + +CustomProperty --> PropertyBook +deactivate CustomProperty +CustomCommandParser -[hidden]-> PropertyBook +destroy CustomProperty + +PropertyBook --> CustomRemoveCommand +deactivate PropertyBook + +CustomRemoveCommand -> Model: updateCustomPropertiesOfAllExercises(model) +activate Model + +Model --> CustomRemoveCommand +deactivate Model + +create CommandResult +CustomRemoveCommand -> CommandResult +activate CommandResult + +CommandResult --> CustomRemoveCommand: result +deactivate CommandResult + +CustomRemoveCommand --> LogicManager: result +deactivate CustomRemoveCommand + +<-- LogicManager :result +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..9cb6427ef80 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -3,7 +3,7 @@ box Logic LOGIC_COLOR_T1 participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ExerciseBookParser" as ExerciseBookParser LOGIC_COLOR participant ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR participant "d:DeleteCommand" as DeleteCommand LOGIC_COLOR participant ":CommandResult" as CommandResult LOGIC_COLOR @@ -13,20 +13,20 @@ box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("delete t/exercise i/1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") -activate AddressBookParser +LogicManager -> ExerciseBookParser : parseCommand("delete t/exercise i/1"") +activate ExerciseBookParser create DeleteCommandParser -AddressBookParser -> DeleteCommandParser +ExerciseBookParser -> DeleteCommandParser activate DeleteCommandParser -DeleteCommandParser --> AddressBookParser +DeleteCommandParser --> ExerciseBookParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +ExerciseBookParser -> DeleteCommandParser : parse("t/exercise i/1") activate DeleteCommandParser create DeleteCommand @@ -36,19 +36,19 @@ activate DeleteCommand DeleteCommand --> DeleteCommandParser : d deactivate DeleteCommand -DeleteCommandParser --> AddressBookParser : d +DeleteCommandParser --> ExerciseBookParser : d deactivate DeleteCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. -DeleteCommandParser -[hidden]-> AddressBookParser +DeleteCommandParser -[hidden]-> ExerciseBookParser destroy DeleteCommandParser -AddressBookParser --> LogicManager : d -deactivate AddressBookParser +ExerciseBookParser --> LogicManager : d +deactivate ExerciseBookParser LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deleteExercise(e) activate Model Model --> DeleteCommand diff --git a/docs/diagrams/EventClassDiagram.puml b/docs/diagrams/EventClassDiagram.puml new file mode 100644 index 00000000000..27174ff18a7 --- /dev/null +++ b/docs/diagrams/EventClassDiagram.puml @@ -0,0 +1,49 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +package Logic { + + package Command { + Interface UndoableCommand <> + Class XYZCommand + Class "{abstract}\nCommand" as Command + } + + package Event { + Interface Event <> + Class XYZEvent + class EventFactory + class EventHistory + class EventPayload + class HiddenEvent #FFFFFF + } + + + package Model{ + Class HiddenModel #FFFFFF + } + + + XYZCommand -up-|> Command + XYZCommand .up.|> UndoableCommand + XYZCommand *--> "1" EventPayload + Command .up.> Model + note left of XYZCommand: XYZCommand = \nAddCommand, \nDeleteCommand, \nEditCommand, \nClearCommand, etc + + + XYZEvent .up.|> Event + XYZEvent .right.> EventPayload + Event ..> Model + EventFactory ..> XYZEvent : creates > + EventFactory .up.> Event #FFFFFF + EventFactory ..> UndoableCommand + EventHistory .up.> EventFactory + EventHistory ..> UndoableCommand + EventHistory *-up-> "0..*" Event + note bottom of XYZEvent: XYZEvent = \nAddExerciseEvent, \nDeleteRegime, \nEditEvent, etc +} + +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 016ef33e2e2..5062015438c 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -8,7 +8,7 @@ package Logic { package Parser { Interface Parser <> -Class AddressBookParser +Class ExerciseBookParser Class XYZCommandParser Class CliSyntax Class ParserUtil @@ -35,8 +35,8 @@ Class HiddenOutside #FFFFFF HiddenOutside ..> Logic LogicManager .up.|> Logic -LogicManager -->"1" AddressBookParser -AddressBookParser .left.> XYZCommandParser: creates > +LogicManager -->"1" ExerciseBookParser +ExerciseBookParser .left.> XYZCommandParser: creates > XYZCommandParser ..> XYZCommand : creates > XYZCommandParser ..|> Parser diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index e85a00d4107..4751313dc68 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -5,52 +5,44 @@ skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR Package Model <>{ -Interface ReadOnlyAddressBook <> -Interface Model <> -Interface ObservableList <> -Class AddressBook -Class ReadOnlyAddressBook -Class Model -Class ModelManager -Class UserPrefs -Class ReadOnlyUserPrefs - -Package Person { -Class Person -Class Address -Class Email -Class Name -Class Phone -Class UniquePersonList -} - -Package Tag { -Class Tag -} + Interface Model <> + Interface ReadOnlyUserPrefs <> + Interface ObservableList <> + + Class Model + Class ModelManager + Class UserPrefs + Class XYZReadOnlyResourceBook + Class XYZUniqueResourceList + + Package resource { + class "{abstract}\nResource" + Class Exercise + Class Regime + Class Schedule + } + + Package property { + Class PropertyBook + } } Class HiddenOutside #FFFFFF HiddenOutside ..> Model -AddressBook .up.|> ReadOnlyAddressBook - ModelManager .up.|> Model Model .right.> ObservableList -ModelManager o--> "1" AddressBook -ModelManager o-left-> "1" UserPrefs -UserPrefs .up.|> ReadOnlyUserPrefs - -AddressBook *--> "1" UniquePersonList -UniquePersonList o--> "*" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag - -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email - -ModelManager -->"1" Person : filtered list + +ModelManager o-right-> "1" PropertyBook + +Exercise -up-|> "{abstract}\nResource" +Regime -up-|> "{abstract}\nResource" +Schedule -left-|> "{abstract}\nResource" + +ModelManager o-left-> "4" XYZReadOnlyResourceBook +XYZReadOnlyResourceBook o--> XYZUniqueResourceList +XYZUniqueResourceList o--> "0..*" "{abstract}\nResource" +ModelManager o-down-> "1" UserPrefs +UserPrefs .right.|> ReadOnlyUserPrefs + @enduml diff --git a/docs/diagrams/RedoActivityDiagram.puml b/docs/diagrams/RedoActivityDiagram.puml new file mode 100644 index 00000000000..e8e1407d913 --- /dev/null +++ b/docs/diagrams/RedoActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +start +:User executes redo; + +if () then ([redo stack not empty]) + +:Pop Event from + redo stack; + +: Executes redo +method of Event; + +: Push Event to + undo stack; + +else([else]) + +endif +stop +@enduml diff --git a/docs/diagrams/ScheduleActivityDiagram.puml b/docs/diagrams/ScheduleActivityDiagram.puml new file mode 100644 index 00000000000..4c20e356c66 --- /dev/null +++ b/docs/diagrams/ScheduleActivityDiagram.puml @@ -0,0 +1,27 @@ +@startuml +skinparam activityShape rectangle + +start +:Entered schedule command; +if () then ([Conflict]) + :ResolveWindow pops up; + repeat + :User enters commands; + if () then ([ResolveCommand]) + if () then ([Valid Name]) + :resolve conflict and\ncreate new regime\n with valid name; + else ([Invalid name]) + :show invalid name message; + endif + else ([Not ResolveCommand]) + :show invalid command message; + endif + repeat while (conflict resolved?) is (no) + -> yes; + :ResolveWindow close; +else ([No Conflict]) +endif +:Schedule a regime\n successfully; +-> Program resumes; +stop +@enduml diff --git a/docs/diagrams/ScheduleConflictSequenceDiagram.puml b/docs/diagrams/ScheduleConflictSequenceDiagram.puml new file mode 100644 index 00000000000..37d1aba3472 --- /dev/null +++ b/docs/diagrams/ScheduleConflictSequenceDiagram.puml @@ -0,0 +1,30 @@ +@startuml ScheduleConflict +!include style.puml + +title __Schedule Conflict__ +actor User USER_COLOR +participant "__:Ui__" as Ui UI_COLOR +participant "__:Logic__" as Logic LOGIC_COLOR +participant "__:Model__" as Model MODEL_COLOR + +User -[USER_COLOR]> Ui: schedule n/cardio d/12/12/2012 +activate Ui UI_COLOR + +Ui -[UI_COLOR]> Logic: execute("n/cardio d/12/12/2012") +activate Logic LOGIC_COLOR + +Logic -[LOGIC_COLOR]> Model: hasSchedule(schedule) +activate Model MODEL_COLOR + +Model --[MODEL_COLOR]> Logic: true +deactivate Model + +Logic --[LOGIC_COLOR]> Ui: CommandResult("Schedule Conflict") +deactivate Logic + +Ui -[UI_COLOR]> Ui: showResolveWindow() +activate Ui UI_COLOR_T1 +return +Ui --[UI_COLOR]> User + +@enduml diff --git a/docs/diagrams/ScheduleResolveClassDiagram.puml b/docs/diagrams/ScheduleResolveClassDiagram.puml new file mode 100644 index 00000000000..0795c83dab7 --- /dev/null +++ b/docs/diagrams/ScheduleResolveClassDiagram.puml @@ -0,0 +1,32 @@ +@startuml +hide Conflict members +hide Schedule members +hide ResolveCommand members +hide Model fields +hide ModelManager members +skinparam classAttributeIconSize 0 + +class Conflict { +} + +class Schedule { +} + +class ResolveCommand { +} + +interface Model { + resolveConflict(Conflict) +} + +class ModelManager { +} + +Conflict *-left-> "1" Schedule : conflicting > +Conflict *--> "1" Schedule : scheduled > +ResolveCommand -left> Model : calls > +ResolveCommand --> "1 " Conflict +ModelManager .up.|> Model +ModelManager -right-> " 0...1 " Conflict : resolves > + +@enduml diff --git a/docs/diagrams/ScheduleResolveSequenceDiagram.puml b/docs/diagrams/ScheduleResolveSequenceDiagram.puml new file mode 100644 index 00000000000..fcd9b315e6b --- /dev/null +++ b/docs/diagrams/ScheduleResolveSequenceDiagram.puml @@ -0,0 +1,47 @@ +@startuml ScheduleResolve +!include style.puml + +title __Schedule Resolve__ +actor User USER_COLOR +participant "__:Ui__" as Ui UI_COLOR +participant "__:Logic__" as Logic LOGIC_COLOR +participant "__:Model__" as Model MODEL_COLOR +participant "__:Storage__" as Storage STORAGE_COLOR + +ref over User +Schedule Conflict +end ref + +User -[USER_COLOR]> Ui: resolve n/cardio2 i/1 r/2 +activate Ui UI_COLOR + +Ui -[UI_COLOR]> Logic: execute("n/cardio2 i/1 r/2") +activate Logic LOGIC_COLOR + +Logic -[LOGIC_COLOR]> Model: resolveConflict(conflict) +activate Model MODEL_COLOR + +Model --[MODEL_COLOR]> Logic +deactivate Model + +Logic -[LOGIC_COLOR]> Storage: saveBook() +activate Storage STORAGE_COLOR + +Storage -[STORAGE_COLOR]> Storage: saveToFile() +activate Storage STORAGE_COLOR_T1 +return + +Storage --[STORAGE_COLOR]> Logic +deactivate Storage + +Logic --[LOGIC_COLOR]> Ui +deactivate Logic + +Ui -[UI_COLOR]> Ui: closeResolveWindow() +activate Ui UI_COLOR_T1 +return + +Ui --[UI_COLOR]> User +deactivate Ui + +@enduml diff --git a/docs/diagrams/StatisticActivityDiagram.puml b/docs/diagrams/StatisticActivityDiagram.puml new file mode 100644 index 00000000000..3a90b28feaa --- /dev/null +++ b/docs/diagrams/StatisticActivityDiagram.puml @@ -0,0 +1,29 @@ +@startuml + +start +-Entered stats command +if () then ([Valid Category Type]) + + if () then ([Valid Chart type]) + if() then ([no date provided]) + -set dates as \n past 7 days + -filter exercises for \n the past 7 days + + else ([dates provided]) + -filter exercises \n within the \n given dates + endif + + -generate statistic + -generate chart + -display chart + + else ([Invalid chart type]) + -invalid chart \n type message + endif + +else ([Invalid Category Type]) +-invalid category \n message +endif + +stop +@enduml diff --git a/docs/diagrams/StatisticClassDiagram.puml b/docs/diagrams/StatisticClassDiagram.puml new file mode 100644 index 00000000000..67e37cfc7a6 --- /dev/null +++ b/docs/diagrams/StatisticClassDiagram.puml @@ -0,0 +1,31 @@ +@startuml +skinparam classAttributeIconSize 0 + +package Statistic <> { + class Statistic + class StatsFactory + StatsFactory - Statistic : creates > +} + +class StatsFactory { + -ObservableList exercises + -String chart + -String category + -Date startDate + -Date endDate + +generateStatistics() : Statistic +} + +class Statistic { + -String category + -String chart + -Date startDate + -Date endDate + -ArrayList properties + -ArrayList values + -double total + -double average + +resetData() : void +} + +@enduml diff --git a/docs/diagrams/StatisticSequenceDiagram.puml b/docs/diagrams/StatisticSequenceDiagram.puml new file mode 100644 index 00000000000..6376abd9214 --- /dev/null +++ b/docs/diagrams/StatisticSequenceDiagram.puml @@ -0,0 +1,88 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ExerciseBookParser" as ExerciseBookParser LOGIC_COLOR +participant ":StatsCommandParser" as StatsCommandParser LOGIC_COLOR +participant "s:StatsCommand" as StatsCommand LOGIC_COLOR +participant ":StatsFactory" as StatsFactory LOGIC_COLOR +participant "statistic:Statistic" as Statistic LOGIC_COLOR +participant "result:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("stats t/calories h/linechart") +activate LogicManager + +LogicManager -> ExerciseBookParser : parseCommand("stats t/calories h/linechart") +activate ExerciseBookParser + +create StatsCommandParser +ExerciseBookParser -> StatsCommandParser +activate StatsCommandParser + +StatsCommandParser --> ExerciseBookParser +deactivate StatsCommandParser + +ExerciseBookParser -> StatsCommandParser : parse("t/calories h/linechart") +activate StatsCommandParser + +create StatsCommand +StatsCommandParser -> StatsCommand : (calories, linechart) +activate StatsCommand + +StatsCommand --> StatsCommandParser : s +deactivate StatsCommand + +StatsCommandParser --> ExerciseBookParser : s +deactivate StatsCommandParser + +ExerciseBookParser --> LogicManager : s +deactivate ExerciseBookParser + +LogicManager -> StatsCommand : execute() +activate StatsCommand + +StatsCommand -> Model : getExerciseBookData() +activate Model + +Model --> StatsCommand : exercises +deactivate Model + +create StatsFactory +StatsCommand -> StatsFactory : (exercises, linechart, calories) +activate StatsFactory + +create Statistic +StatsFactory -> Statistic : generateStatistic() +activate Statistic + +Statistic --> StatsFactory : statistic +deactivate Statistic + +StatsFactory --> StatsCommand : statistic +deactivate StatsFactory + +StatsCommand -> Model : setStatistic(statistic) +activate Model + +Model --> StatsCommand +deactivate Model + +create CommandResult +StatsCommand -> CommandResult +activate CommandResult + +CommandResult --> StatsCommand : result +deactivate CommandResult + +StatsCommand --> LogicManager : result +deactivate StatsCommand + +[<--LogicManager : result +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 6adb2e156bf..e07d70f0cf7 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -4,21 +4,38 @@ skinparam arrowThickness 1.1 skinparam arrowColor STORAGE_COLOR skinparam classBackgroundColor STORAGE_COLOR -Interface Storage <> -Interface UserPrefsStorage <> -Interface AddressBookStorage <> - -Class StorageManager -Class JsonUserPrefsStorage -Class JsonAddressBookStorage - -StorageManager .left.|> Storage -StorageManager o-right-> UserPrefsStorage -StorageManager o--> AddressBookStorage - -JsonUserPrefsStorage .left.|> UserPrefsStorage -JsonAddressBookStorage .left.|> AddressBookStorage -JsonAddressBookStorage .down.> JsonSerializableAddressBookStorage -JsonSerializableAddressBookStorage .right.> JsonSerializablePerson -JsonSerializablePerson .right.> JsonAdaptedTag +package Storage { + package bookstorage { + Interface XYZResourceBookStorage <> + } + + package resource { + class "{abstract}\nXYZJsonAdaptedResource" + } + + package serializablebook { + class "{abstract}\nXYZSerializableResourceBook" + } + + Interface Storage <> + Interface UserPrefsStorage <> + Interface PropertyBookStorage <> + + Class StorageManager + Class JsonUserPrefsStorage + class JsonPropertyBookStorage + class JsonSerializablePropertyBook +} + +StorageManager .up.|> Storage +StorageManager o--> "4 " XYZResourceBookStorage +StorageManager o--> "1" UserPrefsStorage +StorageManager o-> "1" PropertyBookStorage + +"{abstract}\nXYZSerializableResourceBook" o-left-> "0...* " "{abstract}\nXYZJsonAdaptedResource" +XYZResourceBookStorage --> "{abstract}\nXYZSerializableResourceBook" + +JsonUserPrefsStorage ..|> UserPrefsStorage +JsonPropertyBookStorage ..|> PropertyBookStorage +JsonPropertyBookStorage -up-> JsonSerializablePropertyBook @enduml diff --git a/docs/diagrams/SuggestActivityDiagram.puml b/docs/diagrams/SuggestActivityDiagram.puml new file mode 100644 index 00000000000..341c64eccde --- /dev/null +++ b/docs/diagrams/SuggestActivityDiagram.puml @@ -0,0 +1,22 @@ +@startuml +skinparam activityShape rectangle + +start +:Entered suggest possible command; +:check for number of predicate tags; +if() then ([num >= 1]) + :check if operation type is valid; + if() then ([valid operation type]) + :form predicate; + :filter exercise tracker and database; + :display list to user; + else ([invalid operation type]) + :inform user input is invalid; + endif +else ([else]) + :inform user input is invalid; + +endif +-> Program resumes; +stop +@enduml diff --git a/docs/diagrams/SuggestCommandClassDiagram.puml b/docs/diagrams/SuggestCommandClassDiagram.puml new file mode 100644 index 00000000000..5dd3eaaf1f4 --- /dev/null +++ b/docs/diagrams/SuggestCommandClassDiagram.puml @@ -0,0 +1,58 @@ +@startuml +skinparam classAttributeIconSize 0 + +package Suggest <> { + class SuggestPossibleCommand + interface Predicate + class ExercisePredicate + interface BasePropertyPredicate + class ExerciseMusclePredicate + class ExerciseCustomPropertyPredicate + + ExercisePredicate ..|> Predicate + SuggestPossibleCommand o-right-> "1" Predicate : "predicate" + + BasePropertyPredicate --|> Predicate + + ExercisePredicate o-> "1..2" BasePropertyPredicate : "predicates" + ExerciseMusclePredicate .up.|> BasePropertyPredicate + ExerciseCustomPropertyPredicate .up.|> BasePropertyPredicate +} + +interface Predicate { + +test() +} + +class SuggestPossibleCommand { + -Predicate predicate + +execute(model) +} + +class ExercisePredicate { + -BasePropertyPredicate predicates + -boolean isStrict + +test() + -testStrict() + -testLoose() +} + +class ExerciseMusclePredicate { + -Set muscles + -boolean isStrict + +test() + -testStrict() + -testLoose() +} + +class ExerciseCustomPropertyPredicate { + -Map customProperties + -boolean isStrict + +test() + -testStrict() + -testLoose() +} + +interface BasePropertyPredicate { + +test() +} +@enduml diff --git a/docs/diagrams/SuggestPredicateSequenceDiagram.puml b/docs/diagrams/SuggestPredicateSequenceDiagram.puml new file mode 100644 index 00000000000..01707808dc7 --- /dev/null +++ b/docs/diagrams/SuggestPredicateSequenceDiagram.puml @@ -0,0 +1,62 @@ +@startuml +!include style.puml + +participant "SuggestCommandParser" as SuggestCommandParser LOGIC_COLOR +participant "ParserUtil" as ParserUtil LOGIC_COLOR + +SuggestCommandParser -> ParserUtil : parsePredicate(muscles, customProperties, isStrict) + +group form predicate +participant ":ExerciseMusclePredicate" as ExerciseMusclePredicate LOGIC_COLOR +participant ":ExerciseCustomPropertyPredicate" as ExerciseCustomPropertyPredicate LOGIC_COLOR +participant ":ExercisePredicate" as ExercisePredicate LOGIC_COLOR + +activate ParserUtil + +create ExerciseMusclePredicate +ParserUtil -> ExerciseMusclePredicate +activate ExerciseMusclePredicate + +ExerciseMusclePredicate -> ParserUtil : musclesPredicate +deactivate ExerciseMusclePredicate + +create ExerciseCustomPropertyPredicate +ParserUtil -> ExerciseCustomPropertyPredicate +activate ExerciseCustomPropertyPredicate + +ExerciseCustomPropertyPredicate -> ParserUtil : customPropertyPredicate +deactivate ExerciseCustomPropertyPredicate + +create ExercisePredicate +ParserUtil -> ExercisePredicate : new ExercisePredicate(musclesPredicate, isStrict) +activate ExercisePredicate +ExercisePredicate -> ParserUtil : p +deactivate ExercisePredicate + + +end + +ParserUtil -> SuggestCommandParser : p +deactivate ParserUtil + +@enduml +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":SuggestCommandParser" as SuggestCommandParser LOGIC_COLOR +participant ":ParserUtil" as ParserUtil LOGIC_COLOR +end box + +[-> LogicManager : execute("suggest s/possible m/Leg") +activate LogicManager + +[<--LogicManager : result +deactivate LogicManager + + +group form predicate +participant ":SuggestCommandParser" as SuggestCommandParser LOGIC_COLOR +participant ":ParserUtil" as ParserUtil LOGIC_COLOR +participant ":ExercisePredicate" as ExercisePredicate LOGIC_COLOR +participant ":ExerciseMusclePredicate" as ExerciseMusclePredicate LOGIC_COLOR +participant ":ExerciseCustomPropertyPredicate" as ExerciseCustomPropertyPredicate LOGIC_COLOR +end diff --git a/docs/diagrams/SuggestSequenceDiagram.puml b/docs/diagrams/SuggestSequenceDiagram.puml new file mode 100644 index 00000000000..33bf3ed8f7d --- /dev/null +++ b/docs/diagrams/SuggestSequenceDiagram.puml @@ -0,0 +1,79 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":ExerciseBookParser" as ExerciseBookParser LOGIC_COLOR +participant ":SuggestCommandParser" as SuggestCommandParser LOGIC_COLOR +participant ":ParserUtil" as ParserUtil LOGIC_COLOR +participant "s:SuggestPossibleCommand" as SuggestPossibleCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("suggest s/possible m/Leg") +activate LogicManager + +LogicManager -> ExerciseBookParser : parseCommand("suggest s/possible o/or m/Leg") +activate ExerciseBookParser + +create SuggestCommandParser +ExerciseBookParser -> SuggestCommandParser +activate SuggestCommandParser + +SuggestCommandParser --> ExerciseBookParser +deactivate SuggestCommandParser + +ExerciseBookParser -> SuggestCommandParser : parse("s/possible o/or m/Leg") +activate SuggestCommandParser + +SuggestCommandParser -> ParserUtil : parsePredicate(...) +activate ParserUtil + +ref over ParserUtil : form predicate + +ParserUtil -> SuggestCommandParser : p +deactivate ParserUtil + +create SuggestPossibleCommand +SuggestCommandParser -> SuggestPossibleCommand : new SuggestPossibleCommand(p) +activate SuggestPossibleCommand + +SuggestPossibleCommand --> SuggestCommandParser : s +deactivate SuggestPossibleCommand + +SuggestCommandParser --> ExerciseBookParser : s +deactivate SuggestCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +SuggestCommandParser -[hidden]-> ExerciseBookParser +destroy SuggestCommandParser + +ExerciseBookParser --> LogicManager : s +deactivate ExerciseBookParser + +LogicManager -> SuggestPossibleCommand : execute() +activate SuggestPossibleCommand + +SuggestPossibleCommand -> Model : updateSuggestedExerciseList(p) +activate Model + +Model --> SuggestPossibleCommand +deactivate Model + +create CommandResult +SuggestPossibleCommand -> CommandResult +activate CommandResult + +CommandResult --> SuggestPossibleCommand : result +deactivate CommandResult + +SuggestPossibleCommand --> LogicManager : result +deactivate SuggestPossibleCommand + +[<--LogicManager : result +deactivate LogicManager +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 92746f9fcf7..4856fb1e41e 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -10,11 +10,12 @@ Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow Class HelpWindow -Class ResultDisplay -Class PersonListPanel -Class PersonCard -Class StatusBarFooter Class CommandBox +Class ResultDisplay +Class ResolveWindow +Class LeftRightDisplay +Class XYZResourceListPanel +Class XYZResourceCard } package Model <> { @@ -30,31 +31,32 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> MainWindow -MainWindow --> HelpWindow -MainWindow *-down-> CommandBox -MainWindow *-down-> ResultDisplay -MainWindow *-down-> PersonListPanel -MainWindow *-down-> StatusBarFooter -PersonListPanel -down-> PersonCard +MainWindow *--> HelpWindow +MainWindow *--> CommandBox +MainWindow *--> ResultDisplay +MainWindow *--> ResolveWindow +MainWindow *--> "4" XYZResourceListPanel +XYZResourceListPanel *--> XYZResourceCard MainWindow -left-|> UiPart +HelpWindow --|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart -PersonListPanel --|> UiPart -PersonCard --|> UiPart -StatusBarFooter --|> UiPart -HelpWindow -down-|> UiPart +ResolveWindow --|> UiPart +LeftRightDisplay --|> UiPart +XYZResourceListPanel --|> UiPart +XYZResourceCard --|> UiPart + -PersonCard ..> Model -UiManager -right-> Logic -MainWindow -left-> Logic +XYZResourceCard ..> Model +UiManager --> Logic +MainWindow -right-> Logic -PersonListPanel -[hidden]left- HelpWindow -HelpWindow -[hidden]left- CommandBox -CommandBox -[hidden]left- ResultDisplay -ResultDisplay -[hidden]left- StatusBarFooter +ResolveWindow -> LeftRightDisplay +HelpWindow -[hidden]- CommandBox +CommandBox -[hidden]- ResultDisplay MainWindow -[hidden]-|> UiPart @enduml diff --git a/docs/diagrams/UndoActivityDiagram.puml b/docs/diagrams/UndoActivityDiagram.puml new file mode 100644 index 00000000000..fcbac0e51ee --- /dev/null +++ b/docs/diagrams/UndoActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +start +:User executes undo; + +if () then ([undo stack not empty]) + +:Pop Event from + undo stack; + +: Executes undo +method of Event; + +: Push Event to + redo stack; + +else([else]) + +endif +stop +@enduml diff --git a/docs/diagrams/UndoSequenceDiagram.puml b/docs/diagrams/UndoSequenceDiagram.puml index 410aab4e412..f8aa86b0dce 100644 --- a/docs/diagrams/UndoSequenceDiagram.puml +++ b/docs/diagrams/UndoSequenceDiagram.puml @@ -1,53 +1,50 @@ @startuml !include style.puml +title Sequence Diagram for Undo Command on Edit Exercise box Logic LOGIC_COLOR_T1 -participant ":LogicManager" as LogicManager LOGIC_COLOR -participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR -participant "u:UndoCommand" as UndoCommand LOGIC_COLOR +participant ":UndoCommand" as UndoCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +participant ":EventHistory" as EventHistory LOGIC_COLOR +participant ":EditEvent" as EditEvent LOGIC_COLOR end box box Model MODEL_COLOR_T1 -participant ":Model" as Model MODEL_COLOR -participant ":VersionedAddressBook" as VersionedAddressBook MODEL_COLOR +participant "model:Model" as Model MODEL_COLOR end box -[-> LogicManager : execute(undo) -activate LogicManager + [-> UndoCommand : execute(model) + activate UndoCommand -LogicManager -> AddressBookParser : parseCommand(undo) -activate AddressBookParser + UndoCommand -> EventHistory : undo(model) + activate EventHistory -create UndoCommand -AddressBookParser -> UndoCommand -activate UndoCommand + EventHistory -> EditEvent : undo(model) + activate EditEvent -UndoCommand --> AddressBookParser -deactivate UndoCommand + EditEvent -> Model : set(editedExercise, \noriginalExercise) + activate Model -AddressBookParser --> LogicManager : u -deactivate AddressBookParser + EditEvent -[hidden]-> Model -LogicManager -> UndoCommand : execute() -activate UndoCommand + Model --> EditEvent + deactivate Model -UndoCommand -> Model : undoAddressBook() -activate Model -Model -> VersionedAddressBook : undo() -activate VersionedAddressBook + EditEvent --> EventHistory + deactivate EditEvent -VersionedAddressBook -> VersionedAddressBook :resetData(ReadOnlyAddressBook) -VersionedAddressBook --> Model : -deactivate VersionedAddressBook + EventHistory --> UndoCommand + deactivate EventHistory -Model --> UndoCommand -deactivate Model + create CommandResult + UndoCommand -> CommandResult + activate CommandResult -UndoCommand --> LogicManager : result -deactivate UndoCommand -UndoCommand -[hidden]-> LogicManager : result -destroy UndoCommand + CommandResult --> UndoCommand + deactivate CommandResult + + + [<-- UndoCommand : commandResult + deactivate UndoCommand -[<--LogicManager -deactivate LogicManager @enduml diff --git a/docs/diagrams/UndoableCommandSequenceDiagram.puml b/docs/diagrams/UndoableCommandSequenceDiagram.puml new file mode 100644 index 00000000000..f230806a205 --- /dev/null +++ b/docs/diagrams/UndoableCommandSequenceDiagram.puml @@ -0,0 +1,64 @@ +@startuml +!include style.puml +title Sequence Diagram for Edit Command + +box Logic LOGIC_COLOR_T1 +participant ":EditCommand" as EditCommand LOGIC_COLOR +participant ":EventPayload" as EventPayload LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +participant ":EventHistory" as EventHistory LOGIC_COLOR +participant "<>\n:EventFactory" as EventFactory LOGIC_COLOR +participant ":EditEvent" as EditEvent LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "model:Model" as Model MODEL_COLOR +end box + + + +[-> EditCommand : execute(model) +activate EditCommand + + create EventPayload + EditCommand -> EventPayload + activate EventPayload + return payload + + + EditCommand -> EventHistory : addCommandToUndoStack(command) + activate EventHistory + + EventHistory -> EventFactory : commandToEvent(command) + activate EventFactory + + create EditEvent + EventFactory -> EditEvent : EditEvent(payload) + activate EditEvent + + + return event + + return event + + + EventHistory --> EditCommand + deactivate EventHistory + + EditCommand -> Model : set(originalExercise, editedExercise) + activate Model + + EditCommand -[hidden]-> Model + return + + create CommandResult + EditCommand -> CommandResult + activate CommandResult + + CommandResult --> EditCommand : commandResult + deactivate CommandResult + +[<-- EditCommand : commandResult +deactivate EditCommand + +@enduml diff --git a/docs/images/AddCustomRating.png b/docs/images/AddCustomRating.png new file mode 100644 index 00000000000..036bbe90d1c Binary files /dev/null and b/docs/images/AddCustomRating.png differ diff --git a/docs/images/AddExerciseAfterRedo.png b/docs/images/AddExerciseAfterRedo.png new file mode 100644 index 00000000000..ffa7132dafe Binary files /dev/null and b/docs/images/AddExerciseAfterRedo.png differ diff --git a/docs/images/AddExerciseBeforeAdding.png b/docs/images/AddExerciseBeforeAdding.png new file mode 100644 index 00000000000..825af9f490a Binary files /dev/null and b/docs/images/AddExerciseBeforeAdding.png differ diff --git a/docs/images/AddExerciseBeforeRedo.png b/docs/images/AddExerciseBeforeRedo.png new file mode 100644 index 00000000000..5e712c52a43 Binary files /dev/null and b/docs/images/AddExerciseBeforeRedo.png differ diff --git a/docs/images/AddExerciseBeforeUndo.png b/docs/images/AddExerciseBeforeUndo.png new file mode 100644 index 00000000000..62381b86293 Binary files /dev/null and b/docs/images/AddExerciseBeforeUndo.png differ diff --git a/docs/images/AddExerciseWithMuscle.png b/docs/images/AddExerciseWithMuscle.png new file mode 100644 index 00000000000..5c87cd08dc3 Binary files /dev/null and b/docs/images/AddExerciseWithMuscle.png differ diff --git a/docs/images/AddNewRegime.png b/docs/images/AddNewRegime.png new file mode 100644 index 00000000000..d56032214d4 Binary files /dev/null and b/docs/images/AddNewRegime.png differ diff --git a/docs/images/AddRunResult.png b/docs/images/AddRunResult.png new file mode 100644 index 00000000000..84cbf95c4ec Binary files /dev/null and b/docs/images/AddRunResult.png differ diff --git a/docs/images/ArchitectureDiagram.png b/docs/images/ArchitectureDiagram.png index aa2d337d932..4c30fc4b04b 100644 Binary files a/docs/images/ArchitectureDiagram.png and b/docs/images/ArchitectureDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index aa198138f8f..8b1c6ac415c 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/BarChart.png b/docs/images/BarChart.png new file mode 100644 index 00000000000..9bad92c70b2 Binary files /dev/null and b/docs/images/BarChart.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index bc7ed18ae29..6d53099c1f4 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/ClearAfter.png b/docs/images/ClearAfter.png new file mode 100644 index 00000000000..28c9bddd679 Binary files /dev/null and b/docs/images/ClearAfter.png differ diff --git a/docs/images/ClearBefore.png b/docs/images/ClearBefore.png new file mode 100644 index 00000000000..c26f824f1d8 Binary files /dev/null and b/docs/images/ClearBefore.png differ diff --git a/docs/images/CommitActivityDiagram.png b/docs/images/CommitActivityDiagram.png index 4de4fa4bf2b..bff1501b6b9 100644 Binary files a/docs/images/CommitActivityDiagram.png and b/docs/images/CommitActivityDiagram.png differ diff --git a/docs/images/CustomAddActivityDiagram.png b/docs/images/CustomAddActivityDiagram.png new file mode 100644 index 00000000000..eb33c77c8ad Binary files /dev/null and b/docs/images/CustomAddActivityDiagram.png differ diff --git a/docs/images/CustomClassDiagram.png b/docs/images/CustomClassDiagram.png new file mode 100644 index 00000000000..f2483e48d25 Binary files /dev/null and b/docs/images/CustomClassDiagram.png differ diff --git a/docs/images/CustomRating.png b/docs/images/CustomRating.png new file mode 100644 index 00000000000..0bffb6317a4 Binary files /dev/null and b/docs/images/CustomRating.png differ diff --git a/docs/images/CustomRemoveActivityDiagram.png b/docs/images/CustomRemoveActivityDiagram.png new file mode 100644 index 00000000000..04c949ff43e Binary files /dev/null and b/docs/images/CustomRemoveActivityDiagram.png differ diff --git a/docs/images/DancingRatingRemoved.png b/docs/images/DancingRatingRemoved.png new file mode 100644 index 00000000000..16d667e4c6a Binary files /dev/null and b/docs/images/DancingRatingRemoved.png differ diff --git a/docs/images/DeleteEntireRegimeAfter.png b/docs/images/DeleteEntireRegimeAfter.png new file mode 100644 index 00000000000..72515844b71 Binary files /dev/null and b/docs/images/DeleteEntireRegimeAfter.png differ diff --git a/docs/images/DeleteEntireRegimeBefore.png b/docs/images/DeleteEntireRegimeBefore.png new file mode 100644 index 00000000000..452533b50ca Binary files /dev/null and b/docs/images/DeleteEntireRegimeBefore.png differ diff --git a/docs/images/DeleteExerciseFromRegime.png b/docs/images/DeleteExerciseFromRegime.png new file mode 100644 index 00000000000..48cbc25ef7f Binary files /dev/null and b/docs/images/DeleteExerciseFromRegime.png differ diff --git a/docs/images/DeleteExerciseFromRegimeBeforeUndo.png b/docs/images/DeleteExerciseFromRegimeBeforeUndo.png new file mode 100644 index 00000000000..936bbeb1bc8 Binary files /dev/null and b/docs/images/DeleteExerciseFromRegimeBeforeUndo.png differ diff --git a/docs/images/DeleteExerciseFromRegimeRedoOutcome.png b/docs/images/DeleteExerciseFromRegimeRedoOutcome.png new file mode 100644 index 00000000000..e6259b1f723 Binary files /dev/null and b/docs/images/DeleteExerciseFromRegimeRedoOutcome.png differ diff --git a/docs/images/DeleteExerciseFromRegimeUndo.png b/docs/images/DeleteExerciseFromRegimeUndo.png new file mode 100644 index 00000000000..69c411ae917 Binary files /dev/null and b/docs/images/DeleteExerciseFromRegimeUndo.png differ diff --git a/docs/images/DeleteExerciseFromRegimeUndoOutcome.png b/docs/images/DeleteExerciseFromRegimeUndoOutcome.png new file mode 100644 index 00000000000..360b912ac42 Binary files /dev/null and b/docs/images/DeleteExerciseFromRegimeUndoOutcome.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..88457926530 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/EditAnotherCustomRating.png b/docs/images/EditAnotherCustomRating.png new file mode 100644 index 00000000000..2a9324f6b6e Binary files /dev/null and b/docs/images/EditAnotherCustomRating.png differ diff --git a/docs/images/EditCustomRating.png b/docs/images/EditCustomRating.png new file mode 100644 index 00000000000..160a90e9fc9 Binary files /dev/null and b/docs/images/EditCustomRating.png differ diff --git a/docs/images/EditExerciseAfter.png b/docs/images/EditExerciseAfter.png new file mode 100644 index 00000000000..87fea65a06b Binary files /dev/null and b/docs/images/EditExerciseAfter.png differ diff --git a/docs/images/EditExerciseAfter2.png b/docs/images/EditExerciseAfter2.png new file mode 100644 index 00000000000..0219e2195c8 Binary files /dev/null and b/docs/images/EditExerciseAfter2.png differ diff --git a/docs/images/EditExerciseBefore.png b/docs/images/EditExerciseBefore.png new file mode 100644 index 00000000000..b4c1a9a804e Binary files /dev/null and b/docs/images/EditExerciseBefore.png differ diff --git a/docs/images/EditExerciseBefore2.png b/docs/images/EditExerciseBefore2.png new file mode 100644 index 00000000000..ed46e08d1b6 Binary files /dev/null and b/docs/images/EditExerciseBefore2.png differ diff --git a/docs/images/EditedCustomAddSequenceDiagram.png b/docs/images/EditedCustomAddSequenceDiagram.png new file mode 100644 index 00000000000..6a25fc92d7b Binary files /dev/null and b/docs/images/EditedCustomAddSequenceDiagram.png differ diff --git a/docs/images/EditedCustomRemoveSequenceDiagram.png b/docs/images/EditedCustomRemoveSequenceDiagram.png new file mode 100644 index 00000000000..7aa1c923b3d Binary files /dev/null and b/docs/images/EditedCustomRemoveSequenceDiagram.png differ diff --git a/docs/images/EventClassDiagram.png b/docs/images/EventClassDiagram.png new file mode 100644 index 00000000000..edb0f59e357 Binary files /dev/null and b/docs/images/EventClassDiagram.png differ diff --git a/docs/images/HelpDialogBox.png b/docs/images/HelpDialogBox.png new file mode 100644 index 00000000000..5906bd3e63c Binary files /dev/null and b/docs/images/HelpDialogBox.png differ diff --git a/docs/images/HelpWindow.png b/docs/images/HelpWindow.png new file mode 100644 index 00000000000..b7f583c421c Binary files /dev/null and b/docs/images/HelpWindow.png differ diff --git a/docs/images/LineChart.png b/docs/images/LineChart.png new file mode 100644 index 00000000000..d715a199393 Binary files /dev/null and b/docs/images/LineChart.png differ diff --git a/docs/images/ListExerciseResult.png b/docs/images/ListExerciseResult.png new file mode 100644 index 00000000000..73307a6c5bb Binary files /dev/null and b/docs/images/ListExerciseResult.png differ diff --git a/docs/images/ListRegimeResult.png b/docs/images/ListRegimeResult.png new file mode 100644 index 00000000000..1191396f8ee Binary files /dev/null and b/docs/images/ListRegimeResult.png differ diff --git a/docs/images/ListScheduleResult.png b/docs/images/ListScheduleResult.png new file mode 100644 index 00000000000..8e725b892a2 Binary files /dev/null and b/docs/images/ListScheduleResult.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index b9e853cef12..a0df6c7607a 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/LongNamesTooltip.png b/docs/images/LongNamesTooltip.png new file mode 100644 index 00000000000..95e4742124c Binary files /dev/null and b/docs/images/LongNamesTooltip.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 280064118cf..1f5065f1bcc 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/NullMessage.png b/docs/images/NullMessage.png new file mode 100644 index 00000000000..023bff2b565 Binary files /dev/null and b/docs/images/NullMessage.png differ diff --git a/docs/images/PieChart.png b/docs/images/PieChart.png new file mode 100644 index 00000000000..4bf848f7a4b Binary files /dev/null and b/docs/images/PieChart.png differ diff --git a/docs/images/RatingRemainedForSeven.png b/docs/images/RatingRemainedForSeven.png new file mode 100644 index 00000000000..2f4f3d4045b Binary files /dev/null and b/docs/images/RatingRemainedForSeven.png differ diff --git a/docs/images/RatingRemovedForSix.png b/docs/images/RatingRemovedForSix.png new file mode 100644 index 00000000000..606a6580c8e Binary files /dev/null and b/docs/images/RatingRemovedForSix.png differ diff --git a/docs/images/RedoActivityDiagram.png b/docs/images/RedoActivityDiagram.png new file mode 100644 index 00000000000..9eb5df02795 Binary files /dev/null and b/docs/images/RedoActivityDiagram.png differ diff --git a/docs/images/RegimeCardioAfter.png b/docs/images/RegimeCardioAfter.png new file mode 100644 index 00000000000..7e2eb813389 Binary files /dev/null and b/docs/images/RegimeCardioAfter.png differ diff --git a/docs/images/RegimeCardioBefore.png b/docs/images/RegimeCardioBefore.png new file mode 100644 index 00000000000..048fb510c8f Binary files /dev/null and b/docs/images/RegimeCardioBefore.png differ diff --git a/docs/images/ResolveRegimeTracker.png b/docs/images/ResolveRegimeTracker.png new file mode 100644 index 00000000000..a55aa981fa6 Binary files /dev/null and b/docs/images/ResolveRegimeTracker.png differ diff --git a/docs/images/ResolveScheduled.png b/docs/images/ResolveScheduled.png new file mode 100644 index 00000000000..5cff30d801d Binary files /dev/null and b/docs/images/ResolveScheduled.png differ diff --git a/docs/images/ResolveScheduledOrConflicting.png b/docs/images/ResolveScheduledOrConflicting.png new file mode 100644 index 00000000000..63024982f59 Binary files /dev/null and b/docs/images/ResolveScheduledOrConflicting.png differ diff --git a/docs/images/ResolveScheduledResult.png b/docs/images/ResolveScheduledResult.png new file mode 100644 index 00000000000..1337830b2b3 Binary files /dev/null and b/docs/images/ResolveScheduledResult.png differ diff --git a/docs/images/ResolveTakeOneOrOther.png b/docs/images/ResolveTakeOneOrOther.png new file mode 100644 index 00000000000..0dd232d7d3c Binary files /dev/null and b/docs/images/ResolveTakeOneOrOther.png differ diff --git a/docs/images/ResolveTakeResolved.png b/docs/images/ResolveTakeResolved.png new file mode 100644 index 00000000000..bc592f74435 Binary files /dev/null and b/docs/images/ResolveTakeResolved.png differ diff --git a/docs/images/RunningRatingRemoved.png b/docs/images/RunningRatingRemoved.png new file mode 100644 index 00000000000..53872b40b93 Binary files /dev/null and b/docs/images/RunningRatingRemoved.png differ diff --git a/docs/images/ScheduleActivityDiagram.png b/docs/images/ScheduleActivityDiagram.png new file mode 100644 index 00000000000..036d3c6fa7a Binary files /dev/null and b/docs/images/ScheduleActivityDiagram.png differ diff --git a/docs/images/ScheduleBeforeTodayDate.png b/docs/images/ScheduleBeforeTodayDate.png new file mode 100644 index 00000000000..9480f64d4ef Binary files /dev/null and b/docs/images/ScheduleBeforeTodayDate.png differ diff --git a/docs/images/ScheduleCompleteAfterExerciseView.png b/docs/images/ScheduleCompleteAfterExerciseView.png new file mode 100644 index 00000000000..ea3c2f411b0 Binary files /dev/null and b/docs/images/ScheduleCompleteAfterExerciseView.png differ diff --git a/docs/images/ScheduleCompleteAfterScheduleView.png b/docs/images/ScheduleCompleteAfterScheduleView.png new file mode 100644 index 00000000000..15b9c9942b3 Binary files /dev/null and b/docs/images/ScheduleCompleteAfterScheduleView.png differ diff --git a/docs/images/ScheduleCompleteBefore.png b/docs/images/ScheduleCompleteBefore.png new file mode 100644 index 00000000000..f5acae00634 Binary files /dev/null and b/docs/images/ScheduleCompleteBefore.png differ diff --git a/docs/images/ScheduleCompleteExerciseTracker.png b/docs/images/ScheduleCompleteExerciseTracker.png new file mode 100644 index 00000000000..6bb26a2fe5c Binary files /dev/null and b/docs/images/ScheduleCompleteExerciseTracker.png differ diff --git a/docs/images/ScheduleCompleteScheduleTracker.png b/docs/images/ScheduleCompleteScheduleTracker.png new file mode 100644 index 00000000000..8704dbb798b Binary files /dev/null and b/docs/images/ScheduleCompleteScheduleTracker.png differ diff --git a/docs/images/ScheduleCompleteUndo.png b/docs/images/ScheduleCompleteUndo.png new file mode 100644 index 00000000000..22dee8e8dc1 Binary files /dev/null and b/docs/images/ScheduleCompleteUndo.png differ diff --git a/docs/images/ScheduleConflict.png b/docs/images/ScheduleConflict.png new file mode 100644 index 00000000000..0cf51119301 Binary files /dev/null and b/docs/images/ScheduleConflict.png differ diff --git a/docs/images/ScheduleRegimeAfter.png b/docs/images/ScheduleRegimeAfter.png new file mode 100644 index 00000000000..615b48d509b Binary files /dev/null and b/docs/images/ScheduleRegimeAfter.png differ diff --git a/docs/images/ScheduleRegimeAfterResolveRedo.png b/docs/images/ScheduleRegimeAfterResolveRedo.png new file mode 100644 index 00000000000..bdb95847828 Binary files /dev/null and b/docs/images/ScheduleRegimeAfterResolveRedo.png differ diff --git a/docs/images/ScheduleRegimeBefore.png b/docs/images/ScheduleRegimeBefore.png new file mode 100644 index 00000000000..0dd116a0357 Binary files /dev/null and b/docs/images/ScheduleRegimeBefore.png differ diff --git a/docs/images/ScheduleRegimeBeforeConflict.png b/docs/images/ScheduleRegimeBeforeConflict.png new file mode 100644 index 00000000000..3afdb3dd705 Binary files /dev/null and b/docs/images/ScheduleRegimeBeforeConflict.png differ diff --git a/docs/images/ScheduleRegimeBeforeResolve.png b/docs/images/ScheduleRegimeBeforeResolve.png new file mode 100644 index 00000000000..d01bbb1cd50 Binary files /dev/null and b/docs/images/ScheduleRegimeBeforeResolve.png differ diff --git a/docs/images/ScheduleRegimeBeforeResolveRedo.png b/docs/images/ScheduleRegimeBeforeResolveRedo.png new file mode 100644 index 00000000000..8c6a7bddb94 Binary files /dev/null and b/docs/images/ScheduleRegimeBeforeResolveRedo.png differ diff --git a/docs/images/ScheduleRegimeBeforeResolveUndo.png b/docs/images/ScheduleRegimeBeforeResolveUndo.png new file mode 100644 index 00000000000..491dd39678b Binary files /dev/null and b/docs/images/ScheduleRegimeBeforeResolveUndo.png differ diff --git a/docs/images/ScheduleRegimeCardio.png b/docs/images/ScheduleRegimeCardio.png new file mode 100644 index 00000000000..e67f4b0029f Binary files /dev/null and b/docs/images/ScheduleRegimeCardio.png differ diff --git a/docs/images/ScheduleRegimeCardioConflict.png b/docs/images/ScheduleRegimeCardioConflict.png new file mode 100644 index 00000000000..b0a34521f3d Binary files /dev/null and b/docs/images/ScheduleRegimeCardioConflict.png differ diff --git a/docs/images/ScheduleRegimeUndo.png b/docs/images/ScheduleRegimeUndo.png new file mode 100644 index 00000000000..fdf6d82e57c Binary files /dev/null and b/docs/images/ScheduleRegimeUndo.png differ diff --git a/docs/images/ScheduleResolve.png b/docs/images/ScheduleResolve.png new file mode 100644 index 00000000000..f265e33a83f Binary files /dev/null and b/docs/images/ScheduleResolve.png differ diff --git a/docs/images/ScheduleResolveClassDiagram.png b/docs/images/ScheduleResolveClassDiagram.png new file mode 100644 index 00000000000..5247d6af560 Binary files /dev/null and b/docs/images/ScheduleResolveClassDiagram.png differ diff --git a/docs/images/SelectResult.png b/docs/images/SelectResult.png new file mode 100644 index 00000000000..4f582e7483b Binary files /dev/null and b/docs/images/SelectResult.png differ diff --git a/docs/images/StatisticActivityDiagram.png b/docs/images/StatisticActivityDiagram.png new file mode 100644 index 00000000000..36e20591652 Binary files /dev/null and b/docs/images/StatisticActivityDiagram.png differ diff --git a/docs/images/StatisticClassDiagram.png b/docs/images/StatisticClassDiagram.png new file mode 100644 index 00000000000..94257dac1e7 Binary files /dev/null and b/docs/images/StatisticClassDiagram.png differ diff --git a/docs/images/StatisticSequenceDiagram.png b/docs/images/StatisticSequenceDiagram.png new file mode 100644 index 00000000000..4529dcd078a Binary files /dev/null and b/docs/images/StatisticSequenceDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index d87c1216820..30e1e817134 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/SuggestActivityDiagram.png b/docs/images/SuggestActivityDiagram.png new file mode 100644 index 00000000000..3f767c21156 Binary files /dev/null and b/docs/images/SuggestActivityDiagram.png differ diff --git a/docs/images/SuggestCommandClassDiagram.png b/docs/images/SuggestCommandClassDiagram.png new file mode 100644 index 00000000000..80ee232039a Binary files /dev/null and b/docs/images/SuggestCommandClassDiagram.png differ diff --git a/docs/images/SuggestPredicateSequenceDiagram.png b/docs/images/SuggestPredicateSequenceDiagram.png new file mode 100644 index 00000000000..ab89ade1c46 Binary files /dev/null and b/docs/images/SuggestPredicateSequenceDiagram.png differ diff --git a/docs/images/SuggestSequenceDiagram.png b/docs/images/SuggestSequenceDiagram.png new file mode 100644 index 00000000000..eac16c486a0 Binary files /dev/null and b/docs/images/SuggestSequenceDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..296febe28ee 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 7b4b3dbea45..96e3e931666 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoActivityDiagram.png b/docs/images/UndoActivityDiagram.png new file mode 100644 index 00000000000..a57ab19380d Binary files /dev/null and b/docs/images/UndoActivityDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png deleted file mode 100644 index 8f7538cd884..00000000000 Binary files a/docs/images/UndoRedoState0.png and /dev/null differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png deleted file mode 100644 index df9908d0948..00000000000 Binary files a/docs/images/UndoRedoState1.png and /dev/null differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png deleted file mode 100644 index 36519c1015b..00000000000 Binary files a/docs/images/UndoRedoState2.png and /dev/null differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png deleted file mode 100644 index 19959d01712..00000000000 Binary files a/docs/images/UndoRedoState3.png and /dev/null differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png deleted file mode 100644 index 4c623e4f2c5..00000000000 Binary files a/docs/images/UndoRedoState4.png and /dev/null differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png deleted file mode 100644 index 84ad2afa6bd..00000000000 Binary files a/docs/images/UndoRedoState5.png and /dev/null differ diff --git a/docs/images/UndoSequenceDiagram.png b/docs/images/UndoSequenceDiagram.png index 6addcd3a8d9..3da91a0a09e 100644 Binary files a/docs/images/UndoSequenceDiagram.png and b/docs/images/UndoSequenceDiagram.png differ diff --git a/docs/images/UndoableCommandSequenceDiagram.png b/docs/images/UndoableCommandSequenceDiagram.png new file mode 100644 index 00000000000..49031a3410e Binary files /dev/null and b/docs/images/UndoableCommandSequenceDiagram.png differ diff --git a/docs/images/ViewCustom.png b/docs/images/ViewCustom.png new file mode 100644 index 00000000000..cc895eda27e Binary files /dev/null and b/docs/images/ViewCustom.png differ diff --git a/docs/images/damithc.jpg b/docs/images/damithc.jpg deleted file mode 100644 index 12754388389..00000000000 Binary files a/docs/images/damithc.jpg and /dev/null differ diff --git a/docs/images/deleteExerciseAfter.png b/docs/images/deleteExerciseAfter.png new file mode 100644 index 00000000000..e4f8c063028 Binary files /dev/null and b/docs/images/deleteExerciseAfter.png differ diff --git a/docs/images/deleteExerciseBefore.png b/docs/images/deleteExerciseBefore.png new file mode 100644 index 00000000000..686637376b1 Binary files /dev/null and b/docs/images/deleteExerciseBefore.png differ diff --git a/docs/images/garylyp.png b/docs/images/garylyp.png new file mode 100644 index 00000000000..1067c48db25 Binary files /dev/null and b/docs/images/garylyp.png differ diff --git a/docs/images/jietung.png b/docs/images/jietung.png new file mode 100644 index 00000000000..6cf9d544fe3 Binary files /dev/null and b/docs/images/jietung.png differ diff --git a/docs/images/kwekke.png b/docs/images/kwekke.png new file mode 100644 index 00000000000..de421982c42 Binary files /dev/null and b/docs/images/kwekke.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5..00000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593..00000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/suggestBasic.png b/docs/images/suggestBasic.png new file mode 100644 index 00000000000..03d0a8e59aa Binary files /dev/null and b/docs/images/suggestBasic.png differ diff --git a/docs/images/suggestPossibleAfter1.png b/docs/images/suggestPossibleAfter1.png new file mode 100644 index 00000000000..555cd764b57 Binary files /dev/null and b/docs/images/suggestPossibleAfter1.png differ diff --git a/docs/images/suggestPossibleAfter2.png b/docs/images/suggestPossibleAfter2.png new file mode 100644 index 00000000000..17d87b7a3d8 Binary files /dev/null and b/docs/images/suggestPossibleAfter2.png differ diff --git a/docs/images/suggestPossibleAfter3.png b/docs/images/suggestPossibleAfter3.png new file mode 100644 index 00000000000..46a09151382 Binary files /dev/null and b/docs/images/suggestPossibleAfter3.png differ diff --git a/docs/images/suggestPossibleBefore1.png b/docs/images/suggestPossibleBefore1.png new file mode 100644 index 00000000000..046b164b556 Binary files /dev/null and b/docs/images/suggestPossibleBefore1.png differ diff --git a/docs/images/suggestPossibleBefore2.png b/docs/images/suggestPossibleBefore2.png new file mode 100644 index 00000000000..baa78e6abe5 Binary files /dev/null and b/docs/images/suggestPossibleBefore2.png differ diff --git a/docs/images/suggestPossibleBefore3.png b/docs/images/suggestPossibleBefore3.png new file mode 100644 index 00000000000..07d44fa3fa9 Binary files /dev/null and b/docs/images/suggestPossibleBefore3.png differ diff --git a/docs/images/t-cheepeng.png b/docs/images/t-cheepeng.png new file mode 100644 index 00000000000..3c529ee1bf7 Binary files /dev/null and b/docs/images/t-cheepeng.png differ diff --git a/docs/images/weihaw08.png b/docs/images/weihaw08.png new file mode 100644 index 00000000000..3a556a3dbbb Binary files /dev/null and b/docs/images/weihaw08.png differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad940..00000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a73227..00000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/stylesheets/asciidoctor.css b/docs/stylesheets/asciidoctor.css index 36590bf346c..1ee06612582 100644 --- a/docs/stylesheets/asciidoctor.css +++ b/docs/stylesheets/asciidoctor.css @@ -293,9 +293,10 @@ td.hdlist1{font-weight:bold;padding-bottom:1.25em} .colist>table tr>td:first-of-type{padding:0 .75em;line-height:1} .colist>table tr>td:last-of-type{padding:.25em 0} .thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd} +.imageblock{text-align:center} .imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0} .imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em} -.imageblock>.title{margin-bottom:0} +.imageblock>.title{margin-bottom:0;text-align:center} .imageblock.thumb,.imageblock.th{border-width:6px} .imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em} .image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0} diff --git a/docs/team/garylyp.adoc b/docs/team/garylyp.adoc new file mode 100644 index 00000000000..8ce2e789331 --- /dev/null +++ b/docs/team/garylyp.adoc @@ -0,0 +1,81 @@ += Gary Lim - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: ExerHealth + +--- + +== Overview + +Our team modified a basic command line application (AddressBook3) into **ExerHealth**, a desktop application +used for tracking and scheduling the user's exercises. The application provides statistical analysis of exercises +completed by the users. Additionally, it also acts as a personal trainer by suggesting different exercises which +both beginners and advanced users can choose from to incorporate into their exercise regimes. The user interacts +with it using a command line interface, and it has a GUI created with JavaFX. + +Below is a screenshot of what our desktop application looks like: + +image::Ui.png[] + +== Summary of contributions + +* *Major enhancement*: added *the ability to undo/redo previous commands* +** What it does: allows the user to undo previous commands one at a time. Undo commands can be reversed by using the redo command. +** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. +** Highlights: This enhancement requires an in-depth analysis of design alternatives as well as the interaction between all undoable commands and the backend model. +The implementation too was challenging as it required changes to existing commands while allowing ease of integration with future commands. + +* *Minor enhancement 1*: implemented the sorting of exercise list, regime list and schedule list such that entries are listed in a systematic order for users. + +* *Minor enhancement 2*: modified the `list` command to improve the navigability across various lists (exercise tracker, exercise suggestions, regime list, schedule list). + +* *Code contributed*: https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#search=garylyp&sort=groupTitle&sortWithin=title&since=2019-09-06&timeframe=commit&mergegroup=false&groupSelect=groupByRepos&breakdown=false[RepoSense] + +* *Other contributions*: + +** Project management: +*** Managed releases `v1.2` - `v1.4` (3 releases) on GitHub +** Enhancements to existing features: +*** Wrote additional tests for existing features to increase coverage (Pull requests https://github.com/AY1920S1-CS2103T-T09-2/main/pull/124[#124], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/186[#186]) +** Documentation: +*** Did cosmetic tweaks to contents of the User Guide and Developer Guide: https://github.com/AY1920S1-CS2103T-T09-2/main/pull/110[#110], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/231[#231] +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/AY1920S1-CS2103T-T09-2/main/pull/16[#16], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/81[#81], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/114[#114] +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/AY1920S1-CS2103T-T09-4/main/issues/259[1], https://github.com/AY1920S1-CS2103T-T09-4/main/issues/260[2], https://github.com/AY1920S1-CS2103T-F11-4/main/issues/122[3], https://github.com/AY1920S1-CS2103T-F11-4/main/issues/125[4]) + + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=undo] + +include::../UserGuide.adoc[tag=redo] + +include::../UserGuide.adoc[tag=undoredoexample] + +**Step 3**: `redo` + +Redoes the deletion of `Hiking` is deleted from the regime `Level 3` + + +image::DeleteExerciseFromRegimeRedoOutcome.png[] + +{sp} + + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=undoredo] + + + + + + diff --git a/docs/team/jietung.adoc b/docs/team/jietung.adoc new file mode 100644 index 00000000000..bed230ac5e0 --- /dev/null +++ b/docs/team/jietung.adoc @@ -0,0 +1,61 @@ += Chai Jie Tung - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: ExerHealth + +== Overview + +My team of 5 computer science students were tasked with enhancing a basic command line interface desktop Addressbook3 +application for software engineering project. Our team decided to morph the application to ExerHealth. ExerHealth is +a desktop application used for tracking and scheduling the user's exercises. The application has statistical analysis of +exercises user have completed in the past. Additionally, it also acts as a personal trainer by suggesting different +exercises which both beginners and advanced users can choose from to incorporate into their exercise regime. The user +interacts with it using a command line interface, and it has a GUI created with JavaFX. + +Figure below is a screenshot of what our application looks like: + +image::Ui.png[] + +Figure 1. The graphical user interface for ExerHealth + +My role was to develop the add and delete regime feature and statistic feature. The following sections illustrates these +enhancements in more details, as well as the relevant documentation I have added to the user and developer guides in +relation to those enhancements. + +== Summary of contributions +This section shows a summary of my coding, documentation and other helpful contributions to the team project. + +* *Major enhancement*: Added the ability to display statistic and charts which includes pie chart, line chart and bar chart. +** What it does: The `stats` command will allow users to display chart and statistic of the exercise they have completed. +** Justification: This feature will enable users to have an overview of number of exercise they have completed and calories they have burnt. This will help users understand their progress and improve planning of exercises in the future. +** Highlights: This enhancement works well with existing commands and commands to be added in future. It required an in-depth analysis of design alternatives to ensure that future enhancements can be made. + +* *Minor enhancement 1*: Added a `Unit` property to exercise. +* *Minor enhancement 2*: Added add and delete regime command that allows the user to add and delete regime from regime list. + +* *Code contributed*: [https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#search=jietung&sort=groupTitle&sortWithin=title&since=2019-09-06&timeframe=commit&mergegroup=false&groupSelect=groupByRepos&breakdown=false[RepoSense]] + +* *Other contributions*: + +** Enhancements to existing features: +*** Refactored Addressbook3 codebase to ExerHealth (Pull request https://github.com/AY1920S1-CS2103T-T09-2/main/pull/81[#81]) +*** Wrote additional tests for existing and new features to increase coverage (Pull requests https://github.com/AY1920S1-CS2103T-T09-2/main/pull/125[#125], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/127[#127]) + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=statistic] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=statistic] diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index f39e76e49b2..00000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 3 - ---- - -== Overview - -AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -== Summary of contributions - -* *Major enhancement*: added *the ability to undo/redo previous commands* -** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. -** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. -** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. -** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=delete] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/kwekke.adoc b/docs/team/kwekke.adoc new file mode 100644 index 00000000000..07be800f195 --- /dev/null +++ b/docs/team/kwekke.adoc @@ -0,0 +1,70 @@ += Kwek Kee En - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: ExerHealth + +== Overview + +My team of 5 computer science students were tasked with changing a basic command line application. Our team +decided to morph the application into ExerHealth. **ExerHealth** is a desktop application used for tracking +and scheduling the user's exercises. The application contains statistical analysis of exercises that users have completed +in the past. Additionally, it also acts as a personal trainer by suggesting different exercises which both +beginners and advanced users can choose from to incorporate into their exercise regimes. +The user interacts with it using a command line interface, and it has a GUI created with JavaFX. + +Below is a screenshot of what our desktop application looks like: + +.A screenshot of ExerHealth application +image::Ui.png[] + +<<< + +== Summary of contributions + +* *Major enhancement*: added *the ability to search for suggestions* +** What it does: The command `suggest` allows the user to search for suggestions. +** Justification: This feature gives new users a starting point in their exercise regime. +This feature also offers experienced users suggestions based on the type of exercises the user wishes to do. +** Highlights: This enhancement works well with existing features, such as Custom Properties, and can be expanded upon. +It requires an in-depth analysis of design alternatives to ensure that future extensions or further enhancements can be smooth. +The implementation was also challenging as it required multiple new predicate and utility classes. + +* *Minor enhancement 1*: Added the display panel on the left hand side of the UI to show the respective information after a command is executed (Pull request https://github.com/AY1920S1-CS2103T-T09-2/main/pull/121/files[#121]). + +* *Minor enhancement 2*: Allowed the command box to be automatically focused on upon opening the application so that user does not need to click on the box to start typing (Pull request https://github.com/AY1920S1-CS2103T-T09-2/main/pull/128[#128]). + +* *Code contributed*: https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#search=kwekke&sort=groupTitle&sortWithin=title&since=2019-09-06&timeframe=commit&mergegroup=false&groupSelect=groupByRepos&breakdown=false[RepoSense] + +* *Other contributions*: + + +** Project management: +*** Managed releases `v1.2` - `v1.4` (3 releases) on GitHub +*** Wrote additional tests for existing features to increase coverage (Pull requests https://github.com/AY1920S1-CS2103T-T09-2/main/pull/137[#137], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/96[#96]) +** Enhancements to existing features: +*** Refactored the GUI (Pull request https://github.com/AY1920S1-CS2103T-T09-2/main/pull/121[#121]) +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/AY1920S1-CS2103T-T09-2/main/pull/114[#114], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/81[#81] +*** Reported bugs and suggestions for other projects (https://github.com/AY1920S1-CS2103T-F12-1/main/issues/199[#199], https://github.com/AY1920S1-CS2103T-F12-1/main/issues/198[#198], + https://github.com/AY1920S1-CS2103T-F12-1/main/issues/196[#196], https://github.com/AY1920S1-CS2103T-F12-1/main/issues/190[#190], https://github.com/AY1920S1-CS2103T-F12-1/main/issues/185[#185]) + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=suggest] + +<<< + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=suggest] diff --git a/docs/team/t-cheepeng.adoc b/docs/team/t-cheepeng.adoc new file mode 100644 index 00000000000..ae383f9fb23 --- /dev/null +++ b/docs/team/t-cheepeng.adoc @@ -0,0 +1,71 @@ += Tan Chee Peng - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: ExerHealth + +== Overview + +My team of 5 computer science students were tasked with changing a basic command line application. Our team +decided to morph the application into ExerHealth. **ExerHealth** is a desktop application used for tracking +and scheduling the user's exercises. The application has statistical analysis of exercises users have completed +in the past. Additionally, it also acts as a personal trainer by suggesting different exercises which both +beginners and advanced users can choose from to incorporate into their exercise regimes. +The user interacts with it using a command line interface, and it has a GUI created with JavaFX. + +Below is a screenshot of what our desktop application looks like: + +image::Ui.png[] + +My role in this project was to design and develop the scheduling and resolving of scheduling conflicts features. +The following sections will illustrate these additions to the application in finer details, as well as any +relevant documentation that I have added into the user and developer guide. + +== Summary of contributions +This section shows a summary of the contributions I have made to the codebase of ExerHealth. It also shows the overview of the documentation provided and other helpful contributions to the team project. + +* *Major enhancement 1*: Added the ability to `schedule` exercise regimes +** What it does: The `schedule` command will allow users to schedule a stored exercise regime at a particular date of their choice. +** Justification: This feature will enable users to keep track and plan what exercise regimes they are supposed to complete. It also acts as a calendar to help users keep track of the regimes in the next few days, weeks or months. +** Highlights: This enhancement works well with existing features as well as future commands that +will be added to ExerHealth. An in-depth analysis of design alternatives was necessary to ensure that +future developers, including me, are able to extends and enhance this feature if need be. +The implementation itself was challenging because scheduling can lead to conflicts in what regimes to complete +at a particular date. However, this difficulty was remedied as described in *Major enhancement 2*. + +* *Major enhancement 2*: Added the ability to `resolve` scheduling conflicts +** What it does: The `resolve` command will allow users to `resolve` conflicts that arise when two regimes are scheduled on the same date. +** Justification: This feature will allow users the flexibility to select which exercises they wish to do from the conflicting scheduled regimes. It also allows users to store the resolved regime and use it in the future. +** Highlights: This enhancement works well with *major enhancement 1* as there is a direct link between the two. The implementation of this feature was particularly challenging because multiple resources had to be kept track of. Also, there was a need to prevent users from proceeding on with normal usage of the program until they resolve the scheduling conflict. The aforementioned requirement was technically challenging and multiple facets had to be considered in the implementation of the feature. + +* *Minor enhancement 1*: Added the display panel in the center of our UI to display more information of the selected resource. + +* *Code contributed*: [https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#search=t-cheepeng&sort=groupTitle&sortWithin=title&since=2019-09-06&timeframe=commit&mergegroup=false&groupSelect=groupByRepos&breakdown=false[RepoSense]] + +* *Other contributions*: + +** Project management: +*** Maintainer of repository on GitHub +*** Managed releases `v1.2` - `v1.4` (3 releases) on GitHub +** Testing: +*** Refactored large portions of testing codebase for fellow developers: https://github.com/AY1920S1-CS2103T-T09-2/main/pull/120[#120] +*** Wrote tests for existing and new features to increase code coverage (Pull requests: https://github.com/AY1920S1-CS2103T-T09-2/main/pull/119[#119], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/126[#126], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/234[#234]) +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/AY1920S1-CS2103T-T09-2/main/pull/81[#81], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/90[#90], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/102[#102] + +== Contributions to the User Guide +We had to update the User Guide that was provided by the basic application(*addressbook*) so that it will reflect the features that we have added. +|=== +|_Given below are sections I contributed to the *ExerHealth User Guide*. They show the additions I have made for the `schedule` and `resolve` features. The sections below will showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=scheduleresolve] + +== Contributions to the Developer Guide +We had to update the Developer Guide that was provided by the basic application(*addressbook*) so it will convey to future developers the implementation details of our features. +|=== +|_Given below are sections I contributed to the *ExerHealth Developer Guide*. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=resolvefeature] diff --git a/docs/team/weihaw08.adoc b/docs/team/weihaw08.adoc new file mode 100644 index 00000000000..659320fc967 --- /dev/null +++ b/docs/team/weihaw08.adoc @@ -0,0 +1,73 @@ += Ho Wei Haw - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: ExerHealth + +== Overview + +ExerHealth is a desktop application that is created by me and another 4 Computer Science students. It +enables users to track important information about their exercises and also provides other functionality such as +the ability to create their own fitness regimes and the ability to provide statistical analysis of their activities. +Users can also define their own properties which they wish to track for their exercises. + +Below is a screenshot of our application: + +.A screenshot of ExerHealth application +image::Ui.png[align="center", width="550"] + +== Summary of contributions + +* *Major enhancement 1*: Provided a feature that *allows user to define their own custom properties* and *remove them*. + +** What it does: Users can simply add in or remove their own custom properties should they wish to keep track of extra/lesser properties +for each exercise. + +** Justification: This feature provides greater flexibility among various users if they wish to include more information which they wish to track. This customisation can help users to +tailor suit the app for their personal use. + +** Highlights: This feature complements well with the suggestion feature. By having +custom properties, users can now produce exercise suggestions based on the custom properties they had created. Furthermore, the implementation of this feature is +quite challenging as there are a few aspects to take note of: the representation of custom properties in each exercise, the tracking of custom properties that have been created +and storage of custom properties. + +* *Minor enhancement 1*: Added a `viewcustom` command that allows user to view the custom properties that they have defined. + +* *Minor enhancement 2*: Designed the centre panel UI for exercises. This design helps to display both the default and custom +properties of each exercise to the user. + +* *Minor enhancement 3*: Added a `select` command that allows user to select a specific exercise/regime/schedule/suggestion +which they wish to view. + +* *Code contributed*: [https://nus-cs2103-ay1920s1.github.io/tp-dashboard/#search=weihaw08&sort=groupTitle&sortWithin=title&since=2019-09-06&timeframe=commit&mergegroup=false&groupSelect=groupByRepos&breakdown=false[Code Contribution]] + +* *Other contributions*: + +** Enhancements to existing features: +*** Updated the UI (Pull requests https://github.com/AY1920S1-CS2103T-T09-2/main/pull/129[#129], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/133[#133]) +*** Wrote additional tests for existing features to increase coverage (Pull requests https://github.com/AY1920S1-CS2103T-T09-2/main/pull/140[#140], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/194[#194]) +*** Involved in the morphing of AddressBook to suit ExerHealth (Pull requests https://github.com/AY1920S1-CS2103T-T09-2/main/pull/13[#13]) + +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/AY1920S1-CS2103T-T09-2/main/pull/81[#81], https://github.com/AY1920S1-CS2103T-T09-2/main/pull/103[#103] +*** Reviewed and provided suggestions for individuals in the class: https://github.com/nus-cs2103-AY1920S1/duke/pull/86[#86], https://github.com/nus-cs2103-AY1920S1/duke/pull/229[#229], + +== Contributions to the User Guide + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=customfeature] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=customfeature] + + + diff --git a/docs/tutorials/RemovingFields.adoc b/docs/tutorials/RemovingFields.adoc index 5a50b6965a6..f5b43572d31 100644 --- a/docs/tutorials/RemovingFields.adoc +++ b/docs/tutorials/RemovingFields.adoc @@ -24,7 +24,7 @@ Fortunately, the IntelliJ IDEA provides a robust refactoring tool that can ident Let's try to use it as much as we can. === Assisted refactoring -The `address` field in `Person` is actually an instance of the `seedu.address.model.person.Address` class. +The `address` field in `Person` is actually an instance of the `seedu.exercise.model.person.Address` class. Since removing the `Address` class will break the application, we start by identifying ``Address``'s usages. This allows us to see code that depends on `Address` to function properly and edit them on a case-by-case basis. Right-click the `Address` class and select `Refactor` > `Safe Delete` through the menu. diff --git a/docs/tutorials/TracingCode.adoc b/docs/tutorials/TracingCode.adoc index 5f0aaba1741..d6d06cec391 100644 --- a/docs/tutorials/TracingCode.adoc +++ b/docs/tutorials/TracingCode.adoc @@ -49,7 +49,7 @@ However, the execution path through a GUI is often somewhat obscure due to vario used by GUI frameworks, which happens to be the case here too. Therefore, let us put the breakpoint where the UI transfers control to the Logic component. According to the sequence diagram, the UI component yields control to the Logic component through a method named `execute`. Searching through the code base for `execute()` yields a promising candidate in -`seedu.address.ui.CommandBox.CommandExecutor`. +`seedu.exercise.ui.CommandBox.CommandExecutor`. .Using the `Search for target by name` feature. `Navigate` > `Symbol`. image::Execute.png[] diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java deleted file mode 100644 index e5cfb161b73..00000000000 --- a/src/main/java/seedu/address/MainApp.java +++ /dev/null @@ -1,183 +0,0 @@ -package seedu.address; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import javafx.application.Application; -import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; - -/** - * Runs the application. - */ -public class MainApp extends Application { - - public static final Version VERSION = new Version(0, 6, 0, true); - - private static final Logger logger = LogsCenter.getLogger(MainApp.class); - - protected Ui ui; - protected Logic logic; - protected Storage storage; - protected Model model; - protected Config config; - - @Override - public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); - super.init(); - - AppParameters appParameters = AppParameters.parse(getParameters()); - config = initConfig(appParameters.getConfigPath()); - - UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); - UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); - - initLogging(config); - - model = initModelManager(storage, userPrefs); - - logic = new LogicManager(model, storage); - - ui = new UiManager(logic); - } - - /** - * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
- * The data from the sample address book will be used instead if {@code storage}'s address book is not found, - * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. - */ - private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; - try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); - } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); - } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); - } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); - } - - return new ModelManager(initialData, userPrefs); - } - - private void initLogging(Config config) { - LogsCenter.init(config); - } - - /** - * Returns a {@code Config} using the file at {@code configFilePath}.
- * The default file path {@code Config#DEFAULT_CONFIG_FILE} will be used instead - * if {@code configFilePath} is null. - */ - protected Config initConfig(Path configFilePath) { - Config initializedConfig; - Path configFilePathUsed; - - configFilePathUsed = Config.DEFAULT_CONFIG_FILE; - - if (configFilePath != null) { - logger.info("Custom Config file specified " + configFilePath); - configFilePathUsed = configFilePath; - } - - logger.info("Using config file : " + configFilePathUsed); - - try { - Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); - initializedConfig = configOptional.orElse(new Config()); - } catch (DataConversionException e) { - logger.warning("Config file at " + configFilePathUsed + " is not in the correct format. " - + "Using default config properties"); - initializedConfig = new Config(); - } - - //Update config file in case it was missing to begin with or there are new/unused fields - try { - ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); - } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); - } - return initializedConfig; - } - - /** - * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, - * or a new {@code UserPrefs} with default configuration if errors occur when - * reading from the file. - */ - protected UserPrefs initPrefs(UserPrefsStorage storage) { - Path prefsFilePath = storage.getUserPrefsFilePath(); - logger.info("Using prefs file : " + prefsFilePath); - - UserPrefs initializedPrefs; - try { - Optional prefsOptional = storage.readUserPrefs(); - initializedPrefs = prefsOptional.orElse(new UserPrefs()); - } catch (DataConversionException e) { - logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " - + "Using default user prefs"); - initializedPrefs = new UserPrefs(); - } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initializedPrefs = new UserPrefs(); - } - - //Update prefs file in case it was missing to begin with or there are new/unused fields - try { - storage.saveUserPrefs(initializedPrefs); - } catch (IOException e) { - logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); - } - - return initializedPrefs; - } - - @Override - public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); - ui.start(primaryStage); - } - - @Override - public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); - try { - storage.saveUserPrefs(model.getUserPrefs()); - } catch (IOException e) { - logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); - } - } -} diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java deleted file mode 100644 index 1deb3a1e469..00000000000 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ /dev/null @@ -1,13 +0,0 @@ -package seedu.address.commons.core; - -/** - * Container for user visible messages. - */ -public class Messages { - - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; - public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; - public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - -} diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/address/commons/util/CollectionUtil.java deleted file mode 100644 index eafe4dfd681..00000000000 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package seedu.address.commons.util; - -import static java.util.Objects.requireNonNull; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Objects; -import java.util.stream.Stream; - -/** - * Utility methods related to Collections - */ -public class CollectionUtil { - - /** @see #requireAllNonNull(Collection) */ - public static void requireAllNonNull(Object... items) { - requireNonNull(items); - Stream.of(items).forEach(Objects::requireNonNull); - } - - /** - * Throws NullPointerException if {@code items} or any element of {@code items} is null. - */ - public static void requireAllNonNull(Collection items) { - requireNonNull(items); - items.forEach(Objects::requireNonNull); - } - - /** - * Returns true if {@code items} contain any elements that are non-null. - */ - public static boolean isAnyNonNull(Object... items) { - return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); - } -} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java deleted file mode 100644 index 92cd8fa605a..00000000000 --- a/src/main/java/seedu/address/logic/Logic.java +++ /dev/null @@ -1,50 +0,0 @@ -package seedu.address.logic; - -import java.nio.file.Path; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * API of the Logic component - */ -public interface Logic { - /** - * Executes the command and returns the result. - * @param commandText The command as entered by the user. - * @return the result of the command execution. - * @throws CommandException If an error occurs during command execution. - * @throws ParseException If an error occurs during parsing. - */ - CommandResult execute(String commandText) throws CommandException, ParseException; - - /** - * Returns the AddressBook. - * - * @see seedu.address.model.Model#getAddressBook() - */ - ReadOnlyAddressBook getAddressBook(); - - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Set the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); -} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java deleted file mode 100644 index d47ce874b1a..00000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,78 +0,0 @@ -package seedu.address.logic; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; - -/** - * The main LogicManager of the app. - */ -public class LogicManager implements Logic { - public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; - private final Logger logger = LogsCenter.getLogger(LogicManager.class); - - private final Model model; - private final Storage storage; - private final AddressBookParser addressBookParser; - - public LogicManager(Model model, Storage storage) { - this.model = model; - this.storage = storage; - addressBookParser = new AddressBookParser(); - } - - @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - - CommandResult commandResult; - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); - - try { - storage.saveAddressBook(model.getAddressBook()); - } catch (IOException ioe) { - throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); - } - - return commandResult; - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); - } - - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } - - @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); - } - - @Override - public GuiSettings getGuiSettings() { - return model.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - model.setGuiSettings(guiSettings); - } -} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index 71656d7c5c8..00000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index 9c86b1fa6e4..00000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,23 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java deleted file mode 100644 index 92f900b7916..00000000000 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.Objects; - -/** - * Represents the result of a command execution. - */ -public class CommandResult { - - private final String feedbackToUser; - - /** Help information should be shown to the user. */ - private final boolean showHelp; - - /** The application should exit. */ - private final boolean exit; - - /** - * Constructs a {@code CommandResult} with the specified fields. - */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { - this.feedbackToUser = requireNonNull(feedbackToUser); - this.showHelp = showHelp; - this.exit = exit; - } - - /** - * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, - * and other fields set to their default value. - */ - public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); - } - - public String getFeedbackToUser() { - return feedbackToUser; - } - - public boolean isShowHelp() { - return showHelp; - } - - public boolean isExit() { - return exit; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof CommandResult)) { - return false; - } - - CommandResult otherCommandResult = (CommandResult) other; - return feedbackToUser.equals(otherCommandResult.feedbackToUser) - && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; - } - - @Override - public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); - } - -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index 02fd256acba..00000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 7e36114902f..00000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,226 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with - */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index d6b19b0a0de..00000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,42 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; - - private final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 84be6ad2596..00000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e8..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Set; -import java.util.stream.Stream; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the AddCommand - * and returns an AddCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public AddCommand parse(String args) throws ParseException { - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * Returns true if none of the prefixes contains empty {@code Optional} values in the given - * {@code ArgumentMultimap}. - */ - private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { - return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java deleted file mode 100644 index 1e466792b46..00000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,76 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.ExitCommand; -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.commands.HelpCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java deleted file mode 100644 index 75b1a9bf119..00000000000 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ /dev/null @@ -1,15 +0,0 @@ -package seedu.address.logic.parser; - -/** - * Contains Command Line Interface (CLI) syntax definitions common to multiple commands - */ -public class CliSyntax { - - /* Prefix definitions */ - public static final Prefix PREFIX_NAME = new Prefix("n/"); - public static final Prefix PREFIX_PHONE = new Prefix("p/"); - public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); - public static final Prefix PREFIX_TAG = new Prefix("t/"); - -} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 522b93081cc..00000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns a DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java deleted file mode 100644 index 845644b7dea..00000000000 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; -import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; -import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new EditCommand object - */ -public class EditCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the EditCommand - * and returns an EditCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public EditCommand parse(String args) throws ParseException { - requireNonNull(args); - ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); - } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); - } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - return new EditCommand(index, editPersonDescriptor); - } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index 4fb71f23103..00000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns a FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55..00000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 1a943a0781a..00000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - - /* - * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java deleted file mode 100644 index d54df471c1f..00000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; -import java.util.function.Predicate; - -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; - -/** - * The API of the Model component. - */ -public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - - /** - * Replaces user prefs data with the data in {@code userPrefs}. - */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); - - /** - * Returns the user prefs. - */ - ReadOnlyUserPrefs getUserPrefs(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Sets the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Sets the user prefs' address book file path. - */ - void setAddressBookFilePath(Path addressBookFilePath); - - /** - * Replaces address book data with the data in {@code addressBook}. - */ - void setAddressBook(ReadOnlyAddressBook addressBook); - - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - boolean hasPerson(Person person); - - /** - * Deletes the given person. - * The person must exist in the address book. - */ - void deletePerson(Person target); - - /** - * Adds the given person. - * {@code person} must not already exist in the address book. - */ - void addPerson(Person person); - - /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - void setPerson(Person target, Person editedPerson); - - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); - - /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. - */ - void updateFilteredPersonList(Predicate predicate); -} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java deleted file mode 100644 index 0650c954f5c..00000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,151 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.nio.file.Path; -import java.util.function.Predicate; -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Represents the in-memory model of the address book data. - */ -public class ModelManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final AddressBook addressBook; - private final UserPrefs userPrefs; - private final FilteredList filteredPersons; - - /** - * Initializes a ModelManager with the given addressBook and userPrefs. - */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - super(); - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - - this.addressBook = new AddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); - } - - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - //=========== UserPrefs ================================================================================== - - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; - } - - @Override - public GuiSettings getGuiSettings() { - return userPrefs.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - userPrefs.setGuiSettings(guiSettings); - } - - @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); - } - - //=========== AddressBook ================================================================================ - - @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return addressBook; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return addressBook.hasPerson(person); - } - - @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); - } - - @Override - public void addPerson(Person person) { - addressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - addressBook.setPerson(target, editedPerson); - } - - //=========== Filtered Person List Accessors ============================================================= - - /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} - */ - @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - requireNonNull(predicate); - filteredPersons.setPredicate(predicate); - } - - @Override - public boolean equals(Object obj) { - // short circuit if same object - if (obj == this) { - return true; - } - - // instanceof handles nulls - if (!(obj instanceof ModelManager)) { - return false; - } - - // state check - ModelManager other = (ModelManager) obj; - return addressBook.equals(other.addressBook) - && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons); - } - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6ddc2cd9a29..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,17 +0,0 @@ -package seedu.address.model; - -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java deleted file mode 100644 index befd58a4c73..00000000000 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ /dev/null @@ -1,16 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; - -import seedu.address.commons.core.GuiSettings; - -/** - * Unmodifiable view of user prefs. - */ -public interface ReadOnlyUserPrefs { - - GuiSettings getGuiSettings(); - - Path getAddressBookFilePath(); - -} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java deleted file mode 100644 index 25a5fd6eab9..00000000000 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ /dev/null @@ -1,87 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Objects; - -import seedu.address.commons.core.GuiSettings; - -/** - * Represents User's preferences. - */ -public class UserPrefs implements ReadOnlyUserPrefs { - - private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); - - /** - * Creates a {@code UserPrefs} with default values. - */ - public UserPrefs() {} - - /** - * Creates a {@code UserPrefs} with the prefs in {@code userPrefs}. - */ - public UserPrefs(ReadOnlyUserPrefs userPrefs) { - this(); - resetData(userPrefs); - } - - /** - * Resets the existing data of this {@code UserPrefs} with {@code newUserPrefs}. - */ - public void resetData(ReadOnlyUserPrefs newUserPrefs) { - requireNonNull(newUserPrefs); - setGuiSettings(newUserPrefs.getGuiSettings()); - setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); - } - - public GuiSettings getGuiSettings() { - return guiSettings; - } - - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - this.guiSettings = guiSettings; - } - - public Path getAddressBookFilePath() { - return addressBookFilePath; - } - - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - this.addressBookFilePath = addressBookFilePath; - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - if (!(other instanceof UserPrefs)) { //this handles null as well. - return false; - } - - UserPrefs o = (UserPrefs) other; - - return guiSettings.equals(o.guiSettings) - && addressBookFilePath.equals(o.addressBookFilePath); - } - - @Override - public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Gui Settings : " + guiSettings); - sb.append("\nLocal data file location : " + addressBookFilePath); - return sb.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index a5bbe0b6a5f..00000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - private static final String SPECIAL_CHARACTERS = "!#$%&'*+/=?`{|}~^.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + ") .\n" - + "2. This is followed by a '@' and then a domain name. " - + "The domain name must:\n" - + " - be at least 2 characters long\n" - + " - start and end with alphanumeric characters\n" - + " - consist of alphanumeric characters, a period or a hyphen for the characters in between, if any."; - // alphanumeric and special characters - private static final String LOCAL_PART_REGEX = "^[\\w" + SPECIAL_CHARACTERS + "]+"; - private static final String DOMAIN_FIRST_CHARACTER_REGEX = "[^\\W_]"; // alphanumeric characters except underscore - private static final String DOMAIN_MIDDLE_REGEX = "[a-zA-Z0-9.-]*"; // alphanumeric, period and hyphen - private static final String DOMAIN_LAST_CHARACTER_REGEX = "[^\\W_]$"; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" - + DOMAIN_FIRST_CHARACTER_REGEX + DOMAIN_MIDDLE_REGEX + DOMAIN_LAST_CHARACTER_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427c..00000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 557a7a60cd5..00000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model.person; - -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.model.tag.Tag; - -/** - * Represents a Person in the address book. - * Guarantees: details are present and not null, field values are validated, immutable. - */ -public class Person { - - // Identity fields - private final Name name; - private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); - - /** - * Every field must be present and not null. - */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - public Phone getPhone() { - return phone; - } - - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - - /** - * Returns true if both persons of the same name have at least one other identity field that is the same. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); - } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append(" Phone: ") - .append(getPhone()) - .append(" Email: ") - .append(getEmail()) - .append(" Address: ") - .append(getAddress()) - .append(" Tags: "); - getTags().forEach(builder::append); - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index 872c76b382f..00000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; - public final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 0fee4fe57e6..00000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import seedu.address.model.person.exceptions.DuplicatePersonException; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * A list of persons that enforces uniqueness between its elements and does not allow nulls. - * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return internalUnmodifiableList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f59442..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca7..00000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index b0ea7e7dad7..00000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,54 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Tag // instanceof handles nulls - && tagName.equals(((Tag) other).tagName)); // state check - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facf..00000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.model.util; - -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods for populating {@code AddressBook} with sample data. - */ -public class SampleDataUtil { - public static Person[] getSamplePersons() { - return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - return sampleAb; - } - - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - -} diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f9..00000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2ea..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ /dev/null @@ -1,109 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Person}. - */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List tagged = new ArrayList<>(); - - /** - * Constructs a {@code JsonAdaptedPerson} with the given person details. - */ - @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged.addAll(tagged); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); - } - - /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted person. - */ - public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - final Phone modelPhone = new Phone(phone); - - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package seedu.address.storage; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. - * - * @throws IllegalValueException if there were any data constraints violated in the adapted tag. - */ - public Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java deleted file mode 100644 index dfab9daaa0d..00000000000 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.storage; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A class to access AddressBook data stored as a json file on the hard disk. - */ -public class JsonAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); - - private Path filePath; - - public JsonAddressBookStorage(Path filePath) { - this.filePath = filePath; - } - - public Path getAddressBookFilePath() { - return filePath; - } - - @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); - } - - /** - * Similar to {@link #readAddressBook()}. - * - * @param filePath location of the data. Cannot be null. - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(Path filePath) throws DataConversionException { - requireNonNull(filePath); - - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); - if (!jsonAddressBook.isPresent()) { - return Optional.empty(); - } - - try { - return Optional.of(jsonAddressBook.get().toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. - * - * @param filePath location of the data. Cannot be null. - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); - requireNonNull(filePath); - - FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d..00000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java deleted file mode 100644 index beda8bd9f11..00000000000 --- a/src/main/java/seedu/address/storage/Storage.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * API of the Storage component - */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { - - @Override - Optional readUserPrefs() throws DataConversionException, IOException; - - @Override - void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; - - @Override - Path getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataConversionException, IOException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java deleted file mode 100644 index e4f452b6cbf..00000000000 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ /dev/null @@ -1,77 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * Manages storage of AddressBook data in local storage. - */ -public class StorageManager implements Storage { - - private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; - private UserPrefsStorage userPrefsStorage; - - - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { - super(); - this.addressBookStorage = addressBookStorage; - this.userPrefsStorage = userPrefsStorage; - } - - // ================ UserPrefs methods ============================== - - @Override - public Path getUserPrefsFilePath() { - return userPrefsStorage.getUserPrefsFilePath(); - } - - @Override - public Optional readUserPrefs() throws DataConversionException, IOException { - return userPrefsStorage.readUserPrefs(); - } - - @Override - public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { - userPrefsStorage.saveUserPrefs(userPrefs); - } - - - // ================ AddressBook methods ============================== - - @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); - } - - @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); - } - -} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java deleted file mode 100644 index 90bbf11de97..00000000000 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ /dev/null @@ -1,193 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.scene.control.MenuItem; -import javafx.scene.control.TextInputControl; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * The Main Window. Provides the basic application layout containing - * a menu bar and space where other JavaFX elements can be placed. - */ -public class MainWindow extends UiPart { - - private static final String FXML = "MainWindow.fxml"; - - private final Logger logger = LogsCenter.getLogger(getClass()); - - private Stage primaryStage; - private Logic logic; - - // Independent Ui parts residing in this Ui container - private PersonListPanel personListPanel; - private ResultDisplay resultDisplay; - private HelpWindow helpWindow; - - @FXML - private StackPane commandBoxPlaceholder; - - @FXML - private MenuItem helpMenuItem; - - @FXML - private StackPane personListPanelPlaceholder; - - @FXML - private StackPane resultDisplayPlaceholder; - - @FXML - private StackPane statusbarPlaceholder; - - public MainWindow(Stage primaryStage, Logic logic) { - super(FXML, primaryStage); - - // Set dependencies - this.primaryStage = primaryStage; - this.logic = logic; - - // Configure the UI - setWindowDefaultSize(logic.getGuiSettings()); - - setAccelerators(); - - helpWindow = new HelpWindow(); - } - - public Stage getPrimaryStage() { - return primaryStage; - } - - private void setAccelerators() { - setAccelerator(helpMenuItem, KeyCombination.valueOf("F1")); - } - - /** - * Sets the accelerator of a MenuItem. - * @param keyCombination the KeyCombination value of the accelerator - */ - private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { - menuItem.setAccelerator(keyCombination); - - /* - * TODO: the code below can be removed once the bug reported here - * https://bugs.openjdk.java.net/browse/JDK-8131666 - * is fixed in later version of SDK. - * - * According to the bug report, TextInputControl (TextField, TextArea) will - * consume function-key events. Because CommandBox contains a TextField, and - * ResultDisplay contains a TextArea, thus some accelerators (e.g F1) will - * not work when the focus is in them because the key event is consumed by - * the TextInputControl(s). - * - * For now, we add following event filter to capture such key events and open - * help window purposely so to support accelerators even when focus is - * in CommandBox or ResultDisplay. - */ - getRoot().addEventFilter(KeyEvent.KEY_PRESSED, event -> { - if (event.getTarget() instanceof TextInputControl && keyCombination.match(event)) { - menuItem.getOnAction().handle(new ActionEvent()); - event.consume(); - } - }); - } - - /** - * Fills up all the placeholders of this window. - */ - void fillInnerParts() { - personListPanel = new PersonListPanel(logic.getFilteredPersonList()); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); - - resultDisplay = new ResultDisplay(); - resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); - statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - - CommandBox commandBox = new CommandBox(this::executeCommand); - commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); - } - - /** - * Sets the default size based on {@code guiSettings}. - */ - private void setWindowDefaultSize(GuiSettings guiSettings) { - primaryStage.setHeight(guiSettings.getWindowHeight()); - primaryStage.setWidth(guiSettings.getWindowWidth()); - if (guiSettings.getWindowCoordinates() != null) { - primaryStage.setX(guiSettings.getWindowCoordinates().getX()); - primaryStage.setY(guiSettings.getWindowCoordinates().getY()); - } - } - - /** - * Opens the help window or focuses on it if it's already opened. - */ - @FXML - public void handleHelp() { - if (!helpWindow.isShowing()) { - helpWindow.show(); - } else { - helpWindow.focus(); - } - } - - void show() { - primaryStage.show(); - } - - /** - * Closes the application. - */ - @FXML - private void handleExit() { - GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), - (int) primaryStage.getX(), (int) primaryStage.getY()); - logic.setGuiSettings(guiSettings); - helpWindow.hide(); - primaryStage.hide(); - } - - public PersonListPanel getPersonListPanel() { - return personListPanel; - } - - /** - * Executes the command and returns the result. - * - * @see seedu.address.logic.Logic#execute(String) - */ - private CommandResult executeCommand(String commandText) throws CommandException, ParseException { - try { - CommandResult commandResult = logic.execute(commandText); - logger.info("Result: " + commandResult.getFeedbackToUser()); - resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); - - if (commandResult.isShowHelp()) { - handleHelp(); - } - - if (commandResult.isExit()) { - handleExit(); - } - - return commandResult; - } catch (CommandException | ParseException e) { - logger.info("Invalid command: " + commandText); - resultDisplay.setFeedbackToUser(e.getMessage()); - throw e; - } - } -} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index 0684b088868..00000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,74 +0,0 @@ -package seedu.address.ui; - -import java.util.Comparator; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import seedu.address.model.person.Person; - -/** - * An UI component that displays information of a {@code Person}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.fxml"; - - /** - * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. - * As a consequence, UI elements' variable names cannot be set to such keywords - * or an exception will be thrown by JavaFX during runtime. - * - * @see The issue on AddressBook level 4 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonCard)) { - return false; - } - - // state check - PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index 1328917096e..00000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,46 +0,0 @@ -package seedu.address.ui; - -import java.util.logging.Logger; - -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.scene.control.ListCell; -import javafx.scene.control.ListView; -import javafx.scene.layout.Region; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - public PersonListPanel(ObservableList personList) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java deleted file mode 100644 index 7e17911323f..00000000000 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ /dev/null @@ -1,26 +0,0 @@ -package seedu.address.ui; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.Region; - -/** - * A ui for the status bar that is displayed at the footer of the application. - */ -public class StatusBarFooter extends UiPart { - - private static final String FXML = "StatusBarFooter.fxml"; - - @FXML - private Label saveLocationStatus; - - - public StatusBarFooter(Path saveLocation) { - super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); - } - -} diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/seedu/exercise/AppParameters.java similarity index 93% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/seedu/exercise/AppParameters.java index ab552c398f3..5ddfff377e6 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/seedu/exercise/AppParameters.java @@ -1,4 +1,4 @@ -package seedu.address; +package seedu.exercise; import java.nio.file.Path; import java.nio.file.Paths; @@ -7,8 +7,8 @@ import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.util.FileUtil; /** * Represents the parsed command-line parameters given to the application. diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/exercise/Main.java similarity index 97% rename from src/main/java/seedu/address/Main.java rename to src/main/java/seedu/exercise/Main.java index 052a5068631..d5b5d6406ab 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/seedu/exercise/Main.java @@ -1,4 +1,4 @@ -package seedu.address; +package seedu.exercise; import javafx.application.Application; diff --git a/src/main/java/seedu/exercise/MainApp.java b/src/main/java/seedu/exercise/MainApp.java new file mode 100644 index 00000000000..7df9abe69ee --- /dev/null +++ b/src/main/java/seedu/exercise/MainApp.java @@ -0,0 +1,318 @@ +package seedu.exercise; + +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_REGIME_COMPARATOR; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_SCHEDULE_COMPARATOR; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import javafx.application.Application; +import javafx.stage.Stage; +import seedu.exercise.commons.core.Config; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.State; +import seedu.exercise.commons.core.Version; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.commons.util.ConfigUtil; +import seedu.exercise.commons.util.StringUtil; +import seedu.exercise.logic.Logic; +import seedu.exercise.logic.LogicManager; +import seedu.exercise.model.Model; +import seedu.exercise.model.ModelManager; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.ReadOnlyUserPrefs; +import seedu.exercise.model.UserPrefs; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.model.util.DefaultExerciseDatabaseUtil; +import seedu.exercise.model.util.SampleDataUtil; +import seedu.exercise.storage.JsonPropertyBookStorage; +import seedu.exercise.storage.JsonUserPrefsStorage; +import seedu.exercise.storage.PropertyBookStorage; +import seedu.exercise.storage.Storage; +import seedu.exercise.storage.StorageBook; +import seedu.exercise.storage.UserPrefsStorage; +import seedu.exercise.storage.bookstorage.JsonExerciseBookStorage; +import seedu.exercise.storage.bookstorage.JsonRegimeBookStorage; +import seedu.exercise.storage.bookstorage.JsonScheduleBookStorage; +import seedu.exercise.ui.Ui; +import seedu.exercise.ui.UiManager; + +/** + * Runs the application. + *

+ * Additionally, the MainApp wil keep track of the state of the program. + *

+ */ +public class MainApp extends Application { + + public static final Version VERSION = new Version(1, 2, 1, true); + + private static final Logger logger = LogsCenter.getLogger(MainApp.class); + private static State state; + + protected Ui ui; + protected Logic logic; + protected Storage storage; + protected Model model; + protected Config config; + + @Override + public void init() throws Exception { + logger.info("=============================[ Initializing ExerciseBook ]==========================="); + super.init(); + + AppParameters appParameters = AppParameters.parse(getParameters()); + config = initConfig(appParameters.getConfigPath()); + + UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); + UserPrefs userPrefs = initPrefs(userPrefsStorage); + + JsonExerciseBookStorage exerciseBookStorage = new JsonExerciseBookStorage(userPrefs.getExerciseBookFilePath()); + + JsonExerciseBookStorage exerciseDatabaseStorage = + new JsonExerciseBookStorage(userPrefs.getAllExerciseBookFilePath()); + + JsonRegimeBookStorage regimeBookStorage = new JsonRegimeBookStorage(userPrefs.getRegimeBookFilePath()); + + JsonScheduleBookStorage scheduleBookStorage = new JsonScheduleBookStorage(userPrefs.getScheduleBookFilePath()); + + PropertyBookStorage propertyBookStorage = + new JsonPropertyBookStorage(userPrefs.getPropertyBookFilePath()); + + storage = new StorageBook(exerciseBookStorage, exerciseDatabaseStorage, regimeBookStorage, + scheduleBookStorage, userPrefsStorage, propertyBookStorage); + + initLogging(config); + model = initModelManager(storage, userPrefs); + logic = new LogicManager(model, storage); + + ui = new UiManager(logic); + setState(State.NORMAL); + } + + /** + * Returns a {@code ModelManager} with the data from {@code storage}'s exercise book and {@code userPrefs}.
+ * The data from the sample exercise book will be used instead if {@code storage}'s exercise book is not found, + * or an empty exercise book will be used instead if errors occur when reading {@code storage}'s exercise book. + */ + private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { + ReadOnlyResourceBook initialData = readExerciseData(storage, storage.getExerciseBookFilePath()); + ReadOnlyResourceBook initialRegimeData = readRegimeData(storage, storage.getRegimeBookFilePath()); + ReadOnlyResourceBook initialDatabase = + readExerciseData(storage, storage.getExerciseDatabaseFilePath()); + ReadOnlyResourceBook initialScheduleData = readScheduleData(storage); + initialisePropertyBook(storage); + + return new ModelManager(initialData, initialRegimeData, + initialDatabase, initialScheduleData, userPrefs); + } + + /** + * Returns a {@code ReadOnlyResourceBook} using the file at {@code path}.
+ * The data is read from {@code storage}. + */ + private ReadOnlyResourceBook readRegimeData(Storage storage, Path path) { + Optional> regimeBookOptional; + ReadOnlyResourceBook regimeData; + + try { + regimeBookOptional = storage.readRegimeBook(path); + if (regimeBookOptional.isEmpty()) { + logger.info("Data file not found. Will be starting with a sample RegimeBook"); + } + regimeData = regimeBookOptional.orElseGet(SampleDataUtil::getSampleRegimeBook); + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty RegimeBook"); + regimeData = new ReadOnlyResourceBook<>(DEFAULT_REGIME_COMPARATOR); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty RegimeBook"); + regimeData = new ReadOnlyResourceBook<>(DEFAULT_REGIME_COMPARATOR); + + } + + return regimeData; + } + + /** + * Returns a {@code ReadOnlyResourceBook} using the file at {@code path}.
+ * The data is read from {@code storage}. + */ + private ReadOnlyResourceBook readExerciseData(Storage storage, Path path) { + Optional> exerciseBookOptional; + ReadOnlyResourceBook exerciseData; + + try { + exerciseBookOptional = storage.readExerciseBook(path); + if (exerciseBookOptional.isEmpty()) { + logger.info("Data file not found. Will be starting with a sample exercise database book"); + } + + if (path.equals(storage.getExerciseBookFilePath())) { //use sample data util exercise book + exerciseData = exerciseBookOptional.orElseGet(SampleDataUtil::getSampleExerciseBook); + } else { // use default exercise database util database book + exerciseData = exerciseBookOptional.orElseGet(DefaultExerciseDatabaseUtil::getExerciseDatabaseBook); + } + + } catch (DataConversionException e) { + logger.warning("Data file not in correct format. Will be starting with a sample exercise database book"); + exerciseData = DefaultExerciseDatabaseUtil.getExerciseDatabaseBook(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty ExerciseBook"); + exerciseData = new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR); + } + + return exerciseData; + } + + /** + * Returns a {@code ReadOnlyResourceBook} from {@code storage}. + */ + private ReadOnlyResourceBook readScheduleData(Storage storage) { + Optional> scheduleBookOptional; + ReadOnlyResourceBook initialScheduleData; + + try { + scheduleBookOptional = storage.readScheduleBook(); + if (!scheduleBookOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample ScheduleBook"); + } + initialScheduleData = scheduleBookOptional.orElseGet(SampleDataUtil::getSampleScheduleBook); + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty ScheduleBook"); + initialScheduleData = new ReadOnlyResourceBook<>(DEFAULT_SCHEDULE_COMPARATOR); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty ScheduleBook"); + initialScheduleData = new ReadOnlyResourceBook<>(DEFAULT_SCHEDULE_COMPARATOR); + } + + return initialScheduleData; + } + + /** + * Initialises the PropertyBook in {@code storage}. + */ + private void initialisePropertyBook(Storage storage) { + try { + storage.readPropertyBook(); + logger.info("PropertyBook successfully loaded"); + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with " + + " a default PropertyBook"); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with " + + "a default PropertyBook"); + } + } + + private void initLogging(Config config) { + LogsCenter.init(config); + } + + /** + * Returns a {@code Config} using the file at {@code configFilePath}.
+ * The default file path {@code Config#DEFAULT_CONFIG_FILE} will be used instead + * if {@code configFilePath} is null. + */ + protected Config initConfig(Path configFilePath) { + Config initializedConfig; + Path configFilePathUsed; + + configFilePathUsed = Config.DEFAULT_CONFIG_FILE; + + if (configFilePath != null) { + logger.info("Custom Config file specified " + configFilePath); + configFilePathUsed = configFilePath; + } + + logger.info("Using config file : " + configFilePathUsed); + + try { + Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); + initializedConfig = configOptional.orElse(new Config()); + } catch (DataConversionException e) { + logger.warning("Config file at " + configFilePathUsed + " is not in the correct format. " + + "Using default config properties"); + initializedConfig = new Config(); + } + + //Update config file in case it was missing to begin with or there are new/unused fields + try { + ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + return initializedConfig; + } + + /** + * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, + * or a new {@code UserPrefs} with default configuration if errors occur when + * reading from the file. + */ + protected UserPrefs initPrefs(UserPrefsStorage storage) { + Path prefsFilePath = storage.getUserPrefsFilePath(); + logger.info("Using prefs file : " + prefsFilePath); + + UserPrefs initializedPrefs; + try { + Optional prefsOptional = storage.readUserPrefs(); + initializedPrefs = prefsOptional.orElse(new UserPrefs()); + } catch (DataConversionException e) { + logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " + + "Using default user prefs"); + initializedPrefs = new UserPrefs(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty ExerciseBook"); + initializedPrefs = new UserPrefs(); + } + + //Update prefs file in case it was missing to begin with or there are new/unused fields + try { + storage.saveUserPrefs(initializedPrefs); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + + return initializedPrefs; + } + + @Override + public void start(Stage primaryStage) { + logger.info("Starting ExerciseBook " + MainApp.VERSION); + state = State.NORMAL; + ui.start(primaryStage); + } + + @Override + public void stop() { + logger.info("============================ [ Stopping Exercise Book ] ============================="); + try { + storage.saveUserPrefs(model.getUserPrefs()); + } catch (IOException e) { + logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); + } + } + + public static State getState() { + return state; + } + + /** + * Sets the current state of the program. + *

+ * Only subclasses of {@code Command} can and should call this method. + *

+ */ + public static void setState(State newState) { + requireAllNonNull(newState); + logger.info("Application state changing from " + getState() + " to " + newState.toString()); + + state = newState; + } +} diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/exercise/commons/core/Config.java similarity index 97% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/seedu/exercise/commons/core/Config.java index 91145745521..2356ee1f08a 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/exercise/commons/core/Config.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.exercise.commons.core; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/seedu/exercise/commons/core/GuiSettings.java similarity index 92% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/seedu/exercise/commons/core/GuiSettings.java index 5ace559ad15..3cf8c482939 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/seedu/exercise/commons/core/GuiSettings.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.exercise.commons.core; import java.awt.Point; import java.io.Serializable; @@ -10,8 +10,8 @@ */ public class GuiSettings implements Serializable { - private static final double DEFAULT_HEIGHT = 600; - private static final double DEFAULT_WIDTH = 740; + private static final double DEFAULT_HEIGHT = 800; + private static final double DEFAULT_WIDTH = 1000; private final double windowWidth; private final double windowHeight; diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/exercise/commons/core/LogsCenter.java similarity index 97% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/seedu/exercise/commons/core/LogsCenter.java index 431e7185e76..4d25f0716b4 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/exercise/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.exercise.commons.core; import java.io.IOException; import java.util.Arrays; @@ -18,7 +18,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "exerhealth.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; diff --git a/src/main/java/seedu/exercise/commons/core/Messages.java b/src/main/java/seedu/exercise/commons/core/Messages.java new file mode 100644 index 00000000000..7c67e21d11d --- /dev/null +++ b/src/main/java/seedu/exercise/commons/core/Messages.java @@ -0,0 +1,20 @@ +package seedu.exercise.commons.core; + +/** + * Container for user visible messages. + */ +public class Messages { + + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! %1$s"; + public static final String MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX = "The exercise index provided is invalid"; + public static final String MESSAGE_INVALID_REGIME_DISPLAYED_INDEX = "The regime index provided is invalid"; + public static final String MESSAGE_INVALID_SCHEDULE_DISPLAYED_INDEX = "The schedule index provided is invalid"; + public static final String MESSAGE_INVALID_SUGGESTION_DISPLAYED_INDEX = "The suggestion index provided is invalid"; + public static final String MESSAGE_EXERCISES_LISTED_OVERVIEW = "%1$d exercises listed!"; + public static final String MESSAGE_INVALID_CONTEXT = "Unable to issue %1$s here"; + public static final String MESSAGE_INVALID_TYPE = "%1$s can only be \'%2$s\' or \'%3$s\'"; + //For some reason, javafx refuses to display \t characters in some situations. + public static final String MESSAGE_TAB = " "; + +} diff --git a/src/main/java/seedu/exercise/commons/core/State.java b/src/main/java/seedu/exercise/commons/core/State.java new file mode 100644 index 00000000000..3b5c03d3cb2 --- /dev/null +++ b/src/main/java/seedu/exercise/commons/core/State.java @@ -0,0 +1,13 @@ +package seedu.exercise.commons.core; + +import seedu.exercise.MainApp; + +/** + * Represents the current state of the program. + * Only subclasses of {@code Command} can alter the state of the program. + * The main state is held in {@link MainApp}. + */ +public enum State { + IN_CONFLICT, + NORMAL +} diff --git a/src/main/java/seedu/exercise/commons/core/ValidationRegex.java b/src/main/java/seedu/exercise/commons/core/ValidationRegex.java new file mode 100644 index 00000000000..c25b1de6185 --- /dev/null +++ b/src/main/java/seedu/exercise/commons/core/ValidationRegex.java @@ -0,0 +1,12 @@ +package seedu.exercise.commons.core; + +/** + * Encapsulates the different validation regex that help to check if an input is correct. + */ +public class ValidationRegex { + public static final String ONLY_NON_NEGATIVE_INTEGER = "\\d+"; + public static final String ONLY_NON_NEGATIVE_NUMBERS = "\\d+(\\.\\d+)?"; + public static final String ONLY_ALPHABETS_AND_SPACE = "^[ A-Za-z]+$"; + public static final String ONLY_ALPHABETS = "^[A-Za-z]+$"; + public static final String ONLY_ALPHABETS_NUMBERS_AND_SPACE = "^[ A-Za-z0-9]+$"; +} diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/exercise/commons/core/Version.java similarity index 98% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/seedu/exercise/commons/core/Version.java index e117f91b3b2..20f694c722f 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/exercise/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package seedu.exercise.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/seedu/exercise/commons/core/index/Index.java similarity index 87% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/seedu/exercise/commons/core/index/Index.java index 19536439c09..ad93fcabae8 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/seedu/exercise/commons/core/index/Index.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core.index; +package seedu.exercise.commons.core.index; /** * Represents a zero-based or one-based index. @@ -9,6 +9,8 @@ * convert it back to an int if the index will not be passed to a different component again. */ public class Index { + + public static final String MESSAGE_CONSTRAINTS = "Index is not a non-zero unsigned integer."; private int zeroBasedIndex; /** @@ -51,4 +53,9 @@ public boolean equals(Object other) { || (other instanceof Index // instanceof handles nulls && zeroBasedIndex == ((Index) other).zeroBasedIndex); // state check } + + @Override + public int hashCode() { + return ((Integer) zeroBasedIndex).hashCode(); + } } diff --git a/src/main/java/seedu/exercise/commons/core/index/IndexUtil.java b/src/main/java/seedu/exercise/commons/core/index/IndexUtil.java new file mode 100644 index 00000000000..43a28acb18b --- /dev/null +++ b/src/main/java/seedu/exercise/commons/core/index/IndexUtil.java @@ -0,0 +1,33 @@ +package seedu.exercise.commons.core.index; + +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.List; + +/** + * Utility methods for dealing with {@link Index}. + */ +public class IndexUtil { + + /** + * Checks if the {@code index} is out of bounds of the {@code list}. + * {@code index} is always positive. + */ + public static boolean isIndexOutOfBounds(Index index, List list) { + requireAllNonNull(index, list); + return index.getZeroBased() >= list.size(); + } + + /** + * Checks if any of the indexes in {@code indexList} is out of bounds of the given {@code list}. + */ + public static boolean areIndexesOutOfBounds(List indexList, List list) { + requireAllNonNull(indexList, list); + for (Index index : indexList) { + if (isIndexOutOfBounds(index, list)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/seedu/exercise/commons/exceptions/DataConversionException.java similarity index 84% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/seedu/exercise/commons/exceptions/DataConversionException.java index 1f689bd8e3f..b5ab0ec6160 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/seedu/exercise/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.exercise.commons.exceptions; /** * Represents an error during conversion of data from one format to another diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/seedu/exercise/commons/exceptions/IllegalValueException.java similarity index 92% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/seedu/exercise/commons/exceptions/IllegalValueException.java index 19124db485c..cebe8cb99a6 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/seedu/exercise/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package seedu.exercise.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/seedu/exercise/commons/util/AppUtil.java similarity index 61% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/seedu/exercise/commons/util/AppUtil.java index da90201dfd6..c19a80cd71e 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/seedu/exercise/commons/util/AppUtil.java @@ -1,15 +1,18 @@ -package seedu.address.commons.util; +package seedu.exercise.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import seedu.exercise.MainApp; +import seedu.exercise.commons.core.State; /** * A container for App specific utility functions */ public class AppUtil { + public static final String UNEXPECTED_STATE = "State of program is not %1$s"; + public static Image getImage(String imagePath) { requireNonNull(imagePath); return new Image(MainApp.class.getResourceAsStream(imagePath)); @@ -36,4 +39,17 @@ public static void checkArgument(Boolean condition, String errorMessage) { throw new IllegalArgumentException(errorMessage); } } + + /** + * Checks that {@code state} of the program is valid. Used for validating a particular + * state of the program in a method. + * + * @throws IllegalStateException if {@code MainApp's} state is not as expected + */ + public static void requireMainAppState(State state) { + requireNonNull(state); + if (MainApp.getState() != state) { + throw new IllegalStateException(String.format(UNEXPECTED_STATE, state.toString())); + } + } } diff --git a/src/main/java/seedu/exercise/commons/util/CollectionUtil.java b/src/main/java/seedu/exercise/commons/util/CollectionUtil.java new file mode 100644 index 00000000000..d9a5f71a0e8 --- /dev/null +++ b/src/main/java/seedu/exercise/commons/util/CollectionUtil.java @@ -0,0 +1,73 @@ +package seedu.exercise.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +/** + * Utility methods related to Collections + */ +public class CollectionUtil { + + /** + * @see #requireAllNonNull(Collection) + */ + public static void requireAllNonNull(Object... items) { + requireNonNull(items); + Stream.of(items).forEach(Objects::requireNonNull); + } + + /** + * Throws NullPointerException if {@code items} or any element of {@code items} is null. + */ + public static void requireAllNonNull(Collection items) { + requireNonNull(items); + items.forEach(Objects::requireNonNull); + } + + /** + * Returns true if {@code items} contain any elements that are non-null. + */ + public static boolean isAnyNonNull(Object... items) { + return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); + } + + /** + * Appends two list together into a new list. + */ + public static List append(List first, List second) { + requireAllNonNull(first, second); + List result = new ArrayList<>(first); + result.addAll(second); + return result; + } + + /** + * Checks if both lists are empty. Only returns true if both are empty. + */ + public static boolean areListsEmpty(List first, List second) { + requireAllNonNull(first, second); + return first.isEmpty() && second.isEmpty(); + } + + /** + * Converts a hash map into a list of strings containing information in the format "key: value". + */ + public static List mapToStringList(Map toConvert) { + requireNonNull(toConvert); + List result = new ArrayList<>(); + List keyList = new ArrayList<>(toConvert.keySet()); + for (K key : keyList) { + String toAdd = key.toString() + ": " + toConvert.get(key); + result.add(toAdd); + } + return result; + } + +} diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/seedu/exercise/commons/util/ConfigUtil.java similarity index 77% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/seedu/exercise/commons/util/ConfigUtil.java index f7f8a2bd44c..2a64561187e 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/seedu/exercise/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package seedu.exercise.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.exercise.commons.core.Config; +import seedu.exercise.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/exercise/commons/util/FileUtil.java similarity index 98% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/seedu/exercise/commons/util/FileUtil.java index b1e2767cdd9..1ffeb0441a6 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/exercise/commons/util/FileUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.exercise.commons.util; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/exercise/commons/util/JsonUtil.java similarity index 97% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/seedu/exercise/commons/util/JsonUtil.java index 8ef609f055d..5a6f5fd7466 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/exercise/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package seedu.exercise.commons.util; import static java.util.Objects.requireNonNull; @@ -20,8 +20,8 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.DataConversionException; /** * Converts a Java object instance to JSON and vice versa diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/exercise/commons/util/StringUtil.java similarity index 82% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/seedu/exercise/commons/util/StringUtil.java index 61cc8c9a1cb..a4727f28728 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/exercise/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package seedu.exercise.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.exercise.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; @@ -65,4 +65,14 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Formats a single word by capitalising the first letter and setting the remaining + * as lowercase. + */ + public static String capitaliseSingleWord(String word) { + String capitalisedFirstLetter = word.substring(0, 1).toUpperCase(); + String lowercaseRemaining = word.substring(1).toLowerCase(); + return capitalisedFirstLetter + lowercaseRemaining; + } } diff --git a/src/main/java/seedu/exercise/logic/Logic.java b/src/main/java/seedu/exercise/logic/Logic.java new file mode 100644 index 00000000000..6bf792c2f58 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/Logic.java @@ -0,0 +1,94 @@ +package seedu.exercise.logic; + +import java.nio.file.Path; + +import javafx.collections.ObservableList; +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.State; +import seedu.exercise.logic.commands.CommandResult; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * API of the Logic component + */ +public interface Logic { + /** + * Executes the command and returns the result. + * + * @param commandText The command as entered by the user. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + * @throws ParseException If an error occurs during parsing. + */ + CommandResult execute(String commandText) throws CommandException, ParseException; + + /** + * Returns the ExerciseBook. + * + * @see seedu.exercise.model.Model#getExerciseBookData() + */ + ReadOnlyResourceBook getExerciseBook(); + + /** + * Returns an unmodifiable view of the filtered list of exercises. + */ + ObservableList getSortedExerciseList(); + + /** + * Returns the RegimeBook. + * + * @see seedu.exercise.model.Model#getAllRegimeData() + */ + ReadOnlyResourceBook getRegimeBook(); + + ObservableList getSortedRegimeList(); + + /** + * Returns an unmodifiable view of the filtered list of schedules + */ + ObservableList getSortedScheduleList(); + + /** + * Returns the user prefs' exercise book file path. + */ + Path getExerciseBookFilePath(); + + /** + * Returns the user prefs' regime book file path. + */ + Path getRegimeBookFilePath(); + + /** + * Returns an unmodifiable view of the suggested list of exercises. + */ + ObservableList getSuggestedExerciseList(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Set the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the Statistic object currently in focus. + */ + Statistic getStatistic(); + + /** + * Returns the conflict that needs to be resolved in {@code Model}. + * + * This method should only be called when {@code MainApp}'s state is {@link State#IN_CONFLICT}. + */ + Conflict getConflict(); +} diff --git a/src/main/java/seedu/exercise/logic/LogicManager.java b/src/main/java/seedu/exercise/logic/LogicManager.java new file mode 100644 index 00000000000..f45d80d038d --- /dev/null +++ b/src/main/java/seedu/exercise/logic/LogicManager.java @@ -0,0 +1,150 @@ +package seedu.exercise.logic; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_CONTEXT; +import static seedu.exercise.commons.util.AppUtil.requireMainAppState; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import seedu.exercise.MainApp; +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.State; +import seedu.exercise.logic.commands.Command; +import seedu.exercise.logic.commands.CommandResult; +import seedu.exercise.logic.commands.ResolveCommand; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.logic.parser.ExerciseBookParser; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.Model; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.storage.Storage; + +/** + * The main LogicManager of the app. + */ +public class LogicManager implements Logic { + + public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); + + private final Model model; + private final Storage storage; + private final ExerciseBookParser exerciseBookParser; + + public LogicManager(Model model, Storage storage) { + this.model = model; + this.storage = storage; + exerciseBookParser = new ExerciseBookParser(); + } + + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + + CommandResult commandResult; + Command command = exerciseBookParser.parseCommand(commandText); + if (!isCommandExecutedInCorrectState(command)) { + logger.info(command.getClass().getSimpleName() + " executed in wrong state: State." + MainApp.getState()); + throw new CommandException(String.format(MESSAGE_INVALID_CONTEXT, command.getClass().getSimpleName())); + } + + commandResult = command.execute(model); + + try { + saveAllData(); + } catch (IOException ioe) { + logger.info(FILE_OPS_ERROR_MESSAGE + ioe); + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + + return commandResult; + } + + @Override + public ReadOnlyResourceBook getExerciseBook() { + return model.getExerciseBookData(); + } + + @Override + public ObservableList getSortedExerciseList() { + return model.getSortedExerciseList(); + } + + @Override + public ReadOnlyResourceBook getRegimeBook() { + return model.getAllRegimeData(); + } + + @Override + public ObservableList getSortedRegimeList() { + return model.getSortedRegimeList(); + } + + @Override + public ObservableList getSortedScheduleList() { + return model.getSortedScheduleList(); + } + + @Override + public Path getExerciseBookFilePath() { + return model.getExerciseBookFilePath(); + } + + @Override + public Path getRegimeBookFilePath() { + return model.getRegimeBookFilePath(); + } + + @Override + public ObservableList getSuggestedExerciseList() { + return model.getSuggestedExerciseList(); + } + + @Override + public GuiSettings getGuiSettings() { + return model.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + model.setGuiSettings(guiSettings); + } + + @Override + public Statistic getStatistic() { + return model.getStatistic(); + } + + @Override + public Conflict getConflict() { + requireMainAppState(State.IN_CONFLICT); + return model.getConflict(); + + } + + /** + * Saves all book data from ExerHealth to disk + * + * @throws IOException if saving fails + */ + private void saveAllData() throws IOException { + storage.saveExerciseBook(model.getExerciseBookData()); + storage.saveExerciseDatabase(model.getExerciseDatabaseData()); + storage.saveScheduleBook(model.getAllScheduleData()); + storage.saveRegimeBook(model.getAllRegimeData()); + storage.savePropertyBook(); + } + + private boolean isCommandExecutedInCorrectState(Command command) { + return (MainApp.getState() == State.NORMAL && !(command instanceof ResolveCommand)) + || (MainApp.getState() == State.IN_CONFLICT && command instanceof ResolveCommand); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/AddCommand.java b/src/main/java/seedu/exercise/logic/commands/AddCommand.java new file mode 100644 index 00000000000..673534e8b01 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/AddCommand.java @@ -0,0 +1,22 @@ +package seedu.exercise.logic.commands; + +import static seedu.exercise.logic.commands.AddExerciseCommand.MESSAGE_USAGE_EXERCISE; +import static seedu.exercise.logic.commands.AddRegimeCommand.MESSAGE_USAGE_REGIME; + +/** + * Represents an AddCommand with hidden internal logic and the ability to be executed. + */ +public abstract class AddCommand extends Command implements UndoableCommand, TypeDependentCommand { + + public static final String COMMAND_WORD = "add"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds exercise to exercise list or adds regime to regime list.\n" + + "EXERCISE: " + MESSAGE_USAGE_EXERCISE + "\n" + + "REGIME: " + MESSAGE_USAGE_REGIME; + + @Override + public String getUndoableCommandWord() { + return COMMAND_WORD; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/AddExerciseCommand.java b/src/main/java/seedu/exercise/logic/commands/AddExerciseCommand.java new file mode 100644 index 00000000000..45f81567dc8 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/AddExerciseCommand.java @@ -0,0 +1,90 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.commands.events.AddExerciseEvent.KEY_EXERCISE_TO_ADD; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CALORIES; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_QUANTITY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_UNIT; + +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.ui.ListResourceType; + +/** + * Adds an exercise to the exercise book. + */ +public class AddExerciseCommand extends AddCommand implements PayloadCarrierCommand { + + public static final String MESSAGE_USAGE_EXERCISE = "Parameters: " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_NAME + "EXERCISE NAME " + + PREFIX_DATE + "DATE " + + PREFIX_CALORIES + "CALORIES " + + PREFIX_QUANTITY + "QUANTITY " + + PREFIX_UNIT + "UNITS " + + "[" + PREFIX_MUSCLE + "MUSCLE]..." + + "[CUSTOM_PROPERTY_PREFIX_NAME/VALUE]..." + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_CATEGORY + "exercise " + + PREFIX_NAME + "Run " + + PREFIX_DATE + "22/09/2019 " + + PREFIX_CALORIES + "1500 " + + PREFIX_QUANTITY + "2.4 " + + PREFIX_UNIT + "km " + + PREFIX_MUSCLE + "Leg"; + + public static final String MESSAGE_SUCCESS = "New exercise added: %1$s"; + public static final String MESSAGE_DUPLICATE_EXERCISE = "This exercise already exists in the exercise book"; + public static final String RESOURCE_TYPE = "exercise"; + + private Exercise exerciseToAdd; + private EventPayload eventPayload; + + /** + * Creates an AddExerciseCommand to add the specified {@code Exercise} + */ + public AddExerciseCommand(Exercise exercise) { + requireNonNull(exercise); + exerciseToAdd = exercise; + eventPayload = new EventPayload<>(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (model.hasExercise(exerciseToAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_EXERCISE); + } + + model.addExercise(exerciseToAdd); + model.updateStatistic(); + eventPayload.put(KEY_EXERCISE_TO_ADD, exerciseToAdd); + EventHistory.getInstance().addCommandToUndoStack(this); + return new CommandResult(String.format(MESSAGE_SUCCESS, exerciseToAdd), ListResourceType.EXERCISE); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getCommandTypeIdentifier() { + return RESOURCE_TYPE; + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddExerciseCommand // instanceof handles nulls + && exerciseToAdd.equals(((AddExerciseCommand) other).exerciseToAdd)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/AddRegimeCommand.java b/src/main/java/seedu/exercise/logic/commands/AddRegimeCommand.java new file mode 100644 index 00000000000..6c208f90076 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/AddRegimeCommand.java @@ -0,0 +1,243 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.logic.commands.events.AddRegimeEvent.KEY_REGIME_TO_ADD; +import static seedu.exercise.logic.commands.events.EditRegimeEvent.KEY_EDITED_REGIME; +import static seedu.exercise.logic.commands.events.EditRegimeEvent.KEY_IS_REGIME_EDITED; +import static seedu.exercise.logic.commands.events.EditRegimeEvent.KEY_ORIGINAL_REGIME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.util.HashSet; +import java.util.List; + +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.ui.ListResourceType; + +//@@author jietung +/** + * Adds a regime to the regime book. + */ +public class AddRegimeCommand extends AddCommand implements PayloadCarrierCommand { + + public static final String MESSAGE_USAGE_REGIME = "Parameters: " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_NAME + "REGIME NAME " + + PREFIX_INDEX + "INDEX" + + "\tExample: " + COMMAND_WORD + " " + + PREFIX_CATEGORY + "regime " + + PREFIX_NAME + "power set " + + PREFIX_INDEX + "1 " + + PREFIX_INDEX + "2"; + + public static final String MESSAGE_SUCCESS_NEW_REGIME = "Added new regime to regime list."; + public static final String MESSAGE_SUCCESS_ADD_EXERCISE_TO_REGIME = "Added exercises to regime."; + public static final String MESSAGE_DUPLICATE_EXERCISE_IN_REGIME = "Duplicate exercise found in regime."; + public static final String MESSAGE_NO_EXERCISES_ADDED = "No index provided, nothing changes."; + public static final String MESSAGE_DUPLICATE_INDEX = "There is duplicate index."; + public static final String RESOURCE_TYPE = "regime"; + + private List toAddIndexes; + private Name name; + private EventPayload eventPayload; + + public AddRegimeCommand(List indexes, Name name) { + requireAllNonNull(indexes, name); + this.name = name; + this.toAddIndexes = indexes; + this.eventPayload = new EventPayload<>(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + List lastShownList = model.getSortedExerciseList(); + checkDuplicateIndexes(toAddIndexes); + checkValidIndexes(toAddIndexes, lastShownList); + + CommandResult commandResult; + if (!isRegimeInModel(model)) { + commandResult = addNewRegimeToModel(model); + } else { + commandResult = addExercisesToExistingRegime(model); + } + EventHistory.getInstance().addCommandToUndoStack(this); + return commandResult; + } + + /** + * Adds a new regime with exercises added based on the list of {@code Index} passed into the command. + * + * @param model {@code Model} which the command should operate on + * @return feedback message of the operation result for display + */ + private CommandResult addNewRegimeToModel(Model model) throws CommandException { + Regime regime = new Regime(name, new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR)); + addExercisesToRegime(regime, model); + model.addRegime(regime); + addToEventPayloadForAddRegime(regime); + return new CommandResult(MESSAGE_SUCCESS_NEW_REGIME, ListResourceType.REGIME); + } + + /** + * Adds exercises to the specified regime based on the list of {@code Index} passed into the command. + * + * @param model {@code Model} which the command should operate on + * @return feedback message of the operation result for display + */ + private CommandResult addExercisesToExistingRegime(Model model) throws CommandException { + checkIndexesNotEmpty(); + Regime originalRegime = getRegimeFromModel(model); + Regime editedRegime = originalRegime.deepCopy(); + addExercisesToRegime(editedRegime, model); + addToEventPayloadForEditRegime(originalRegime, editedRegime); + + model.setRegime(originalRegime, editedRegime); + return new CommandResult(MESSAGE_SUCCESS_ADD_EXERCISE_TO_REGIME, ListResourceType.REGIME); + } + + /** + * Returns the actual regime object with all the existing exercises. + * + * @param model {@code Model} which the command should operate on + * @return the existing regime from model + */ + private Regime getRegimeFromModel(Model model) { + List regimes = model.getSortedRegimeList(); + int regimeIndex = model.getRegimeIndex( + new Regime(name, new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR))); + return regimes.get(regimeIndex); + } + + /** + * Adds all exercises into the specified regime based on the given indexes. + * + * @param regime the regime to add exercises to + * @param model {@code Model} which the command should operate on + * @throws CommandException If duplicate exercises are found + */ + private void addExercisesToRegime(Regime regime, Model model) throws CommandException { + List lastShownList = model.getSortedExerciseList(); + for (Index index : toAddIndexes) { + Exercise exercise = lastShownList.get(index.getZeroBased()); + checkDuplicateExerciseInRegime(exercise, regime); + regime.addExercise(exercise); + } + } + + /** + * Checks whether the {@code Model} contains a regime with the same name. + * + * @param model {@code Model} which the command should operate on + * @return true if a regime of the same name exists, false otherwise + */ + private boolean isRegimeInModel(Model model) { + Regime regime = new Regime(name, new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR)); + return model.hasRegime(regime); + } + + /** + * Checks whether an exercise is already found in the regime. + * + * @param exercise exercise to be checked against the regime's list + * @param regime the regime to be checked with + * @throws CommandException If a duplicate exercise is found in the regime + */ + private void checkDuplicateExerciseInRegime(Exercise exercise, Regime regime) throws CommandException { + if (regime.getRegimeExercises().contains(exercise)) { + throw new CommandException(MESSAGE_DUPLICATE_EXERCISE_IN_REGIME); + } + } + + /** + * Checks whether the given indexes contain duplicates. + * + * @throws CommandException If a duplicate index is found + */ + private void checkDuplicateIndexes(List indexes) throws CommandException { + HashSet set = new HashSet<>(indexes); + if (set.size() < indexes.size()) { + throw new CommandException(MESSAGE_DUPLICATE_INDEX); + } + } + + /** + * Checks if the given indexes is empty. + * + * @throws CommandException If no indexes are provided at all + */ + private void checkIndexesNotEmpty() throws CommandException { + if (toAddIndexes.size() == 0) { + throw new CommandException(MESSAGE_NO_EXERCISES_ADDED); + } + } + + /** + * Checks whether the list of indexes provided is valid, + * + * @param indexes the list of {@code Index} passed into the command + * @param exerciseList the current exercise list of the regime + * @throws CommandException If any one of the indexes is greater than the size of the regime's exercise list + */ + private void checkValidIndexes(List indexes, List exerciseList) throws CommandException { + for (Index targetIndex : indexes) { + if (targetIndex.getZeroBased() >= exerciseList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + } + } + + /** + * Stores the regime to be added in this command. + * + * @param regimeToAdd the regime to be added + */ + private void addToEventPayloadForAddRegime(Regime regimeToAdd) { + eventPayload.put(KEY_IS_REGIME_EDITED, false); + eventPayload.put(KEY_REGIME_TO_ADD, regimeToAdd); + } + + /** + * Stores the various states of the exercise to the payload. + * + * @param originalRegime the regime before it is edited + * @param editedRegime the regime after it is edited + */ + private void addToEventPayloadForEditRegime(Regime originalRegime, Regime editedRegime) { + eventPayload.put(KEY_IS_REGIME_EDITED, true); + eventPayload.put(KEY_ORIGINAL_REGIME, originalRegime); + eventPayload.put(KEY_EDITED_REGIME, editedRegime); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getCommandTypeIdentifier() { + return RESOURCE_TYPE; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddRegimeCommand // instanceof handles nulls + && toAddIndexes.equals(((AddRegimeCommand) other).toAddIndexes)); + } + + +} diff --git a/src/main/java/seedu/exercise/logic/commands/ClearCommand.java b/src/main/java/seedu/exercise/logic/commands/ClearCommand.java new file mode 100644 index 00000000000..d1cc4df49ac --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ClearCommand.java @@ -0,0 +1,57 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.commands.events.ClearEvent.KEY_EXERCISE_BOOK_CLEARED; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.ui.ListResourceType; + +/** + * Clears the exercise book. + */ +public class ClearCommand extends Command implements UndoableCommand, PayloadCarrierCommand { + + public static final String COMMAND_WORD = "clear"; + public static final String MESSAGE_SUCCESS = "Exercise book has been cleared!"; + public static final String MESSAGE_EMPTY_EXERCISE_LIST = "Exercise book is already empty."; + + private EventPayload> eventPayload; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + ReadOnlyResourceBook exerciseBookCleared = + new ReadOnlyResourceBook<>(model.getExerciseBookData(), DEFAULT_EXERCISE_COMPARATOR); + if (checkIsExerciseBookEmpty(exerciseBookCleared)) { + throw new CommandException(MESSAGE_EMPTY_EXERCISE_LIST); + } + eventPayload = new EventPayload<>(); + eventPayload.put(KEY_EXERCISE_BOOK_CLEARED, exerciseBookCleared); + EventHistory.getInstance().addCommandToUndoStack(this); + + model.setExerciseBook(new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR)); + model.updateStatistic(); + return new CommandResult(MESSAGE_SUCCESS, ListResourceType.EXERCISE); + } + + @Override + public String getUndoableCommandWord() { + return COMMAND_WORD; + } + + private boolean checkIsExerciseBookEmpty(ReadOnlyResourceBook exerciseBook) { + return exerciseBook.getSortedResourceList().isEmpty(); + } + + @Override + public EventPayload> getPayload() { + return eventPayload; + } + +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/exercise/logic/commands/Command.java similarity index 78% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/seedu/exercise/logic/commands/Command.java index 64f18992160..71d1b60f3c5 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/exercise/logic/commands/Command.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package seedu.exercise.logic.commands; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. diff --git a/src/main/java/seedu/exercise/logic/commands/CommandResult.java b/src/main/java/seedu/exercise/logic/commands/CommandResult.java new file mode 100644 index 00000000000..be987828d62 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/CommandResult.java @@ -0,0 +1,156 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; +import java.util.Optional; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.ui.ListResourceType; + +/** + * Represents the result of a command execution. + */ +public class CommandResult { + + private final String feedbackToUser; + + /** + * Help information should be shown to the user. + */ + private boolean showHelp; + + /** + * The application should exit. + */ + private boolean exit; + + /** + * Show the resolve window to user due to scheduling conflict + */ + private boolean showResolve; + + /** + * Show the custom properties window + */ + private boolean showCustomProperties; + + /** + * The type of resource to be shown in the GUI. + */ + private ListResourceType showListResourceType; + + /** + * The index of the resource to be selected + */ + private Optional index; + + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + public CommandResult(String feedbackToUser, boolean showHelp, + boolean isExit, boolean showResolve, boolean showCustomProperties, + ListResourceType listResourceType, Optional index) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = showHelp; + this.exit = isExit; + this.showResolve = showResolve; + this.showCustomProperties = showCustomProperties; + this.showListResourceType = listResourceType; + this.index = index; + } + + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean showResolve, + boolean showCustomProperties) { + this(feedbackToUser, showHelp, exit, showResolve, showCustomProperties, ListResourceType.NULL, + Optional.empty()); + } + + + public CommandResult(String feedbackToUser, ListResourceType listResourceType) { + this(feedbackToUser); + this.showListResourceType = listResourceType; + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser} and {@code index} and other + * fields set to their default value. + */ + public CommandResult(String feedbackToUser, ListResourceType listResourceType, Optional index) { + this(feedbackToUser); + this.showListResourceType = listResourceType; + this.index = index; + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, + * and other fields set to their default value. + */ + public CommandResult(String feedbackToUser) { + this(feedbackToUser, false, false, false, false, + ListResourceType.NULL, Optional.empty()); + } + + public String getFeedbackToUser() { + return feedbackToUser; + } + + public boolean isShowHelp() { + return showHelp; + } + + public boolean isExit() { + return exit; + } + + public boolean isShowResolve() { + return showResolve; + } + + public boolean isShowCustomProperties() { + return showCustomProperties; + } + + public boolean isSelectResource() { + return index.isPresent(); + } + + public ListResourceType getShowListResourceType() { + return showListResourceType; + } + + public Optional getSelectedIndex() { + return index; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof CommandResult)) { + return false; + } + + CommandResult otherCommandResult = (CommandResult) other; + return feedbackToUser.equals(otherCommandResult.feedbackToUser) + && showHelp == otherCommandResult.showHelp + && exit == otherCommandResult.exit + && showResolve == otherCommandResult.showResolve + && showCustomProperties == otherCommandResult.showCustomProperties + && showListResourceType.equals(otherCommandResult.showListResourceType) + && index.equals(otherCommandResult.index); + } + + @Override + public int hashCode() { + return Objects.hash(feedbackToUser, showHelp, exit, showResolve, + showCustomProperties, showListResourceType, index); + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/CustomAddCommand.java b/src/main/java/seedu/exercise/logic/commands/CustomAddCommand.java new file mode 100644 index 00000000000..f1d9f57ac01 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/CustomAddCommand.java @@ -0,0 +1,77 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CUSTOM_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_FULL_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_PARAMETER_TYPE; + +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.logic.parser.Prefix; +import seedu.exercise.model.Model; +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.property.custom.CustomProperty; + +//@@author weihaw08 +/** + * Adds a custom property for the exercises. + */ +public class CustomAddCommand extends CustomCommand { + + public static final String MESSAGE_USAGE_CUSTOM_ADD = "Parameters: " + + PREFIX_CUSTOM_NAME + "PREFIX NAME " + + PREFIX_FULL_NAME + "FULL NAME " + + PREFIX_PARAMETER_TYPE + "PARAMETER TYPE\t" + + "Example: " + COMMAND_WORD + " " + + PREFIX_CUSTOM_NAME + "a " + + PREFIX_FULL_NAME + "Ratings " + + PREFIX_PARAMETER_TYPE + "Number"; + + public static final String MESSAGE_SUCCESS = "New custom property added: %1$s."; + public static final String MESSAGE_DUPLICATE_FULL_NAME = "This full name has been used for an " + + "existing property."; + public static final String MESSAGE_DUPLICATE_PREFIX_NAME = "This prefix name has been used for an " + + "existing parameter in add/edit command."; + + private static final Logger logger = LogsCenter.getLogger(CustomAddCommand.class); + private final CustomProperty toAdd; + + /** + * Creates a CustomAddCommand to add the specified {@code CustomProperty}. + */ + public CustomAddCommand(CustomProperty customProperty) { + requireNonNull(customProperty); + toAdd = customProperty; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + String fullName = toAdd.getFullName(); + PropertyBook propertyBook = PropertyBook.getInstance(); + if (propertyBook.isFullNameUsed(fullName)) { + logger.warning("Full name has been used by a property/parameter"); + throw new CommandException(MESSAGE_DUPLICATE_FULL_NAME); + } + + Prefix prefix = toAdd.getPrefix(); + if (propertyBook.isPrefixUsed(prefix)) { + logger.warning("Prefix has been used by a property/parameter"); + throw new CommandException(MESSAGE_DUPLICATE_PREFIX_NAME); + } + + logger.info("Custom property added to model: " + fullName); + propertyBook.addCustomProperty(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CustomAddCommand // instanceof handles nulls + && toAdd.equals(((CustomAddCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/CustomCommand.java b/src/main/java/seedu/exercise/logic/commands/CustomCommand.java new file mode 100644 index 00000000000..e63fb187d31 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/CustomCommand.java @@ -0,0 +1,17 @@ +package seedu.exercise.logic.commands; + +import static seedu.exercise.logic.commands.CustomAddCommand.MESSAGE_USAGE_CUSTOM_ADD; +import static seedu.exercise.logic.commands.CustomRemoveCommand.MESSAGE_USAGE_CUSTOM_REMOVE; + +//@@author weihaw08 +/** + * Represents a CustomCommand with hidden internal logic and the ability to be executed. + */ +public abstract class CustomCommand extends Command { + public static final String COMMAND_WORD = "custom"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Add or remove custom property for exercises.\n" + + "ADD: " + MESSAGE_USAGE_CUSTOM_ADD + + "\nREMOVE: " + MESSAGE_USAGE_CUSTOM_REMOVE; + +} diff --git a/src/main/java/seedu/exercise/logic/commands/CustomRemoveCommand.java b/src/main/java/seedu/exercise/logic/commands/CustomRemoveCommand.java new file mode 100644 index 00000000000..757c983b75e --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/CustomRemoveCommand.java @@ -0,0 +1,140 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_REMOVE_CUSTOM; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.core.index.IndexUtil; +import seedu.exercise.logic.commands.builder.EditExerciseBuilder; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.resource.Exercise; + +//@@author weihaw08 +/** + * Removes the custom property with the given full name. + */ +public class CustomRemoveCommand extends CustomCommand { + + public static final String MESSAGE_USAGE_CUSTOM_REMOVE = "Parameters: " + + PREFIX_REMOVE_CUSTOM + "FULL NAME" + + " [" + PREFIX_INDEX + "INDEX" + "]\t" + + "Example: " + COMMAND_WORD + " " + + PREFIX_REMOVE_CUSTOM + "Rating "; + + public static final String MESSAGE_SUCCESS_ALL_REMOVED = "Custom property removed: %1$s"; + public static final String MESSAGE_SUCCESS_SINGLE_REMOVED = "%1$s removed for exercise %2$s"; + public static final String MESSAGE_FULL_NAME_NOT_FOUND = "This full name is not used by an " + + "existing custom property"; + + private static final Logger logger = LogsCenter.getLogger(CustomRemoveCommand.class); + private final String toRemove; + private final Optional index; + + public CustomRemoveCommand(String toRemove, Optional index) { + requireAllNonNull(toRemove, index); + this.toRemove = toRemove; + this.index = index; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + PropertyBook propertyBook = PropertyBook.getInstance(); + if (!propertyBook.isFullNameUsedByCustomProperty(toRemove)) { + throw new CommandException(MESSAGE_FULL_NAME_NOT_FOUND); + } + + if (index.isEmpty()) { + logger.info("Removing " + toRemove + " from the app"); + propertyBook.removeCustomProperty(toRemove); + updateCustomPropertiesOfAllExercises(model); + return new CommandResult(String.format(MESSAGE_SUCCESS_ALL_REMOVED, toRemove)); + } else { + logger.info("Removing " + toRemove + " from a single exercise"); + Index indexToRemove = index.get(); + updateCustomPropertiesOfSingleExercise(model, indexToRemove); + return new CommandResult(String.format(MESSAGE_SUCCESS_SINGLE_REMOVED, toRemove, + indexToRemove.getOneBased())); + } + + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof CustomRemoveCommand) + && (toRemove.equals(((CustomRemoveCommand) other).toRemove)) + && (index.equals(((CustomRemoveCommand) other).index)); + } + + + /** + * Updates the old custom properties map of an exercise with the updated custom properties. + * + * @param oldPropertiesMap the old custom properties map of an exercise + * @return a new map consisting of the updated custom properties + */ + private Map updateCustomPropertiesMap(Map oldPropertiesMap) { + Map updatedMap = new TreeMap<>(); + Set keySet = oldPropertiesMap.keySet(); + for (String property : keySet) { + if (!property.equals(toRemove)) { + updatedMap.put(property, oldPropertiesMap.get(property)); + } + } + return updatedMap; + } + + /** + * Updates the custom properties of the given {@code exercise}. + * + * @return a new {@code Exercise} object containing the updated custom properties. The other properties are + * kept the same. + */ + private Exercise updateExerciseCustomProperty(Exercise exercise) { + EditExerciseBuilder editExerciseBuilder = new EditExerciseBuilder(exercise); + Map oldCustomProperties = exercise.getCustomPropertiesMap(); + Map newCustomProperties = updateCustomPropertiesMap(oldCustomProperties); + editExerciseBuilder.setCustomProperties(newCustomProperties); + return editExerciseBuilder.buildEditedExercise(); + } + + /** + * Updates the custom properties of the exercise at the given index in the model. + */ + private void updateCustomPropertiesOfSingleExercise(Model model, Index index) throws CommandException { + ObservableList modelList = model.getSortedExerciseList(); + if (IndexUtil.isIndexOutOfBounds(index, modelList)) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + Exercise oldExercise = modelList.get(index.getZeroBased()); + Exercise updatedExercise = updateExerciseCustomProperty(oldExercise); + model.setExercise(oldExercise, updatedExercise); + } + + /** + * Updates the custom properties of all the exercises in the given {@code model}. + */ + private void updateCustomPropertiesOfAllExercises(Model model) { + List exerciseList = model.getSortedExerciseList(); + for (Exercise oldExercise : exerciseList) { + Exercise updatedExercise = updateExerciseCustomProperty(oldExercise); + model.setExercise(oldExercise, updatedExercise); + } + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/DeleteCommand.java b/src/main/java/seedu/exercise/logic/commands/DeleteCommand.java new file mode 100644 index 00000000000..240b0603a00 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/DeleteCommand.java @@ -0,0 +1,22 @@ +package seedu.exercise.logic.commands; + +import static seedu.exercise.logic.commands.DeleteExerciseCommand.MESSAGE_USAGE_EXERCISE; +import static seedu.exercise.logic.commands.DeleteRegimeCommand.MESSAGE_USAGE_REGIME; + +/** + * Represents a DeleteCommand with hidden internal logic and the ability to be executed. + */ +public abstract class DeleteCommand extends Command implements UndoableCommand, TypeDependentCommand { + public static final String COMMAND_WORD = "delete"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the exercise identified by the index number used in the displayed exercise list " + + "OR Deletes the regimes/exercise in regime identified by name/index in the displayed regime list\n" + + "EXERCISE: " + MESSAGE_USAGE_EXERCISE + "\n" + + "REGIME: " + MESSAGE_USAGE_REGIME; + + @Override + public String getUndoableCommandWord() { + return COMMAND_WORD; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/DeleteExerciseCommand.java b/src/main/java/seedu/exercise/logic/commands/DeleteExerciseCommand.java new file mode 100644 index 00000000000..5c72e8b4b28 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/DeleteExerciseCommand.java @@ -0,0 +1,77 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.commands.events.DeleteExerciseEvent.KEY_EXERCISE_TO_DELETE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; + +import java.util.List; + +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.core.index.IndexUtil; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.ui.ListResourceType; + +/** + * Deletes an exercise identified using it's displayed index from the exercise book. + */ +public class DeleteExerciseCommand extends DeleteCommand implements PayloadCarrierCommand { + + public static final String MESSAGE_USAGE_EXERCISE = "Parameters: " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_INDEX + "INDEX (must be a positive integer) " + + "\tExample: " + + COMMAND_WORD + " " + + PREFIX_CATEGORY + "exercise " + + PREFIX_INDEX + "1"; + public static final String MESSAGE_DELETE_EXERCISE_SUCCESS = "Deleted Exercise: %1$s"; + public static final String RESOURCE_TYPE = "exercise"; + + private final Index targetIndex; + private EventPayload eventPayload; + + public DeleteExerciseCommand(Index targetIndex) { + this.targetIndex = targetIndex; + this.eventPayload = new EventPayload<>(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedExerciseList(); + + if (IndexUtil.isIndexOutOfBounds(targetIndex, lastShownList)) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + + Exercise exerciseToDelete = lastShownList.get(targetIndex.getZeroBased()); + eventPayload.put(KEY_EXERCISE_TO_DELETE, exerciseToDelete); + model.deleteExercise(exerciseToDelete); + model.updateStatistic(); + EventHistory.getInstance().addCommandToUndoStack(this); + return new CommandResult(String.format(MESSAGE_DELETE_EXERCISE_SUCCESS, exerciseToDelete), + ListResourceType.EXERCISE); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getCommandTypeIdentifier() { + return RESOURCE_TYPE; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteExerciseCommand // instanceof handles nulls + && targetIndex.equals(((DeleteExerciseCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/DeleteRegimeCommand.java b/src/main/java/seedu/exercise/logic/commands/DeleteRegimeCommand.java new file mode 100644 index 00000000000..e9b82975cf5 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/DeleteRegimeCommand.java @@ -0,0 +1,206 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.commands.events.DeleteRegimeEvent.KEY_REGIME_TO_DELETE; +import static seedu.exercise.logic.commands.events.EditRegimeEvent.KEY_EDITED_REGIME; +import static seedu.exercise.logic.commands.events.EditRegimeEvent.KEY_IS_REGIME_EDITED; +import static seedu.exercise.logic.commands.events.EditRegimeEvent.KEY_ORIGINAL_REGIME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.util.HashSet; +import java.util.List; + +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.ui.ListResourceType; + +//@@author jietung +/** + * Deletes a regime identified using it's name or deletes exercises in regime. + */ +public class DeleteRegimeCommand extends DeleteCommand implements PayloadCarrierCommand { + + public static final String MESSAGE_USAGE_REGIME = "Parameters: " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_NAME + "REGIME NAME " + + PREFIX_INDEX + "INDEX (must be a positive integer) " + + "\tExample: " + + COMMAND_WORD + " " + + PREFIX_CATEGORY + "regime " + + PREFIX_NAME + "level 1 " + + PREFIX_INDEX + "1"; + public static final String MESSAGE_DELETE_REGIME_SUCCESS = "Deleted Regime: %1$s\n%2$s"; + public static final String MESSAGE_REGIME_DOES_NOT_EXIST = "No such regime in regime book."; + public static final String MESSAGE_DELETE_EXERCISE_IN_REGIME_SUCCESS = "Deleted exercises in regime."; + public static final String MESSAGE_DUPLICATE_INDEX = "There is duplicate index."; + public static final String RESOURCE_TYPE = "regime"; + + private final List indexes; + private final Name name; + private final EventPayload eventPayload; + + public DeleteRegimeCommand(Name name, List indexes) { + this.name = name; + this.indexes = indexes; + this.eventPayload = new EventPayload<>(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedRegimeList(); + Regime regime = new Regime(name, new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR)); + checkValidRegime(regime, model); + + int indexOfRegime = model.getRegimeIndex(regime); + Regime regimeToDelete = lastShownList.get(indexOfRegime); + + CommandResult commandResult; + if (indexes == null) { + commandResult = deleteRegimeFromModel(regimeToDelete, model); + } else { + commandResult = deleteExercisesFromRegime(regimeToDelete, model); + } + EventHistory.getInstance().addCommandToUndoStack(this); + return commandResult; + } + + /** + * Deletes the specified regime completely from the model's regime list. + * + * @param regimeToDelete the regime to delete + * @param model {@code Model} which the command should operate on. + * @return feedback message of the operation result for display + */ + private CommandResult deleteRegimeFromModel(Regime regimeToDelete, Model model) { + model.deleteRegime(regimeToDelete); + addToEventPayloadForDeleteRegime(regimeToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_REGIME_SUCCESS, name, regimeToDelete), + ListResourceType.REGIME); + } + + /** + * Deletes exercises from the specified regime based on the list of {@code Index} passed into the command. + * + * @param originalRegime the specified regime to delete exercises from + * @param model {@code Model} which the command should operate on + * @return feedback message of the operation result for display + */ + private CommandResult deleteExercisesFromRegime(Regime originalRegime, Model model) throws CommandException { + Regime editedRegime = originalRegime.deepCopy(); + List currentExerciseList = originalRegime.getRegimeExercises().asUnmodifiableObservableList(); + checkValidIndexes(indexes, currentExerciseList); + checkDuplicateIndexes(indexes); + + for (Index targetIndex : indexes) { + Exercise exerciseToDelete = currentExerciseList.get(targetIndex.getZeroBased()); + editedRegime.deleteExercise(exerciseToDelete); + } + + addToEventPayloadForEditRegime(originalRegime, editedRegime); + model.setRegime(originalRegime, editedRegime); + return new CommandResult(String.format(MESSAGE_DELETE_EXERCISE_IN_REGIME_SUCCESS, editedRegime), + ListResourceType.REGIME); + } + + /** + * Checks whether the specified regime exists in the model's regime list. + * + * @param regime the regime to check validity for + * @param model {@code Model} which the command should operate on + * @throws CommandException If the specified regime does not exist in the model's regime list + */ + private void checkValidRegime(Regime regime, Model model) throws CommandException { + if (!model.hasRegime(regime)) { + throw new CommandException(MESSAGE_REGIME_DOES_NOT_EXIST); + } + } + + /** + * Checks whether the list of indexes provided is valid, + * + * @param indexes the list of {@code Index} passed into the command + * @param exerciseList the current exercise list of the regime + * @throws CommandException If any one of the indexes is greater than the size of the regime's exercise list + */ + private void checkValidIndexes(List indexes, List exerciseList) throws CommandException { + for (Index targetIndex : indexes) { + if (targetIndex.getZeroBased() >= exerciseList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + } + } + + /** + * Checks whether the given indexes contain duplicates. + * + * @throws CommandException If a duplicate index is found + */ + private void checkDuplicateIndexes(List indexes) throws CommandException { + HashSet set = new HashSet<>(indexes); + if (set.size() < indexes.size()) { + throw new CommandException(MESSAGE_DUPLICATE_INDEX); + } + } + + /** + * Stores the regime to be deleted in this command. + * + * @param regimeToDelete the regime to be deleted + */ + private void addToEventPayloadForDeleteRegime(Regime regimeToDelete) { + eventPayload.put(KEY_IS_REGIME_EDITED, false); + eventPayload.put(KEY_REGIME_TO_DELETE, regimeToDelete); + } + + /** + * Stores the various states of the exercise to the payload. + * + * @param originalRegime the regime before it is edited + * @param editedRegime the regime after it is edited + */ + private void addToEventPayloadForEditRegime(Regime originalRegime, Regime editedRegime) { + eventPayload.put(KEY_IS_REGIME_EDITED, true); + eventPayload.put(KEY_ORIGINAL_REGIME, originalRegime); + eventPayload.put(KEY_EDITED_REGIME, editedRegime); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getCommandTypeIdentifier() { + return RESOURCE_TYPE; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof DeleteRegimeCommand) { + if (indexes == null) { + return name.equals(((DeleteRegimeCommand) other).name); + } else { + return name.equals(((DeleteRegimeCommand) other).name) + && indexes.equals(((DeleteRegimeCommand) other).indexes); + } + } + + return false; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/EditCommand.java b/src/main/java/seedu/exercise/logic/commands/EditCommand.java new file mode 100644 index 00000000000..994ee2cf3ff --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/EditCommand.java @@ -0,0 +1,135 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.commands.builder.EditExerciseBuilder.createEditedExercise; +import static seedu.exercise.logic.commands.events.EditExerciseEvent.KEY_EDITED_EXERCISE; +import static seedu.exercise.logic.commands.events.EditExerciseEvent.KEY_ORIGINAL_EXERCISE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CALORIES; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_QUANTITY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_UNIT; + +import java.util.List; + +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.core.index.IndexUtil; +import seedu.exercise.logic.commands.builder.EditExerciseBuilder; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.ui.ListResourceType; + +/** + * Edits the details of an existing exercise in the exercise book. + */ +public class EditCommand extends Command implements UndoableCommand, PayloadCarrierCommand { + + public static final String COMMAND_WORD = "edit"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the exercise identified " + + "by the index number used in the displayed exercise list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: " + + PREFIX_INDEX + "INDEX (must be a positive integer) " + + "[" + PREFIX_NAME + "NAME] " + + "[" + PREFIX_DATE + "DATE] " + + "[" + PREFIX_CALORIES + "CALORIES] " + + "[" + PREFIX_QUANTITY + "QUANTITY] " + + "[" + PREFIX_UNIT + "UNIT] " + + "[" + PREFIX_MUSCLE + "MUSCLE]..." + + "[CUSTOM_PROPERTY_PREFIX_NAME/VALUE]...\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_INDEX + "1 " + + PREFIX_DATE + "03/10/2019 " + + PREFIX_CALORIES + "800"; + + public static final String MESSAGE_EDIT_EXERCISE_SUCCESS = "Edited Exercise: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_EXERCISE = "This exercise already exists in the exercise book."; + + private final Index index; + private final EditExerciseBuilder editExerciseBuilder; + private EventPayload eventPayload; + + /** + * @param index of the exercise in the filtered exercise list to edit + * @param editExerciseBuilder details to edit the person with + */ + public EditCommand(Index index, EditExerciseBuilder editExerciseBuilder) { + requireNonNull(index); + requireNonNull(editExerciseBuilder); + + this.index = index; + this.eventPayload = new EventPayload<>(); + this.editExerciseBuilder = new EditExerciseBuilder(editExerciseBuilder); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getSortedExerciseList(); + + if (IndexUtil.isIndexOutOfBounds(index, lastShownList)) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + + Exercise exerciseToEdit = lastShownList.get(index.getZeroBased()); + Exercise editedExercise = createEditedExercise(exerciseToEdit, editExerciseBuilder); + + if (!exerciseToEdit.isSameResource(editedExercise) && model.hasExercise(editedExercise)) { + throw new CommandException(MESSAGE_DUPLICATE_EXERCISE); + } + + addToEventPayload(exerciseToEdit, editedExercise); + model.setExercise(exerciseToEdit, editedExercise); + EventHistory.getInstance().addCommandToUndoStack(this); + model.updateStatistic(); + return new CommandResult(String.format(MESSAGE_EDIT_EXERCISE_SUCCESS, editedExercise), + ListResourceType.EXERCISE); + } + + @Override + public String getUndoableCommandWord() { + return COMMAND_WORD; + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + /** + * Stores the various states of the exercise to the payload. + * + * @param exerciseToEdit the exercise before it is edited + * @param editedExercise the exercise after it is edited + */ + private void addToEventPayload(Exercise exerciseToEdit, Exercise editedExercise) { + eventPayload.put(KEY_ORIGINAL_EXERCISE, exerciseToEdit); + eventPayload.put(KEY_EDITED_EXERCISE, editedExercise); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditCommand)) { + return false; + } + + // state check + EditCommand e = (EditCommand) other; + return index.equals(e.index) + && editExerciseBuilder.equals(e.editExerciseBuilder); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/exercise/logic/commands/ExitCommand.java similarity index 70% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/seedu/exercise/logic/commands/ExitCommand.java index 3dd85a8ba90..d728e8c1ef8 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/exercise/logic/commands/ExitCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package seedu.exercise.logic.commands; -import seedu.address.model.Model; +import seedu.exercise.model.Model; /** * Terminates the program. @@ -9,11 +9,10 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; + public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Exercise Book as requested ..."; @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false, false); } - } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/exercise/logic/commands/HelpCommand.java similarity index 78% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/seedu/exercise/logic/commands/HelpCommand.java index bf824f91bd0..fb7e04e4648 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/exercise/logic/commands/HelpCommand.java @@ -1,6 +1,6 @@ -package seedu.address.logic.commands; +package seedu.exercise.logic.commands; -import seedu.address.model.Model; +import seedu.exercise.model.Model; /** * Format full help instructions for every command for display. @@ -10,12 +10,13 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" - + "Example: " + COMMAND_WORD; + + "Example: " + COMMAND_WORD; public static final String SHOWING_HELP_MESSAGE = "Opened help window."; @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false, false); } + } diff --git a/src/main/java/seedu/exercise/logic/commands/ListCommand.java b/src/main/java/seedu/exercise/logic/commands/ListCommand.java new file mode 100644 index 00000000000..45f9d6681aa --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ListCommand.java @@ -0,0 +1,38 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; + +import seedu.exercise.model.Model; +import seedu.exercise.ui.ListResourceType; + +/** + * Lists all exercises in the exercise book to the user. + */ +public class ListCommand extends Command { + + public static final String COMMAND_WORD = "list"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": List items.\n" + + "Parameters: " + + PREFIX_CATEGORY + "LIST_TYPE" + "\t" + + "Example: " + + COMMAND_WORD + " " + + PREFIX_CATEGORY + "schedule"; + public static final String MESSAGE_SUCCESS = "Listed all %1$s items"; + + private ListResourceType listResourceType; + + public ListCommand(ListResourceType listResourceType) { + requireNonNull(listResourceType); + this.listResourceType = listResourceType; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + return new CommandResult(String.format(MESSAGE_SUCCESS, listResourceType.toString().toLowerCase()), + listResourceType); + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/PayloadCarrierCommand.java b/src/main/java/seedu/exercise/logic/commands/PayloadCarrierCommand.java new file mode 100644 index 00000000000..4a80af6bc5f --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/PayloadCarrierCommand.java @@ -0,0 +1,17 @@ +package seedu.exercise.logic.commands; + +import seedu.exercise.logic.commands.events.EventPayload; +//@@author garylyp +/** + * Interface for commands which need to store an EventPayload object to store key information + * that can be accessed by others. + */ +public interface PayloadCarrierCommand { + + /** + * Returns the payload that stores the regime that has been deleted or edited in this command. + * + * @return {@code EventPayload} containing relevant objects can be accessed using specific keys + */ + EventPayload getPayload(); +} diff --git a/src/main/java/seedu/exercise/logic/commands/RedoCommand.java b/src/main/java/seedu/exercise/logic/commands/RedoCommand.java new file mode 100644 index 00000000000..e0dbf83b6e9 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/RedoCommand.java @@ -0,0 +1,71 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.exercise.logic.commands.events.AddExerciseEvent; +import seedu.exercise.logic.commands.events.AddRegimeEvent; +import seedu.exercise.logic.commands.events.ClearEvent; +import seedu.exercise.logic.commands.events.DeleteExerciseEvent; +import seedu.exercise.logic.commands.events.DeleteRegimeEvent; +import seedu.exercise.logic.commands.events.EditExerciseEvent; +import seedu.exercise.logic.commands.events.EditRegimeEvent; +import seedu.exercise.logic.commands.events.Event; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.ResolveEvent; +import seedu.exercise.logic.commands.events.ScheduleCompleteEvent; +import seedu.exercise.logic.commands.events.ScheduleRegimeEvent; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.ui.ListResourceType; +//@@author garylyp +/** + * Undoes the last executed command. + */ +public class RedoCommand extends Command { + + public static final String COMMAND_WORD = "redo"; + + public static final String MESSAGE_SUCCESS = "Command redone: \n%1$s."; + public static final String MESSAGE_EMPTY_REDO_STACK = "There is no command to redo."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + EventHistory eventHistory = EventHistory.getInstance(); + + if (eventHistory.isRedoStackEmpty()) { + throw new CommandException(MESSAGE_EMPTY_REDO_STACK); + } + + Event eventToRedo = eventHistory.redo(model); + model.updateStatistic(); + ListResourceType type = getListResourceType(eventToRedo); + return new CommandResult( + String.format(MESSAGE_SUCCESS, eventToRedo), type); + } + + /** + * Returns the list resource type for display based on the type of event that has been redone. + * + * @param event the event that was redone + * @return a list resource type enum object + */ + private ListResourceType getListResourceType(Event event) { + ListResourceType type = ListResourceType.NULL; + if (event instanceof AddExerciseEvent + || event instanceof DeleteExerciseEvent + || event instanceof EditExerciseEvent + || event instanceof ClearEvent + || event instanceof ScheduleCompleteEvent) { + type = ListResourceType.EXERCISE; + } else if (event instanceof AddRegimeEvent + || event instanceof DeleteRegimeEvent + || event instanceof EditRegimeEvent) { + type = ListResourceType.REGIME; + } else if (event instanceof ScheduleRegimeEvent + || event instanceof ResolveEvent) { + type = ListResourceType.SCHEDULE; + } + return type; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/ResolveCommand.java b/src/main/java/seedu/exercise/logic/commands/ResolveCommand.java new file mode 100644 index 00000000000..0926dd56fe2 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ResolveCommand.java @@ -0,0 +1,196 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_CONTEXT; +import static seedu.exercise.commons.core.Messages.MESSAGE_TAB; +import static seedu.exercise.commons.util.CollectionUtil.areListsEmpty; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.logic.commands.events.ResolveEvent.KEY_CONFLICT; +import static seedu.exercise.logic.commands.events.ResolveEvent.KEY_RESOLVED_SCHEDULE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CONFLICT_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.util.List; + +import seedu.exercise.MainApp; +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.State; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.core.index.IndexUtil; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.ui.ListResourceType; + +//@@author t-cheepeng +/** + * Represents a Resolve Command that resolves scheduling conflicts. + */ +public class ResolveCommand extends Command implements UndoableCommand, PayloadCarrierCommand { + + public static final String COMMAND_WORD = "resolve"; + + public static final String MESSAGE_USAGE = MESSAGE_TAB + COMMAND_WORD + + " command usage:\n" + + "Usage 1: Take one regime completely. " + MESSAGE_TAB + + "Parameters: " + + PREFIX_NAME + "SCHEDULED_OR_CONFLICTING" + + MESSAGE_TAB + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "scheduled\nUsage 2: Take some exercise from both schedule." + MESSAGE_TAB + + "Parameters: " + + PREFIX_NAME + "NEW_REGIME_NAME " + + "[" + PREFIX_INDEX + "INDEX_OF_SCHEDULED" + "]" + + "[" + PREFIX_CONFLICT_INDEX + "INDEX_OF_CONFLICTING" + "]" + + MESSAGE_TAB + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "cardio new " + + PREFIX_INDEX + "1 " + + PREFIX_INDEX + "3 " + + PREFIX_CONFLICT_INDEX + "2 "; + + public static final String TAKE_FROM_SCHEDULED = "scheduled"; + public static final String TAKE_FROM_CONFLICTING = "conflicting"; + public static final String MESSAGE_SUCCESS = "Successfully resolved conflict between regime %1$s and regime %2$s"; + public static final String MESSAGE_DUPLICATE_NAME = "Regime name %1$s already exists. Try another name"; + public static final String MESSAGE_INVALID_NAME = "Name provided is neither " + TAKE_FROM_SCHEDULED + + " nor is it " + TAKE_FROM_CONFLICTING + ". Please input " + PREFIX_NAME + TAKE_FROM_SCHEDULED + + " or " + PREFIX_NAME + TAKE_FROM_CONFLICTING + "."; + public static final String MESSAGE_DUPLICATE_EXERCISE_SELECTED = + "You have selected some exercises that are the same from both schedules.\n" + + "You only have to select one of them."; + + private Name scheduledOrConflicting; + private Conflict conflict; + private List indexToTakeFromSchedule; + private List indexToTakeFromConflict; + private EventPayload eventPayload; + + public ResolveCommand(Name scheduledOrConflicting, List indexToTakeFromSchedule, + List indexToTakeFromConflict) { + requireAllNonNull(scheduledOrConflicting, indexToTakeFromConflict, indexToTakeFromSchedule); + this.scheduledOrConflicting = scheduledOrConflicting; + this.indexToTakeFromSchedule = indexToTakeFromSchedule; + this.indexToTakeFromConflict = indexToTakeFromConflict; + this.eventPayload = new EventPayload<>(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + checkIfProgramStateIsValid(); + + conflict = getConflictFromModel(model); + + checkValidIndexes(); + if (!areListsEmpty(indexToTakeFromSchedule, indexToTakeFromConflict)) { + checkNonDuplicateRegimeNameFromModel(model); + checkSelectedIndexesDoNotHaveDuplicatesFromModel(model); + } else { + checkNameIsScheduledOrConflicting(); + } + + Schedule resolvedSchedule = resolveConflict(model); + eventPayload.put(KEY_RESOLVED_SCHEDULE, resolvedSchedule); + eventPayload.put(KEY_CONFLICT, conflict); + EventHistory.getInstance().addCommandToUndoStack(this); + return new CommandResult(String.format(MESSAGE_SUCCESS, + conflict.getScheduledName(), + conflict.getConflictedName()), ListResourceType.SCHEDULE); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getUndoableCommandWord() { + return COMMAND_WORD; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + //instanceof handles nulls + if (!(other instanceof ResolveCommand)) { + return false; + } + + ResolveCommand otherCommand = (ResolveCommand) other; + return scheduledOrConflicting.equals(otherCommand.scheduledOrConflicting) + && indexToTakeFromConflict.equals(otherCommand.indexToTakeFromConflict) + && indexToTakeFromSchedule.equals(otherCommand.indexToTakeFromSchedule); + } + + /** + * Calls the model to resolve the conflict and return a resolved Schedule object. + */ + private Schedule resolveConflict(Model model) { + requireNonNull(model); + Schedule resolvedSchedule = model.resolveConflict(scheduledOrConflicting, + indexToTakeFromSchedule, indexToTakeFromConflict); + MainApp.setState(State.NORMAL); + return resolvedSchedule; + } + + private Conflict getConflictFromModel(Model model) { + requireNonNull(model); + return model.getConflict(); + } + + /** + * Checks if indexes passed into the {@code Command} are valid and not out of bounds + */ + private void checkValidIndexes() throws CommandException { + if (IndexUtil.areIndexesOutOfBounds(indexToTakeFromSchedule, conflict.getScheduledExerciseList()) + || IndexUtil.areIndexesOutOfBounds(indexToTakeFromConflict, conflict.getConflictedExerciseList())) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + } + + private void checkIfProgramStateIsValid() throws CommandException { + if (MainApp.getState() != State.IN_CONFLICT) { + throw new CommandException(String.format(MESSAGE_INVALID_CONTEXT, getClass().getSimpleName())); + } + } + + private void checkSelectedIndexesDoNotHaveDuplicatesFromModel(Model model) throws CommandException { + if (model.isSelectedIndexesFromRegimeDuplicate(indexToTakeFromSchedule, indexToTakeFromConflict)) { + throw new CommandException(MESSAGE_DUPLICATE_EXERCISE_SELECTED); + } + } + + /** + * Checks if there are duplicate {@code regime names} in the {@code model}. + * Method is to be called only if there are indexes provided to the Regime Command. + */ + private void checkNonDuplicateRegimeNameFromModel(Model model) throws CommandException { + requireNonNull(model); + if (model.hasRegime( + new Regime(scheduledOrConflicting, new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR)))) { + throw new CommandException(String.format(MESSAGE_DUPLICATE_NAME, scheduledOrConflicting.toString())); + } + } + + /** + * Checks if the name passed in is either {@code conflicting} or {@code scheduled}. + */ + private void checkNameIsScheduledOrConflicting() throws CommandException { + String name = scheduledOrConflicting.toString(); + if (!name.equals(TAKE_FROM_SCHEDULED) && !name.equals(TAKE_FROM_CONFLICTING)) { + throw new CommandException(MESSAGE_INVALID_NAME); + } + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/ScheduleCommand.java b/src/main/java/seedu/exercise/logic/commands/ScheduleCommand.java new file mode 100644 index 00000000000..155b17a6cb9 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ScheduleCommand.java @@ -0,0 +1,33 @@ +package seedu.exercise.logic.commands; + +import static seedu.exercise.commons.core.Messages.MESSAGE_TAB; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; + +//@@author t-cheepeng +/** + * Schedules a regime or completes a schedule on a certain date + */ +public abstract class ScheduleCommand extends Command implements UndoableCommand, TypeDependentCommand { + + public static final String COMMAND_WORD = "schedule"; + + public static final String MESSAGE_USAGE = MESSAGE_TAB + COMMAND_WORD + + " command usage:\n" + + "Usage 1: Schedules a regime at a specific date." + MESSAGE_TAB + "Parameters: " + + PREFIX_NAME + "REGIME_NAME " + + PREFIX_DATE + "DATE" + MESSAGE_TAB + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "cardio " + + PREFIX_DATE + "12/12/2019\n" + + "Usage 2: Completes a schedule and adds to exercise tracker." + MESSAGE_TAB + "Parameters: " + + PREFIX_INDEX + "INDEX_OF_SCHEDULE" + MESSAGE_TAB + + "Example: " + COMMAND_WORD + " " + + PREFIX_INDEX + "1"; + + @Override + public String getUndoableCommandWord() { + return COMMAND_WORD; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/ScheduleCompleteCommand.java b/src/main/java/seedu/exercise/logic/commands/ScheduleCompleteCommand.java new file mode 100644 index 00000000000..6d4200d4ba5 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ScheduleCompleteCommand.java @@ -0,0 +1,75 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.commands.events.ScheduleCompleteEvent.KEY_TO_COMPLETE; + +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.core.index.IndexUtil; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.ui.ListResourceType; + +//@@author t-cheepeng +/** + * Completes a schedule and removes it from the schedule list. + */ +public class ScheduleCompleteCommand extends ScheduleCommand implements PayloadCarrierCommand { + + public static final String MESSAGE_SUCCESS = "Schedule %1$s completed and added to exercise tracker."; + public static final String UNIQUE_IDENTIFIER = "scheduleComplete"; + + private Index index; + private EventPayload eventPayload; + + public ScheduleCompleteCommand(Index index) { + requireNonNull(index); + this.index = index; + this.eventPayload = new EventPayload<>(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + checkValidIndex(model); + Schedule toComplete = completeSchedule(model); + model.updateStatistic(); + eventPayload.put(KEY_TO_COMPLETE, toComplete); + EventHistory.getInstance().addCommandToUndoStack(this); + return new CommandResult(String.format(MESSAGE_SUCCESS, Integer.toString(index.getOneBased())), + ListResourceType.EXERCISE); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getCommandTypeIdentifier() { + return UNIQUE_IDENTIFIER; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ScheduleCompleteCommand // instanceof handles nulls + && index.equals(((ScheduleCompleteCommand) other).index)); + } + + private Schedule completeSchedule(Model model) { + Schedule toComplete = model.getSortedScheduleList().get(index.getZeroBased()); + model.completeSchedule(toComplete); + return toComplete; + } + + private void checkValidIndex(Model model) throws CommandException { + if (IndexUtil.isIndexOutOfBounds(index, model.getSortedScheduleList())) { + throw new CommandException(Messages.MESSAGE_INVALID_SCHEDULE_DISPLAYED_INDEX); + } + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/ScheduleRegimeCommand.java b/src/main/java/seedu/exercise/logic/commands/ScheduleRegimeCommand.java new file mode 100644 index 00000000000..292782a5be4 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ScheduleRegimeCommand.java @@ -0,0 +1,158 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.logic.commands.events.ScheduleRegimeEvent.KEY_TO_SCHEDULE; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.util.Collection; + +import seedu.exercise.MainApp; +import seedu.exercise.commons.core.State; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.EventPayload; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.model.util.DateChangerUtil; +import seedu.exercise.ui.ListResourceType; + +//@@author t-cheepeng +/** + * Schedules a regime at a specific date. + */ +public class ScheduleRegimeCommand extends ScheduleCommand implements PayloadCarrierCommand { + + public static final String MESSAGE_SUCCESS = "Regime %1$s scheduled on %2$s."; + public static final String MESSAGE_REGIME_NOT_FOUND = "Regime %1$s not in regime book."; + public static final String MESSAGE_CONFLICT = "Regime to be scheduled conflicts with another scheduled regime. " + + "Opening resolve window..."; + public static final String MESSAGE_DATE_BEFORE_CURRENT_DATE = "Input date falls before today's date. \n" + + "Please choose a date after the today's date: %1$s"; + public static final String UNIQUE_IDENTIFIER = "scheduleRegime"; + + private Regime regime; + private Date dateToSchedule; + private EventPayload eventPayload; + + public ScheduleRegimeCommand(Name regimeName, Date date) { + requireAllNonNull(regimeName, date); + + this.regime = new Regime(regimeName, new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR)); + this.eventPayload = new EventPayload<>(); + dateToSchedule = date; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + checkInputDateAfterCurrentDate(); + checkExistenceOfRegime(model); + Schedule toSchedule = getScheduleFromModel(model); + + if (toSchedule == null) { + return new CommandResult(MESSAGE_CONFLICT, false, false, true, false); + } + + schedule(model, toSchedule); + eventPayload.put(KEY_TO_SCHEDULE, toSchedule); + EventHistory.getInstance().addCommandToUndoStack(this); + return new CommandResult(String.format(MESSAGE_SUCCESS, regime.getRegimeName(), dateToSchedule), + ListResourceType.SCHEDULE); + } + + @Override + public EventPayload getPayload() { + return eventPayload; + } + + @Override + public String getCommandTypeIdentifier() { + return UNIQUE_IDENTIFIER; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ScheduleRegimeCommand // instanceof handles nulls + && regime.equals(((ScheduleRegimeCommand) other).regime) + && dateToSchedule.equals(((ScheduleRegimeCommand) other).dateToSchedule)); + } + + private void checkExistenceOfRegime(Model model) throws CommandException { + if (!model.hasRegime(regime)) { + throw new CommandException(String.format(MESSAGE_REGIME_NOT_FOUND, regime.getRegimeName())); + } + } + + /** + * Checks if the input date if after the current date as given by {@link Date#getToday()}. + */ + private void checkInputDateAfterCurrentDate() throws CommandException { + Date currentDate = Date.getToday(); + if (!Date.isEndDateAfterStartDate(currentDate.toString(), dateToSchedule.toString())) { + throw new CommandException(String.format(MESSAGE_DATE_BEFORE_CURRENT_DATE, currentDate.toString())); + } + } + + /** + * Checks for scheduling conflicts and returns a valid schedule if no conflicts are found. + * If a conflict is found, returns a null schedule + */ + private Schedule getScheduleFromModel(Model model) { + int indexOfRegime = model.getRegimeIndex(regime); + Regime regimeToSchedule = getRegimeWithUpdatedExerciseDate( + model.getSortedRegimeList().get(indexOfRegime)); + + Schedule toSchedule = new Schedule(regimeToSchedule, dateToSchedule); + + if (model.hasSchedule(toSchedule)) { + setConflictState(); + Conflict conflict = buildConflict(model, toSchedule); + setConflictForModel(model, conflict); + return null; + } + + return toSchedule; + } + + /** + * Returns a regime with all exercises' Date updated to the given date to schedule. + */ + private Regime getRegimeWithUpdatedExerciseDate(Regime regime) { + Collection regimeExercises = + DateChangerUtil + .changeAllDate(regime.getRegimeExercises().asUnmodifiableObservableList(), dateToSchedule); + SortedUniqueResourceList exercisesWithUpdatedDate = + new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + for (Exercise exercise : regimeExercises) { + exercisesWithUpdatedDate.add(exercise); + } + return new Regime(regime.getRegimeName(), exercisesWithUpdatedDate); + } + + private void schedule(Model model, Schedule toSchedule) { + model.addSchedule(toSchedule); + } + + private void setConflictState() { + MainApp.setState(State.IN_CONFLICT); + } + + private Conflict buildConflict(Model model, Schedule toSchedule) { + int indexOfScheduled = model.getAllScheduleData().getResourceIndex(toSchedule); + Schedule scheduled = model.getSortedScheduleList().get(indexOfScheduled); + return new Conflict(scheduled, toSchedule); + } + + private void setConflictForModel(Model model, Conflict conflict) { + model.setConflict(conflict); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/SelectCommand.java b/src/main/java/seedu/exercise/logic/commands/SelectCommand.java new file mode 100644 index 00000000000..1a8e2b32145 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/SelectCommand.java @@ -0,0 +1,132 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.ui.ListResourceType.EXERCISE; +import static seedu.exercise.ui.ListResourceType.REGIME; +import static seedu.exercise.ui.ListResourceType.SCHEDULE; + +import java.util.List; +import java.util.Optional; + +import seedu.exercise.commons.core.Messages; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.core.index.IndexUtil; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.ui.ListResourceType; + +//@@author weihaw08 +/** + * Selects the resource at the given index of the desired resource list. + */ +public class SelectCommand extends Command { + public static final String COMMAND_WORD = "select"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + " : Selects the exercise/regime/schedule/suggestion" + + " identified" + " by the index number in their respective list.\n" + + "Parameters: " + + PREFIX_CATEGORY + "LIST_TYPE " + PREFIX_INDEX + "INDEX (must be a positive integer)\t" + + "Example: " + COMMAND_WORD + " " + + PREFIX_CATEGORY + "exercise " + + PREFIX_INDEX + "1"; + public static final String MESSAGE_SUCCESS = "Selected %1$s %2$s"; + + private final Index index; + private final ListResourceType listResourceType; + + public SelectCommand(Index index, ListResourceType listResourceType) { + requireAllNonNull(index, listResourceType); + this.index = index; + this.listResourceType = listResourceType; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (listResourceType == EXERCISE) { + checkValidExerciseIndex(model); + } else if (listResourceType == REGIME) { + checkValidRegimeIndex(model); + } else if (listResourceType == SCHEDULE) { + checkValidScheduleIndex(model); + } else { + checkValidSuggestionIndex(model); + } + + String selectedResource = listResourceType.toString().toLowerCase(); + int selectedIndex = index.getOneBased(); + return new CommandResult(String.format(MESSAGE_SUCCESS, selectedResource, selectedIndex), listResourceType, + Optional.of(index)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof SelectCommand)) { + return false; + } + + SelectCommand selectCommand = (SelectCommand) other; + return index.equals(selectCommand.index) + && listResourceType.equals(selectCommand.listResourceType); + } + + /** + * Checks if the given index is within the range of the exercise list in the model. + * + * @throws CommandException if the given index is out of range in the exercise list + */ + private void checkValidExerciseIndex(Model model) throws CommandException { + List exerciseList = model.getSortedExerciseList(); + if (IndexUtil.isIndexOutOfBounds(index, exerciseList)) { + throw new CommandException(Messages.MESSAGE_INVALID_EXERCISE_DISPLAYED_INDEX); + } + } + + /** + * Checks if the given index is within the range of the regime list in the model. + * + * @throws CommandException if the given index is out of range in the regime list + */ + private void checkValidRegimeIndex(Model model) throws CommandException { + List regimeList = model.getSortedRegimeList(); + if (IndexUtil.isIndexOutOfBounds(index, regimeList)) { + throw new CommandException(Messages.MESSAGE_INVALID_REGIME_DISPLAYED_INDEX); + } + } + + /** + * Checks if the given index is within the range of the schedule list in the model. + * + * @throws CommandException if the given index is out of range in the schedule list + */ + private void checkValidScheduleIndex(Model model) throws CommandException { + List scheduleList = model.getSortedScheduleList(); + if (IndexUtil.isIndexOutOfBounds(index, scheduleList)) { + throw new CommandException(Messages.MESSAGE_INVALID_SCHEDULE_DISPLAYED_INDEX); + } + } + + /** + * Checks if the given index is within the range of the suggestion list in the model. + * + * @throws CommandException if the given index is out of range in the suggestion list + */ + private void checkValidSuggestionIndex(Model model) throws CommandException { + List suggestionList = model.getSuggestedExerciseList(); + if (IndexUtil.isIndexOutOfBounds(index, suggestionList)) { + throw new CommandException(Messages.MESSAGE_INVALID_SUGGESTION_DISPLAYED_INDEX); + } + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/SuggestBasicCommand.java b/src/main/java/seedu/exercise/logic/commands/SuggestBasicCommand.java new file mode 100644 index 00000000000..d73cc5c1724 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/SuggestBasicCommand.java @@ -0,0 +1,36 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_SUGGEST_TYPE; +import static seedu.exercise.model.util.DefaultExerciseDatabaseUtil.getBasicExercises; + +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.ui.ListResourceType; + +//@@author kwekke +/** + * Lists basic exercises in the exercise database to the user. + */ +public class SuggestBasicCommand extends SuggestCommand { + + public static final String MESSAGE_USAGE_SUGGEST_BASIC = "Parameters: " + + PREFIX_SUGGEST_TYPE + "SUGGEST_TYPE" + + "\t\tExample: " + COMMAND_WORD + " " + + PREFIX_SUGGEST_TYPE + "basic"; + + public static final String MESSAGE_SUCCESS = "Listed all suggested basic exercises."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.setSuggestions(getBasicExercises()); + return new CommandResult(MESSAGE_SUCCESS, ListResourceType.SUGGESTION); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SuggestBasicCommand); // instanceof handles nulls + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/SuggestCommand.java b/src/main/java/seedu/exercise/logic/commands/SuggestCommand.java new file mode 100644 index 00000000000..422e6938bd8 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/SuggestCommand.java @@ -0,0 +1,20 @@ +package seedu.exercise.logic.commands; + +import static seedu.exercise.logic.commands.SuggestBasicCommand.MESSAGE_USAGE_SUGGEST_BASIC; +import static seedu.exercise.logic.commands.SuggestPossibleCommand.MESSAGE_USAGE_SUGGEST_POSSIBLE; + +//@@author kwekke +/** + * Represents an SuggestCommand with hidden internal logic and the ability to be executed. + */ +public abstract class SuggestCommand extends Command { + + public static final String COMMAND_WORD = "suggest"; + + public static final String MESSAGE_SUCCESS = "Listed all suggested exercises"; + + public static final String MESSAGE_USAGE = "Suggest: suggests exercises. \n" + + "BASIC: " + MESSAGE_USAGE_SUGGEST_BASIC + "\n" + + "POSSIBLE: " + MESSAGE_USAGE_SUGGEST_POSSIBLE; + +} diff --git a/src/main/java/seedu/exercise/logic/commands/SuggestPossibleCommand.java b/src/main/java/seedu/exercise/logic/commands/SuggestPossibleCommand.java new file mode 100644 index 00000000000..6a447365890 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/SuggestPossibleCommand.java @@ -0,0 +1,53 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_OPERATION_TYPE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_SUGGEST_TYPE; + +import java.util.function.Predicate; + +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.ui.ListResourceType; + +//@@author kwekke + +/** + * Lists possible exercises to the user. + */ +public class SuggestPossibleCommand extends SuggestCommand { + + public static final String MESSAGE_USAGE_SUGGEST_POSSIBLE = "Parameters: " + + PREFIX_SUGGEST_TYPE + "SUGGEST_TYPE " + + PREFIX_OPERATION_TYPE + "OPERATION_TYPE" + + "[" + PREFIX_MUSCLE + "MUSCLE] " + + "[" + "CUSTOM_PROPERTY_PREFIX" + "/" + "VALUE]" + + "\t\tExample: " + COMMAND_WORD + " " + + PREFIX_SUGGEST_TYPE + "possible " + + PREFIX_OPERATION_TYPE + "and " + + PREFIX_MUSCLE + "Legs"; + + public static final String MESSAGE_SUCCESS = "Listed all suggested exercises."; + + private Predicate predicate; + + public SuggestPossibleCommand(Predicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + model.updateSuggestedExerciseList(predicate); + return new CommandResult(MESSAGE_SUCCESS, ListResourceType.SUGGESTION); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SuggestPossibleCommand) // instanceof handles nulls + && predicate.equals(((SuggestPossibleCommand) other).predicate); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/TypeDependentCommand.java b/src/main/java/seedu/exercise/logic/commands/TypeDependentCommand.java new file mode 100644 index 00000000000..abb2d78cde9 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/TypeDependentCommand.java @@ -0,0 +1,14 @@ +package seedu.exercise.logic.commands; +//@@author garylyp +/** + * Interface for commands which uses the same command word but carry out different operations. + */ +public interface TypeDependentCommand { + + /** + * Returns the unique identifier of the command. + * + * @return the name of the identifier (E.g. "exercise" or "regime") + */ + String getCommandTypeIdentifier(); +} diff --git a/src/main/java/seedu/exercise/logic/commands/UndoCommand.java b/src/main/java/seedu/exercise/logic/commands/UndoCommand.java new file mode 100644 index 00000000000..b3087f5c135 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/UndoCommand.java @@ -0,0 +1,72 @@ +package seedu.exercise.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.exercise.logic.commands.events.AddExerciseEvent; +import seedu.exercise.logic.commands.events.AddRegimeEvent; +import seedu.exercise.logic.commands.events.ClearEvent; +import seedu.exercise.logic.commands.events.DeleteExerciseEvent; +import seedu.exercise.logic.commands.events.DeleteRegimeEvent; +import seedu.exercise.logic.commands.events.EditExerciseEvent; +import seedu.exercise.logic.commands.events.EditRegimeEvent; +import seedu.exercise.logic.commands.events.Event; +import seedu.exercise.logic.commands.events.EventHistory; +import seedu.exercise.logic.commands.events.ResolveEvent; +import seedu.exercise.logic.commands.events.ScheduleCompleteEvent; +import seedu.exercise.logic.commands.events.ScheduleRegimeEvent; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +import seedu.exercise.ui.ListResourceType; +//@@author garylyp +/** + * Undoes the last executed command. + */ +public class UndoCommand extends Command { + + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Undoes the latest command called.\n" + + "Example: " + COMMAND_WORD; + public static final String MESSAGE_SUCCESS = "Command undone: \n%1$s."; + public static final String MESSAGE_EMPTY_UNDO_STACK = "There is no command to undo."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + EventHistory eventHistory = EventHistory.getInstance(); + + if (eventHistory.isUndoStackEmpty()) { + throw new CommandException(MESSAGE_EMPTY_UNDO_STACK); + } + + Event eventToUndo = eventHistory.undo(model); + model.updateStatistic(); + ListResourceType type = getListResourceType(eventToUndo); + return new CommandResult(String.format(MESSAGE_SUCCESS, eventToUndo), type); + } + + /** + * Returns the list resource type for display based on the type of event that has been undone. + * + * @param event the event that was undone + * @return a list resource type enum object + */ + private ListResourceType getListResourceType(Event event) { + ListResourceType type = ListResourceType.NULL; + if (event instanceof AddExerciseEvent + || event instanceof DeleteExerciseEvent + || event instanceof EditExerciseEvent + || event instanceof ClearEvent) { + type = ListResourceType.EXERCISE; + } else if (event instanceof AddRegimeEvent + || event instanceof DeleteRegimeEvent + || event instanceof EditRegimeEvent) { + type = ListResourceType.REGIME; + } else if (event instanceof ScheduleCompleteEvent + || event instanceof ScheduleRegimeEvent + || event instanceof ResolveEvent) { + type = ListResourceType.SCHEDULE; + } + return type; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/UndoableCommand.java b/src/main/java/seedu/exercise/logic/commands/UndoableCommand.java new file mode 100644 index 00000000000..bf723e9af1c --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/UndoableCommand.java @@ -0,0 +1,14 @@ +package seedu.exercise.logic.commands; +//@@author garylyp +/** + * Represents a command that can be undone. + */ +public interface UndoableCommand { + + /** + * Returns the command word that is used to call the undoable command. + * + * @return command word of the undoable command + */ + String getUndoableCommandWord(); +} diff --git a/src/main/java/seedu/exercise/logic/commands/ViewCustomCommand.java b/src/main/java/seedu/exercise/logic/commands/ViewCustomCommand.java new file mode 100644 index 00000000000..649d802e799 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/ViewCustomCommand.java @@ -0,0 +1,23 @@ +package seedu.exercise.logic.commands; + +import seedu.exercise.model.Model; + +/** + * Produces a window for user to view the table of custom properties they have defined. + */ +public class ViewCustomCommand extends Command { + + public static final String COMMAND_WORD = "viewcustom"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows all defined custom properties.\n" + + "Example: " + COMMAND_WORD; + + public static final String SHOWING_VIEW_CUSTOM_MESSAGE = "Opened custom properties window."; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(SHOWING_VIEW_CUSTOM_MESSAGE, false, false, false, true); + } + +} + diff --git a/src/main/java/seedu/exercise/logic/commands/builder/EditExerciseBuilder.java b/src/main/java/seedu/exercise/logic/commands/builder/EditExerciseBuilder.java new file mode 100644 index 00000000000..a0a0c757b59 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/builder/EditExerciseBuilder.java @@ -0,0 +1,199 @@ +package seedu.exercise.logic.commands.builder; + +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; + +import seedu.exercise.commons.util.CollectionUtil; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.resource.Exercise; + +/** + * Represents a builder class that helps to build edited exercise. + */ +public class EditExerciseBuilder { + private Name name; + private Calories calories; + private Date date; + private Quantity quantity; + private Unit unit; + private Set muscles; + private Map customProperties; + + public EditExerciseBuilder() { + } + + /** + * Copy constructor. + * A defensive copy of {@code muscles} is used internally. + */ + public EditExerciseBuilder(EditExerciseBuilder toCopy) { + setName(toCopy.name); + setCalories(toCopy.calories); + setDate(toCopy.date); + setQuantity(toCopy.quantity); + setUnit(toCopy.unit); + setMuscles(toCopy.muscles); + setCustomProperties(toCopy.customProperties); + } + + /** + * A constructor that copies all of the information from an exercise. + */ + public EditExerciseBuilder(Exercise toEdit) { + setName(toEdit.getName()); + setCalories(toEdit.getCalories()); + setDate(toEdit.getDate()); + setQuantity(toEdit.getQuantity()); + setUnit(toEdit.getUnit()); + setMuscles(toEdit.getMuscles()); + setCustomProperties(toEdit.getCustomPropertiesMap()); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, calories, date, quantity, unit, muscles, customProperties); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setCalories(Calories calories) { + this.calories = calories; + } + + public Optional getCalories() { + return Optional.ofNullable(calories); + } + + public void setDate(Date date) { + this.date = date; + } + + public Optional getDate() { + return Optional.ofNullable(date); + } + + public void setQuantity(Quantity quantity) { + this.quantity = quantity; + } + + public Optional getQuantity() { + return Optional.ofNullable(quantity); + } + + public void setUnit(Unit unit) { + this.unit = unit; + } + + public Optional getUnit() { + return Optional.ofNullable(unit); + } + + /** + * Sets {@code muscles} to this object's {@code muscles}. + * A defensive copy of {@code muscles} is used internally. + */ + public void setMuscles(Set muscles) { + this.muscles = (muscles != null) ? new HashSet<>(muscles) : null; + } + + /** + * Returns an unmodifiable muscle set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code muscles} is null. + */ + public Optional> getMuscles() { + return (muscles != null) ? Optional.of(Collections.unmodifiableSet(muscles)) : Optional.empty(); + } + + /** + * Returns an unmodifiable custom properties map, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code customProperties} is null. + */ + public Optional> getCustomProperties() { + return (customProperties != null) ? Optional.of(Collections.unmodifiableMap(customProperties)) + : Optional.empty(); + } + + /** + * Sets {@code customProperties} to this object's {@code customProperties}. + */ + public void setCustomProperties(Map customProperties) { + this.customProperties = (customProperties != null) ? new TreeMap<>(customProperties) : null; + } + + /** + * Creates and returns a {@code Exercise} with the details of {@code exerciseToEdit} + * edited with {@code editExerciseBuilder}. + */ + public static Exercise createEditedExercise( + Exercise exerciseToEdit, EditExerciseBuilder editExerciseBuilder) { + assert exerciseToEdit != null; + + Name updatedName = editExerciseBuilder.getName().orElse(exerciseToEdit.getName()); + Calories updatedCalories = editExerciseBuilder.getCalories().orElse(exerciseToEdit.getCalories()); + Date updatedDate = editExerciseBuilder.getDate().orElse(exerciseToEdit.getDate()); + Quantity updatedQuantity = editExerciseBuilder.getQuantity().orElse(exerciseToEdit.getQuantity()); + Unit updatedUnit = editExerciseBuilder.getUnit().orElse(exerciseToEdit.getUnit()); + Set updatedMuscles = editExerciseBuilder.getMuscles().orElse(exerciseToEdit.getMuscles()); + Map updatedCustomProperties = new TreeMap<>(exerciseToEdit.getCustomPropertiesMap()); + Map newCustomProperties = editExerciseBuilder.getCustomProperties() + .orElse(new TreeMap<>()); + updatedCustomProperties.putAll(newCustomProperties); + + return new Exercise(updatedName, updatedDate, updatedCalories, updatedQuantity, updatedUnit, + updatedMuscles, updatedCustomProperties); + } + + /** + * Builds an edited exercise based on the fields of the {@code EditExerciseBuilder}. + * Precondition: All fields must not be null. + */ + public Exercise buildEditedExercise() { + requireAllNonNull(name, date, calories, quantity, unit, muscles, customProperties); + return new Exercise(name, date, calories, quantity, unit, muscles, customProperties); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditExerciseBuilder)) { + return false; + } + + // state check + EditExerciseBuilder e = (EditExerciseBuilder) other; + + return getName().equals(e.getName()) + && getCalories().equals(e.getCalories()) + && getDate().equals(e.getDate()) + && getQuantity().equals(e.getQuantity()) + && getUnit().equals(e.getUnit()) + && getMuscles().equals(e.getMuscles()) + && getCustomProperties().equals(e.getCustomProperties()); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/AddExerciseEvent.java b/src/main/java/seedu/exercise/logic/commands/events/AddExerciseEvent.java new file mode 100644 index 00000000000..c870a474f72 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/AddExerciseEvent.java @@ -0,0 +1,59 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +//@@author garylyp +/** + * Represents a particular add exercise event that can be redone or undone. + */ +public class AddExerciseEvent implements Event { + + public static final String KEY_EXERCISE_TO_ADD = "exerciseToAdd"; + private static final String EVENT_DESCRIPTION = "Add exercise: %1$s"; + + /** + * The exercise that has been added during the event. + */ + private final Exercise exercise; + + /** + * Creates an AddExerciseEvent to store the particular event of an exercise being added to the exercise book. + * + * @param eventPayload a wrapper class that stores the essential information for undo and redo + */ + public AddExerciseEvent(EventPayload eventPayload) { + this.exercise = (Exercise) eventPayload.get(KEY_EXERCISE_TO_ADD); + } + + @Override + public void undo(Model model) { + model.deleteExercise(exercise); + } + + @Override + public void redo(Model model) { + model.addExercise(exercise); + } + + /** + * Returns the exercise that was added. + * + * @return exercise that is passed into constructor of AddExerciseEvent + */ + public Exercise getExercise() { + return exercise; + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, exercise); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddExerciseEvent // instanceof handles nulls + && exercise.equals(((AddExerciseEvent) other).getExercise())); + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/AddRegimeEvent.java b/src/main/java/seedu/exercise/logic/commands/events/AddRegimeEvent.java new file mode 100644 index 00000000000..0f3a478168a --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/AddRegimeEvent.java @@ -0,0 +1,48 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Regime; +//@@author garylyp +/** + * Represents a particular add regime event that can be redone or undone. + */ +public class AddRegimeEvent implements Event { + + public static final String KEY_REGIME_TO_ADD = "regimeToAdd"; + private static final String EVENT_DESCRIPTION = "Add regime: %1$s"; + + private final Regime regimeToAdd; + + /** + * Creates an AddRegimeEvent to store the particular event of a regime being added to + * the regime book. + * + * @param eventPayload a wrapper class that stores the essential information for undo and redo + */ + public AddRegimeEvent(EventPayload eventPayload) { + this.regimeToAdd = (Regime) eventPayload.get(KEY_REGIME_TO_ADD); + } + + @Override + public void undo(Model model) { + model.deleteRegime(regimeToAdd); + } + + @Override + public void redo(Model model) { + model.addRegime(regimeToAdd); + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, + regimeToAdd.getRegimeName()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddRegimeEvent // instanceof handles nulls + && regimeToAdd.equals(((AddRegimeEvent) other).regimeToAdd)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/ClearEvent.java b/src/main/java/seedu/exercise/logic/commands/events/ClearEvent.java new file mode 100644 index 00000000000..7d91c2bd687 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/ClearEvent.java @@ -0,0 +1,63 @@ +package seedu.exercise.logic.commands.events; + +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import seedu.exercise.model.Model; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Exercise; +//@@author garylyp +/** + * Represents a particular add event that can be redone or undone. + */ +public class ClearEvent implements Event { + + public static final String KEY_EXERCISE_BOOK_CLEARED = "exerciseBookCleared"; + private static final String EVENT_DESCRIPTION = "Clear Exercise Book: %1$s"; + + /** + * The exercise book that exists before the clear event. + */ + private final ReadOnlyResourceBook exerciseBookCleared; + + /** + * Creates a ClearEvent to store the particular event of the exercise book being cleared. + * + * @param eventPayload a wrapper class that stores the exercise book in the state before the ClearEvent. + */ + @SuppressWarnings("unchecked") + public ClearEvent(EventPayload> eventPayload) { + this.exerciseBookCleared = (ReadOnlyResourceBook) eventPayload.get(KEY_EXERCISE_BOOK_CLEARED); + } + + @Override + public void undo(Model model) { + model.setExerciseBook(exerciseBookCleared); + } + + @Override + public void redo(Model model) { + model.setExerciseBook(new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR)); + } + + /** + * Returns the exercise book that exists before the clear event. + * + * @return an exercise book in the state before the ClearEvent. + */ + public ReadOnlyResourceBook getExerciseBookCleared() { + return exerciseBookCleared; + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, exerciseBookCleared); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ClearEvent // instanceof handles nulls + && exerciseBookCleared.equals(((ClearEvent) other).getExerciseBookCleared())); + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/DeleteExerciseEvent.java b/src/main/java/seedu/exercise/logic/commands/events/DeleteExerciseEvent.java new file mode 100644 index 00000000000..9efa1738ddc --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/DeleteExerciseEvent.java @@ -0,0 +1,59 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +//@@author garylyp +/** + * Represents a particular delete exercise event that can be redone or undone. + */ +public class DeleteExerciseEvent implements Event { + + public static final String KEY_EXERCISE_TO_DELETE = "exerciseToDelete"; + private static final String EVENT_DESCRIPTION = "Delete exercise: %1$s"; + + /** + * The exercise that has been deleted during the event. + */ + private final Exercise exerciseToDelete; + + /** + * Creates a DeleteExerciseEvent to store the particular event of an exercise being deleted from the exercise book. + * + * @param eventPayload a wrapper class that stores the essential information for undo and redo + */ + public DeleteExerciseEvent(EventPayload eventPayload) { + this.exerciseToDelete = (Exercise) eventPayload.get(KEY_EXERCISE_TO_DELETE); + } + + @Override + public void undo(Model model) { + model.addExercise(exerciseToDelete); + } + + @Override + public void redo(Model model) { + model.deleteExercise(exerciseToDelete); + } + + /** + * Returns the exercise that was deleted. + * + * @return exercise that is passed into constructor of DeleteExerciseEvent + */ + public Exercise getExerciseToDelete() { + return exerciseToDelete; + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, exerciseToDelete); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteExerciseEvent // instanceof handles nulls + && exerciseToDelete.equals(((DeleteExerciseEvent) other).getExerciseToDelete())); + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/DeleteRegimeEvent.java b/src/main/java/seedu/exercise/logic/commands/events/DeleteRegimeEvent.java new file mode 100644 index 00000000000..5b1c073aa63 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/DeleteRegimeEvent.java @@ -0,0 +1,48 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Regime; +//@@author garylyp +/** + * Represents a particular delete regime event that can be redone or undone. + */ +public class DeleteRegimeEvent implements Event { + + public static final String KEY_REGIME_TO_DELETE = "regimeToDelete"; + private static final String EVENT_DESCRIPTION = "Delete regime: %1$s"; + + private final Regime regimeToDelete; + + /** + * Creates a DeleteRegimeEvent to store the particular event of a regime being deleted from + * the regime book. + * + * @param eventPayload a wrapper class that stores the essential information for undo and redo + */ + public DeleteRegimeEvent(EventPayload eventPayload) { + this.regimeToDelete = (Regime) eventPayload.get(KEY_REGIME_TO_DELETE); + } + + @Override + public void undo(Model model) { + model.addRegime(regimeToDelete); + } + + @Override + public void redo(Model model) { + model.deleteRegime(regimeToDelete); + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, + regimeToDelete.getRegimeName()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteRegimeEvent // instanceof handles nulls + && regimeToDelete.equals(((DeleteRegimeEvent) other).regimeToDelete)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/EditExerciseEvent.java b/src/main/java/seedu/exercise/logic/commands/events/EditExerciseEvent.java new file mode 100644 index 00000000000..d241511bb09 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/EditExerciseEvent.java @@ -0,0 +1,77 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Exercise; +//@@author garylyp +/** + * Represents a particular edit event that can be redone or undone. + */ +public class EditExerciseEvent implements Event { + + public static final String KEY_ORIGINAL_EXERCISE = "originalExercise"; + public static final String KEY_EDITED_EXERCISE = "editedExercise"; + private static final String EVENT_DESCRIPTION = "Edit\t: %1$s\nTo\t: %2$s"; + + /** + * The exercise that has been edited during the event. + */ + private final Exercise originalExercise; + + /** + * The newly edited exercise after the edit event. + */ + private final Exercise editedExercise; + + /** + * Creates a EditExerciseEvent to store the particular event of an exercise being edited + * in the exercise book. + * + * @param eventPayload a wrapper class that stores the essential information for undo and redo + */ + public EditExerciseEvent(EventPayload eventPayload) { + this.originalExercise = (Exercise) eventPayload.get(KEY_ORIGINAL_EXERCISE); + this.editedExercise = (Exercise) eventPayload.get(KEY_EDITED_EXERCISE); + } + + @Override + public void undo(Model model) { + model.setExercise(editedExercise, originalExercise); + } + + @Override + public void redo(Model model) { + model.setExercise(originalExercise, editedExercise); + } + + /** + * Returns the exercise that has been edited. + * + * @return exercise before the edit event happens + */ + public Exercise getOriginalExercise() { + return originalExercise; + } + + /** + * Returns the newly edited exercise after the edit event. + * + * @return exercise after the edit event happens + */ + public Exercise getEditedExercise() { + return editedExercise; + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, originalExercise, editedExercise); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EditExerciseEvent // instanceof handles nulls + && originalExercise.equals(((EditExerciseEvent) other).getOriginalExercise()) + && editedExercise.equals(((EditExerciseEvent) other).getEditedExercise())); + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/EditRegimeEvent.java b/src/main/java/seedu/exercise/logic/commands/events/EditRegimeEvent.java new file mode 100644 index 00000000000..e2800e87b7a --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/EditRegimeEvent.java @@ -0,0 +1,55 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Regime; +//@@author garylyp +/** + * Represents a particular edit regime event that can be redone or undone. Edit regime events + * are induced using AddRegimeCommand or DeleteRegimeCommand on an existing command. + */ +public class EditRegimeEvent implements Event { + + public static final String KEY_IS_REGIME_EDITED = "isRegimeEdited"; + public static final String KEY_ORIGINAL_REGIME = "originalRegime"; + public static final String KEY_EDITED_REGIME = "editedRegime"; + private static final String EVENT_DESCRIPTION = "Edit: %1$s from %2$s exercises to %3$s exercises"; + + private final Regime originalRegime; + private final Regime editedRegime; + + /** + * Creates an EditRegimeEvent to store the particular event of a regime being edited in the regime book. + * + * @param eventPayload a data carrier that stores the essential information for undo and redo + */ + public EditRegimeEvent(EventPayload eventPayload) { + this.originalRegime = (Regime) eventPayload.get(KEY_ORIGINAL_REGIME); + this.editedRegime = (Regime) eventPayload.get(KEY_EDITED_REGIME); + } + + @Override + public void undo(Model model) { + model.setRegime(editedRegime, originalRegime); + } + + @Override + public void redo(Model model) { + model.setRegime(originalRegime, editedRegime); + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, + editedRegime.getRegimeName(), + editedRegime.getRegimeExercises().asUnmodifiableObservableList().size(), + originalRegime.getRegimeExercises().asUnmodifiableObservableList().size()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EditRegimeEvent // instanceof handles nulls + && originalRegime.equals(((EditRegimeEvent) other).originalRegime) + && editedRegime.equals(((EditRegimeEvent) other).editedRegime)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/Event.java b/src/main/java/seedu/exercise/logic/commands/events/Event.java new file mode 100644 index 00000000000..fc6b46f8e5f --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/Event.java @@ -0,0 +1,23 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +//@@author garylyp +/** + * Represents an Event that can be undone or redone. + */ +public interface Event { + + /** + * Executes the reverse of the event. + * + * @param model {@code Model} which the command should operate on. + */ + void undo(Model model); + + /** + * Executes the event again. + * + * @param model {@code Model} which the command should operate on. + */ + void redo(Model model); +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/EventFactory.java b/src/main/java/seedu/exercise/logic/commands/events/EventFactory.java new file mode 100644 index 00000000000..bfc548e1737 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/EventFactory.java @@ -0,0 +1,161 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.logic.commands.AddCommand; +import seedu.exercise.logic.commands.AddExerciseCommand; +import seedu.exercise.logic.commands.AddRegimeCommand; +import seedu.exercise.logic.commands.ClearCommand; +import seedu.exercise.logic.commands.DeleteCommand; +import seedu.exercise.logic.commands.DeleteExerciseCommand; +import seedu.exercise.logic.commands.DeleteRegimeCommand; +import seedu.exercise.logic.commands.EditCommand; +import seedu.exercise.logic.commands.ResolveCommand; +import seedu.exercise.logic.commands.ScheduleCommand; +import seedu.exercise.logic.commands.ScheduleCompleteCommand; +import seedu.exercise.logic.commands.ScheduleRegimeCommand; +import seedu.exercise.logic.commands.UndoableCommand; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Schedule; +//@@author garylyp +/** + * A utility class to generate specific Event objects depending on requirements. + */ +public class EventFactory { + + public static final String MESSAGE_COMMAND_NOT_UNDOABLE = + "The command \'%1$s\' cannot be stored as an undoable event."; + public static final String MESSAGE_UNIQUE_IDENTIFIER_NOT_FOUND = + "The resource type \'%1$s\' of the \'%2$s\' command is not known."; + + /** + * Generates an Event object that can execute the behaviour of a given Command as well + * as its opposite behaviour. + * + * @param command a {@code UndoableCommand} to be represented with using an Event object + * @return an {@code Event} that can be undone or redone + * @throws CommandException if command provided is not undoable + */ + public static Event commandToEvent(UndoableCommand command) throws CommandException { + String commandWord = command.getUndoableCommandWord(); + + switch (commandWord) { + case AddCommand.COMMAND_WORD: + return generateEventFromAddCommand((AddCommand) command); + + case DeleteCommand.COMMAND_WORD: + return generateEventFromDeleteCommand((DeleteCommand) command); + + case EditCommand.COMMAND_WORD: + return new EditExerciseEvent(((EditCommand) command).getPayload()); + + case ClearCommand.COMMAND_WORD: + return new ClearEvent(((ClearCommand) command).getPayload()); + + case ScheduleCommand.COMMAND_WORD: + return generateEventFromScheduleCommand((ScheduleCommand) command); + + case ResolveCommand.COMMAND_WORD: + return new ResolveEvent(((ResolveCommand) command).getPayload()); + + default: + throw new CommandException( + String.format(MESSAGE_COMMAND_NOT_UNDOABLE, commandWord)); + } + } + + /** + * Generates a schedule regime or schedule complete event based on the command type. + * + * @param command a {@link ScheduleCommand} to be represented with using an Event object + * @return a {@link ScheduleRegimeEvent} or a {@link ScheduleCompleteEvent} + * that can be undone or redone + */ + static Event generateEventFromScheduleCommand(ScheduleCommand command) throws CommandException { + String resourceType = command.getCommandTypeIdentifier(); + EventPayload eventPayload; + switch (resourceType) { + case ScheduleRegimeCommand.UNIQUE_IDENTIFIER: + eventPayload = ((ScheduleRegimeCommand) command).getPayload(); + return new ScheduleRegimeEvent(eventPayload); + + case ScheduleCompleteCommand.UNIQUE_IDENTIFIER: + eventPayload = ((ScheduleCompleteCommand) command).getPayload(); + return new ScheduleCompleteEvent(eventPayload); + + default: + throw new CommandException( + String.format(MESSAGE_UNIQUE_IDENTIFIER_NOT_FOUND, + resourceType, + command.getUndoableCommandWord())); + } + } + + /** + * Generates a add exercise or add regime event based on the command type. + * + * @param command a {@link AddCommand} to be represented with using an Event object + * @return an {@link AddExerciseEvent}, {@link AddRegimeEvent} or {@link EditRegimeEvent} + * that can be undone or redone + */ + protected static Event generateEventFromAddCommand(AddCommand command) throws CommandException { + String resourceType = command.getCommandTypeIdentifier(); + switch (resourceType) { + case AddExerciseCommand.RESOURCE_TYPE: + EventPayload eventPayload = ((AddExerciseCommand) command).getPayload(); + return new AddExerciseEvent(eventPayload); + + case AddRegimeCommand.RESOURCE_TYPE: + AddRegimeCommand addRegimeCommand = (AddRegimeCommand) command; + boolean isRegimeEdited = (boolean) addRegimeCommand + .getPayload() + .get(EditRegimeEvent.KEY_IS_REGIME_EDITED); + + if (isRegimeEdited) { + return new EditRegimeEvent(addRegimeCommand.getPayload()); + } else { + return new AddRegimeEvent(addRegimeCommand.getPayload()); + } + + default: + throw new CommandException( + String.format(MESSAGE_UNIQUE_IDENTIFIER_NOT_FOUND, + resourceType, + command.getUndoableCommandWord())); + } + } + + /** + * Generates a delete exercise or delete regime event based on the command type. + * + * @param command a {@link DeleteCommand} to be represented with using an Event object + * @return an {@link DeleteExerciseEvent}, {@link DeleteRegimeEvent} or {@link EditRegimeEvent} + * that can be undone or redone + */ + protected static Event generateEventFromDeleteCommand(DeleteCommand command) throws CommandException { + String resourceType = command.getCommandTypeIdentifier(); + switch (resourceType) { + case DeleteExerciseCommand.RESOURCE_TYPE: + EventPayload eventPayload = ((DeleteExerciseCommand) command).getPayload(); + return new DeleteExerciseEvent(eventPayload); + + case DeleteRegimeCommand.RESOURCE_TYPE: + DeleteRegimeCommand deleteRegimeCommand = (DeleteRegimeCommand) command; + boolean isRegimeEdited = (boolean) deleteRegimeCommand + .getPayload() + .get(EditRegimeEvent.KEY_IS_REGIME_EDITED); + + if (isRegimeEdited) { + return new EditRegimeEvent(deleteRegimeCommand.getPayload()); + } else { + return new DeleteRegimeEvent(deleteRegimeCommand.getPayload()); + } + + default: + throw new CommandException( + String.format(MESSAGE_UNIQUE_IDENTIFIER_NOT_FOUND, + resourceType, + command.getUndoableCommandWord())); + } + } + +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/EventHistory.java b/src/main/java/seedu/exercise/logic/commands/events/EventHistory.java new file mode 100644 index 00000000000..e2c88c6ef62 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/EventHistory.java @@ -0,0 +1,117 @@ +package seedu.exercise.logic.commands.events; + +import java.util.Stack; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.logic.commands.UndoableCommand; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.model.Model; +//@@author garylyp +/** + * A singleton class that tracks a single history of undoable events. + */ +public class EventHistory { + + public static final String MESSAGE_ADD_TO_EVENT_HISTORY_SUCCESS = + "Command added to Event History: %1$s"; + private static final Logger logger = LogsCenter.getLogger(EventHistory.class); + private static EventHistory eventHistory; + private Stack undoStack; + private Stack redoStack; + + /** + * Initializes both undo and redo history if no undo history exists. + */ + private EventHistory() { + undoStack = new Stack<>(); + redoStack = new Stack<>(); + } + + /** + * Returns an EventHistory object that tracks the history of undoable events. + * + * @return an instance of EventHistory that can be used to access the undo and redo history. + */ + public static EventHistory getInstance() { + if (eventHistory == null) { + eventHistory = new EventHistory(); + } + return eventHistory; + } + + /** + * Stores a command as an event in the EventHistory. + * + * @param command an undoable command to be stored in history + * @throws CommandException if {@code command} cannot be represented with an {@code Event} + */ + public void addCommandToUndoStack(UndoableCommand command) throws CommandException { + Event event = EventFactory.commandToEvent(command); + undoStack.add(event); + redoStack.clear(); + logger.info(String.format(MESSAGE_ADD_TO_EVENT_HISTORY_SUCCESS, command.getUndoableCommandWord())); + } + + /** + * Returns the next event to undo. + * + * @param model {@code Model} which the command should operate on. + * @return undoable event + */ + public Event undo(Model model) { + assert(!undoStack.isEmpty()); + Event eventToUndo = undoStack.pop(); + eventToUndo.undo(model); + redoStack.push(eventToUndo); + return eventToUndo; + } + + /** + * Returns the next event to redo. + * + * @param model {@code Model} which the command should operate on. + * @return undoable event + */ + public Event redo(Model model) { + assert(!redoStack.isEmpty()); + Event eventToRedo = redoStack.pop(); + eventToRedo.redo(model); + undoStack.push(eventToRedo); + return eventToRedo; + } + + /** + * Clears both undo and redo history. + */ + public void reset() { + undoStack.clear(); + redoStack.clear(); + } + + /** + * Checks if the undo history is empty. + * + * @return true if undo stack is empty, false otherwise + */ + public boolean isUndoStackEmpty() { + return undoStack.isEmpty(); + } + + /** + * Checks if the redo history is empty. + * + * @return true if redo stack is empty, false otherwise + */ + public boolean isRedoStackEmpty() { + return redoStack.isEmpty(); + } + + public Stack getUndoStack() { + return undoStack; + } + + public Stack getRedoStack() { + return redoStack; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/EventPayload.java b/src/main/java/seedu/exercise/logic/commands/events/EventPayload.java new file mode 100644 index 00000000000..a21b8c0b96a --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/EventPayload.java @@ -0,0 +1,47 @@ +package seedu.exercise.logic.commands.events; + +import java.util.HashMap; +//@@author garylyp +/** + * A wrapper class to share essential data between Command and the corresponding Event + * for undo and redo commands. Data is accessed using keys specified by the Command + * classes. + * + * @param The data stored can be of multiple types + */ +public class EventPayload { + + private HashMap payload = new HashMap<>(); + + /** + * Stores the given key data pair into the event payload. + * + * @param key the key with which the specified data is to be associated + * @param data the data to be stored in the payload under the given key + * @return the current instance of the EventPayload with the key-data + * mapping added + */ + public EventPayload put(String key, T data) { + payload.put(key, data); + return this; + } + + /** + * Returns the data to which the specified key is mapped, + * or null if this payload contains no mapping for the key. + * + * @param key the key whose associated data is to be returned + * @return the data to which the specified key is mapped, + * or null if this payload contains no mapping for the key + */ + public Object get(String key) { + return payload.get(key); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EventPayload // instanceof handles nulls + && payload.equals(((EventPayload) other).payload)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/ResolveEvent.java b/src/main/java/seedu/exercise/logic/commands/events/ResolveEvent.java new file mode 100644 index 00000000000..a16d16f1caa --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/ResolveEvent.java @@ -0,0 +1,76 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.resource.Schedule; +//@@author garylyp +/** + * Represents a particular resolve schedule conflict event that can be redone or undone. + */ +public class ResolveEvent implements Event { + + public static final String KEY_RESOLVED_SCHEDULE = "resolvedSchedule"; + public static final String KEY_CONFLICT = "conflict"; + private static final String EVENT_DESCRIPTION = "Scheduled: Regime %1$s on %2$s"; + + private final Schedule resolvedSchedule; + private final Conflict conflict; + private final boolean isNewRegimeCreated; + + /** + * Creates an ResolveEvent to store the particular event of a schedule conflict + * being resolved. + * + * @param eventPayload a data carrier that stores the essential information for undo and redo + */ + public ResolveEvent(EventPayload eventPayload) { + this.resolvedSchedule = (Schedule) eventPayload.get(KEY_RESOLVED_SCHEDULE); + this.conflict = (Conflict) eventPayload.get(KEY_CONFLICT); + this.isNewRegimeCreated = isNewRegimeCreated(); + } + + @Override + public void undo(Model model) { + if (isNewRegimeCreated) { + model.deleteRegime(resolvedSchedule.getRegime()); + } + model.removeSchedule(resolvedSchedule); + model.addSchedule(conflict.getScheduled()); + } + + @Override + public void redo(Model model) { + if (isNewRegimeCreated) { + model.addRegime(resolvedSchedule.getRegime()); + } + model.removeSchedule(conflict.getScheduled()); + model.addSchedule(resolvedSchedule); + } + + /** + * Checks if the regime in the resolved schedule is newly created and added to the regime book + * when the resolve command is executed. The regime is created if the regime in the resolved schedule + * is NOT in any of the two schedules in conflict. + * + * @return true if the regime is newly created when the resolve is executed, false otherwise + */ + private boolean isNewRegimeCreated() { + return !(resolvedSchedule.getRegime().equals(conflict.getScheduled().getRegime()) + || resolvedSchedule.getRegime().equals(conflict.getConflicted().getRegime())); + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, + resolvedSchedule.getRegimeName(), + resolvedSchedule.getDate()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ResolveEvent // instanceof handles nulls + && resolvedSchedule.equals(((ResolveEvent) other).resolvedSchedule) + && conflict.equals(((ResolveEvent) other).conflict)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/ScheduleCompleteEvent.java b/src/main/java/seedu/exercise/logic/commands/events/ScheduleCompleteEvent.java new file mode 100644 index 00000000000..243f9c21b5b --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/ScheduleCompleteEvent.java @@ -0,0 +1,61 @@ +package seedu.exercise.logic.commands.events; + +import java.util.Collection; + +import seedu.exercise.model.Model; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Schedule; +//@@author garylyp +/** + * Represents a particular schedule complete event that can be redone or undone. + */ +public class ScheduleCompleteEvent implements Event { + + public static final String KEY_TO_COMPLETE = "toComplete"; + private static final String EVENT_DESCRIPTION = "Completed: Regime %1$s on %2$s"; + + private final Schedule toComplete; + + /** + * Creates an ScheduleCompleteEvent to store the particular event of a schedule being marked as completed. + * + * @param eventPayload a data carrier that stores the essential information for undo and redo + */ + public ScheduleCompleteEvent(EventPayload eventPayload) { + this.toComplete = (Schedule) eventPayload.get(KEY_TO_COMPLETE); + } + + @Override + public void undo(Model model) { + Collection scheduledExercises = toComplete.getExercises(); + ReadOnlyResourceBook exerciseBook = model.getExerciseBookData(); + for (Exercise exercise : scheduledExercises) { + if (exerciseBook.hasResource(exercise)) { + exerciseBook.removeResource(exercise); + } + } + + ReadOnlyResourceBook scheduleBook = model.getAllScheduleData(); + scheduleBook.addResource(toComplete); + } + + @Override + public void redo(Model model) { + model.completeSchedule(toComplete); + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, + toComplete.getRegimeName(), + toComplete.getDate()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ScheduleCompleteEvent // instanceof handles nulls + && toComplete.equals(((ScheduleCompleteEvent) other).toComplete)); + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/events/ScheduleRegimeEvent.java b/src/main/java/seedu/exercise/logic/commands/events/ScheduleRegimeEvent.java new file mode 100644 index 00000000000..cc4bc46c9ef --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/events/ScheduleRegimeEvent.java @@ -0,0 +1,48 @@ +package seedu.exercise.logic.commands.events; + +import seedu.exercise.model.Model; +import seedu.exercise.model.resource.Schedule; +//@@author garylyp +/** + * Represents a particular schedule regime event that can be redone or undone. + */ +public class ScheduleRegimeEvent implements Event { + + public static final String KEY_TO_SCHEDULE = "toSchedule"; + private static final String EVENT_DESCRIPTION = "Scheduled: Regime %1$s on %2$s"; + + private final Schedule toSchedule; + + /** + * Creates an ScheduleRegimeEvent to store the particular event of a regime being scheduled to the schedule list. + * + * @param eventPayload a data carrier that stores the essential information for undo and redo + */ + public ScheduleRegimeEvent(EventPayload eventPayload) { + this.toSchedule = (Schedule) eventPayload.get(KEY_TO_SCHEDULE); + } + + @Override + public void undo(Model model) { + model.removeSchedule(toSchedule); + } + + @Override + public void redo(Model model) { + model.addSchedule(toSchedule); + } + + @Override + public String toString() { + return String.format(EVENT_DESCRIPTION, + toSchedule.getRegimeName(), + toSchedule.getDate()); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ScheduleRegimeEvent // instanceof handles nulls + && toSchedule.equals(((ScheduleRegimeEvent) other).toSchedule)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/seedu/exercise/logic/commands/exceptions/CommandException.java similarity index 67% rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java rename to src/main/java/seedu/exercise/logic/commands/exceptions/CommandException.java index a16bd14f2cd..02d164553f2 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/seedu/exercise/logic/commands/exceptions/CommandException.java @@ -1,7 +1,9 @@ -package seedu.address.logic.commands.exceptions; +package seedu.exercise.logic.commands.exceptions; + +import seedu.exercise.logic.commands.Command; /** - * Represents an error which occurs during execution of a {@link Command}. + * Represents an exception which occurs during execution of a {@link Command}. */ public class CommandException extends Exception { public CommandException(String message) { diff --git a/src/main/java/seedu/exercise/logic/commands/statistic/Statistic.java b/src/main/java/seedu/exercise/logic/commands/statistic/Statistic.java new file mode 100644 index 00000000000..1a64c9f8a94 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/statistic/Statistic.java @@ -0,0 +1,150 @@ +package seedu.exercise.logic.commands.statistic; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; + +import seedu.exercise.model.property.Date; + +/** + * Represents a Statistic with data needed to generate chart. + */ +public class Statistic { + + public static final String MESSAGE_INVALID_CATEGORY = "Category can only be \'exercise\' or \'calories\'"; + public static final String MESSAGE_INVALID_CHART_TYPE = "Chart type can only be \'piechart\' or " + + "\'linechart\' or \'barchart\'"; + + private String category; + private String chart; + private Date startDate; + private Date endDate; + private ArrayList properties; + private ArrayList values; + private double total; + private double average; + + /** + * Every field must be present and not null. + */ + public Statistic(String category, String chart, Date startDate, Date endDate, + ArrayList properties, ArrayList values, double total, double average) { + requireAllNonNull(category, chart, startDate, endDate, properties, values); + this.category = category; + this.chart = chart; + this.startDate = startDate; + this.endDate = endDate; + this.properties = properties; + this.values = values; + this.total = total; + this.average = average; + } + + /** + * Resets the existing data of this {@code Statistic} with {@code newStatistic}. + */ + public void resetData(Statistic newStatistic) { + requireNonNull(newStatistic); + setCategory(newStatistic.getCategory()); + setChart(newStatistic.getChart()); + setStartDate(newStatistic.getStartDate()); + setEndDate(newStatistic.getEndDate()); + setProperties(newStatistic.getProperties()); + setValues(newStatistic.getValues()); + setTotal(newStatistic.getTotal()); + setAverage(newStatistic.getAverage()); + } + + /** + * Calculates and returns the percentage of value. + */ + public static double percentage(double value, double total) { + return value / total * 100; + } + + private void setCategory(String category) { + requireNonNull(category); + this.category = category; + } + + private void setChart(String chart) { + requireNonNull(chart); + this.chart = chart; + } + + private void setStartDate(Date startDate) { + requireNonNull(startDate); + this.startDate = startDate; + } + + private void setEndDate(Date endDate) { + requireNonNull(endDate); + this.endDate = endDate; + } + + private void setProperties(ArrayList properties) { + requireNonNull(properties); + this.properties = properties; + } + + private void setValues(ArrayList values) { + requireNonNull(values); + this.values = values; + } + + private void setTotal(double total) { + requireNonNull(total); + this.total = total; + } + private void setAverage(double average) { + requireNonNull(average); + this.average = average; + } + + public String getCategory() { + return category; + } + + public String getChart() { + return chart; + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public ArrayList getProperties() { + return properties; + } + + public ArrayList getValues() { + return values; + } + + public double getTotal() { + return total; + } + + public double getAverage() { + return average; + } + + @Override + public boolean equals(Object other) { + return this == other + || (other instanceof Statistic + && category.equals(((Statistic) other).category) + && chart.equals(((Statistic) other).chart) + && startDate.equals(((Statistic) other).startDate) + && endDate.equals(((Statistic) other).endDate) + && properties.equals(((Statistic) other).properties) + && values.equals(((Statistic) other).values)) + && total == ((Statistic) other).total + && average == ((Statistic) other).average; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/statistic/StatsCommand.java b/src/main/java/seedu/exercise/logic/commands/statistic/StatsCommand.java new file mode 100644 index 00000000000..d7312d92fe3 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/statistic/StatsCommand.java @@ -0,0 +1,90 @@ +package seedu.exercise.logic.commands.statistic; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CHART; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_END_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_START_DATE; + +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.logic.commands.Command; +import seedu.exercise.logic.commands.CommandResult; +import seedu.exercise.model.Model; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.resource.Exercise; + +//@@author jietung +/** + * Generate statistic with given parameters. + */ +public class StatsCommand extends Command { + + public static final String COMMAND_WORD = "stats"; + + public static final String MESSAGE_STATS_DISPLAY_SUCCESS = "Chart displayed."; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Display statistic of completed exercises\n" + + "Parameters: " + + PREFIX_CATEGORY + "CATEGORY " + + PREFIX_CHART + "CHART TYPE " + + PREFIX_START_DATE + "START DATE " + + PREFIX_END_DATE + "END DATE " + "\t" + + "Example: " + COMMAND_WORD + " " + + PREFIX_CATEGORY + "calories " + + PREFIX_CHART + "barchart " + + PREFIX_START_DATE + "30/03/2019 " + + PREFIX_END_DATE + "05/04/2019 "; + + private static final Logger logger = LogsCenter.getLogger(StatsCommand.class); + private final String chart; + private final String category; + private final Date startDate; + private final Date endDate; + + /** + * Creates a StatsCommand to generate statistic. + */ + public StatsCommand(String chart, String category, Date startDate, Date endDate) { + this.chart = chart; + this.category = category; + this.startDate = startDate; + this.endDate = endDate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + ReadOnlyResourceBook exercises = model.getExerciseBookData(); + StatsFactory statsFactory = new StatsFactory(exercises, chart, category, startDate, endDate); + Statistic statistic = statsFactory.generateStatistic(); + model.setStatistic(statistic); + logger.info("Set statistic in model"); + return new CommandResult(MESSAGE_STATS_DISPLAY_SUCCESS); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other instanceof StatsCommand) { + if (this.startDate == null && this.endDate == null) { + return this.category.equals(((StatsCommand) other).category) + && this.chart.equals(((StatsCommand) other).chart) + && ((StatsCommand) other).startDate == null + && ((StatsCommand) other).endDate == null; + } else { + return this.category.equals(((StatsCommand) other).category) + && this.chart.equals(((StatsCommand) other).chart) + && this.startDate.equals(((StatsCommand) other).startDate) + && this.endDate.equals(((StatsCommand) other).endDate); + } + } + + return false; + } +} diff --git a/src/main/java/seedu/exercise/logic/commands/statistic/StatsFactory.java b/src/main/java/seedu/exercise/logic/commands/statistic/StatsFactory.java new file mode 100644 index 00000000000..dac73d7b0e5 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/commands/statistic/StatsFactory.java @@ -0,0 +1,322 @@ +package seedu.exercise.logic.commands.statistic; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.resource.Exercise; + +/** + * A class to generate Statistic depending on the chart type. + */ +public class StatsFactory { + + private static final String DEFAULT_BAR_CHART = "barchart"; + private static final String DEFAULT_LINE_CHART = "linechart"; + private static final String DEFAULT_PIE_CHART = "piechart"; + private static final String DEFAULT_CALORIES = "calories"; + private static final String DEFAULT_EXERCISE = "exercise"; + + private static final Logger logger = LogsCenter.getLogger(StatsFactory.class); + private ObservableList exercises; + private String chart; + private String category; + private Date startDate; + private Date endDate; + + /** + * Generates a StatsFactory object that can generate Statistic. + * If start date and end date is not given, + * it will set end date to today's date and start date to be one week before. + */ + public StatsFactory(ReadOnlyResourceBook exercises, String chart, String category, + Date startDate, Date endDate) { + this.exercises = exercises.getSortedResourceList(); + this.chart = chart; + this.category = category; + if (startDate == null && endDate == null) { + this.startDate = Date.getOneWeekBeforeToday(); + this.endDate = Date.getToday(); + } else { + this.startDate = startDate; + this.endDate = endDate; + } + } + + /** + * Generates and returns statistic for different chart type. + */ + public Statistic generateStatistic() { + switch(chart) { + + case DEFAULT_PIE_CHART: + logger.info("Chart type is pie chart. Pie Chart statistic generated."); + return generatePieChartStatistic(); + + case DEFAULT_LINE_CHART: + logger.info("Chart type is line chart. Line Chart statistic generated."); + return generateLineChartStatistic(); + + case DEFAULT_BAR_CHART: + logger.info("Chart type is bar chart. Bar Chart statistic generated."); + return generateBarChartStatistic(); + + default: + logger.info("Chart type is not correct. Default chart statistic will be generated."); + return getDefaultStatistic(); + } + } + + /** + * Generate statistic for line chart. + */ + private Statistic generateLineChartStatistic() { + ArrayList dates = Date.getListOfDates(startDate, endDate); + ArrayList values; + + if (category.equals(DEFAULT_EXERCISE)) { + values = exerciseFrequencyByDate(getFilteredExercise(), dates); + } else { + values = caloriesByDate(getFilteredExercise(), dates); + } + + double total = getTotal(values); + double average = getAverage(total); + + return new Statistic(category, chart, startDate, endDate, datesToString(dates), values, total, average); + } + + /** + * Generate statistic for bar chart. + */ + private Statistic generateBarChartStatistic() { + HashMap data; + if (category.equals(DEFAULT_EXERCISE)) { + data = getTotalExerciseFrequency(); + } else { //calories + data = getTotalCaloriesData(); + } + + ArrayList names = hashMapNameToList(data); + ArrayList values = hashMapIntegerToList(data, names); + double total = getTotal(values); + double average = getAverage(total); + + return new Statistic(category, chart, startDate, endDate, names, values, total, average); + } + + /** + * Generate statistic for pie chart. + */ + private Statistic generatePieChartStatistic() { + HashMap data; + if (category.equals(DEFAULT_EXERCISE)) { + data = getTotalExerciseFrequency(); + } else { //calories + data = getTotalCaloriesData(); + } + + ArrayList names = hashMapNameToList(data); + ArrayList values = hashMapIntegerToList(data, names); + removeZeroValues(values, names); + double total = getTotal(values); + double average = getAverage(total); + + return new Statistic(category, chart, startDate, endDate, names, values, total, average); + } + + /** + * Remove zero values from data for pie chart + */ + private void removeZeroValues(ArrayList values, ArrayList properties) { + int size = values.size(); + int i = 0; + while (i < size) { + if (values.get(i) == 0) { + values.remove(i); + properties.remove(i); + size = values.size(); + } else { + i++; + } + } + } + + /** + * Returns the sum of all values. + */ + private double getTotal(ArrayList values) { + double total = 0; + for (int d : values) { + total += d; + } + return total; + } + + /** + * Returns the average value. + */ + private double getAverage(double total) { + int numberOfDays = Date.numberOfDaysBetween(startDate, endDate) + 1; + return total / numberOfDays; + } + + /** + * Compute exercise count with filtered exercises list. + */ + private HashMap getTotalExerciseFrequency() { + ArrayList filteredExercise = getFilteredExercise(); + HashMap data = new HashMap<>(); + + for (Exercise e : filteredExercise) { + Name name = e.getName(); + Unit unit = e.getUnit(); + String nameWithUnit = name.toString() + " (" + unit.toString() + ")"; + int count = 1; + + if (data.containsKey(nameWithUnit)) { + count = data.get(nameWithUnit) + 1; + } + + data.put(nameWithUnit, count); + } + + return data; + } + + /** + * Compute calories with filtered exercises list. + */ + private HashMap getTotalCaloriesData() { + ArrayList filteredExercise = getFilteredExercise(); + HashMap data = new HashMap<>(); + + for (Exercise e : filteredExercise) { + String nameWithUnit = e.getName().toString() + " (" + e.getUnit().toString() + ")"; + int calories = Integer.parseInt(e.getCalories().toString()); + + if (data.containsKey(nameWithUnit)) { + int temp = data.get(nameWithUnit); + calories += temp; + } + + data.put(nameWithUnit, calories); + } + + return data; + } + + /** + * Get all exercises between start and end date. + */ + private ArrayList getFilteredExercise() { + ArrayList filteredExercise = new ArrayList<>(); + for (Exercise e : exercises) { + Date date = e.getDate(); + if (Date.isBetweenStartAndEndDate(date, startDate, endDate)) { + filteredExercise.add(e); + } + } + + return filteredExercise; + } + + /** + * Get list of names from data computed. + */ + private ArrayList hashMapNameToList(HashMap data) { + return new ArrayList<>(data.keySet()); + } + + /** + * Get list of values from data computed. + */ + private ArrayList hashMapIntegerToList(HashMap data, ArrayList names) { + ArrayList values = new ArrayList<>(); + + for (String n : names) { + values.add(data.get(n)); + } + + return values; + } + + /** + * Convert list of dates to list of Strings. + */ + private ArrayList datesToString(ArrayList dates) { + ArrayList list = new ArrayList<>(); + for (Date d : dates) { + list.add(d.toString()); + } + return list; + } + + /** + * Generate a list of given size with all zeroes. + */ + private ArrayList listWithZeroes(int listSize) { + ArrayList list = new ArrayList<>(); + for (int i = 0; i < listSize; i++) { + list.add(0); + } + return list; + } + + /** + * Compute exercise count by dates with filtered exercises list. + */ + private ArrayList exerciseFrequencyByDate(ArrayList exercises, ArrayList dates) { + + int size = dates.size(); + ArrayList values = listWithZeroes(size); + + for (Exercise e : exercises) { + Date date = e.getDate(); + int index = dates.indexOf(date); + int frequency = values.get(index) + 1; + values.set(index, frequency); + } + + return values; + } + + /** + * Compute calories by date with filtered exercises list. + */ + private ArrayList caloriesByDate(ArrayList exercises, ArrayList dates) { + + int size = dates.size(); + ArrayList values = listWithZeroes(size); + + for (Exercise e : exercises) { + Date date = e.getDate(); + int index = dates.indexOf(date); + int calories = Integer.parseInt(e.getCalories().toString()) + values.get(index); + values.set(index, calories); + } + + return values; + } + + /** + * Returns line chart of calories of the most recent 7 days. + */ + public Statistic getDefaultStatistic() { + ArrayList filteredExercise = getFilteredExercise(); + ArrayList dates = Date.getListOfDates(startDate, endDate); + ArrayList properties = datesToString(dates); + ArrayList values = caloriesByDate(filteredExercise, dates); + double total = getTotal(values); + double average = getAverage(total); + logger.info("Generate default statistic"); + return new Statistic(DEFAULT_CALORIES, DEFAULT_LINE_CHART, startDate, endDate, + properties, values, total, average); + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/AddCommandParser.java b/src/main/java/seedu/exercise/logic/parser/AddCommandParser.java new file mode 100644 index 00000000000..643566bcfbc --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/AddCommandParser.java @@ -0,0 +1,120 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CALORIES; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_QUANTITY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_UNIT; +import static seedu.exercise.logic.parser.CliSyntax.getPropertyPrefixesArray; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.AddCommand; +import seedu.exercise.logic.commands.AddExerciseCommand; +import seedu.exercise.logic.commands.AddRegimeCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.resource.Exercise; + +/** + * Parses input arguments and creates a new AddExerciseCommand object + */ +public class AddCommandParser implements Parser { + + public static final String ADD_CATEGORY_EXERCISE = "exercise"; + public static final String ADD_CATEGORY_REGIME = "regime"; + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public AddCommand parse(String args) throws ParseException { + Prefix[] commandPrefixes = getPrefixes(); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, commandPrefixes); + + if (!argMultimap.arePrefixesPresent(PREFIX_CATEGORY) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + } + + String category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get()); + + if (category.equals(ADD_CATEGORY_EXERCISE)) { + return parseExercise(argMultimap); + } + + if (category.equals(ADD_CATEGORY_REGIME)) { + return parseRegime(argMultimap); + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExerciseCommand.MESSAGE_USAGE)); + } + + /** + * Parses arguments and returns AddExerciseCommand for execution + */ + private AddExerciseCommand parseExercise(ArgumentMultimap argMultimap) throws ParseException { + if (!argMultimap.arePrefixesPresent(PREFIX_NAME, PREFIX_DATE, PREFIX_CALORIES, PREFIX_QUANTITY, PREFIX_UNIT) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AddExerciseCommand.MESSAGE_USAGE)); + } + + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get()); + Calories calories = ParserUtil.parseCalories(argMultimap.getValue(PREFIX_CALORIES).get()); + Quantity quantity = ParserUtil.parseQuantity(argMultimap.getValue(PREFIX_QUANTITY).get()); + Unit unit = ParserUtil.parseUnit(argMultimap.getValue(PREFIX_UNIT).get()); + Set muscleList = ParserUtil.parseMuscles(argMultimap.getAllValues(PREFIX_MUSCLE)); + Map customPropertiesMap = + ParserUtil.parseCustomProperties(argMultimap.getAllCustomProperties()); + + Exercise exercise = new Exercise(name, date, calories, quantity, unit, muscleList, customPropertiesMap); + + return new AddExerciseCommand(exercise); + } + + /** + * Parses arguments and returns AddRegimeCommand for execution + */ + private AddRegimeCommand parseRegime(ArgumentMultimap argMultimap) throws ParseException { + if (!argMultimap.arePrefixesPresent(PREFIX_NAME) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddRegimeCommand.MESSAGE_USAGE)); + } + + Name regimeName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + List indexes = ParserUtil.parseIndexes(argMultimap.getAllValues(PREFIX_INDEX)); + + return new AddRegimeCommand(indexes, regimeName); + } + + /** + * Returns an array of prefixes to parse for. + */ + private Prefix[] getPrefixes() { + Set prefixes = new HashSet<>(); + prefixes.addAll(List.of(PREFIX_CATEGORY, PREFIX_INDEX, PREFIX_NAME, PREFIX_DATE, + PREFIX_CALORIES, PREFIX_QUANTITY, PREFIX_UNIT, PREFIX_MUSCLE)); + + // Includes any custom properties that have been added + prefixes.addAll(Arrays.asList(getPropertyPrefixesArray())); + return prefixes.toArray(new Prefix[prefixes.size()]); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/exercise/logic/parser/ArgumentMultimap.java similarity index 58% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/seedu/exercise/logic/parser/ArgumentMultimap.java index 954c8e18f8e..5d18a809819 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/exercise/logic/parser/ArgumentMultimap.java @@ -1,10 +1,16 @@ -package seedu.address.logic.parser; +package seedu.exercise.logic.parser; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Stream; + +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.property.custom.CustomProperty; /** * Stores mapping of prefixes to their respective arguments. @@ -15,7 +21,9 @@ */ public class ArgumentMultimap { - /** Prefixes mapped to their respective arguments**/ + /** + * Prefixes mapped to their respective arguments + **/ private final Map> argMultimap = new HashMap<>(); /** @@ -51,10 +59,36 @@ public List getAllValues(Prefix prefix) { return new ArrayList<>(argMultimap.get(prefix)); } + /** + * Returns a map consisting all of the custom properties that are present together with their respective value. + * If no custom properties are defined or no values are present, this will return an empty map. + * Modifying the returned map will not affect the underlying data structure of the ArgumentMultimap. + */ + public Map getAllCustomProperties() { + Set currentCustomProperties = PropertyBook.getInstance().getCustomProperties(); + Map customPropertiesMap = new TreeMap<>(); + for (CustomProperty property : currentCustomProperties) { + Prefix currentPrefix = property.getPrefix(); + Optional propertyValue = getValue(currentPrefix); + if (propertyValue.isPresent()) { + customPropertiesMap.put(property.getFullName(), propertyValue.get()); + } + } + return customPropertiesMap; + } + /** * Returns the preamble (text before the first valid prefix). Trims any leading/trailing spaces. */ public String getPreamble() { return getValue(new Prefix("")).orElse(""); } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + public boolean arePrefixesPresent(Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> this.getValue(prefix).isPresent()); + } } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/exercise/logic/parser/ArgumentTokenizer.java similarity index 87% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/seedu/exercise/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..6bf09a02f76 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/exercise/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package seedu.exercise.logic.parser; import java.util.ArrayList; import java.util.Arrays; @@ -7,11 +7,11 @@ /** * Tokenizes arguments string of the form: {@code preamble value value ...}
- * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
+ * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
* 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
* 2. Leading and trailing whitespaces of an argument value will be discarded.
* 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} - * in the above example.
+ * in the above example.
*/ public class ArgumentTokenizer { @@ -21,7 +21,7 @@ public class ArgumentTokenizer { * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to tokenize the arguments string with - * @return ArgumentMultimap object that maps prefixes to their arguments + * @return ArgumentMultimap object that maps prefixes to their arguments */ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { List positions = findAllPrefixPositions(argsString, prefixes); @@ -33,12 +33,12 @@ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to find in the arguments string - * @return List of zero-based prefix positions in the given arguments string + * @return List of zero-based prefix positions in the given arguments string */ private static List findAllPrefixPositions(String argsString, Prefix... prefixes) { return Arrays.stream(prefixes) - .flatMap(prefix -> findPrefixPositions(argsString, prefix).stream()) - .collect(Collectors.toList()); + .flatMap(prefix -> findPrefixPositions(argsString, prefix).stream()) + .collect(Collectors.toList()); } /** @@ -62,7 +62,7 @@ private static List findPrefixPositions(String argsString, Prefi * {@code argsString} starting from index {@code fromIndex}. An occurrence * is valid if there is a whitespace before {@code prefix}. Returns -1 if no * such occurrence can be found. - * + *

* E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and * {@code fromIndex} = 0, this method returns -1 as there are no valid * occurrences of "p/" with whitespace before it. However, if @@ -72,7 +72,7 @@ private static List findPrefixPositions(String argsString, Prefi private static int findPrefixPosition(String argsString, String prefix, int fromIndex) { int prefixIndex = argsString.indexOf(" " + prefix, fromIndex); return prefixIndex == -1 ? -1 - : prefixIndex + 1; // +1 as offset for whitespace + : prefixIndex + 1; // +1 as offset for whitespace } /** @@ -82,7 +82,7 @@ private static int findPrefixPosition(String argsString, String prefix, int from * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixPositions Zero-based positions of all prefixes in {@code argsString} - * @return ArgumentMultimap object that maps prefixes to their arguments + * @return ArgumentMultimap object that maps prefixes to their arguments */ private static ArgumentMultimap extractArguments(String argsString, List prefixPositions) { @@ -114,8 +114,8 @@ private static ArgumentMultimap extractArguments(String argsString, List PROPERTY_PREFIXES_SET = new HashSet<>(); + + /** + * Updates the prefixes in {@code PREFIXES_SET} with the input {@code prefixes}. + */ + public static void setPropertyPrefixesSet(Set prefixes) { + PROPERTY_PREFIXES_SET.addAll(prefixes); + PROPERTY_PREFIXES_SET.retainAll(prefixes); + } + + /** + * Returns an array that contains the prefixes in {@code PREFIXES_SET}. + * This prefix array can be used for {@link ArgumentTokenizer#tokenize}. + */ + public static Prefix[] getPropertyPrefixesArray() { + return PROPERTY_PREFIXES_SET.toArray(new Prefix[0]); + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/CustomCommandParser.java b/src/main/java/seedu/exercise/logic/parser/CustomCommandParser.java new file mode 100644 index 00000000000..0f0afa6d569 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/CustomCommandParser.java @@ -0,0 +1,105 @@ +package seedu.exercise.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CUSTOM_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_FULL_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_PARAMETER_TYPE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_REMOVE_CUSTOM; + +import java.util.Optional; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.CustomAddCommand; +import seedu.exercise.logic.commands.CustomCommand; +import seedu.exercise.logic.commands.CustomRemoveCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.custom.CustomProperty; +import seedu.exercise.model.property.custom.ParameterType; + +//@@author weihaw08 +/** + * Parses input arguments and creates a new CustomAddCommand object. + */ +public class CustomCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the CustomAddCommand and + * returns a {@code CustomAddCommand} object for execution. + * + * @param args the arguments for a custom command + * @return a {@code CustomAddCommand} object representing the command to be executed + * @throws ParseException if the user does not conform to the expected format + */ + public CustomCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CUSTOM_NAME, PREFIX_FULL_NAME, + PREFIX_PARAMETER_TYPE, PREFIX_REMOVE_CUSTOM, PREFIX_INDEX); + + if (areValidPrefixesForAddCustom(argMultimap)) { + return parseCustomAdd(argMultimap); + } else if (isValidPrefixForRemoveCustom(argMultimap)) { + return parseCustomRemove(argMultimap); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CustomCommand.MESSAGE_USAGE)); + } + } + + /** + * Returns true if the prefixes required for adding a new custom property are all present. + */ + private boolean areAddPrefixesPresent(ArgumentMultimap argMultimap) { + return argMultimap.arePrefixesPresent(PREFIX_CUSTOM_NAME, PREFIX_FULL_NAME, PREFIX_PARAMETER_TYPE); + } + + /** + * Returns true if the prefix required for removing a custom property is present. + */ + private boolean isRemovePrefixPresent(ArgumentMultimap argMultimap) { + return argMultimap.arePrefixesPresent(PREFIX_REMOVE_CUSTOM); + } + + /** + * Returns true if the only prefixes present are the prefixes required for adding a new custom property. + */ + private boolean areValidPrefixesForAddCustom(ArgumentMultimap argMultimap) { + return areAddPrefixesPresent(argMultimap) && !isRemovePrefixPresent(argMultimap) + && argMultimap.getPreamble().isEmpty(); + } + + /** + * Returns true if the only prefix present for the custom command is the prefix required for removing a + * custom property. + */ + private boolean isValidPrefixForRemoveCustom(ArgumentMultimap argMultimap) { + return isRemovePrefixPresent(argMultimap) + && argMultimap.getPreamble().isEmpty() + && !argMultimap.arePrefixesPresent(PREFIX_CUSTOM_NAME) + && !argMultimap.arePrefixesPresent(PREFIX_FULL_NAME) + && !argMultimap.arePrefixesPresent(PREFIX_PARAMETER_TYPE); + } + + /** + * Returns a new {@code CustomAddCommand} based on the values in the {@code argMultimap}. + */ + private CustomAddCommand parseCustomAdd(ArgumentMultimap argMultimap) throws ParseException { + Prefix prefix = ParserUtil.parsePrefixName(argMultimap.getValue(PREFIX_CUSTOM_NAME).get()); + String fullName = ParserUtil.parseFullName(argMultimap.getValue(PREFIX_FULL_NAME).get()); + ParameterType paramType = ParserUtil.parseParameterType(argMultimap.getValue(PREFIX_PARAMETER_TYPE).get()); + CustomProperty customProperty = new CustomProperty(prefix, fullName, paramType); + return new CustomAddCommand(customProperty); + } + + /** + * Returns a new {@code CustomRemoveCommand} based on the values in the {@code argMultimap}. + */ + private CustomRemoveCommand parseCustomRemove(ArgumentMultimap argMultimap) throws ParseException { + String fullName = ParserUtil.parseFullName(argMultimap.getValue(PREFIX_REMOVE_CUSTOM).get()); + Optional index = Optional.empty(); + if (argMultimap.arePrefixesPresent(PREFIX_INDEX)) { + index = Optional.of(ParserUtil.parseIndex(argMultimap.getValue(PREFIX_INDEX).get())); + } + return new CustomRemoveCommand(fullName, index); + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/exercise/logic/parser/DeleteCommandParser.java new file mode 100644 index 00000000000..ce1c337db37 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/DeleteCommandParser.java @@ -0,0 +1,83 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.List; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.DeleteCommand; +import seedu.exercise.logic.commands.DeleteExerciseCommand; +import seedu.exercise.logic.commands.DeleteRegimeCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Name; + +/** + * Parses input arguments and creates a new DeleteExerciseCommand object + */ +public class DeleteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns a DeleteCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_NAME, PREFIX_INDEX); + + if (!argMultimap.arePrefixesPresent(PREFIX_CATEGORY) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + String category = ParserUtil.parseCategory(argMultimap.getValue(PREFIX_CATEGORY).get()); + + if (category.equals("exercise")) { + return parseExercise(argMultimap); + } + + if (category.equals("regime")) { + return parseRegime(argMultimap); + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + /** + * Parses arguments and returns DeleteExerciseCommand for execution + */ + private DeleteExerciseCommand parseExercise(ArgumentMultimap argMultimap) throws ParseException { + if (!argMultimap.arePrefixesPresent(PREFIX_INDEX)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteExerciseCommand.MESSAGE_USAGE)); + } + + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_INDEX).get()); + return new DeleteExerciseCommand(index); + } + + /** + * Parses arguments and returns DeleteRegimeCommand for execution + */ + private DeleteRegimeCommand parseRegime(ArgumentMultimap argMultimap) throws ParseException { + if (!argMultimap.arePrefixesPresent(PREFIX_NAME)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteRegimeCommand.MESSAGE_USAGE)); + } + + Name regimeName = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + + // index present, delete exercise in regime + if (argMultimap.arePrefixesPresent(PREFIX_INDEX)) { + + List indexes = ParserUtil.parseIndexes(argMultimap.getAllValues(PREFIX_INDEX)); + return new DeleteRegimeCommand(regimeName, indexes); + + } else { //index not present delete regime + return new DeleteRegimeCommand(regimeName, null); + } + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/EditCommandParser.java b/src/main/java/seedu/exercise/logic/parser/EditCommandParser.java new file mode 100644 index 00000000000..ddce7845340 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/EditCommandParser.java @@ -0,0 +1,133 @@ +package seedu.exercise.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CALORIES; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_QUANTITY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_UNIT; +import static seedu.exercise.logic.parser.CliSyntax.getPropertyPrefixesArray; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.EditCommand; +import seedu.exercise.logic.commands.builder.EditExerciseBuilder; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Muscle; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditCommandParser implements Parser { + + private final Logger logger = LogsCenter.getLogger(EditCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public EditCommand parse(String args) throws ParseException { + requireNonNull(args); + Prefix[] commandPrefixes = getPrefixes(); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, commandPrefixes); + + if (!argMultimap.arePrefixesPresent(PREFIX_INDEX) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_INDEX).get()); + + EditExerciseBuilder editExerciseBuilder = new EditExerciseBuilder(); + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editExerciseBuilder.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editExerciseBuilder.setDate(ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get())); + } + if (argMultimap.getValue(PREFIX_CALORIES).isPresent()) { + editExerciseBuilder.setCalories(ParserUtil.parseCalories(argMultimap.getValue(PREFIX_CALORIES).get())); + } + if (argMultimap.getValue(PREFIX_QUANTITY).isPresent()) { + editExerciseBuilder.setQuantity(ParserUtil.parseQuantity(argMultimap.getValue(PREFIX_QUANTITY).get())); + } + if (argMultimap.getValue(PREFIX_UNIT).isPresent()) { + editExerciseBuilder.setUnit(ParserUtil.parseUnit(argMultimap.getValue(PREFIX_UNIT).get())); + } + + parseMusclesForEdit(argMultimap.getAllValues(PREFIX_MUSCLE)).ifPresent(editExerciseBuilder::setMuscles); + + + parseCustomPropertiesForEdit(argMultimap.getAllCustomProperties()) + .ifPresent(editExerciseBuilder::setCustomProperties); + + + if (!editExerciseBuilder.isAnyFieldEdited()) { + logger.info("None of the exercise filed is edited."); + throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); + } + + return new EditCommand(index, editExerciseBuilder); + } + + /** + * Parses {@code Collection muscles} into a {@code Set} if {@code muscles} is non-empty. + * If {@code muscles} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero muscles. + */ + private Optional> parseMusclesForEdit(Collection muscles) throws ParseException { + requireNonNull(muscles); + + if (muscles.isEmpty()) { + return Optional.empty(); + } + Collection tagSet = muscles.size() == 1 && muscles.contains("") ? Collections.emptySet() : muscles; + return Optional.of(ParserUtil.parseMuscles(tagSet)); + } + + /** + * Parses {@code Map customProperties} into a {@code Map} if + * {@code customProperties} is non-empty. + * If {@code customProperties} is empty, a {@code Optional.empty()} is returned instead. + * + * @throws ParseException if the user input does not conform the expected format + */ + private Optional> parseCustomPropertiesForEdit(Map customProperties) + throws ParseException { + requireNonNull(customProperties); + + if (customProperties.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(ParserUtil.parseCustomProperties(customProperties)); + } + + /** + * Returns an array of prefixes to parse for. + */ + private Prefix[] getPrefixes() { + Set prefixes = new HashSet<>(); + prefixes.addAll(List.of(PREFIX_INDEX, PREFIX_NAME, PREFIX_DATE, + PREFIX_CALORIES, PREFIX_QUANTITY, PREFIX_UNIT, PREFIX_MUSCLE)); + + prefixes.addAll(Arrays.asList(getPropertyPrefixesArray())); + return prefixes.toArray(new Prefix[prefixes.size()]); + } + + +} diff --git a/src/main/java/seedu/exercise/logic/parser/ExerciseBookParser.java b/src/main/java/seedu/exercise/logic/parser/ExerciseBookParser.java new file mode 100644 index 00000000000..3605290a0b9 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/ExerciseBookParser.java @@ -0,0 +1,108 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.exercise.logic.commands.AddExerciseCommand; +import seedu.exercise.logic.commands.ClearCommand; +import seedu.exercise.logic.commands.Command; +import seedu.exercise.logic.commands.CustomCommand; +import seedu.exercise.logic.commands.DeleteExerciseCommand; +import seedu.exercise.logic.commands.EditCommand; +import seedu.exercise.logic.commands.ExitCommand; +import seedu.exercise.logic.commands.HelpCommand; +import seedu.exercise.logic.commands.ListCommand; +import seedu.exercise.logic.commands.RedoCommand; +import seedu.exercise.logic.commands.ResolveCommand; +import seedu.exercise.logic.commands.ScheduleCommand; +import seedu.exercise.logic.commands.SelectCommand; +import seedu.exercise.logic.commands.SuggestCommand; +import seedu.exercise.logic.commands.UndoCommand; +import seedu.exercise.logic.commands.ViewCustomCommand; +import seedu.exercise.logic.commands.statistic.StatsCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class ExerciseBookParser { + + /** + * Used for initial separation of command word and args. + */ + private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + + case AddExerciseCommand.COMMAND_WORD: + return new AddCommandParser().parse(arguments); + + case EditCommand.COMMAND_WORD: + return new EditCommandParser().parse(arguments); + + case DeleteExerciseCommand.COMMAND_WORD: + return new DeleteCommandParser().parse(arguments); + + case ClearCommand.COMMAND_WORD: + return new ClearCommand(); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + + case ListCommand.COMMAND_WORD: + return new ListCommandParser().parse(arguments); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case ScheduleCommand.COMMAND_WORD: + return new ScheduleCommandParser().parse(arguments); + + case CustomCommand.COMMAND_WORD: + return new CustomCommandParser().parse(arguments); + + case ViewCustomCommand.COMMAND_WORD: + return new ViewCustomCommand(); + + case SuggestCommand.COMMAND_WORD: + return new SuggestCommandParser().parse(arguments); + + case StatsCommand.COMMAND_WORD: + return new StatsCommandParser().parse(arguments); + + case ResolveCommand.COMMAND_WORD: + return new ResolveCommandParser().parse(arguments); + + case SelectCommand.COMMAND_WORD: + return new SelectCommandParser().parse(arguments); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + +} diff --git a/src/main/java/seedu/exercise/logic/parser/ListCommandParser.java b/src/main/java/seedu/exercise/logic/parser/ListCommandParser.java new file mode 100644 index 00000000000..55df1872729 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/ListCommandParser.java @@ -0,0 +1,33 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; + +import seedu.exercise.logic.commands.ListCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.ui.ListResourceType; + +/** + * Parses input arguments and creates a new SuggestCommand object + */ +public class ListCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SuggestCommand + * and returns a SuggestCommand object for execution. + * + * @throws ParseException if the user does not conform to the expected format + */ + public ListCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY); + + if (!argMultimap.arePrefixesPresent(PREFIX_CATEGORY) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListCommand.MESSAGE_USAGE)); + } + + ListResourceType listResourceType = ParserUtil + .parseListResourceType(argMultimap.getValue(PREFIX_CATEGORY).get()); + return new ListCommand(listResourceType); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/exercise/logic/parser/Parser.java similarity index 71% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/seedu/exercise/logic/parser/Parser.java index d6551ad8e3f..c3df4170ea0 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/exercise/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package seedu.exercise.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.exercise.logic.commands.Command; +import seedu.exercise.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. @@ -10,6 +10,7 @@ public interface Parser { /** * Parses {@code userInput} into a command and returns it. + * * @throws ParseException if {@code userInput} does not conform the expected format */ T parse(String userInput) throws ParseException; diff --git a/src/main/java/seedu/exercise/logic/parser/ParserUtil.java b/src/main/java/seedu/exercise/logic/parser/ParserUtil.java new file mode 100644 index 00000000000..c118dbdee05 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/ParserUtil.java @@ -0,0 +1,473 @@ +package seedu.exercise.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_TYPE; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.logic.parser.AddCommandParser.ADD_CATEGORY_EXERCISE; +import static seedu.exercise.logic.parser.AddCommandParser.ADD_CATEGORY_REGIME; +import static seedu.exercise.logic.parser.SuggestCommandParser.SUGGEST_TYPE_BASIC; +import static seedu.exercise.logic.parser.SuggestCommandParser.SUGGEST_TYPE_POSSIBLE; +import static seedu.exercise.logic.parser.predicate.PredicateUtil.OPERATION_TYPE_AND; +import static seedu.exercise.logic.parser.predicate.PredicateUtil.OPERATION_TYPE_OR; +import static seedu.exercise.ui.ListResourceType.LIST_RESOURCE_TYPE_CONSTRAINTS; +import static seedu.exercise.ui.ListResourceType.LIST_TYPE_EXERCISE; +import static seedu.exercise.ui.ListResourceType.LIST_TYPE_REGIME; +import static seedu.exercise.ui.ListResourceType.LIST_TYPE_SCHEDULE; +import static seedu.exercise.ui.ListResourceType.LIST_TYPE_SUGGESTION; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Predicate; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.commons.util.StringUtil; +import seedu.exercise.logic.commands.SuggestCommand; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.logic.parser.predicate.PredicateUtil; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.property.custom.CustomProperty; +import seedu.exercise.model.property.custom.ParameterType; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.ui.ListResourceType; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil { + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(Index.MESSAGE_CONSTRAINTS); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses {@code Collection indexes} into a {@code ArrayList}. + */ + public static ArrayList parseIndexes(Collection indexes) throws ParseException { + requireNonNull(indexes); + final ArrayList indexSet = new ArrayList<>(); + for (String index : indexes) { + indexSet.add(parseIndex(index)); + } + return indexSet; + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.MESSAGE_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String unit} into a {@code Unit}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Unit parseUnit(String unit) throws ParseException { + requireNonNull(unit); + String trimmedUnit = unit.trim(); + if (!Unit.isValidUnit(trimmedUnit)) { + throw new ParseException(Unit.MESSAGE_CONSTRAINTS); + } + return new Unit(trimmedUnit); + } + + /** + * Parses a {@code String calories} into a {@code Calories}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code calories} is invalid. + */ + public static Calories parseCalories(String calories) throws ParseException { + requireNonNull(calories); + String trimmedCalories = calories.trim(); + if (!Calories.isValidCalories(trimmedCalories)) { + throw new ParseException(Calories.MESSAGE_CONSTRAINTS); + } + return new Calories(trimmedCalories); + } + + /** + * Parses a {@code String quantity} into an {@code Quantity}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code quantity} is invalid. + */ + public static Quantity parseQuantity(String quantity) throws ParseException { + requireNonNull(quantity); + String trimmedQuantity = quantity.trim(); + if (!Quantity.isValidQuantity(trimmedQuantity)) { + throw new ParseException(Quantity.MESSAGE_CONSTRAINTS); + } + return new Quantity(trimmedQuantity); + } + + /** + * Parses a {@code String date} into an {@code Date}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code date} is invalid. + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + if (!Date.isValidDate(trimmedDate)) { + throw new ParseException(Date.MESSAGE_CONSTRAINTS); + } + return new Date(trimmedDate); + } + + /** + * Parses a {@code String endDate} into an {@code Date}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code endDate} is invalid. + */ + public static Date parseEndDate(Date startDate, String endDate) throws ParseException { + requireAllNonNull(startDate, endDate); + String trimmedEndDate = endDate.trim(); + parseDate(trimmedEndDate); + if (!Date.isEndDateAfterStartDate(startDate.toString(), trimmedEndDate)) { + throw new ParseException(Date.MESSAGE_INVALID_END_DATE); + } + return new Date(trimmedEndDate); + } + + /** + * Parses a {@code String muscle} into a {@code Muscle}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code muscle} is invalid. + */ + public static Muscle parseMuscle(String muscle) throws ParseException { + requireNonNull(muscle); + String trimmedMuscle = muscle.trim(); + if (!Muscle.isValidMuscleName(trimmedMuscle)) { + throw new ParseException(Muscle.MESSAGE_CONSTRAINTS); + } + return new Muscle(trimmedMuscle); + } + + /** + * Parses {@code Collection muscles} into a {@code Set}. + */ + public static Set parseMuscles(Collection muscles) throws ParseException { + requireNonNull(muscles); + final Set muscleSet = new HashSet<>(); + for (String muscleName : muscles) { + muscleSet.add(parseMuscle(muscleName)); + } + return muscleSet; + } + + /** + * Parses a {@code category} into a String. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code category} is invalid + */ + public static String parseCategory(String category) throws ParseException { + requireNonNull(category); + String trimmedCategory = category.trim(); + if (!trimmedCategory.equals(ADD_CATEGORY_EXERCISE) + && !trimmedCategory.equals(ADD_CATEGORY_REGIME)) { + throw new ParseException("Category can only be \'" + ADD_CATEGORY_EXERCISE + "\'" + + " or \'" + ADD_CATEGORY_REGIME + "\'"); + } + return trimmedCategory; + } + + /** + * Parses a {@code listType} into a String. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code listType} is invalid + */ + public static ListResourceType parseListResourceType(String listType) throws ParseException { + requireNonNull(listType); + String trimmedCategory = listType.trim(); + switch (trimmedCategory) { + case LIST_TYPE_EXERCISE: + return ListResourceType.EXERCISE; + + case LIST_TYPE_REGIME: + return ListResourceType.REGIME; + + case LIST_TYPE_SCHEDULE: + return ListResourceType.SCHEDULE; + + case LIST_TYPE_SUGGESTION: + return ListResourceType.SUGGESTION; + + default: + throw new ParseException(LIST_RESOURCE_TYPE_CONSTRAINTS); + } + } + + /** + * Parses and trims all of the values in {@code Map customProperties}. + * + * @throws ParseException if any of the keys present in {@code customProperties} is invalid. + */ + public static Map parseCustomProperties(Map customProperties) + throws ParseException { + requireNonNull(customProperties); + Set allCustomProperties = PropertyBook.getInstance().getCustomProperties(); + final Map customPropertiesMap = new TreeMap<>(); + for (CustomProperty property : allCustomProperties) { + String propertyName = property.getFullName(); + if (customProperties.containsKey(propertyName)) { + String rawResult = customProperties.get(propertyName); + String trimmedResult = parseCustomProperty(property, rawResult); + customPropertiesMap.put(propertyName, trimmedResult); + } + } + return customPropertiesMap; + } + + /** + * Trims, validates and formats the full name of a {@code String fullName}. + * + * @param fullName the full name of a custom property + * @return a trimmed full name of a custom property in Start Case style. + * @throws ParseException if the given full name is invalid. + */ + public static String parseFullName(String fullName) throws ParseException { + requireNonNull(fullName); + String trimmedFullName = fullName.trim(); + if (!CustomProperty.isValidFullName(trimmedFullName)) { + throw new ParseException(CustomProperty.FULL_NAME_CONSTRAINTS); + } + return toStartCase(trimmedFullName); + } + + /** + * Parses and trims the leading and trailing whitespaces of {@code String prefixName}. + * + * @param prefixName the intended prefix name for a custom property + * @return a {@code Prefix} object containing the trimmed prefix name for a custom property + * @throws ParseException if the given prefix name is invalid + */ + public static Prefix parsePrefixName(String prefixName) throws ParseException { + requireNonNull(prefixName); + String trimmedPrefixName = prefixName.trim(); + if (!CustomProperty.isValidPrefixName(trimmedPrefixName)) { + throw new ParseException(CustomProperty.PREFIX_NAME_CONSTRAINTS); + } + return new Prefix(prefixName + "/"); + } + + /** + * Parses a {@code String parameterType} into a {@code ParameterType}. + * + * @param parameterType the intended parameter type for a custom property + * @return a {@code ParameterType} that corresponds with the intended parameter type + * @throws ParseException if the intended parameter type is invalid + */ + public static ParameterType parseParameterType(String parameterType) throws ParseException { + requireNonNull(parameterType); + String trimmedParameterType = parameterType.trim(); + + if (!ParameterType.isValidParameterType(trimmedParameterType)) { + throw new ParseException(ParameterType.PARAMETER_CONSTRAINTS); + } + + String numParam = ParameterType.NUMBER.getParameterName(); + String textParam = ParameterType.TEXT.getParameterName(); + + if (trimmedParameterType.equals(numParam)) { + return ParameterType.NUMBER; + } else if (trimmedParameterType.equals(textParam)) { + return ParameterType.TEXT; + } else { + return ParameterType.DATE; + } + } + + /** + * Parses a {@code String suggestType} into a String. + * Leading and trailing whitespaces will be trimmed. + * + * @param suggestType the intended suggest type + * @return a trimmed suggest type of a suggest command + * @throws ParseException if the intended suggest type is invalid + */ + public static String parseSuggestType(String suggestType) throws ParseException { + requireNonNull(suggestType); + String trimmedSuggestType = suggestType.trim(); + if (!trimmedSuggestType.equals(SUGGEST_TYPE_BASIC) && !trimmedSuggestType.equals(SUGGEST_TYPE_POSSIBLE)) { + throw new ParseException( + String.format(MESSAGE_INVALID_TYPE, "Suggest type", SUGGEST_TYPE_BASIC, SUGGEST_TYPE_POSSIBLE)); + } + return trimmedSuggestType; + } + + /** + * Parses a {@code String operationType} into a boolean. + * + * @param operationType the intended operation type + * @return a {@code boolean} representing the whether the operation type is "and" or "or". + * @throws ParseException if the intended operation type is invalid + */ + public static boolean parseOperationType(String operationType) throws ParseException { + requireNonNull(operationType); + String trimmedOperationType = operationType.trim(); + + if (trimmedOperationType.equals(OPERATION_TYPE_AND)) { + return true; + } + + if (trimmedOperationType.equals(OPERATION_TYPE_OR)) { + return false; + } + + throw new ParseException(PredicateUtil.OPERATION_TYPE_CONSTRAINTS); + } + + /** + * Parses {@code Set muscles}, {@code Map customProperties} and {@code boolean isStrict} + * into a {@code Predicate}. + */ + public static Predicate parsePredicate( + Set muscles, Map customProperties, boolean isStrict) throws ParseException { + requireNonNull(muscles); + requireNonNull(customProperties); + + if (muscles.isEmpty() && customProperties.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SuggestCommand.MESSAGE_USAGE)); + } + + return PredicateUtil.getExercisePredicate(muscles, customProperties, isStrict); + } + + + /** + * Formats a single word by capitalising the first letter and setting the remaining + * as lowercase. + */ + private static String capitaliseSingleWord(String word) { + String capitalisedFirstLetter = word.substring(0, 1).toUpperCase(); + String lowercaseRemaining = word.substring(1).toLowerCase(); + return capitalisedFirstLetter + lowercaseRemaining; + } + + /** + * Formats a group of words by capitalising the first letter and setting the remaining + * as lower case for each word present. Any additional spaces between 2 words are now + * reduced to a single space. + */ + private static String toStartCase(String words) { + String[] tokens = words.split("\\s+"); + StringBuilder builder = new StringBuilder(); + for (String token : tokens) { + builder.append(capitaliseSingleWord(token)).append(" "); + } + return builder.toString().stripTrailing(); + } + + /** + * Parses and trims {@code String propertyValue} based on the {@code CustomProperty}. + */ + private static String parseCustomProperty(CustomProperty property, String propertyValue) + throws ParseException { + requireNonNull(property, propertyValue); + ParameterType paramType = property.getParameterType(); + if (paramType.equals(ParameterType.DATE)) { + return parseDate(propertyValue).toString(); + } else if (paramType.equals(ParameterType.NUMBER)) { + return parseNumber(propertyValue); + } else { + return parseText(propertyValue); + } + } + + /** + * Parses and trims the leading and trailing white spaces of {@code String text}. + * + * @throws ParseException if the given {@code text} is invalid. + */ + private static String parseText(String text) throws ParseException { + requireNonNull(text); + String trimmedText = text.trim(); + if (!ParameterType.isValidText(trimmedText)) { + throw new ParseException(ParameterType.TEXT_CONSTRAINTS); + } + return trimmedText; + } + + /** + * Parses and trims the leading and trailing white spaces of {@code String number}. + * + * @throws ParseException if the given {@code number} is invalid. + */ + private static String parseNumber(String number) throws ParseException { + requireNonNull(number); + String trimmedNumber = number.trim(); + if (!ParameterType.isValidNumber(trimmedNumber)) { + throw new ParseException(ParameterType.NUMBER_CONSTRAINTS); + } + return trimmedNumber; + } + + /** + * Parses a {@code String chart} into a String. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code chart} is invalid + */ + public static String parseChart(String chart) throws ParseException { + requireNonNull(chart); + String trimmedCategory = chart.trim(); + if (!trimmedCategory.equals("piechart") && !trimmedCategory.equals("linechart") + && !trimmedCategory.equals("barchart")) { + throw new ParseException(Statistic.MESSAGE_INVALID_CHART_TYPE); + } + return trimmedCategory; + } + + /** + * Parses a {@code String statisticCategory} into a String. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code statisticCategory} is invalid + */ + public static String parseStatisticCategory(String statisticCategory) throws ParseException { + requireNonNull(statisticCategory); + String trimmedCategory = statisticCategory.trim(); + if (!trimmedCategory.equals("exercise") && !trimmedCategory.equals("calories")) { + throw new ParseException(Statistic.MESSAGE_INVALID_CATEGORY); + } + return trimmedCategory; + } +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/seedu/exercise/logic/parser/Prefix.java similarity index 65% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/seedu/exercise/logic/parser/Prefix.java index c859d5fa5db..9929bdc2d7f 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/seedu/exercise/logic/parser/Prefix.java @@ -1,8 +1,11 @@ -package seedu.address.logic.parser; +package seedu.exercise.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. - * E.g. 't/' in 'add James t/ friend'. + * E.g. 'm/' in 'add Run m/Leg'. + *

+ * The name of the prefix refers to the text before '/'. + * E.g. the name of 'm/' is 'm'. */ public class Prefix { private final String prefix; @@ -11,10 +14,20 @@ public Prefix(String prefix) { this.prefix = prefix; } + /** + * Returns the name of the prefix together with the "/". + */ public String getPrefix() { return prefix; } + /** + * Returns the name of the prefix. + */ + public String getPrefixName() { + return prefix.substring(0, prefix.length() - 1); + } + public String toString() { return getPrefix(); } diff --git a/src/main/java/seedu/exercise/logic/parser/ResolveCommandParser.java b/src/main/java/seedu/exercise/logic/parser/ResolveCommandParser.java new file mode 100644 index 00000000000..37df99b5998 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/ResolveCommandParser.java @@ -0,0 +1,49 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CONFLICT_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; + +import java.util.List; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.ResolveCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Name; + +//@@author t-cheepeng +/** + * Parses an input and returns a {@code ResolveCommand} + */ +public class ResolveCommandParser implements Parser { + + @Override + public ResolveCommand parse(String userInput) throws ParseException { + ArgumentMultimap commandMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_NAME, PREFIX_INDEX, PREFIX_CONFLICT_INDEX); + + checkValidResolveCommand(commandMultimap); + + return parseResolveCommand(commandMultimap); + } + + private void checkValidResolveCommand(ArgumentMultimap multimap) throws ParseException { + if (!(multimap.arePrefixesPresent(PREFIX_NAME) && multimap.getPreamble().isEmpty())) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ResolveCommand.MESSAGE_USAGE)); + } + } + + /** + * Parses a valid resolve command. + * + * The list of indexes are empty if no indexes are provided by the user. + */ + private ResolveCommand parseResolveCommand(ArgumentMultimap multimap) throws ParseException { + Name scheduledOrConflicting = ParserUtil.parseName(multimap.getValue(PREFIX_NAME).get()); + List scheduledIndex = ParserUtil.parseIndexes(multimap.getAllValues(PREFIX_INDEX)); + List conflictingIndex = ParserUtil.parseIndexes(multimap.getAllValues(PREFIX_CONFLICT_INDEX)); + + return new ResolveCommand(scheduledOrConflicting, scheduledIndex, conflictingIndex); + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/ScheduleCommandParser.java b/src/main/java/seedu/exercise/logic/parser/ScheduleCommandParser.java new file mode 100644 index 00000000000..a851991a0b6 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/ScheduleCommandParser.java @@ -0,0 +1,80 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; + +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.ScheduleCommand; +import seedu.exercise.logic.commands.ScheduleCompleteCommand; +import seedu.exercise.logic.commands.ScheduleRegimeCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Name; + +//@@author t-cheepeng +/** + * Parses input arguments and creates a new ScheduleCommand object + */ +public class ScheduleCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the + * ScheduleCommand and returns a ScheduleCommand object for execution. + * + * @throws ParseException if the user input is not the expected format + */ + @Override + public ScheduleCommand parse(String userInput) throws ParseException { + ArgumentMultimap regimeCommandMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_NAME, PREFIX_DATE); + ArgumentMultimap completeCommandMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_INDEX); + + checkValidScheduleCommand(regimeCommandMultimap, completeCommandMultimap); + + if (areScheduleRegimePrefixesPresent(regimeCommandMultimap)) { + return parseScheduleRegimeCommand(regimeCommandMultimap); + } else { + return parseScheduleCompleteCommand(completeCommandMultimap); + } + } + + /** + * Parses a valid {@code ScheduleRegimeCommand}. + */ + private ScheduleCommand parseScheduleRegimeCommand(ArgumentMultimap regimeMultimap) throws ParseException { + Name regimeName = ParserUtil.parseName(regimeMultimap.getValue(PREFIX_NAME).get()); + Date date = ParserUtil.parseDate(regimeMultimap.getValue(PREFIX_DATE).get()); + + return new ScheduleRegimeCommand(regimeName, date); + } + + private ScheduleCommand parseScheduleCompleteCommand(ArgumentMultimap completeMultimap) throws ParseException { + Index index = ParserUtil.parseIndex(completeMultimap.getValue(PREFIX_INDEX).get()); + + return new ScheduleCompleteCommand(index); + } + + /** + * Checks if user input is either {@code ScheduleRegimeCommand} or {@code ScheduleCompleteCommand}. + * + * @throws ParseException when prefix for either commands are missing. + */ + private void checkValidScheduleCommand(ArgumentMultimap regimeCommand, ArgumentMultimap completeCommand) + throws ParseException { + if (!areScheduleCompletePrefixesPresent(completeCommand) && !areScheduleRegimePrefixesPresent(regimeCommand)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE)); + } + } + + private static boolean areScheduleRegimePrefixesPresent(ArgumentMultimap multimap) { + return multimap.arePrefixesPresent(PREFIX_NAME, PREFIX_DATE) && multimap.getPreamble().isEmpty(); + } + + private static boolean areScheduleCompletePrefixesPresent(ArgumentMultimap multimap) { + return multimap.arePrefixesPresent(PREFIX_INDEX) && multimap.getPreamble().isEmpty(); + } + +} diff --git a/src/main/java/seedu/exercise/logic/parser/SelectCommandParser.java b/src/main/java/seedu/exercise/logic/parser/SelectCommandParser.java new file mode 100644 index 00000000000..974bbf45e98 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/SelectCommandParser.java @@ -0,0 +1,46 @@ +package seedu.exercise.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; + +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.SelectCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.ui.ListResourceType; + +//@@author weihaw08 +/** + * Parses input arguments and creates a new SelectCommand object. + */ +public class SelectCommandParser implements Parser { + + private final Logger logger = LogsCenter.getLogger(SelectCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the SelectCommand + * and returns a SelectCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public SelectCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_INDEX); + + if (!argMultimap.arePrefixesPresent(PREFIX_CATEGORY, PREFIX_INDEX) + || !argMultimap.getPreamble().isEmpty()) { + logger.info("Not all prefixes for select command are present."); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE)); + } + + ListResourceType listResourceType = ParserUtil + .parseListResourceType(argMultimap.getValue(PREFIX_CATEGORY).get()); + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_INDEX).get()); + return new SelectCommand(index, listResourceType); + } + +} diff --git a/src/main/java/seedu/exercise/logic/parser/StatsCommandParser.java b/src/main/java/seedu/exercise/logic/parser/StatsCommandParser.java new file mode 100644 index 00000000000..382741e359b --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/StatsCommandParser.java @@ -0,0 +1,68 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CHART; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_END_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_START_DATE; + +import seedu.exercise.logic.commands.statistic.StatsCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Date; + +//@@author jietung +/** + * Parses input arguments and creates a new StatsCommand object. + */ +public class StatsCommandParser implements Parser { + + public static final String MESSAGE_INVALID_DATE_RANGE = "Start date and end date are too far apart. " + + "Maximum range is 31 days."; + public static final String MESSAGE_INVALID_COMMAND = "Both start date and end date must be present"; + + /** + * Parses the given {@code String} of arguments in the context of the StatsCommand + * and returns a StatsCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + public StatsCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_CATEGORY, PREFIX_CHART, + PREFIX_START_DATE, PREFIX_END_DATE); + + if (!argMultimap.arePrefixesPresent(PREFIX_CATEGORY, PREFIX_CHART) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatsCommand.MESSAGE_USAGE)); + } + + String category = ParserUtil.parseStatisticCategory(argMultimap.getValue(PREFIX_CATEGORY).get()); + String chart = ParserUtil.parseChart(argMultimap.getValue(PREFIX_CHART).get()); + Date startDate = null; + Date endDate = null; + + //date provided + if (argMultimap.arePrefixesPresent(PREFIX_START_DATE, PREFIX_END_DATE)) { //both dates present + + startDate = ParserUtil.parseDate(argMultimap.getValue(PREFIX_START_DATE).get()); + endDate = ParserUtil.parseEndDate(startDate, argMultimap.getValue(PREFIX_END_DATE).get()); + + int numberOfDaysApart = Date.numberOfDaysBetween(startDate, endDate); + + if (numberOfDaysApart > 31) { + throw new ParseException(MESSAGE_INVALID_DATE_RANGE); + } + + } else if (argMultimap.arePrefixesPresent(PREFIX_START_DATE) + && !argMultimap.arePrefixesPresent(PREFIX_END_DATE)) { //only start date present + + throw new ParseException(MESSAGE_INVALID_COMMAND); + + } else if (argMultimap.arePrefixesPresent(PREFIX_END_DATE) + && !argMultimap.arePrefixesPresent(PREFIX_START_DATE)) { //only end date present + + throw new ParseException(MESSAGE_INVALID_COMMAND); + + } + + return new StatsCommand(chart, category, startDate, endDate); + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/SuggestCommandParser.java b/src/main/java/seedu/exercise/logic/parser/SuggestCommandParser.java new file mode 100644 index 00000000000..65b10b3afad --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/SuggestCommandParser.java @@ -0,0 +1,106 @@ +package seedu.exercise.logic.parser; + +import static seedu.exercise.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_OPERATION_TYPE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_SUGGEST_TYPE; +import static seedu.exercise.logic.parser.ParserUtil.parsePredicate; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.logic.commands.SuggestBasicCommand; +import seedu.exercise.logic.commands.SuggestCommand; +import seedu.exercise.logic.commands.SuggestPossibleCommand; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.property.custom.CustomProperty; +import seedu.exercise.model.resource.Exercise; + +//@@author kwekke +/** + * Parses input arguments and creates a new SuggestCommand object + */ +public class SuggestCommandParser implements Parser { + + public static final String SUGGEST_TYPE_BASIC = "basic"; + public static final String SUGGEST_TYPE_POSSIBLE = "possible"; + private static final Logger logger = LogsCenter.getLogger(SuggestCommandParser.class); + + /** + * Parses the given {@code String} of arguments in the context of the SuggestCommand + * and returns a SuggestCommand object for execution. + * + * @throws ParseException if the user does not conform to the expected format + */ + public SuggestCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, getPrefixes()); + + if (!argMultimap.arePrefixesPresent(PREFIX_SUGGEST_TYPE) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SuggestCommand.MESSAGE_USAGE)); + } + + String suggestType = ParserUtil.parseSuggestType(argMultimap.getValue(PREFIX_SUGGEST_TYPE).get()); + + if (suggestType.equals(SUGGEST_TYPE_BASIC)) { + return new SuggestBasicCommand(); + } + + if (suggestType.equals(SUGGEST_TYPE_POSSIBLE)) { + return parsePossible(argMultimap); + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SuggestCommand.MESSAGE_USAGE)); + } + + private Prefix[] getPrefixes() { + ArrayList prefixes = new ArrayList<>(); + prefixes.add(PREFIX_OPERATION_TYPE); + prefixes.add(PREFIX_SUGGEST_TYPE); + prefixes.add(PREFIX_MUSCLE); + logger.info("Retrieving custom properties"); + Set customProperties = PropertyBook.getInstance().getCustomProperties(); + for (CustomProperty cp : customProperties) { + prefixes.add(cp.getPrefix()); + } + return prefixes.toArray(new Prefix[prefixes.size() - 1]); + } + + /** + * Parses arguments and returns SuggestPossibleCommand for execution + */ + private static SuggestCommand parsePossible(ArgumentMultimap argMultimap) throws ParseException { + Set muscles = ParserUtil.parseMuscles(argMultimap.getAllValues(PREFIX_MUSCLE)); + Map customPropertiesMap = + ParserUtil.parseCustomProperties(argMultimap.getAllCustomProperties()); + boolean isStrict = true; + int numberOfPredicateTags = getNumberOfPredicateTags(muscles, customPropertiesMap); + + if (numberOfPredicateTags == 0) { + logger.info("Invalid suggest possible command - no predicate tags"); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SuggestCommand.MESSAGE_USAGE)); + } + + if (numberOfPredicateTags > 1) { + if ((!argMultimap.arePrefixesPresent(PREFIX_OPERATION_TYPE) || !argMultimap.getPreamble().isEmpty())) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SuggestCommand.MESSAGE_USAGE)); + } + } + + if (argMultimap.arePrefixesPresent(PREFIX_OPERATION_TYPE)) { + isStrict = ParserUtil.parseOperationType(argMultimap.getValue(PREFIX_OPERATION_TYPE).get()); + } + + Predicate predicate = parsePredicate(muscles, customPropertiesMap, isStrict); + return new SuggestPossibleCommand(predicate); + } + + private static int getNumberOfPredicateTags(Set muscles, Map customPropertiesMap) { + return muscles.size() + customPropertiesMap.size(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/seedu/exercise/logic/parser/exceptions/ParseException.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/seedu/exercise/logic/parser/exceptions/ParseException.java index 158a1a54c1c..a8211f45cc1 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/seedu/exercise/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package seedu.exercise.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import seedu.exercise.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/seedu/exercise/logic/parser/predicate/BasePropertyPredicate.java b/src/main/java/seedu/exercise/logic/parser/predicate/BasePropertyPredicate.java new file mode 100644 index 00000000000..607ec4210d7 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/predicate/BasePropertyPredicate.java @@ -0,0 +1,15 @@ +package seedu.exercise.logic.parser.predicate; + +import java.util.function.Predicate; + +import seedu.exercise.model.resource.Exercise; + +//@@author kwekke +/** + * Represents a {@code Predicate} that filters {@code Exercise} which matches the given property. + */ +public interface BasePropertyPredicate extends Predicate { + + boolean test(Exercise exercise); + +} diff --git a/src/main/java/seedu/exercise/logic/parser/predicate/ExerciseCustomPropertyPredicate.java b/src/main/java/seedu/exercise/logic/parser/predicate/ExerciseCustomPropertyPredicate.java new file mode 100644 index 00000000000..de4b706d796 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/predicate/ExerciseCustomPropertyPredicate.java @@ -0,0 +1,67 @@ +package seedu.exercise.logic.parser.predicate; + +import static java.util.Objects.requireNonNull; + +import java.util.Map; + +import seedu.exercise.model.resource.Exercise; + +//@@author kwekke +/** + * Tests whether an {@code Exercise} matches the {@code predicate} + */ +public class ExerciseCustomPropertyPredicate implements BasePropertyPredicate { + + private final Map customProperties; + private final boolean isStrict; + + public ExerciseCustomPropertyPredicate(Map customProperties, boolean isStrict) { + requireNonNull(customProperties); + this.customProperties = customProperties; + this.isStrict = isStrict; + } + + @Override + public boolean test(Exercise exercise) { + if (isStrict) { + return testStrict(exercise); + } else { + return testLoose(exercise); + } + } + + /** + * Returns true if a {@code exercise} has all the {@code CustomProperty} targeted + */ + private boolean testStrict(Exercise exercise) { + Map exerciseCustomProperties = exercise.getCustomPropertiesMap(); + for (String key : customProperties.keySet()) { + if (!(customProperties.get(key).equals(exerciseCustomProperties.get(key)))) { + return false; + } + } + return true; + } + + /** + * Returns true if a {@code exercise} has at least one {@code CustomProperty} targeted + */ + private boolean testLoose(Exercise exercise) { + Map exerciseCustomProperties = exercise.getCustomPropertiesMap(); + for (String key : customProperties.keySet()) { + if (customProperties.get(key).equals(exerciseCustomProperties.get(key))) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object other) { + return other == this //short circuit if same object + || (other instanceof ExerciseCustomPropertyPredicate //instanceof handles null + && customProperties.equals(((ExerciseCustomPropertyPredicate) other).customProperties) + && isStrict == ((ExerciseCustomPropertyPredicate) other).isStrict); + } + +} diff --git a/src/main/java/seedu/exercise/logic/parser/predicate/ExerciseMusclePredicate.java b/src/main/java/seedu/exercise/logic/parser/predicate/ExerciseMusclePredicate.java new file mode 100644 index 00000000000..cab42106980 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/predicate/ExerciseMusclePredicate.java @@ -0,0 +1,85 @@ +package seedu.exercise.logic.parser.predicate; + +import static java.util.Objects.requireNonNull; + +import java.util.Set; + +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.resource.Exercise; + +//@@author kwekke +/** + * Tests whether an {@code Exercise} matches the {@code predicate} + */ +public class ExerciseMusclePredicate implements BasePropertyPredicate { + + private final Set muscles; + private final boolean isStrict; + + public ExerciseMusclePredicate(Set muscles, boolean isStrict) { + requireNonNull(muscles); + this.muscles = muscles; + this.isStrict = isStrict; + } + + @Override + public boolean test(Exercise exercise) { + if (isStrict) { + return testStrict(exercise); + } else { + return testLoose(exercise); + } + } + + /** + * Returns true if a {@code exercise} has all the {@code Muscle} targeted + */ + private boolean testStrict(Exercise exercise) { + Set exerciseMuscles = exercise.getMuscles(); + for (Muscle muscle : muscles) { + if (!(exerciseMuscles.contains(muscle))) { + return false; + } + } + return true; + } + + /** + * Returns true if a {@code exercise} has at least one {@code Muscle} targeted + */ + private boolean testLoose(Exercise exercise) { + Set exerciseMuscles = exercise.getMuscles(); + for (Muscle muscle : exerciseMuscles) { + if (muscles.contains(muscle)) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object other) { + return other == this //short circuit if same object + || (other instanceof ExerciseMusclePredicate //instanceof handles null + && musclesEquals(muscles, (((ExerciseMusclePredicate) other).muscles)) + && isStrict == ((ExerciseMusclePredicate) other).isStrict); + } + + /** + * Returns true if two sets of {@code Muscle} contain the same keys. + */ + private boolean musclesEquals(Set muscles, Set otherMuscles) { + if (muscles.size() != otherMuscles.size()) { + return false; + } + + if (muscles.containsAll(otherMuscles) && otherMuscles.containsAll(muscles)) { + return true; + } else { + return false; + } + } + + + +} diff --git a/src/main/java/seedu/exercise/logic/parser/predicate/ExercisePredicate.java b/src/main/java/seedu/exercise/logic/parser/predicate/ExercisePredicate.java new file mode 100644 index 00000000000..931ac675847 --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/predicate/ExercisePredicate.java @@ -0,0 +1,68 @@ +package seedu.exercise.logic.parser.predicate; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import seedu.exercise.model.resource.Exercise; + +//@@author kwekke +/** + * Encapsulates a {@code predicate} for an {@code Exercise} + */ +public class ExercisePredicate implements Predicate { + private List predicates = new ArrayList<>(); + private boolean isStrict; + + @SafeVarargs + public ExercisePredicate(boolean isStrict, BasePropertyPredicate... predicates) { + requireNonNull(predicates); + this.isStrict = isStrict; + this.predicates = Arrays.asList(predicates); + } + + + @Override + public boolean test(Exercise exercise) { + if (isStrict) { + return testStrict(exercise); + } else { + return testLoose(exercise); + } + } + + /** + * Returns true if a {@code exercise} passes all the given {@code predicates} + */ + private boolean testStrict(Exercise exercise) { + for (Predicate predicate : predicates) { + if (!(predicate.test(exercise))) { + return false; + } + } + return true; + } + + /** + * Returns true if a {@code exercise} passes at least one of the given {@code predicates} + */ + private boolean testLoose(Exercise exercise) { + for (Predicate predicate : predicates) { + if (predicate.test(exercise)) { + return true; + } + } + return false; + } + + @Override + public boolean equals(Object other) { + return other == this //short circuit if same object + || (other instanceof ExercisePredicate //instanceof handles null + && isStrict == ((ExercisePredicate) other).isStrict + && predicates.equals(((ExercisePredicate) other).predicates)); + } +} diff --git a/src/main/java/seedu/exercise/logic/parser/predicate/PredicateUtil.java b/src/main/java/seedu/exercise/logic/parser/predicate/PredicateUtil.java new file mode 100644 index 00000000000..02f25ca95ce --- /dev/null +++ b/src/main/java/seedu/exercise/logic/parser/predicate/PredicateUtil.java @@ -0,0 +1,59 @@ +package seedu.exercise.logic.parser.predicate; + +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.resource.Exercise; + +//@@author kwekke +/** + * Contains Utility methods used for generating Predicates. + */ +public class PredicateUtil { + + public static final String OPERATION_TYPE_AND = "and"; + public static final String OPERATION_TYPE_OR = "or"; + + public static final String OPERATION_TYPE_CONSTRAINTS = "Operation type should either be \'" + OPERATION_TYPE_AND + + "\' or \'" + OPERATION_TYPE_OR + "\'"; + + /** + * {@code Predicate} that always evaluate to true + */ + public static final Predicate PREDICATE_SHOW_ALL_EXERCISES = unused -> true; + + public static BasePropertyPredicate getBasePredicateMuscle(Set targetMuscles, boolean isStrict) { + return new ExerciseMusclePredicate(targetMuscles, isStrict); + } + + public static BasePropertyPredicate getBasePredicateCustomProperty( + Map targetCustomPropertiesMap, boolean isStrict) { + return new ExerciseCustomPropertyPredicate(targetCustomPropertiesMap, isStrict); + } + + /** + * Returns a {@code ExercisePredicate} that correspond to + * {@code muscles}, {@code customProperties} and {@code isStrict} + */ + public static ExercisePredicate getExercisePredicate( + Set muscles, Map customProperties, boolean isStrict) { + assert(!(muscles.isEmpty() && customProperties.isEmpty())); + + BasePropertyPredicate musclePredicate = getBasePredicateMuscle(muscles, isStrict); + BasePropertyPredicate customPropertiesPredicate = getBasePredicateCustomProperty(customProperties, isStrict); + + if (muscles.isEmpty()) { + return new ExercisePredicate(isStrict, customPropertiesPredicate); + } + + if (customProperties.isEmpty()) { + return new ExercisePredicate(isStrict, musclePredicate); + } + + ExercisePredicate predicate = new ExercisePredicate(isStrict, musclePredicate, customPropertiesPredicate); + return predicate; + } + +} diff --git a/src/main/java/seedu/exercise/model/Model.java b/src/main/java/seedu/exercise/model/Model.java new file mode 100644 index 00000000000..4f265f47fbd --- /dev/null +++ b/src/main/java/seedu/exercise/model/Model.java @@ -0,0 +1,268 @@ +package seedu.exercise.model; + +import java.nio.file.Path; +import java.util.List; +import java.util.function.Predicate; + +import javafx.collections.ObservableList; +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.State; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * The API of the Model component. + */ +public interface Model { + + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + + /** + * Returns the user prefs. + */ + ReadOnlyUserPrefs getUserPrefs(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Sets the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the user prefs' exercise book file path. + */ + Path getExerciseBookFilePath(); + + /** + * Sets the user prefs' exercise book file path. + */ + void setExerciseBookFilePath(Path exerciseBookFilePath); + + /** + * Replaces exercise book data with the data in {@code anotherBook}. + */ + void setExerciseBook(ReadOnlyResourceBook anotherBook); + + /** + * Returns the data in the exercise book + */ + ReadOnlyResourceBook getExerciseBookData(); + + /** + * Returns true if an exercise with the same identity as {@code exercise} exists in the exercise book. + */ + boolean hasExercise(Exercise exercise); + + /** + * Deletes the given exercise. + * The exercise must exist in the exercise book. + */ + void deleteExercise(Exercise target); + + /** + * Adds the given exercise. + * {@code exercise} must not already exist in exercise book. + */ + void addExercise(Exercise exercise); + + /** + * Replaces the given exercise {@code target} with {@code editedExercise}. + * {@code target} must exist in exercise book. + * The exercise identity of {@code editedExercise} must not be the same as another existing exercise in + * the exercise book. + */ + void setExercise(Exercise target, Exercise editedExercise); + + /** + * Returns an unmodifiable view of the filtered exercise list + */ + ObservableList getSortedExerciseList(); + + /** + * Returns an unmodifiable view of the filtered regime list + */ + ObservableList getSortedRegimeList(); + + /** + * Returns an unmodifiable view of the filtered schedule list + */ + ObservableList getSortedScheduleList(); + + /** + * Returns the user prefs' regime book file path. + */ + Path getRegimeBookFilePath(); + + /** + * Sets the user prefs' regime book file path. + */ + void setRegimeBookFilePath(Path regimeBookFilePath); + + /** + * Replaces regime book data with the data in {@code anotherBook}. + */ + void setRegimeBook(ReadOnlyResourceBook anotherBook); + + /** + * Returns the data in the regime book + */ + ReadOnlyResourceBook getAllRegimeData(); + + /** + * Returns true if an regime with the same identity as {@code regime} exists in the regime book. + */ + boolean hasRegime(Regime regime); + + /** + * Adds the given regime. + * {@code regime} must not already exist in regime book. + */ + void addRegime(Regime regime); + + /** + * Replaces the given regime {@code target} with {@code editedRegime}. + * {@code target} must exist in regime book. + * The regime identity of {@code editedRegime} must not be the same as another existing regime in + * the regime book. + */ + void setRegime(Regime target, Regime editedRegime); + + /** + * Deletes the given regime. + * The regime must exist in the regime book. + */ + void deleteRegime(Regime regime); + + /** + * Returns the index of regime in regime book. + */ + int getRegimeIndex(Regime regime); + + /** + * Returns true if another schedule has been scheduled on the same date as {@code schedule}. + */ + boolean hasSchedule(Schedule schedule); + + /** + * Schedules a {@code schedule} for the user. + * It must be guaranteed that there is no existing schedule in the {@code ReadOnlyResourceBook} + */ + void addSchedule(Schedule schedule); + + /** + * Removes a {@code schedule} for the user. + * This method will not add the schedule to exercise tracker for tracking. + */ + void removeSchedule(Schedule schedule); + + + /** + * Returns the data in the schedule book + */ + ReadOnlyResourceBook getAllScheduleData(); + + /** + * Deletes a Schedule and adds it to {@code ReadOnlyResourceBook} for tracking. + *

+ * If the schedule has some exercises that are duplicates exercises as + * specified by {@link Exercise#isSameResource}, that exercise will + * be ignored and not be added into the exercise tracker. + *

+ * All exercises added will have their dates changed to be the date + * of the schedule itself. + * + * @param schedule to complete + */ + void completeSchedule(Schedule schedule); + + /** + * Resolves a conflict based on the indexes provided by user. + * + * The state of the program must be {@link State#IN_CONFLICT} before calling this method. + * The state of the program is not changed after execution of the method. Only command subclasses + * can change {@code MainApp}'s state. + * + * If both list of indexes are empty, the {@code regimeName} provided + * will be taken as the resolved schedule and the non-mentioned name is discarded. + * + * @return resolved schedule to be added to schedule book + */ + Schedule resolveConflict(Name regimeName, List indexFromSchedule, List indexFromConflict); + + /** + * Returns the conflict that is currently happening. + * + * The state of the program must be {@link State#IN_CONFLICT} before calling this method. + * Only then will a conflict be available for fetching from the {@code Model}. + */ + Conflict getConflict(); + + /** + * Sets the current conflicting schedule to {@code conflict} + * + * The state of the program must be {@link State#IN_CONFLICT} before calling this method. + */ + void setConflict(Conflict conflict); + + /** + * Checks if the indexes from scheduled and conflict regimes are duplicates. + * + * The state of the program must be {@link State#IN_CONFLICT} before calling this method. + */ + boolean isSelectedIndexesFromRegimeDuplicate(List scheduledIndex, List conflictingIndex); + + /** + * Returns an unmodifiable view of the list of suggested exercises + */ + ObservableList getSuggestedExerciseList(); + + /** + * Replaces suggestions with the those in {@code suggestions}. + */ + void setSuggestions(List suggestions); + + /** + * Updates the list of suggested exercises to filter by the given {@code predicate}. + * + * @throws NullPointerException if {@code predicate} is null. + */ + void updateSuggestedExerciseList(Predicate predicateShowAllExercises); + + + /** + * Returns the data of all exercises in the database + */ + ReadOnlyResourceBook getDatabaseBook(); + + /** + * Update statistic with updated exercises. + */ + void updateStatistic(); + + /** + * Set the statistic to the updated statistic. + */ + void setStatistic(Statistic statistic); + + /** + * Returns the Statistic object currently in focus. + */ + Statistic getStatistic(); + + /** + * Returns the data in the exercise database + */ + ReadOnlyResourceBook getExerciseDatabaseData(); +} diff --git a/src/main/java/seedu/exercise/model/ModelManager.java b/src/main/java/seedu/exercise/model/ModelManager.java new file mode 100644 index 00000000000..1d71d7fc87f --- /dev/null +++ b/src/main/java/seedu/exercise/model/ModelManager.java @@ -0,0 +1,528 @@ +package seedu.exercise.model; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.AppUtil.requireMainAppState; +import static seedu.exercise.commons.util.CollectionUtil.append; +import static seedu.exercise.commons.util.CollectionUtil.areListsEmpty; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_REGIME_COMPARATOR; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_SCHEDULE_COMPARATOR; +import static seedu.exercise.model.util.DateChangerUtil.changeAllDate; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.State; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.commands.ResolveCommand; +import seedu.exercise.logic.commands.builder.EditExerciseBuilder; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.logic.commands.statistic.StatsFactory; +import seedu.exercise.logic.parser.ParserUtil; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.conflict.Conflict; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * Represents the in-memory model of the exercise book data. + */ +public class ModelManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + + private final ReadOnlyResourceBook exerciseBook; + private final ReadOnlyResourceBook regimeBook; + private final ReadOnlyResourceBook databaseBook; + private final ReadOnlyResourceBook scheduleBook; + private final UserPrefs userPrefs; + private final SortedList sortedExercises; + private final SortedList sortedRegimes; + private final SortedList sortedSchedules; + private final ObservableList suggestions = FXCollections.observableArrayList(); + private final Statistic statistic; + + private Conflict conflict; + + /** + * Initializes a ModelManager with the given exerciseBook and userPrefs. + */ + public ModelManager(ReadOnlyResourceBook exerciseBook, ReadOnlyResourceBook regimeBook, + ReadOnlyResourceBook databaseBook, ReadOnlyResourceBook scheduleBook, + ReadOnlyUserPrefs userPrefs) { + super(); + requireAllNonNull(exerciseBook, regimeBook, databaseBook, scheduleBook, userPrefs); + + logger.fine("Initializing with exercise book: " + exerciseBook + " and user prefs " + userPrefs); + + this.exerciseBook = new ReadOnlyResourceBook<>(exerciseBook, DEFAULT_EXERCISE_COMPARATOR); + this.databaseBook = new ReadOnlyResourceBook<>(databaseBook, DEFAULT_EXERCISE_COMPARATOR); + this.regimeBook = new ReadOnlyResourceBook<>(regimeBook, DEFAULT_REGIME_COMPARATOR); + this.scheduleBook = new ReadOnlyResourceBook<>(scheduleBook, DEFAULT_SCHEDULE_COMPARATOR); + this.userPrefs = new UserPrefs(userPrefs); + sortedExercises = new SortedList<>(this.exerciseBook.getSortedResourceList(), + DEFAULT_EXERCISE_COMPARATOR); + removeInvalidCustomProperties(); + sortedRegimes = new SortedList<>(this.regimeBook.getSortedResourceList(), + DEFAULT_REGIME_COMPARATOR); + sortedSchedules = new SortedList<>(this.scheduleBook.getSortedResourceList(), + DEFAULT_SCHEDULE_COMPARATOR); + StatsFactory statsFactory = new StatsFactory(exerciseBook, "linechart", "calories", null, null); + this.statistic = statsFactory.getDefaultStatistic(); + conflict = null; + } + + public ModelManager() { + this(new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR), + new ReadOnlyResourceBook<>(DEFAULT_REGIME_COMPARATOR), + new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR), + new ReadOnlyResourceBook<>(DEFAULT_SCHEDULE_COMPARATOR), new UserPrefs()); + } + + //=========== UserPrefs ================================================================================== + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; + } + + @Override + public GuiSettings getGuiSettings() { + return userPrefs.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + userPrefs.setGuiSettings(guiSettings); + } + + @Override + public Path getExerciseBookFilePath() { + return userPrefs.getExerciseBookFilePath(); + } + + @Override + public void setExerciseBookFilePath(Path exerciseBookFilePath) { + requireNonNull(exerciseBookFilePath); + userPrefs.setExerciseBookFilePath(exerciseBookFilePath); + } + + @Override + public Path getRegimeBookFilePath() { + return userPrefs.getRegimeBookFilePath(); + } + + @Override + public void setRegimeBookFilePath(Path regimeBookFilePath) { + requireNonNull(regimeBookFilePath); + userPrefs.setRegimeBookFilePath(regimeBookFilePath); + } + + //=========== ExerciseBook ================================================================================ + + @Override + public void setExerciseBook(ReadOnlyResourceBook anotherBook) { + this.exerciseBook.resetData(anotherBook); + } + + @Override + public ReadOnlyResourceBook getExerciseBookData() { + return exerciseBook; + } + + @Override + public boolean hasExercise(Exercise exercise) { + requireNonNull(exercise); + return exerciseBook.hasResource(exercise); + } + + @Override + public void deleteExercise(Exercise target) { + exerciseBook.removeResource(target); + } + + /** + * Adds an {@code Exercise} object into the exercise book. + */ + public void addExercise(Exercise exercise) { + exerciseBook.addResource(exercise); + } + + public void setExercise(Exercise target, Exercise editedExercise) { + requireAllNonNull(target, editedExercise); + exerciseBook.removeResource(target); + exerciseBook.addResource(editedExercise); + } + + //===================RegimeBook============================================================================== + + @Override + public void setRegimeBook(ReadOnlyResourceBook anotherBook) { + this.regimeBook.resetData(anotherBook); + } + + @Override + public ReadOnlyResourceBook getAllRegimeData() { + return regimeBook; + } + + /** + * Adds a {@code Regime} object into the regime book. + */ + @Override + public void addRegime(Regime regime) { + regimeBook.addResource(regime); + } + + @Override + public void deleteRegime(Regime target) { + regimeBook.removeResource(target); + } + + @Override + public void setRegime(Regime target, Regime editedRegime) { + regimeBook.setResource(target, editedRegime); + } + + @Override + public boolean hasRegime(Regime regime) { + requireNonNull(regime); + return regimeBook.hasResource(regime); + } + + @Override + public int getRegimeIndex(Regime regime) { + return regimeBook.getResourceIndex(regime); + } + + //===================ReadOnlyResourceBook=============================================================== + @Override + public boolean hasSchedule(Schedule schedule) { + requireNonNull(schedule); + return scheduleBook.hasResource(schedule); + } + + @Override + public void addSchedule(Schedule schedule) { + requireNonNull(schedule); + scheduleBook.addResource(schedule); + } + + @Override + public void removeSchedule(Schedule schedule) { + requireNonNull(schedule); + scheduleBook.removeResource(schedule); + } + + @Override + public void completeSchedule(Schedule schedule) { + requireNonNull(schedule); + scheduleBook.removeResource(schedule); + Collection scheduledExercises = schedule.getExercises(); + for (Exercise exercise : scheduledExercises) { + if (!exerciseBook.hasResource(exercise)) { + exerciseBook.addResource(exercise); + } + } + } + + @Override + public ReadOnlyResourceBook getAllScheduleData() { + return scheduleBook; + } + + //===================Conflicts=============================================================== + + @Override + public Schedule resolveConflict(Name name, List indexFromSchedule, List indexFromConflict) { + requireAllNonNull(name, indexFromSchedule, indexFromConflict); + requireMainAppState(State.IN_CONFLICT); + + removeOldSchedule(); + Schedule resolvedSchedule; + if (areListsEmpty(indexFromConflict, indexFromSchedule)) { + if (name.toString().equals(ResolveCommand.TAKE_FROM_SCHEDULED)) { + resolvedSchedule = conflict.getScheduled(); + } else { + resolvedSchedule = conflict.getConflicted(); + } + addResolvedSchedule(resolvedSchedule); + } else { + SortedUniqueResourceList resolvedExercises = + getResolvedExerciseList(indexFromSchedule, indexFromConflict); + resolvedSchedule = getResolvedSchedule(name, resolvedExercises); + addCombinedRegime(resolvedSchedule.getRegime()); + addResolvedSchedule(resolvedSchedule); + } + + logger.info("Schedule conflict resolved: " + resolvedSchedule.getRegimeName() + + " on " + resolvedSchedule.getDate()); + return resolvedSchedule; + } + + @Override + public Conflict getConflict() { + requireMainAppState(State.IN_CONFLICT); + + return conflict; + } + + @Override + public void setConflict(Conflict conflict) { + requireMainAppState(State.IN_CONFLICT); + requireNonNull(conflict); + + logger.info("Conflict set:\n" + conflict); + this.conflict = conflict; + } + + @Override + public boolean isSelectedIndexesFromRegimeDuplicate(List scheduledIndex, List conflictingIndex) { + requireMainAppState(State.IN_CONFLICT); + requireAllNonNull(scheduledIndex, conflictingIndex); + requireNonNull(conflict); + + return isIndexesForRegimeDuplicate(scheduledIndex, conflictingIndex); + } + + //=========== Filtered Exercise List Accessors ============================================================= + + /** + * Returns an unmodifiable view of the list of {@code Exercise} backed by the internal list of + * {@code exerciseBook} and sorted by Date. + */ + @Override + public ObservableList getSortedExerciseList() { + return sortedExercises; + } + + + //=========== Filtered Regime List Accessors =============================================================== + + /** + * Returns an unmodifiable view of the list of {@code Regime} backed by the internal list of + * {@code regimeBook}. + */ + public ObservableList getSortedRegimeList() { + return sortedRegimes; + } + + + //=========== Filtered Schedule List Accessors =============================================================== + + /** + * Returns an unmodifiable view of the list of {@code Schedule} backed by the internal list of + * {@code scheduleBook} + */ + public ObservableList getSortedScheduleList() { + return sortedSchedules; + } + + //=========== ExerciseDatabase =============================================================== + + @Override + public ReadOnlyResourceBook getDatabaseBook() { + return databaseBook; + } + + public ReadOnlyResourceBook getExerciseDatabaseData() { + return databaseBook; + } + + //=========== Suggested Exercise Accessors =============================================================== + + @Override + public ObservableList getSuggestedExerciseList() { + return suggestions; + } + + @Override + public void setSuggestions(List suggestions) { + this.suggestions.setAll(suggestions); + } + + @Override + public void updateSuggestedExerciseList(Predicate predicate) { + requireNonNull(predicate); + List allSuggestions = generateAllSuggestions(predicate); + List suggestionsTodayDate = updateSuggestionsDate(allSuggestions); + setSuggestions(suggestionsTodayDate); + } + + /** + * Returns a list of {@code exercises} with dates updated to today. + */ + private List updateSuggestionsDate(List suggestions) { + return new ArrayList<>(changeAllDate(suggestions, Date.getToday())); + } + + /** + * Returns an unmodifiable view of the list of {@code suggestions} backed by the internal list of + * {@code exerciseBook} and {@code databaseBook} + */ + private ObservableList generateAllSuggestions(Predicate predicate) { + List trackedExercises = getExerciseBookData().getSortedResourceList().filtered(predicate); + List databaseExercises = getDatabaseBook().getSortedResourceList().filtered(predicate); + ObservableList allSuggestions = FXCollections.observableArrayList(); + ObservableList addedTrackedExercises = addExerciseList(allSuggestions, trackedExercises); + return addExerciseList(addedTrackedExercises, databaseExercises); + } + + /** + * Returns an unmodifiable view of the list of exercises from {@code exerciseList} to an {@code exerciseList}, + * excluding any exercise that has a duplicate name. + */ + private ObservableList addExerciseList(ObservableList originalExerciseList, + List exerciseList) { + ObservableList newExerciseList = FXCollections.observableArrayList(originalExerciseList); + for (Exercise exercise : exerciseList) { + boolean isDuplicate = false; + for (Exercise e : newExerciseList) { + if (exercise.getName().equals(e.getName())) { + isDuplicate = true; + break; + } + } + if (!isDuplicate) { + newExerciseList.add(exercise); + } + } + return newExerciseList; + } + + @Override + public void updateStatistic() { + ReadOnlyResourceBook exercises = getExerciseBookData(); + Statistic outdatedStatistic = getStatistic(); + StatsFactory statsFactory = new StatsFactory(exercises, outdatedStatistic.getChart(), + outdatedStatistic.getCategory(), outdatedStatistic.getStartDate(), outdatedStatistic.getEndDate()); + Statistic statistic = statsFactory.generateStatistic(); + this.statistic.resetData(statistic); + } + + @Override + public void setStatistic(Statistic statistic) { + this.statistic.resetData(statistic); + } + + @Override + public Statistic getStatistic() { + return statistic; + } + + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return exerciseBook.equals(other.exerciseBook) + && regimeBook.equals(other.regimeBook) + && scheduleBook.equals(other.scheduleBook) + && userPrefs.equals(other.userPrefs) + && sortedExercises.equals(other.sortedExercises) + && sortedRegimes.equals(other.sortedRegimes) + && sortedSchedules.equals(other.sortedSchedules) + && databaseBook.equals(other.databaseBook) + && suggestions.equals(other.suggestions); + } + + private SortedUniqueResourceList getResolvedExerciseList(List indexFromSchedule, + List indexFromConflict) { + Regime scheduledRegime = conflict.getScheduledRegime(); + Regime conflictRegime = conflict.getConflictingRegime(); + List exercisesToAddFromScheduled = scheduledRegime.getRegimeExercises() + .getAllResourcesIndex(indexFromSchedule); + List exercisesToAddFromConflicted = conflictRegime.getRegimeExercises() + .getAllResourcesIndex(indexFromConflict); + List resolvedExercises = append(exercisesToAddFromScheduled, exercisesToAddFromConflicted); + SortedUniqueResourceList uniqueResolveList = + new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + uniqueResolveList.setAll(resolvedExercises); + return uniqueResolveList; + } + + /** + * Checks if the provided indexes have some duplicate exercises they are referring to + */ + private boolean isIndexesForRegimeDuplicate(List scheduledIndex, List conflictingIndex) { + SortedUniqueResourceList listToAdd = new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + List scheduledExercises = conflict + .getScheduledRegime().getRegimeExercises().getAllResourcesIndex(scheduledIndex); + List conflictExercises = conflict + .getConflictingRegime().getRegimeExercises().getAllResourcesIndex(conflictingIndex); + listToAdd.setAll(scheduledExercises); + for (Exercise conflicted : conflictExercises) { + if (listToAdd.contains(conflicted)) { + return true; + } + listToAdd.add(conflicted); + } + return false; + } + + private Schedule getResolvedSchedule(Name regimeName, SortedUniqueResourceList exerciseList) { + Regime regime = new Regime(regimeName, exerciseList); + return new Schedule(regime, conflict.getConflictDate()); + } + + private void removeOldSchedule() { + scheduleBook.removeResource(conflict.getScheduled()); + } + + private void addResolvedSchedule(Schedule resolvedSchedule) { + scheduleBook.addResource(resolvedSchedule); + } + + private void addCombinedRegime(Regime regime) { + regimeBook.addResource(regime); + } + + /** + * Removes invalid custom properties that are present in the exercises. + * This ensures that undefined custom properties and custom properties of invalid values do not exist. + */ + private void removeInvalidCustomProperties() { + for (Exercise exercise : sortedExercises) { + Map toCheck = exercise.getCustomPropertiesMap(); + Map newMap; + try { + newMap = ParserUtil.parseCustomProperties(toCheck); + } catch (ParseException e) { + newMap = new TreeMap<>(); + } + EditExerciseBuilder editor = new EditExerciseBuilder(exercise); + editor.setCustomProperties(newMap); + setExercise(exercise, editor.buildEditedExercise()); + } + } +} diff --git a/src/main/java/seedu/exercise/model/ReadOnlyResourceBook.java b/src/main/java/seedu/exercise/model/ReadOnlyResourceBook.java new file mode 100644 index 00000000000..dfb3e6741c7 --- /dev/null +++ b/src/main/java/seedu/exercise/model/ReadOnlyResourceBook.java @@ -0,0 +1,116 @@ +package seedu.exercise.model; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Comparator; +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.exercise.model.resource.Resource; + +/** + * Encapsulates a Resource Book that can contain {@code Resource} objects of type {@code T}. + */ +public class ReadOnlyResourceBook { + + private final SortedUniqueResourceList resources; + + public ReadOnlyResourceBook(Comparator comparator) { + resources = new SortedUniqueResourceList<>(comparator); + } + + public ReadOnlyResourceBook(ReadOnlyResourceBook toBeCopied, Comparator comparator) { + this(comparator); + resetData(toBeCopied); + } + + /** + * Sets the data of a {@code ReadOnlyResourceBook} with the {@code resources}. + */ + public void setResources(List resources) { + requireNonNull(resources); + this.resources.setAll(resources); + } + + /** + * Resets the data of a {@code ReadOnlyResourceBook} with the {@code newData}. + */ + public void resetData(ReadOnlyResourceBook newData) { + requireNonNull(newData); + setResources(newData.getSortedResourceList()); + } + + /** + * Returns true if the {@code ReadOnlyResourceBook} instance contains {@code resource}. + */ + public boolean hasResource(T resource) { + requireNonNull(resource); + return resources.contains(resource); + } + + /** + * Adds {@code resource} into the {@code ReadOnlyResourceBook} instance. + */ + public void addResource(T resource) { + requireNonNull(resource); + resources.add(resource); + } + + /** + * Replaces {@code target} with {@code editResource} in the {@code ReadOnlyResourceBook} instance. + */ + public void setResource(T target, T editedResource) { + requireAllNonNull(target, editedResource); + resources.set(target, editedResource); + } + + /** + * Removes {@code key} from the {@code ReadOnlyResourceBook} instance. + */ + public void removeResource(T key) { + requireNonNull(key); + resources.remove(key); + } + + + /** + * Retrieves the index of {@code toGet} from the sorted list. + * Returns -1 if the item is not present in the sorted list. + */ + public int getResourceIndex(T toGet) { + int i = 0; + for (T resource : resources) { + if (resource.equals(toGet)) { + return i; + } + i++; + } + return -1; + } + + /** + * Returns an unmodifiable sorted list of {@code Resource} of type {@code T}. + */ + public ObservableList getSortedResourceList() { + return resources.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ReadOnlyResourceBook // instanceof handles nulls + && resources.equals(((ReadOnlyResourceBook) other).resources)); + } + + @Override + public int hashCode() { + return resources.hashCode(); + } + + + @Override + public String toString() { + return "" + resources.asUnmodifiableObservableList().size() + " items"; + } +} diff --git a/src/main/java/seedu/exercise/model/ReadOnlyUserPrefs.java b/src/main/java/seedu/exercise/model/ReadOnlyUserPrefs.java new file mode 100644 index 00000000000..7b90456ef54 --- /dev/null +++ b/src/main/java/seedu/exercise/model/ReadOnlyUserPrefs.java @@ -0,0 +1,23 @@ +package seedu.exercise.model; + +import java.nio.file.Path; + +import seedu.exercise.commons.core.GuiSettings; + +/** + * Unmodifiable view of user prefs. + */ +public interface ReadOnlyUserPrefs { + + GuiSettings getGuiSettings(); + + Path getExerciseBookFilePath(); + + Path getPropertyBookFilePath(); + + Path getRegimeBookFilePath(); + + Path getScheduleBookFilePath(); + + Path getAllExerciseBookFilePath(); +} diff --git a/src/main/java/seedu/exercise/model/SortedUniqueResourceList.java b/src/main/java/seedu/exercise/model/SortedUniqueResourceList.java new file mode 100644 index 00000000000..e3b46cb562f --- /dev/null +++ b/src/main/java/seedu/exercise/model/SortedUniqueResourceList.java @@ -0,0 +1,164 @@ +package seedu.exercise.model; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.model.exceptions.DuplicateResourceException; +import seedu.exercise.model.exceptions.ResourceNotFoundException; +import seedu.exercise.model.resource.Resource; + +/** + * A list of resources of type {@code T} that enforces uniqueness between its elements and does not allow nulls. + * The list is also sorted as given by {@code comparator} passed into the constructor. + * An resource is considered unique by comparing using {@link Resource#isSameResource(Resource)}. + * As such, adding and updating of resources uses Resource#isSameResource(Resource) for equality so as to ensure that + * the resource being added or updated is unique in terms of identity in the UniqueResourceList. + * However, the removal of a resource uses Resource#equals(Object) so as to ensure that the resource with exactly the + * same fields will be removed. + *

+ * Supports a minimal set of list operations. + *

+ * + * @see Resource#isSameResource(Resource) + */ +public class SortedUniqueResourceList implements Iterable { + + private final ObservableList internalList; + private final SortedList internalUnmodifiableSortedList; + + public SortedUniqueResourceList(Comparator comparator) { + internalList = FXCollections.observableArrayList(); + internalUnmodifiableSortedList = new SortedList<>(internalList, comparator); + } + + /** + * Returns true if the {@code UniqueResourceList} instance invoking this method contains {@code toCheck}. + */ + public boolean contains(T toCheck) { + requireNonNull(toCheck); + for (T resource : internalList) { + if (resource.isSameResource(toCheck)) { + return true; + } + } + return false; + } + + /** + * Adds {@code toAdd} into the {@code UniqueResourceList} only if the {@code toAdd} object is not + * present in the list. + */ + public void add(T toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateResourceException(); + } + internalList.add(toAdd); + } + + /** + * Replaces {@code target} with {@code editedTarget} only if {@code target} is present in the list and + * {@code editedTarget} is not present in the list. + */ + public void set(T target, T editedTarget) { + requireAllNonNull(target, editedTarget); + int index = internalList.indexOf(target); + + if (index == -1) { + throw new ResourceNotFoundException(); + } + + if (!target.isSameResource(editedTarget) && contains(editedTarget)) { + throw new DuplicateResourceException(); + } + + internalList.set(index, editedTarget); + } + + /** + * Removes {@code toRemove} from a {@code UniqueResourceList}. + * The {@code toRemove} object must be present in the list. + */ + public void remove(T toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ResourceNotFoundException(); + } + } + + /** + * Replaces all of the data in the {@code UniqueResourceList} instance with {@code replacement}. + */ + public void setAll(SortedUniqueResourceList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Works similarly to {@link #setAll(SortedUniqueResourceList)}. + */ + public void setAll(List replacement) { + requireNonNull(replacement); + if (!resourceAreUnique(replacement)) { + throw new DuplicateResourceException(); + } + internalList.setAll(replacement); + } + + public List getAllResourcesIndex(Collection indexes) { + List resultingList = new ArrayList<>(); + List unmodifiableList = asUnmodifiableObservableList(); + for (Index index : indexes) { + resultingList.add(unmodifiableList.get(index.getZeroBased())); + } + return resultingList; + } + + /** + * Returns an unmodifiable list of the data in a {@code UniqueResourceList}. + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableSortedList; + } + + @Override + public Iterator iterator() { + return internalUnmodifiableSortedList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SortedUniqueResourceList)// instanceof handles nulls + && internalUnmodifiableSortedList.equals(((SortedUniqueResourceList) other).internalUnmodifiableSortedList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if there are no duplicate resources inside the list. + */ + private boolean resourceAreUnique(List resources) { + for (int i = 0; i < resources.size() - 1; i++) { + for (int j = i + 1; j < resources.size(); j++) { + if (resources.get(i).isSameResource(resources.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/exercise/model/UserPrefs.java b/src/main/java/seedu/exercise/model/UserPrefs.java new file mode 100644 index 00000000000..00597ae7a39 --- /dev/null +++ b/src/main/java/seedu/exercise/model/UserPrefs.java @@ -0,0 +1,136 @@ +package seedu.exercise.model; + +import static java.util.Objects.requireNonNull; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.LogsCenter; + +/** + * Represents User's preferences. + */ +public class UserPrefs implements ReadOnlyUserPrefs { + + private static final Logger logger = LogsCenter.getLogger(UserPrefs.class); + + private GuiSettings guiSettings = new GuiSettings(); + private Path exerciseBookFilePath = Paths.get("data" , "exercisebook.json"); + private Path propertyBookFilePath = Paths.get("data", "propertybook.json"); + private Path exerciseDatabaseFilePath = Paths.get("data", "exercisedatabase.json"); + private Path regimeBookFilePath = Paths.get("data", "regimebook.json"); + private Path scheduleBookFilePath = Paths.get("data", "schedulebook.json"); + + /** + * Creates a {@code UserPrefs} with default values. + */ + public UserPrefs() {} + + /** + * Creates a {@code UserPrefs} with the prefs in {@code userPrefs}. + */ + public UserPrefs(ReadOnlyUserPrefs userPrefs) { + this(); + resetData(userPrefs); + } + + /** + * Resets the existing data of this {@code UserPrefs} with {@code newUserPrefs}. + */ + public void resetData(ReadOnlyUserPrefs newUserPrefs) { + requireNonNull(newUserPrefs); + setGuiSettings(newUserPrefs.getGuiSettings()); + setExerciseBookFilePath(newUserPrefs.getExerciseBookFilePath()); + setRegimeBookFilePath(newUserPrefs.getRegimeBookFilePath()); + setPropertyBookFilePath(newUserPrefs.getPropertyBookFilePath()); + setAllExerciseBookFilePath(newUserPrefs.getAllExerciseBookFilePath()); + + logger.info("All user preferences reset to:\n" + newUserPrefs); + } + + public GuiSettings getGuiSettings() { + return guiSettings; + } + + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + this.guiSettings = guiSettings; + } + + public Path getExerciseBookFilePath() { + return exerciseBookFilePath; + } + + public void setExerciseBookFilePath(Path exerciseBookFilePath) { + requireNonNull(exerciseBookFilePath); + this.exerciseBookFilePath = exerciseBookFilePath; + } + + public Path getRegimeBookFilePath() { + return regimeBookFilePath; + } + + public void setRegimeBookFilePath(Path regimeBookFilePath) { + requireNonNull(regimeBookFilePath); + this.regimeBookFilePath = regimeBookFilePath; + } + + public Path getScheduleBookFilePath() { + return scheduleBookFilePath; + } + + public Path getPropertyBookFilePath() { + return propertyBookFilePath; + } + + public void setPropertyBookFilePath(Path propertyBookFilePath) { + requireNonNull(propertyBookFilePath); + this.propertyBookFilePath = propertyBookFilePath; + } + + public Path getAllExerciseBookFilePath() { + return exerciseDatabaseFilePath; + } + + public void setAllExerciseBookFilePath(Path allExerciseFilePath) { + requireNonNull(allExerciseFilePath); + this.exerciseDatabaseFilePath = allExerciseFilePath; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof UserPrefs)) { //this handles null as well. + return false; + } + + UserPrefs o = (UserPrefs) other; + + return guiSettings.equals(o.guiSettings) + && exerciseBookFilePath.equals(o.exerciseBookFilePath) + && regimeBookFilePath.equals(o.regimeBookFilePath) + && propertyBookFilePath.equals(o.propertyBookFilePath) + && exerciseDatabaseFilePath.equals(o.exerciseDatabaseFilePath); + } + + @Override + public int hashCode() { + return Objects.hash(guiSettings, + exerciseBookFilePath, regimeBookFilePath, + propertyBookFilePath, exerciseDatabaseFilePath); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Gui Settings : " + guiSettings); + sb.append("\nLocal data file location : " + exerciseBookFilePath); + return sb.toString(); + } + +} diff --git a/src/main/java/seedu/exercise/model/conflict/Conflict.java b/src/main/java/seedu/exercise/model/conflict/Conflict.java new file mode 100644 index 00000000000..d73d4f9f5f0 --- /dev/null +++ b/src/main/java/seedu/exercise/model/conflict/Conflict.java @@ -0,0 +1,102 @@ +package seedu.exercise.model.conflict; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Objects; + +import javafx.collections.ObservableList; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * Represents a scheduling conflict between two schedules. + * + * A conflict can only happen when the {@code date} between two + * {@code schedules} are the same as specified by {@link Schedule#isSameResource}. + */ +public class Conflict { + + private final Schedule scheduled; + private final Schedule conflicted; + + public Conflict(Schedule scheduled, Schedule conflicted) { + this.scheduled = scheduled; + this.conflicted = conflicted; + } + + public Schedule getScheduled() { + return scheduled; + } + + public Schedule getConflicted() { + return conflicted; + } + + public Regime getScheduledRegime() { + return scheduled.getRegime(); + } + + public Regime getConflictingRegime() { + return conflicted.getRegime(); + } + + public Date getConflictDate() { + return scheduled.getDate(); + } + + public Schedule getScheduleByRegime(Regime regime) { + requireNonNull(regime); + + return scheduled.getRegime().equals(regime) + ? scheduled + : conflicted.getRegime().equals(regime) + ? conflicted + : null; + } + + public List getScheduledExerciseList() { + return scheduled.getExercises(); + } + + public List getConflictedExerciseList() { + return conflicted.getExercises(); + } + + public ObservableList getScheduledUnmodifiableExerciseList() { + return scheduled.getRegime().getRegimeExercises().asUnmodifiableObservableList(); + } + + public ObservableList getConflictedUnmodifiableExerciseList() { + return conflicted.getRegime().getRegimeExercises().asUnmodifiableObservableList(); + } + + public String getScheduledName() { + return scheduled.getRegimeName(); + } + + public String getConflictedName() { + return conflicted.getRegimeName(); + } + + @Override + public boolean equals(Object other) { + return (other == this) + || (other instanceof Conflict) + && ((scheduled.equals(((Conflict) other).scheduled) + && conflicted.equals(((Conflict) other).conflicted))); + } + + @Override + public int hashCode() { + return Objects.hash(scheduled, conflicted); + } + + @Override + public String toString() { + return "Scheduled: " + scheduled.getRegimeName() + "\nConflicting: " + conflicted.getRegimeName() + + "\nDate of conflict: " + scheduled.getDate(); + } +} diff --git a/src/main/java/seedu/exercise/model/exceptions/DuplicateResourceException.java b/src/main/java/seedu/exercise/model/exceptions/DuplicateResourceException.java new file mode 100644 index 00000000000..a4d651e17f4 --- /dev/null +++ b/src/main/java/seedu/exercise/model/exceptions/DuplicateResourceException.java @@ -0,0 +1,12 @@ +package seedu.exercise.model.exceptions; + +/** + * Represents an exception which occurs if 2 identical {@code Resource} objects are + * going to be added into the same {@code UniqueResourceList} + */ +public class DuplicateResourceException extends RuntimeException { + + public DuplicateResourceException() { + super("Operation would result in duplicate exercises/regimes/schedules."); + } +} diff --git a/src/main/java/seedu/exercise/model/exceptions/ResourceNotFoundException.java b/src/main/java/seedu/exercise/model/exceptions/ResourceNotFoundException.java new file mode 100644 index 00000000000..69132b30def --- /dev/null +++ b/src/main/java/seedu/exercise/model/exceptions/ResourceNotFoundException.java @@ -0,0 +1,9 @@ +package seedu.exercise.model.exceptions; + +/** + * Represents an exception that will occur if a given {@code Resource} cannot be found in a + * {@code UniqueResourceList}. + */ +public class ResourceNotFoundException extends RuntimeException { + +} diff --git a/src/main/java/seedu/exercise/model/property/Calories.java b/src/main/java/seedu/exercise/model/property/Calories.java new file mode 100644 index 00000000000..3ea05749e7b --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/Calories.java @@ -0,0 +1,59 @@ +package seedu.exercise.model.property; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_NON_NEGATIVE_INTEGER; +import static seedu.exercise.commons.util.AppUtil.checkArgument; + +/** + * Represents the estimated amount of calories burnt in an exercise. + * Guarantees: immutable; is valid as declared in {@link #isValidCalories(String)} + */ +public class Calories { + public static final String PROPERTY_CALORIES = "Calories"; + public static final String MESSAGE_CONSTRAINTS = "Calories should only contain non-negative integer and " + + "should be less than or equal to 50,000"; + private final String value; + + /** + * Constructs a {@code Calories}. + * + * @param calories A valid calories burnt. + */ + public Calories(String calories) { + requireNonNull(calories); + checkArgument(isValidCalories(calories), MESSAGE_CONSTRAINTS); + value = removeLeadingZeros(calories); + } + + /** + * Returns true if a given string is a valid calories burnt. + */ + public static boolean isValidCalories(String test) { + return test.matches(ONLY_NON_NEGATIVE_INTEGER) && Integer.parseInt(test) <= 50000; + } + + private String removeLeadingZeros(String calories) { + return calories.replaceFirst("^0*", ""); + } + + @Override + public String toString() { + if (value.length() == 0) { + return "0"; + } + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Calories // instanceof handles nulls + && value.equals(((Calories) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/exercise/model/property/Date.java b/src/main/java/seedu/exercise/model/property/Date.java new file mode 100644 index 00000000000..1b624892b36 --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/Date.java @@ -0,0 +1,200 @@ +package seedu.exercise.model.property; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; + +/** + * Represents date in ExerHealth. + * Guarantees: immutable; is valid as declared in {@link #isValidDate(String)} + */ +public class Date { + + public static final String PROPERTY_DATE = "Date"; + public static final String DAYS = "day(s)"; + public static final String WEEKS = "week(s)"; + public static final String MESSAGE_CONSTRAINTS = "Dates should be of the format dd/MM/yyyy and must be valid."; + public static final String MESSAGE_INVALID_END_DATE = "End date must be after start date"; + public static final String MESSAGE_PRETTY_PRINT_ONE_UNIT = "%1$s %2$s left..."; + public static final String MESSAGE_PRETTY_PRINT_TWO_UNITS = "%1$s and %2$s left..."; + + private static final String DATE_FORMAT = "dd/MM/yyyy"; + public static final DateTimeFormatter STANDARD_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(DATE_FORMAT); + + public final LocalDate value; + + /** + * Constructs a {@code Date}. + * + * @param date A valid date. + */ + public Date(String date) { + requireNonNull(date); + checkArgument(isValidDate(date), MESSAGE_CONSTRAINTS); + value = LocalDate.parse(date, STANDARD_DATE_TIME_FORMATTER); + } + + /** + * Returns if a given string is a valid date. + */ + public static boolean isValidDate(String test) { + try { + LocalDate.parse(test, STANDARD_DATE_TIME_FORMATTER); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + /** + * Returns true if given end date is before given start date. + */ + public static boolean isEndDateAfterStartDate(String startDate, String endDate) { + try { + LocalDate sDate = LocalDate.parse(startDate, STANDARD_DATE_TIME_FORMATTER); + LocalDate eDate = LocalDate.parse(endDate, STANDARD_DATE_TIME_FORMATTER); + return eDate.compareTo(sDate) >= 0; + } catch (DateTimeParseException e) { + return false; + } + } + + /** + * Returns true if given date is between start date and end date. + */ + public static boolean isBetweenStartAndEndDate(Date date, Date startDate, Date endDate) { + LocalDate sDate; + LocalDate eDate; + LocalDate d; + + try { + d = LocalDate.parse(date.toString(), STANDARD_DATE_TIME_FORMATTER); + sDate = LocalDate.parse(startDate.toString(), STANDARD_DATE_TIME_FORMATTER); + eDate = LocalDate.parse(endDate.toString(), STANDARD_DATE_TIME_FORMATTER); + } catch (DateTimeParseException e) { + return false; + } + + return d.compareTo(sDate) >= 0 && d.compareTo(eDate) <= 0; + } + + /** + * Returns the number of days between start date and end date. + */ + public static int numberOfDaysBetween(Date startDate, Date endDate) { + LocalDate sDate; + LocalDate eDate; + try { + sDate = LocalDate.parse(startDate.toString(), STANDARD_DATE_TIME_FORMATTER); + eDate = LocalDate.parse(endDate.toString(), STANDARD_DATE_TIME_FORMATTER); + } catch (DateTimeParseException e) { + return -1; + } + + return (int) ChronoUnit.DAYS.between(sDate, eDate); + } + + /** + * Returns today's Date. + */ + public static Date getToday() { + LocalDate today = LocalDate.now(ZoneId.systemDefault()); + return new Date(today.format(STANDARD_DATE_TIME_FORMATTER)); + } + + /** + * Returns the date of a week before today. + */ + public static Date getOneWeekBeforeToday() { + LocalDate today = LocalDate.parse(getToday().toString(), STANDARD_DATE_TIME_FORMATTER); + LocalDate oneWeekBefore = today.minusDays(6); + return new Date(oneWeekBefore.format(STANDARD_DATE_TIME_FORMATTER)); + } + + /** + * Returns a list of dates between start date and end date. + */ + public static ArrayList getListOfDates(Date startDate, Date endDate) { + int days; + LocalDate sDate; + LocalDate eDate; + + try { + sDate = LocalDate.parse(startDate.toString(), STANDARD_DATE_TIME_FORMATTER); + eDate = LocalDate.parse(endDate.toString(), STANDARD_DATE_TIME_FORMATTER); + } catch (DateTimeParseException e) { + return new ArrayList<>(); + } + days = (int) ChronoUnit.DAYS.between(sDate, eDate) + 1; + + ArrayList dates = new ArrayList<>(); + for (int i = 0; i < days; i++) { + LocalDate temp = sDate.plusDays(i); + Date date = new Date(temp.format(STANDARD_DATE_TIME_FORMATTER)); + dates.add(date); + } + + return dates; + } + + /** + * Convenient overloaded method to pretty print a date of format dd/MM/YYYY. + */ + public static String prettyPrint(String date) { + requireNonNull(date); + Date localDate = new Date(date); + return prettyPrint(localDate); + } + + /** + * Returns a representation of {@code date} that is more natural to human language. + * {@code prettyPrint} will use {@link Date#getToday()} to determine how to print the dates. + */ + public static String prettyPrint(Date date) { + int daysBetween = numberOfDaysBetween(Date.getToday(), date); + + //Within a week + if (daysBetween < 7) { + return String.format(MESSAGE_PRETTY_PRINT_ONE_UNIT, daysBetween, DAYS); + } else { + int numOfWeeksLeft = daysBetween / 7; + int numOfDaysLeft = daysBetween % 7; + + // A full week distance away. Don't show xxx days remaining. + if (numOfDaysLeft == 0) { + return String.format(MESSAGE_PRETTY_PRINT_ONE_UNIT, numOfWeeksLeft, WEEKS); + } else { + return String.format(MESSAGE_PRETTY_PRINT_TWO_UNITS, numOfWeeksLeft + " " + WEEKS, + numOfDaysLeft + " " + DAYS); + } + } + } + + /** + * Returns the dd/MM/YYYY representation of the date. + * Call {@link Date#prettyPrint} to return a more natural form of + * the date. + */ + @Override + public String toString() { + return value.format(STANDARD_DATE_TIME_FORMATTER); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Date // instanceof handles nulls + && value.equals(((Date) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/exercise/model/property/Muscle.java b/src/main/java/seedu/exercise/model/property/Muscle.java new file mode 100644 index 00000000000..8fbe121ad0b --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/Muscle.java @@ -0,0 +1,55 @@ +package seedu.exercise.model.property; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_ALPHABETS_AND_SPACE; +import static seedu.exercise.commons.util.AppUtil.checkArgument; + +import seedu.exercise.commons.util.StringUtil; + +/** + * Represents a Muscle tag in ExerHealth. + * Guarantees: immutable; name is valid as declared in {@link #isValidMuscleName(String)} + */ +public class Muscle { + public static final String PROPERTY_MUSCLE = "Muscle"; + public static final String MESSAGE_CONSTRAINTS = "Muscle groups should contain only alphabetical characters"; + public final String muscleName; + + /** + * Constructs a {@code Muscle}. + * + * @param muscleName A valid muscle name. + */ + public Muscle(String muscleName) { + requireNonNull(muscleName); + checkArgument(isValidMuscleName(muscleName), MESSAGE_CONSTRAINTS); + this.muscleName = StringUtil.capitaliseSingleWord(muscleName); + } + + /** + * Returns true if a given string is a valid muscle name. + */ + public static boolean isValidMuscleName(String test) { + return test.matches(ONLY_ALPHABETS_AND_SPACE); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Muscle // instanceof handles nulls + && muscleName.equals(((Muscle) other).muscleName)); // state check + } + + @Override + public int hashCode() { + return muscleName.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + muscleName + ']'; + } + +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/exercise/model/property/Name.java similarity index 57% rename from src/main/java/seedu/address/model/person/Name.java rename to src/main/java/seedu/exercise/model/property/Name.java index 79244d71cf7..ce3d1ecd694 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/exercise/model/property/Name.java @@ -1,23 +1,17 @@ -package seedu.address.model.person; +package seedu.exercise.model.property; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_ALPHABETS_NUMBERS_AND_SPACE; +import static seedu.exercise.commons.util.AppUtil.checkArgument; /** - * Represents a Person's name in the address book. + * Represents an Exercise's name in the exercise book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ public class Name { - + public static final String PROPERTY_NAME = "Name"; public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; - + "Names should only contain alphabets, numbers and spaces, and it should not be blank"; public final String fullName; /** @@ -35,7 +29,7 @@ public Name(String name) { * Returns true if a given string is a valid name. */ public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); + return test.matches(ONLY_ALPHABETS_NUMBERS_AND_SPACE); } @@ -47,8 +41,8 @@ public String toString() { @Override public boolean equals(Object other) { return other == this // short circuit if same object - || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check + || (other instanceof Name // instanceof handles nulls + && fullName.equals(((Name) other).fullName)); // state check } @Override diff --git a/src/main/java/seedu/exercise/model/property/PropertyBook.java b/src/main/java/seedu/exercise/model/property/PropertyBook.java new file mode 100644 index 00000000000..7fdb67fa5d1 --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/PropertyBook.java @@ -0,0 +1,216 @@ +package seedu.exercise.model.property; + +import static seedu.exercise.logic.parser.CliSyntax.setPropertyPrefixesSet; +import static seedu.exercise.model.util.DefaultPropertyBookUtil.getDefaultFullNames; +import static seedu.exercise.model.util.DefaultPropertyBookUtil.getDefaultPrefixes; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.logic.parser.Prefix; +import seedu.exercise.model.property.custom.CustomProperty; + +/** + * Helps to keep track of all the existing prefixes and full names for both default and custom properties. + * It also helps to keep track of all the existing custom properties that have been defined by the user. + * + * This solution was partly inspired by + * https://github.com/yunpengn/main/blob/master/src/main/java/seedu/address/model/property/PropertyManager.java. + */ +public class PropertyBook { + public static final String MESSAGE_DUPLICATE_NAME_OR_PREFIX = "The full name or the prefix of the custom" + + " clashes with another property/parameter"; + private static final Logger logger = LogsCenter.getLogger(PropertyBook.class); + private static PropertyBook propertyBook; + + // Helps to ensure that the prefixes used in add/edit command and full names of default + // properties are always present. + private final Set defaultPrefixes = getDefaultPrefixes(); + private final Set defaultFullNames = getDefaultFullNames(); + + private final Set customPrefixes = new HashSet<>(); + private final Set customFullNames = new HashSet<>(); + private final Set customProperties = new HashSet<>(); + + private PropertyBook() { + + } + + public static PropertyBook getInstance() { + if (propertyBook == null) { + logger.info("PropertyBook first initialised"); + propertyBook = new PropertyBook(); + } + return propertyBook; + } + + /** + * Adds in all the custom properties that are present in the given {@code Set customProperties} + * into the PropertyBook. + * + * @param customProperties the custom properties to be added + */ + public void addCustomProperties(Set customProperties) { + this.customProperties.addAll(customProperties); + setPrefixesAndFullNames(customProperties); + } + + /** + * Clears all of the custom properties in PropertyBook. + */ + public void clearCustomProperties() { + customProperties.clear(); + customPrefixes.clear(); + customFullNames.clear(); + } + + /** + * Returns an immutable custom properties set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getCustomProperties() { + return Collections.unmodifiableSet(customProperties); + } + + /** + * Returns an observable list of custom properties for display. + */ + public ObservableList getObservableCustomProperties() { + return FXCollections.observableList(new ArrayList<>(customProperties)); + } + + /** + * Adds the newly defined custom property into the PropertyBook. + */ + public void addCustomProperty(CustomProperty customProperty) { + Prefix newPrefix = customProperty.getPrefix(); + String newFullName = customProperty.getFullName(); + addPrefix(newPrefix); + addFullName(newFullName); + customProperties.add(customProperty); + updatePropertyPrefixes(); + } + + /** + * Removes the custom property with the associated {@code fullName}, if such a custom property + * exists. + */ + public void removeCustomProperty(String fullName) { + Optional toRemove = retrieveCustomProperty(fullName); + if (toRemove.isPresent()) { + removeCustomProperty(toRemove.get()); + } + } + + /** + * Removes the given {@code customProperty} and its associated prefix and full name from + * PropertyBook. + */ + private void removeCustomProperty(CustomProperty toRemove) { + Prefix prefixToRemove = toRemove.getPrefix(); + String fullNameToRemove = toRemove.getFullName(); + removePrefix(prefixToRemove); + removeFullName(fullNameToRemove); + customProperties.remove(toRemove); + updatePropertyPrefixes(); + } + + /** + * Returns true if the prefix has already been used by a property. + */ + public boolean isPrefixUsed(Prefix prefix) { + return customPrefixes.contains(prefix) || defaultPrefixes.contains(prefix); + } + + /** + * Returns true if the full name has already been used by a property. + */ + public boolean isFullNameUsed(String fullName) { + return customFullNames.contains(fullName) || defaultFullNames.contains(fullName); + } + + /** + * Returns true if the full name is used by a custom property. + */ + public boolean isFullNameUsedByCustomProperty(String fullName) { + return customFullNames.contains(fullName); + } + + /** + * Returns true if the given custom property is using any default/used custom names and prefixes. + */ + public boolean hasClashingPrefixOrName(CustomProperty customProperty) { + return isPrefixUsed(customProperty.getPrefix()) + || isFullNameUsed(customProperty.getFullName()); + } + + /** + * Adds the prefix and full name of each of the custom property in {@code customProperties} into + * the sets. + */ + private void setPrefixesAndFullNames(Set customProperties) { + for (CustomProperty property : customProperties) { + this.addPrefix(property.getPrefix()); + this.addFullName(property.getFullName()); + } + } + + /** + * Adds the prefix of a newly defined custom property. + */ + private void addPrefix(Prefix prefix) { + customPrefixes.add(prefix); + } + + /** + * Adds the full name of a newly defined custom property. + */ + private void addFullName(String fullName) { + customFullNames.add(fullName); + } + + /** + * Removes the prefix of a custom property from {@code prefixes}. + */ + private void removePrefix(Prefix prefix) { + customPrefixes.remove(prefix); + } + + /** + * Removes the full name of a custom property from {@code fullNames}. + */ + private void removeFullName(String fullName) { + customFullNames.remove(fullName); + } + + /** + * Updates the property prefixes in {@code CliSyntax} class for use in add/edit command. + */ + private void updatePropertyPrefixes() { + Set combinedSet = new HashSet<>(); + combinedSet.addAll(defaultPrefixes); + combinedSet.addAll(customPrefixes); + setPropertyPrefixesSet(Collections.unmodifiableSet(combinedSet)); + } + + /** + * Retrieves the custom property with the given {@code fullName}. + */ + private Optional retrieveCustomProperty(String fullName) { + Optional retrieved = Optional.empty(); + for (CustomProperty customProperty : customProperties) { + if (customProperty.getFullName().equals(fullName)) { + retrieved = Optional.of(customProperty); + break; + } + } + return retrieved; + } +} diff --git a/src/main/java/seedu/exercise/model/property/Quantity.java b/src/main/java/seedu/exercise/model/property/Quantity.java new file mode 100644 index 00000000000..fefdfa4921a --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/Quantity.java @@ -0,0 +1,59 @@ +package seedu.exercise.model.property; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_NON_NEGATIVE_NUMBERS; +import static seedu.exercise.commons.util.AppUtil.checkArgument; + +/** + * Represents a the quantity of an exercise done in the exercise book. + * Guarantees: immutable; is valid as declared in {@link #isValidQuantity(String)} + */ +public class Quantity { + public static final String PROPERTY_QUANTITY = "Quantity"; + public static final String MESSAGE_CONSTRAINTS = "Quantity should only contain numbers, and it should not be blank"; + private final String value; + + /** + * Constructs an {@code Quantity}. + * + * @param quantity A valid quantity. + */ + public Quantity(String quantity) { + requireNonNull(quantity); + checkArgument(isValidQuantity(quantity), MESSAGE_CONSTRAINTS); + value = quantity; + } + + /** + * Returns true if a given string is a valid quantity. + */ + public static boolean isValidQuantity(String test) { + return test.matches(ONLY_NON_NEGATIVE_NUMBERS); + } + + @Override + public String toString() { + if (!value.contains(".")) { + return Integer.toString(Integer.parseInt(value)); + } else { + double dValue = Double.parseDouble(value); + if (dValue == (int) dValue) { + return Integer.toString((int) dValue); + } + return Double.toString(dValue); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Quantity // instanceof handles nulls + && value.equals(((Quantity) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/exercise/model/property/Unit.java b/src/main/java/seedu/exercise/model/property/Unit.java new file mode 100644 index 00000000000..bc4a7ccecbc --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/Unit.java @@ -0,0 +1,53 @@ +package seedu.exercise.model.property; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_ALPHABETS; +import static seedu.exercise.commons.util.AppUtil.checkArgument; + +/** + * Represents an Exercise's unit in the exercise book. + * Guarantees: immutable; is valid as declared in {@link #isValidUnit(String)} + */ +public class Unit { + public static final String PROPERTY_UNIT = "Unit"; + public static final String MESSAGE_CONSTRAINTS = + "Units should only contain alphabets and it should not be blank"; + public final String unit; + + /** + * Constructs a {@code Unit}. + * + * @param unit A valid unit. + */ + public Unit(String unit) { + requireNonNull(unit); + checkArgument(isValidUnit(unit), MESSAGE_CONSTRAINTS); + this.unit = unit; + } + + /** + * Returns true if a given string is a valid unit. + */ + public static boolean isValidUnit(String test) { + return test.matches(ONLY_ALPHABETS); + } + + + @Override + public String toString() { + return unit; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Unit // instanceof handles nulls + && unit.equals(((Unit) other).unit)); // state check + } + + @Override + public int hashCode() { + return unit.hashCode(); + } + +} diff --git a/src/main/java/seedu/exercise/model/property/custom/CustomProperty.java b/src/main/java/seedu/exercise/model/property/custom/CustomProperty.java new file mode 100644 index 00000000000..5ffec21f888 --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/custom/CustomProperty.java @@ -0,0 +1,120 @@ +package seedu.exercise.model.property.custom; + +import static seedu.exercise.commons.core.ValidationRegex.ONLY_ALPHABETS; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_ALPHABETS_AND_SPACE; +import static seedu.exercise.commons.util.AppUtil.checkArgument; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.exercise.logic.parser.Prefix; + +/** + * Represents a custom property that an {@code Exercise} object can have. + * + * This solution was partly inspired by + * https://github.com/yunpengn/main/blob/master/src/main/java/seedu/address/model/property/Property.java. + */ +public class CustomProperty { + public static final String FULL_NAME_CONSTRAINTS = "Full names should contain only alphabets and should" + + " not be blank."; + public static final String PREFIX_NAME_CONSTRAINTS = "Prefix names should contain only alphabets, should" + + " have no spaces and should not be blank."; + + private final Prefix prefix; + private final String fullName; + private final ParameterType parameterType; + + /** + * Instantiates a new {@code CustomProperty} instance. + * + * @param fullName the full name of the custom property + * @param prefix the prefix for the custom property + * @param parameterType the string used to validate an input for the custom property + */ + public CustomProperty(Prefix prefix, String fullName, ParameterType parameterType) { + requireAllNonNull(fullName, prefix, parameterType); + checkArgument(isValidFullName(fullName), FULL_NAME_CONSTRAINTS); + checkArgument(isValidPrefixName(prefix.getPrefixName()), PREFIX_NAME_CONSTRAINTS); + this.fullName = fullName; + this.prefix = prefix; + this.parameterType = parameterType; + } + + /** + * Checks if the given full name is valid. + * + * @param test the full name of a custom property + * @return true if and only if the full name contains only alphabets + */ + public static boolean isValidFullName(String test) { + return test.matches(ONLY_ALPHABETS_AND_SPACE); + } + + /** + * Checks if the given prefix name is valid. The prefix name of a custom property refers to the text before + * '/' in its prefix. + * + * @param test the prefix name of a custom property + * @return true if and only if the prefix name contains only alphabets and does not contain any whitespaces. + */ + public static boolean isValidPrefixName(String test) { + return test.matches(ONLY_ALPHABETS); + } + + /** + * Retrieves the full name of the {@code CustomProperty} instance. + */ + public String getFullName() { + return fullName; + } + + /** + * Returns the prefix of the {@code CustomProperty} instance. + * + * @return a {@code Prefix} object that represents the prefix of the instance + */ + public Prefix getPrefix() { + return new Prefix(prefix.toString()); + } + + /** + * Retrieves the parameter type of the {@code CustomProperty} instance. + * + * @return a {@code ParameterType} object that represents the parameter type of the instance + */ + public ParameterType getParameterType() { + return this.parameterType; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof CustomProperty)) { + return false; + } + + CustomProperty otherProperty = (CustomProperty) other; + return fullName.equals(otherProperty.fullName) + && prefix.equals(otherProperty.prefix) + && parameterType.equals(otherProperty.parameterType); + } + + @Override + public int hashCode() { + return Objects.hash(fullName, prefix, parameterType); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(" Full Name: ") + .append(fullName) + .append(" Prefix Name: ") + .append(prefix.getPrefixName()); + return builder.toString(); + } +} diff --git a/src/main/java/seedu/exercise/model/property/custom/ParameterType.java b/src/main/java/seedu/exercise/model/property/custom/ParameterType.java new file mode 100644 index 00000000000..367017daed2 --- /dev/null +++ b/src/main/java/seedu/exercise/model/property/custom/ParameterType.java @@ -0,0 +1,71 @@ +package seedu.exercise.model.property.custom; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_ALPHABETS_AND_SPACE; +import static seedu.exercise.commons.core.ValidationRegex.ONLY_NON_NEGATIVE_INTEGER; + +/** + * Encapsulates the different parameter types that a custom property can take in. + */ +public enum ParameterType { + TEXT("Text"), + DATE("Date"), + NUMBER("Number"); + + public static final String PARAMETER_CONSTRAINTS = "Parameter types should be one of the following: Number, " + + "Date or Text"; + public static final String TEXT_CONSTRAINTS = "Text should contain only alphabets and spaces and " + + "should not be blank."; + public static final String NUMBER_CONSTRAINTS = "Number should be a non-negative integer and should not " + + "be blank."; + private final String parameterName; + + ParameterType(String parameterName) { + this.parameterName = parameterName; + } + + /** + * Checks if the given {@code String string} is a valid parameter type. + * + * @param string the parameter type to be validated + * @return true if and only if the given string is a valid parameter type + */ + public static boolean isValidParameterType(String string) { + requireNonNull(string); + return string.equals(TEXT.parameterName) + || string.equals(DATE.parameterName) + || string.equals(NUMBER.parameterName); + } + + /** + * Checks if the given {@code String value} is a valid text for the parameter type {@code Text}. + * {@code Text} requires the value to contain only alphabets. + * + * @param value the value to be checked + * @return true if and only if the the given value is a valid text + */ + public static boolean isValidText(String value) { + return value.matches(ONLY_ALPHABETS_AND_SPACE); + } + + /** + * Checks if the given {@code String value} is a valid value for the parameter type {@code Number}. + * {@code Number} requires the value to contain only numbers. + * + * @param value the value to be checked + * @return true if and only if the given value is a valid number + */ + public static boolean isValidNumber(String value) { + return value.matches(ONLY_NON_NEGATIVE_INTEGER); + } + + /** + * Returns the parameter name of the {@code ParameterType} instance. + * + * @return a string representing the name of the {@code ParameterType} instance + */ + public String getParameterName() { + return this.parameterName; + } + +} diff --git a/src/main/java/seedu/exercise/model/resource/Exercise.java b/src/main/java/seedu/exercise/model/resource/Exercise.java new file mode 100644 index 00000000000..abe340d1c23 --- /dev/null +++ b/src/main/java/seedu/exercise/model/resource/Exercise.java @@ -0,0 +1,192 @@ +package seedu.exercise.model.resource; + +import static seedu.exercise.commons.util.CollectionUtil.mapToStringList; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.storage.resource.JsonAdaptedExercise; + +/** + * Represents an Exercise in the exercise book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Exercise extends Resource { + + // Identity fields + private final Name name; + private final Date date; + private final Quantity quantity; + private final Unit unit; + private final Calories calories; + + // Optional fields + private final Set muscles = new HashSet<>(); + private final Map customProperties = new TreeMap<>(); + + /** + * Every field must be present and not null. + */ + public Exercise(Name name, Date date, Calories calories, Quantity quantity, Unit unit, Set muscles, + Map customProperties) { + requireAllNonNull(name, date, calories, quantity, unit, muscles); + this.name = name; + this.date = date; + this.calories = calories; + this.quantity = quantity; + this.unit = unit; + this.muscles.addAll(muscles); + this.customProperties.putAll(customProperties); + } + + public Name getName() { + return name; + } + + public Calories getCalories() { + return calories; + } + + public Date getDate() { + return date; + } + + public Quantity getQuantity() { + return quantity; + } + + public Unit getUnit() { + return unit; + } + + + /** + * Returns an immutable muscle set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getMuscles() { + return Collections.unmodifiableSet(muscles); + } + + /** + * Returns an immutable custom properties map, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Map getCustomPropertiesMap() { + return Collections.unmodifiableMap(customProperties); + } + + /** + * Returns an Observable List that can be used by the UI component for display. + */ + public ObservableList getObservableCustomPropertiesList() { + List propertiesList = mapToStringList(customProperties); + return FXCollections.observableList(propertiesList); + } + + /** + * Returns true if both exercises of the same name have at least one other identity field that is the same. + * This defines a weaker notion of equality between two exercises. + */ + @Override + public boolean isSameResource(Resource otherResource) { + if (otherResource == this) { + return true; + } + + if (!(otherResource instanceof Exercise)) { + return false; + } + + Exercise otherExercise = (Exercise) otherResource; + return otherExercise != null + && otherExercise.getName().equals(getName()) + && otherExercise.getDate().equals(getDate()); + } + + /** + * Returns true if both exercises have the same identity and data fields. + * This defines a stronger notion of equality between two exercises. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Exercise)) { + return false; + } + + Exercise otherExercise = (Exercise) other; + return otherExercise.getName().equals(getName()) + && otherExercise.getCalories().equals(getCalories()) + && otherExercise.getDate().equals(getDate()) + && otherExercise.getQuantity().equals(getQuantity()) + && otherExercise.getUnit().equals(getUnit()) + && otherExercise.getMuscles().equals(getMuscles()) + && otherExercise.getCustomPropertiesMap().equals(getCustomPropertiesMap()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(name, date, calories, quantity, unit, muscles, customProperties); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getName()) + .append(" Date: ") + .append(getDate()) + .append(" Calories: ") + .append(getCalories()) + .append(" Quantity: ") + .append(getQuantity()) + .append(" Unit: ") + .append(getUnit()); + appendMuscles(builder); + builder.append(" "); + appendCustomProperties(builder); + return builder.toString().stripTrailing(); + } + + @Override + public JsonAdaptedExercise toJsonType() { + return new JsonAdaptedExercise(this); + } + + /** + * Appends muscle information into the input StringBuilder, if there are any. + */ + private void appendMuscles(StringBuilder builder) { + if (!muscles.isEmpty()) { + builder.append(" Muscle(s): "); + getMuscles().forEach(builder::append); + } + } + + /** + * Appends custom properties information into the input StringBuilder, if there are any. + */ + private void appendCustomProperties(StringBuilder builder) { + if (!customProperties.isEmpty()) { + getCustomPropertiesMap().forEach((x, y) -> builder.append(x + ": " + y + " ")); + } + } +} diff --git a/src/main/java/seedu/exercise/model/resource/Regime.java b/src/main/java/seedu/exercise/model/resource/Regime.java new file mode 100644 index 00000000000..c0cbc196879 --- /dev/null +++ b/src/main/java/seedu/exercise/model/resource/Regime.java @@ -0,0 +1,95 @@ +package seedu.exercise.model.resource; + +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.util.Objects; + +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.property.Name; +import seedu.exercise.storage.resource.JsonAdaptedRegime; + +/** + * Represents a Regime in the regime book. + */ +public class Regime extends Resource { + private final Name regimeName; + private final SortedUniqueResourceList regimeExercises; + + public Regime(Name regimeName, SortedUniqueResourceList regimeExercises) { + this.regimeName = regimeName; + this.regimeExercises = regimeExercises; + } + + public void addExercise(Exercise exercise) { + regimeExercises.add(exercise); + } + + public void deleteExercise(Exercise exercise) { + regimeExercises.remove(exercise); + } + + public Name getRegimeName() { + return regimeName; + } + + public SortedUniqueResourceList getRegimeExercises() { + return regimeExercises; + } + + /** + * Returns a deep copy of the current regime. + * + * @return a regime object with the same name and same list of exercises + */ + public Regime deepCopy() { + Name newName = new Name(regimeName.toString()); + SortedUniqueResourceList newRegimeExercises = + new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + newRegimeExercises.setAll(regimeExercises); + return new Regime(newName, newRegimeExercises); + } + + public int getTotalCalorieCount() { + int count = 0; + for (Exercise e : regimeExercises) { + count += Integer.parseInt(e.getCalories().toString()); + } + return count; + } + + /** + * Returns true if both regimes have the same name. + */ + @Override + public boolean isSameResource(Resource otherResource) { + return this.equals(otherResource); + } + + @Override + public String toString() { + String str = ""; + int i = 1; + for (Exercise e : regimeExercises) { + str += "Exercise " + i + ": " + e.getName().toString() + "\n"; + i++; + } + return str; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Regime) + && regimeName.equals(((Regime) other).getRegimeName()); + } + + @Override + public int hashCode() { + return Objects.hash(regimeName, regimeExercises); + } + + @Override + public JsonAdaptedRegime toJsonType() { + return new JsonAdaptedRegime(this); + } +} diff --git a/src/main/java/seedu/exercise/model/resource/Resource.java b/src/main/java/seedu/exercise/model/resource/Resource.java new file mode 100644 index 00000000000..7f2e36270e4 --- /dev/null +++ b/src/main/java/seedu/exercise/model/resource/Resource.java @@ -0,0 +1,15 @@ +package seedu.exercise.model.resource; + +import seedu.exercise.storage.resource.JsonAdaptedResource; + +/** + * Encapsulates the various resources that will be tracked by the app. + * Resources tracked by the app are {@code Exercise}, {@code Regime} and {@code Schedule}. + */ +public abstract class Resource { + + public abstract boolean isSameResource(Resource otherResource); + + public abstract JsonAdaptedResource toJsonType(); + +} diff --git a/src/main/java/seedu/exercise/model/resource/ResourceComparator.java b/src/main/java/seedu/exercise/model/resource/ResourceComparator.java new file mode 100644 index 00000000000..3141c56059d --- /dev/null +++ b/src/main/java/seedu/exercise/model/resource/ResourceComparator.java @@ -0,0 +1,28 @@ +package seedu.exercise.model.resource; + +import java.util.Comparator; + +/** + * Holds comparators for sorting of resource lists. + */ +public class ResourceComparator { + + public static final Comparator DEFAULT_EXERCISE_COMPARATOR = new Comparator() { + @Override + public int compare(Exercise exercise, Exercise t1) { + int dateCompare = -exercise.getDate().value.compareTo(t1.getDate().value); + if (dateCompare == 0) { + int nameCompare = exercise.getName().fullName.compareTo(t1.getName().fullName); + return nameCompare; + } else { + return dateCompare; + } + } + }; + + public static final Comparator DEFAULT_REGIME_COMPARATOR = + Comparator.comparing(o -> o.getRegimeName().toString()); + + public static final Comparator DEFAULT_SCHEDULE_COMPARATOR = + Comparator.comparing(o -> o.getDate().value); +} diff --git a/src/main/java/seedu/exercise/model/resource/Schedule.java b/src/main/java/seedu/exercise/model/resource/Schedule.java new file mode 100644 index 00000000000..13928fe2318 --- /dev/null +++ b/src/main/java/seedu/exercise/model/resource/Schedule.java @@ -0,0 +1,68 @@ +package seedu.exercise.model.resource; + +import java.util.List; +import java.util.Objects; + +import seedu.exercise.model.property.Date; +import seedu.exercise.storage.resource.JsonAdaptedSchedule; + +/** + * Represents a schedule for a regime at a certain date. + */ +public class Schedule extends Resource { + private static final String SCHEDULE_STRING_FORMATTER = "%s (%s)\n%s"; + private final Regime regime; + private final Date date; + + public Schedule(Regime regime, Date date) { + this.regime = regime; + this.date = date; + } + + public Date getDate() { + return date; + } + + public Regime getRegime() { + return regime; + } + + public String getRegimeName() { + return regime.getRegimeName().toString(); + } + + public List getExercises() { + return regime.getRegimeExercises().asUnmodifiableObservableList(); + } + + /** + * Returns true if both {@code schedules} have the same date + */ + @Override + public boolean isSameResource(Resource otherResource) { + return this.equals(otherResource); + } + + @Override + public boolean equals(Object other) { + return (other == this) + || (other instanceof Schedule) + && date.equals(((Schedule) other).date); + } + + @Override + public int hashCode() { + return Objects.hash(regime, date); + } + + @Override + public String toString() { + return String.format(SCHEDULE_STRING_FORMATTER, getRegimeName(), date.toString(), regime.toString()); + } + + @Override + public JsonAdaptedSchedule toJsonType() { + return new JsonAdaptedSchedule(this); + } + +} diff --git a/src/main/java/seedu/exercise/model/util/DateChangerUtil.java b/src/main/java/seedu/exercise/model/util/DateChangerUtil.java new file mode 100644 index 00000000000..e7d9bbd22bf --- /dev/null +++ b/src/main/java/seedu/exercise/model/util/DateChangerUtil.java @@ -0,0 +1,32 @@ +package seedu.exercise.model.util; + +import java.util.Collection; +import java.util.stream.Collectors; + +import seedu.exercise.model.property.Date; +import seedu.exercise.model.resource.Exercise; + +/** + * Contains utility method to change dates of exercises + */ +public class DateChangerUtil { + + /** + * Changes all {@code exercise} to the date sepecified by {@code changedDate}. + *

+ * Operation will create a whole new list that is not backed by {@code exercises}. + *

+ */ + public static Collection changeAllDate(Collection exercises, Date changedDate) { + return exercises.stream() + .map(exercise -> new Exercise( + exercise.getName(), + changedDate, + exercise.getCalories(), + exercise.getQuantity(), + exercise.getUnit(), + exercise.getMuscles(), + exercise.getCustomPropertiesMap())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/seedu/exercise/model/util/DefaultExerciseDatabaseUtil.java b/src/main/java/seedu/exercise/model/util/DefaultExerciseDatabaseUtil.java new file mode 100644 index 00000000000..e75f5f1a27a --- /dev/null +++ b/src/main/java/seedu/exercise/model/util/DefaultExerciseDatabaseUtil.java @@ -0,0 +1,78 @@ +package seedu.exercise.model.util; + +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; +import static seedu.exercise.model.util.DateChangerUtil.changeAllDate; +import static seedu.exercise.model.util.SampleDataUtil.getMuscleSet; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.TreeMap; + +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.resource.Exercise; + +/** + * Contains utility methods for exercise database. + */ +public class DefaultExerciseDatabaseUtil { + + public static Exercise[] getExerciseDatabase() { + return new Exercise[]{ + new Exercise(new Name("Push Ups"), new Date("11/11/2019"), new Calories("30"), + new Quantity("100"), new Unit("counts"), + getMuscleSet("Chest", "Triceps"), new TreeMap<>()), + new Exercise(new Name("Sit ups"), new Date("10/11/2019"), new Calories("30"), + new Quantity("100"), new Unit("counts"), + getMuscleSet("Abs"), new TreeMap<>()), + new Exercise(new Name("Bench Press"), new Date("11/11/2019"), new Calories("50"), + new Quantity("70"), new Unit("kg"), + getMuscleSet("Chest", "Triceps"), new TreeMap<>()), + new Exercise(new Name("Deadlift"), new Date("11/11/2019"), new Calories("80"), + new Quantity("120"), new Unit("kg"), + getMuscleSet("Legs", "Back"), new TreeMap<>()), + new Exercise(new Name("Run"), new Date("11/11/2019"), new Calories("200"), + new Quantity("10"), new Unit("km"), + getMuscleSet("Triceps"), new TreeMap<>()), + new Exercise(new Name("Squat"), new Date("11/11/2019"), new Calories("100"), + new Quantity("100"), new Unit("kg"), + getMuscleSet("Legs", "Back"), new TreeMap<>()), + new Exercise(new Name("Pull Ups"), new Date("11/11/2019"), new Calories("50"), + new Quantity("50"), new Unit("counts"), + getMuscleSet("Back", "Biceps"), new TreeMap<>()) + }; + } + + public static List getBasicExercises() { + Collection basicExercises = Arrays.asList( + new Exercise(new Name("Push Ups"), new Date("07/11/2019"), new Calories("80"), + new Quantity("100"), new Unit("counts"), + getMuscleSet("Chest", "Triceps"), new TreeMap<>()), + new Exercise(new Name("Sit ups"), new Date("07/11/2019"), new Calories("50"), + new Quantity("100"), new Unit("counts"), + getMuscleSet("Abs"), new TreeMap<>()), + new Exercise(new Name("Squats"), new Date("07/11/2019"), new Calories("100"), + new Quantity("100"), new Unit("counts"), + getMuscleSet("Legs"), new TreeMap<>()), + new Exercise(new Name("Run"), new Date("07/11/2019"), new Calories("300"), + new Quantity("10"), new Unit("km"), + getMuscleSet("Legs", "Cardio"), new TreeMap<>()) + ); + return new ArrayList<>(changeAllDate(basicExercises, Date.getToday())); + } + + public static ReadOnlyResourceBook getExerciseDatabaseBook() { + ReadOnlyResourceBook exerciseDatabaseBook = new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR); + Collection exerciseDatabase = changeAllDate(Arrays.asList(getExerciseDatabase()), Date.getToday()); + for (Exercise exercise : exerciseDatabase) { + exerciseDatabaseBook.addResource(exercise); + } + return exerciseDatabaseBook; + } +} diff --git a/src/main/java/seedu/exercise/model/util/DefaultPropertyBookUtil.java b/src/main/java/seedu/exercise/model/util/DefaultPropertyBookUtil.java new file mode 100644 index 00000000000..d28706c8092 --- /dev/null +++ b/src/main/java/seedu/exercise/model/util/DefaultPropertyBookUtil.java @@ -0,0 +1,57 @@ +package seedu.exercise.model.util; + +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CALORIES; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_CATEGORY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_INDEX; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_MUSCLE; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_QUANTITY; +import static seedu.exercise.logic.parser.CliSyntax.PREFIX_UNIT; +import static seedu.exercise.model.property.Calories.PROPERTY_CALORIES; +import static seedu.exercise.model.property.Date.PROPERTY_DATE; +import static seedu.exercise.model.property.Muscle.PROPERTY_MUSCLE; +import static seedu.exercise.model.property.Name.PROPERTY_NAME; +import static seedu.exercise.model.property.Quantity.PROPERTY_QUANTITY; +import static seedu.exercise.model.property.Unit.PROPERTY_UNIT; + +import java.util.HashSet; +import java.util.Set; + +import seedu.exercise.logic.parser.Prefix; + +/** + * Contains utility methods for initialising a default {@code PropertyBook}. + */ +public class DefaultPropertyBookUtil { + + /** + * Creates a new {@code Set} that contains all the prefixes of the default exercise properties. + */ + public static Set getDefaultPrefixes() { + Set defaultPrefixes = new HashSet<>(); + defaultPrefixes.add(PREFIX_NAME); + defaultPrefixes.add(PREFIX_DATE); + defaultPrefixes.add(PREFIX_CALORIES); + defaultPrefixes.add(PREFIX_QUANTITY); + defaultPrefixes.add(PREFIX_MUSCLE); + defaultPrefixes.add(PREFIX_UNIT); + defaultPrefixes.add(PREFIX_INDEX); + defaultPrefixes.add(PREFIX_CATEGORY); + return defaultPrefixes; + } + + /** + * Creates a new {@code Set} that contains all the full names of the default exercise properties. + */ + public static Set getDefaultFullNames() { + Set defaultFullNames = new HashSet<>(); + defaultFullNames.add(PROPERTY_NAME); + defaultFullNames.add(PROPERTY_DATE); + defaultFullNames.add(PROPERTY_CALORIES); + defaultFullNames.add(PROPERTY_QUANTITY); + defaultFullNames.add(PROPERTY_MUSCLE); + defaultFullNames.add(PROPERTY_UNIT); + return defaultFullNames; + } +} diff --git a/src/main/java/seedu/exercise/model/util/SampleDataUtil.java b/src/main/java/seedu/exercise/model/util/SampleDataUtil.java new file mode 100644 index 00000000000..95f0b9a6477 --- /dev/null +++ b/src/main/java/seedu/exercise/model/util/SampleDataUtil.java @@ -0,0 +1,138 @@ +package seedu.exercise.model.util; + +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_REGIME_COMPARATOR; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_SCHEDULE_COMPARATOR; + +import java.util.Arrays; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * Contains utility methods for populating {@code ReadOnlyResourceBook} with sample data. + */ +public class SampleDataUtil { + public static Exercise[] getSampleExercises() { + return new Exercise[]{ + new Exercise(new Name("Rope Skipping"), new Date("12/11/2019"), new Calories("100"), + new Quantity("10"), new Unit("counts"), + getMuscleSet("Legs"), new TreeMap<>()), + new Exercise(new Name("Cycling"), new Date("11/11/2019"), new Calories("50"), + new Quantity("5"), new Unit("km"), + getMuscleSet("Legs"), new TreeMap<>()), + new Exercise(new Name("Strength Training"), new Date("10/11/2019"), new Calories("249"), + new Quantity("20"), new Unit("counts"), + getMuscleSet("Chest"), new TreeMap<>()), + new Exercise(new Name("Swimming"), new Date("09/11/2019"), new Calories("160"), + new Quantity("10"), new Unit("laps"), + getMuscleSet("Calves"), new TreeMap<>()), + new Exercise(new Name("Bench Press"), new Date("08/11/2019"), new Calories("182"), + new Quantity("30"), new Unit("counts"), + getMuscleSet("Triceps"), new TreeMap<>()), + new Exercise(new Name("Running"), new Date("07/11/2019"), new Calories("40"), + new Quantity("2.4"), new Unit("km"), + getMuscleSet("Legs"), new TreeMap<>()) + }; + } + + public static Regime[] getSampleRegimes() { + SortedUniqueResourceList list1 = new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + list1.add(new Exercise(new Name("Rope Skipping"), new Date("17/11/2019"), new Calories("330"), + new Quantity("10"), new Unit("counts"), + getMuscleSet("Legs"), new TreeMap<>())); + list1.add(new Exercise(new Name("Bench Press"), new Date("17/11/2019"), new Calories("222"), + new Quantity("30"), new Unit("counts"), + getMuscleSet("Triceps"), new TreeMap<>())); + + + SortedUniqueResourceList list2 = new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + list2.add(new Exercise(new Name("Running"), new Date("20/11/2019"), new Calories("127"), + new Quantity("2.4"), new Unit("km"), + getMuscleSet("Legs"), new TreeMap<>())); + list2.add(new Exercise(new Name("Bench Press"), new Date("20/11/2019"), new Calories("222"), + new Quantity("30"), new Unit("counts"), + getMuscleSet("Triceps"), new TreeMap<>())); + list2.add(new Exercise(new Name("Swimming"), new Date("20/11/2019"), new Calories("354"), + new Quantity("10"), new Unit("laps"), + getMuscleSet("Calves"), new TreeMap<>())); + + SortedUniqueResourceList list3 = new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + list3.add(new Exercise(new Name("Rope Skipping"), new Date("22/11/2019"), new Calories("230"), + new Quantity("10"), new Unit("counts"), + getMuscleSet("Legs"), new TreeMap<>())); + list3.add(new Exercise(new Name("Swimming"), new Date("22/11/2019"), new Calories("254"), + new Quantity("10"), new Unit("laps"), + getMuscleSet("Calves"), new TreeMap<>())); + list3.add(new Exercise(new Name("Bench Press"), new Date("22/11/2019"), new Calories("122"), + new Quantity("30"), new Unit("counts"), + getMuscleSet("Triceps"), new TreeMap<>())); + list3.add(new Exercise(new Name("Cycling"), new Date("22/11/2019"), new Calories("184"), + new Quantity("5"), new Unit("km"), + getMuscleSet("Legs"), new TreeMap<>())); + list3.add(new Exercise(new Name("Strength Training"), new Date("22/11/2019"), new Calories("241"), + new Quantity("20"), new Unit("counts"), + getMuscleSet("Chest"), new TreeMap<>())); + + return new Regime[]{ + new Regime(new Name("Level 1"), list1), + new Regime(new Name("Level 2"), list2), + new Regime(new Name("Level 3"), list3) + }; + } + + public static Schedule[] getSampleSchedules() { + Regime[] sampleRegimes = getSampleRegimes(); + return new Schedule[]{ + new Schedule(sampleRegimes[0], new Date("17/11/2019")), + new Schedule(sampleRegimes[1], new Date("20/11/2019")), + new Schedule(sampleRegimes[2], new Date("22/11/2019")) + }; + } + + /** + * Returns a muscle set containing the list of strings given. + */ + public static Set getMuscleSet(String... strings) { + return Arrays.stream(strings) + .map(Muscle::new) + .collect(Collectors.toSet()); + } + + + public static ReadOnlyResourceBook getSampleExerciseBook() { + ReadOnlyResourceBook sampleEb = new ReadOnlyResourceBook<>(DEFAULT_EXERCISE_COMPARATOR); + for (Exercise sampleExercise : getSampleExercises()) { + sampleEb.addResource(sampleExercise); + } + return sampleEb; + } + + public static ReadOnlyResourceBook getSampleScheduleBook() { + ReadOnlyResourceBook sampleSb = new ReadOnlyResourceBook<>(DEFAULT_SCHEDULE_COMPARATOR); + for (Schedule sampleSchedule : getSampleSchedules()) { + sampleSb.addResource(sampleSchedule); + } + return sampleSb; + } + + public static ReadOnlyResourceBook getSampleRegimeBook() { + ReadOnlyResourceBook sampleRb = new ReadOnlyResourceBook<>(DEFAULT_REGIME_COMPARATOR); + for (Regime sampleRegime : getSampleRegimes()) { + sampleRb.addResource(sampleRegime); + } + return sampleRb; + } +} diff --git a/src/main/java/seedu/exercise/storage/JsonAdaptedCustomProperty.java b/src/main/java/seedu/exercise/storage/JsonAdaptedCustomProperty.java new file mode 100644 index 00000000000..a89c527b5cf --- /dev/null +++ b/src/main/java/seedu/exercise/storage/JsonAdaptedCustomProperty.java @@ -0,0 +1,114 @@ +package seedu.exercise.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.logic.parser.Prefix; +import seedu.exercise.model.property.custom.CustomProperty; +import seedu.exercise.model.property.custom.ParameterType; + +/** + * Jackson-friendly version of {@link CustomProperty}; + */ +public class JsonAdaptedCustomProperty { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "CustomProperty's %s field is missing!"; + + private final String prefixName; + private final String fullName; + private final String parameterType; + + /** + * Constructs a {@code JsonAdaptedCustomProperty} with the given custom property details. + */ + @JsonCreator + public JsonAdaptedCustomProperty(@JsonProperty("prefixName") String prefixName, + @JsonProperty("fullName") String fullName, + @JsonProperty("parameterType") String parameterType) { + this.prefixName = prefixName; + this.fullName = fullName; + this.parameterType = parameterType; + } + + /** + * Constructs a given {@code CustomProperty} into this class for Jackson use. + */ + public JsonAdaptedCustomProperty(CustomProperty source) { + this.prefixName = source.getPrefix().getPrefixName(); + this.fullName = source.getFullName(); + this.parameterType = source.getParameterType().getParameterName(); + } + + /** + * Converts a Jackson-friendly adapted custom property object into its corresponding + * model's {@code CustomProperty} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted custom property + */ + public CustomProperty toModelType() throws IllegalValueException { + final Prefix modelPrefix = toModelPrefix(); + final String modelFullName = toModelFullName(); + final ParameterType modelParameterType = toModelParameterType(); + return new CustomProperty(modelPrefix, modelFullName, modelParameterType); + } + + /** + * Converts the prefix name of a Jackson-friendly adapted custom property object into its corresponding + * model's {@code Prefix} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted custom property + */ + private Prefix toModelPrefix() throws IllegalValueException { + if (prefixName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Prefix.class.getSimpleName())); + } + if (!CustomProperty.isValidPrefixName(prefixName)) { + throw new IllegalValueException(CustomProperty.PREFIX_NAME_CONSTRAINTS); + } + return new Prefix(prefixName + "/"); + } + + /** + * Converts the full name of a Jackson-friendly adapted custom property object into its corresponding + * model's name. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted custom property + */ + private String toModelFullName() throws IllegalValueException { + String fullNameField = "Full Name"; + if (fullName == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, fullNameField)); + } + if (!CustomProperty.isValidFullName(fullName)) { + throw new IllegalValueException(CustomProperty.FULL_NAME_CONSTRAINTS); + } + return fullName.trim(); + } + + /** + * Converts the parameter type of a Jackson-friendly adapted custom property object into its corresponding + * model's {@code ParameterType} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted custom property + */ + private ParameterType toModelParameterType() throws IllegalValueException { + if (parameterType == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + ParameterType.class.getSimpleName())); + } + + if (!ParameterType.isValidParameterType(parameterType)) { + throw new IllegalValueException(ParameterType.PARAMETER_CONSTRAINTS); + } + + if (parameterType.equals(ParameterType.NUMBER.getParameterName())) { + return ParameterType.NUMBER; + } else if (parameterType.equals(ParameterType.TEXT.getParameterName())) { + return ParameterType.TEXT; + } else { + return ParameterType.DATE; + } + } +} diff --git a/src/main/java/seedu/exercise/storage/JsonAdaptedMuscle.java b/src/main/java/seedu/exercise/storage/JsonAdaptedMuscle.java new file mode 100644 index 00000000000..fbde28ecccb --- /dev/null +++ b/src/main/java/seedu/exercise/storage/JsonAdaptedMuscle.java @@ -0,0 +1,48 @@ +package seedu.exercise.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.property.Muscle; + +/** + * Jackson-friendly version of {@link Muscle}. + */ +public class JsonAdaptedMuscle { + + private final String muscle; + + /** + * Constructs a {@code JsonAdaptedMuscle} with the given {@code muscle}. + */ + @JsonCreator + public JsonAdaptedMuscle(String muscle) { + this.muscle = muscle; + } + + /** + * Converts a given {@code Muscle} into this class for Jackson use. + */ + public JsonAdaptedMuscle(Muscle source) { + muscle = source.muscleName; + } + + @JsonValue + public String getMuscle() { + return muscle; + } + + /** + * Converts this Jackson-friendly adapted muscle object into the model's {@code Muscle} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted muscle. + */ + public Muscle toModelType() throws IllegalValueException { + if (!Muscle.isValidMuscleName(muscle)) { + throw new IllegalValueException(Muscle.MESSAGE_CONSTRAINTS); + } + return new Muscle(muscle); + } + +} diff --git a/src/main/java/seedu/exercise/storage/JsonPropertyBookStorage.java b/src/main/java/seedu/exercise/storage/JsonPropertyBookStorage.java new file mode 100644 index 00000000000..b111a9ea7b6 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/JsonPropertyBookStorage.java @@ -0,0 +1,78 @@ +package seedu.exercise.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.commons.util.FileUtil; +import seedu.exercise.commons.util.JsonUtil; +import seedu.exercise.model.property.PropertyBook; + +/** + * A class to access PropertyBook data stored as a json file in the hard disk. + */ +public class JsonPropertyBookStorage implements PropertyBookStorage { + private static final Logger logger = LogsCenter.getLogger(JsonPropertyBookStorage.class); + + private Path filePath; + + public JsonPropertyBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getPropertyBookFilePath() { + return this.filePath; + } + + @Override + public void readPropertyBook() throws DataConversionException { + readPropertyBook(filePath); + } + + /** + * Similar to {@link #readPropertyBook()} + * + * @param filePath location of the data. Cannot be null. + * @throws DataConversionException if the file is not in the correct format + */ + public void readPropertyBook(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonPropertyBook = + JsonUtil.readJsonFile(filePath, JsonSerializablePropertyBook.class); + + if (jsonPropertyBook.isEmpty()) { + PropertyBook.getInstance(); + return; + } + + try { + jsonPropertyBook.get().toModelBook(); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + public void savePropertyBook() throws IOException { + savePropertyBook(filePath); + } + + /** + * Similar to {@link #savePropertyBook()} + * + * @param filePath location of the data. Cannot be null. + */ + public void savePropertyBook(Path filePath) throws IOException { + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializablePropertyBook(), filePath); + } +} diff --git a/src/main/java/seedu/exercise/storage/JsonSerializablePropertyBook.java b/src/main/java/seedu/exercise/storage/JsonSerializablePropertyBook.java new file mode 100644 index 00000000000..a464afe9615 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/JsonSerializablePropertyBook.java @@ -0,0 +1,72 @@ +package seedu.exercise.storage; + +import static seedu.exercise.model.property.PropertyBook.MESSAGE_DUPLICATE_NAME_OR_PREFIX; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.property.custom.CustomProperty; + +/** + * A PropertyBook that is serializable to JSON format. + */ +@JsonRootName(value = "propertybook") +public class JsonSerializablePropertyBook { + + private static final Logger logger = LogsCenter.getLogger(JsonSerializablePropertyBook.class); + + private final List customProperties = new ArrayList<>(); + + /** + * Constructs a {@code JsonSerializablePropertyBook} from the given parameters. + * + * @param customProperties a list containing {@code JsonAdaptedCustomProperty} + */ + @JsonCreator + public JsonSerializablePropertyBook(@JsonProperty("customProperties") List + customProperties) { + if (customProperties != null) { + this.customProperties.addAll(customProperties); + } + } + + /** + * Converts a given {@code PropertyBook} object into this class for Jackson use. + */ + public JsonSerializablePropertyBook() { + Set sourceCustomProperties = PropertyBook.getInstance().getCustomProperties(); + customProperties.addAll(sourceCustomProperties.stream() + .map(JsonAdaptedCustomProperty::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly PropertyBook into the model's {@code PropertyBook} object. + * If any clashes in property name or prefix are found, a default PropertyBook will be loaded instead. + * + * @throws IllegalValueException if there were any data constraints violated + */ + public PropertyBook toModelBook() throws IllegalValueException { + PropertyBook propertyBook = PropertyBook.getInstance(); + for (JsonAdaptedCustomProperty jsonCustomProperty : customProperties) { + CustomProperty modelProperty = jsonCustomProperty.toModelType(); + if (propertyBook.hasClashingPrefixOrName(modelProperty)) { + propertyBook.clearCustomProperties(); + logger.info("Duplicates found in serializable property book."); + throw new IllegalValueException(MESSAGE_DUPLICATE_NAME_OR_PREFIX); + } + propertyBook.addCustomProperty(modelProperty); + } + return propertyBook; + } +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/exercise/storage/JsonUserPrefsStorage.java similarity index 82% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/seedu/exercise/storage/JsonUserPrefsStorage.java index bc2bbad84aa..40e5312d266 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/exercise/storage/JsonUserPrefsStorage.java @@ -1,13 +1,13 @@ -package seedu.address.storage; +package seedu.exercise.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.commons.util.JsonUtil; +import seedu.exercise.model.ReadOnlyUserPrefs; +import seedu.exercise.model.UserPrefs; /** * A class to access UserPrefs stored in the hard disk as a json file diff --git a/src/main/java/seedu/exercise/storage/PropertyBookStorage.java b/src/main/java/seedu/exercise/storage/PropertyBookStorage.java new file mode 100644 index 00000000000..4cf73e2d8f1 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/PropertyBookStorage.java @@ -0,0 +1,42 @@ +package seedu.exercise.storage; + +import java.io.IOException; +import java.nio.file.Path; + +import seedu.exercise.commons.exceptions.DataConversionException; + +/** + * Represents a storage for {@code PropertyBook} + */ +public interface PropertyBookStorage { + + /** + * Returns the file path of the PropertyBook data file. + */ + Path getPropertyBookFilePath(); + + /** + * Reads the data of the {@code PropertyBook} in storage. + * + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + void readPropertyBook() throws DataConversionException, IOException; + + /** + * @see #readPropertyBook() + */ + void readPropertyBook(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the {@code PropertyBook} to the storage. + * + * @throws IOException if there was any problem writing to the file. + */ + void savePropertyBook() throws IOException; + + /** + * @see #savePropertyBook() + */ + void savePropertyBook(Path filePath) throws IOException; +} diff --git a/src/main/java/seedu/exercise/storage/Storage.java b/src/main/java/seedu/exercise/storage/Storage.java new file mode 100644 index 00000000000..a471de79a4e --- /dev/null +++ b/src/main/java/seedu/exercise/storage/Storage.java @@ -0,0 +1,87 @@ +package seedu.exercise.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.ReadOnlyUserPrefs; +import seedu.exercise.model.UserPrefs; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * API of the Storage component. + * The Storage component is a container that contains all of the different storage for the different resources in + * ExerHealth. + */ +public interface Storage extends UserPrefsStorage, PropertyBookStorage { + + // ================ UserPref methods ============================== + @Override + Optional readUserPrefs() throws DataConversionException, IOException; + + @Override + void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; + + + // ================ ExerciseBook methods ============================== + Path getExerciseBookFilePath(); + + Optional> readExerciseBook() throws DataConversionException, IOException; + + Optional> readExerciseBook(Path filePath) + throws DataConversionException, IOException; + + void saveExerciseBook(ReadOnlyResourceBook exerciseBook) throws IOException; + + void saveExerciseBook(ReadOnlyResourceBook exerciseBook, Path filePath) throws IOException; + + Path getExerciseDatabaseFilePath(); + + /** + * Returns AllExerciseBook data as a {@link ReadOnlyResourceBook}. + * Returns {@code Optional.empty()} if storage file is not found. + * + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional> readExerciseDatabase() throws DataConversionException, IOException; + + /** + * @see #getExerciseDatabaseFilePath() + */ + Optional> readExerciseDatabase(Path filePath) + throws DataConversionException, IOException; + + void saveExerciseDatabase(ReadOnlyResourceBook exerciseDatabase) throws IOException; + + // ================ RegimeBook methods ============================== + Path getRegimeBookFilePath(); + + Optional> readRegimeBook() throws DataConversionException, IOException; + + Optional> readRegimeBook(Path filePath) + throws DataConversionException, IOException; + + void saveRegimeBook(ReadOnlyResourceBook regimeBook) throws IOException; + + void saveRegimeBook(ReadOnlyResourceBook exerciseBook, Path filePath) throws IOException; + + + // ================ ScheduleBook methods ============================== + Path getScheduleBookFilePath(); + + Optional> readScheduleBook() throws DataConversionException, IOException; + + Optional> readScheduleBook(Path filePath) + throws DataConversionException, IOException; + + void saveScheduleBook(ReadOnlyResourceBook scheduleBook) throws IOException; + + void saveScheduleBook(ReadOnlyResourceBook exerciseBook, Path filePath) throws IOException; + + +} diff --git a/src/main/java/seedu/exercise/storage/StorageBook.java b/src/main/java/seedu/exercise/storage/StorageBook.java new file mode 100644 index 00000000000..bcf30d52ae4 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/StorageBook.java @@ -0,0 +1,206 @@ +package seedu.exercise.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.ReadOnlyUserPrefs; +import seedu.exercise.model.UserPrefs; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.storage.bookstorage.ResourceBookStorage; + +/** + * Manages storage of ExerHealth data in local storage. + */ +public class StorageBook implements Storage { + + private static final Logger logger = LogsCenter.getLogger(StorageBook.class); + + private ResourceBookStorage exerciseBookStorage; + private ResourceBookStorage exerciseDatabaseStorage; + private ResourceBookStorage regimeBookStorage; + private ResourceBookStorage scheduleBookStorage; + private UserPrefsStorage userPrefsStorage; + private PropertyBookStorage propertyBookStorage; + + public StorageBook(ResourceBookStorage exerciseBookStorage, + ResourceBookStorage exerciseDatabaseStorage, + ResourceBookStorage regimeBookStorage, + ResourceBookStorage scheduleBookStorage, + UserPrefsStorage userPrefsStorage, + PropertyBookStorage propertyBookStorage) { + super(); + this.exerciseBookStorage = exerciseBookStorage; + this.exerciseDatabaseStorage = exerciseDatabaseStorage; + this.regimeBookStorage = regimeBookStorage; + this.scheduleBookStorage = scheduleBookStorage; + this.userPrefsStorage = userPrefsStorage; + this.propertyBookStorage = propertyBookStorage; + } + + // ================ UserPrefs methods ============================== + + @Override + public Path getUserPrefsFilePath() { + return userPrefsStorage.getUserPrefsFilePath(); + } + + @Override + public Optional readUserPrefs() throws DataConversionException, IOException { + return userPrefsStorage.readUserPrefs(); + } + + @Override + public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { + userPrefsStorage.saveUserPrefs(userPrefs); + } + + + // ================ ExerciseBook methods ============================== + + @Override + public Path getExerciseBookFilePath() { + return exerciseBookStorage.getResourceBookFilePath(); + } + + @Override + public Optional> readExerciseBook() throws DataConversionException, IOException { + return readExerciseBook(exerciseBookStorage.getResourceBookFilePath()); + } + + @Override + public Optional> readExerciseBook(Path filePath) + throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return exerciseBookStorage.readResourceBook(filePath); + } + + @Override + public void saveExerciseBook(ReadOnlyResourceBook exerciseBook) throws IOException { + saveExerciseBook(exerciseBook, exerciseBookStorage.getResourceBookFilePath()); + } + + @Override + public void saveExerciseBook(ReadOnlyResourceBook exerciseBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + exerciseBookStorage.saveResourceBook(exerciseBook, filePath); + } + + // ================ ExerciseDatabase methods ============================== + + @Override + public Path getExerciseDatabaseFilePath() { + return exerciseDatabaseStorage.getResourceBookFilePath(); + } + + @Override + public Optional> readExerciseDatabase() + throws DataConversionException, IOException { + return readExerciseBook(exerciseDatabaseStorage.getResourceBookFilePath()); + } + + @Override + public Optional> readExerciseDatabase(Path filePath) + throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return exerciseDatabaseStorage.readResourceBook(); + } + + @Override + public void saveExerciseDatabase(ReadOnlyResourceBook exerciseDatabase) throws IOException { + saveExerciseBook(exerciseDatabase, this.exerciseDatabaseStorage.getResourceBookFilePath()); + } + + //===============RegimeBook methods============================================= + @Override + public Path getRegimeBookFilePath() { + return regimeBookStorage.getResourceBookFilePath(); + } + + @Override + public Optional> readRegimeBook() throws DataConversionException, IOException { + return readRegimeBook(regimeBookStorage.getResourceBookFilePath()); + } + + @Override + public Optional> readRegimeBook(Path filePath) + throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return regimeBookStorage.readResourceBook(filePath); + } + + @Override + public void saveRegimeBook(ReadOnlyResourceBook regimeBook) throws IOException { + saveRegimeBook(regimeBook, regimeBookStorage.getResourceBookFilePath()); + } + + @Override + public void saveRegimeBook(ReadOnlyResourceBook regimeBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + regimeBookStorage.saveResourceBook(regimeBook, filePath); + } + + //===============ScheduleBook methods========================================= + @Override + public Path getScheduleBookFilePath() { + return scheduleBookStorage.getResourceBookFilePath(); + } + + @Override + public Optional> readScheduleBook() throws DataConversionException, IOException { + return readScheduleBook(getScheduleBookFilePath()); + } + + @Override + public Optional> readScheduleBook(Path filePath) + throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return scheduleBookStorage.readResourceBook(filePath); + } + + @Override + public void saveScheduleBook(ReadOnlyResourceBook scheduleBook) throws IOException { + saveScheduleBook(scheduleBook, getScheduleBookFilePath()); + } + + @Override + public void saveScheduleBook(ReadOnlyResourceBook scheduleBook, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + scheduleBookStorage.saveResourceBook(scheduleBook, filePath); + } + + // ================ PropertyBook methods ============================== + @Override + public Path getPropertyBookFilePath() { + return propertyBookStorage.getPropertyBookFilePath(); + } + + @Override + public void readPropertyBook() throws DataConversionException, IOException { + readPropertyBook(propertyBookStorage.getPropertyBookFilePath()); + } + + @Override + public void readPropertyBook(Path filePath) throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + propertyBookStorage.readPropertyBook(filePath); + } + + @Override + public void savePropertyBook() throws IOException { + savePropertyBook(propertyBookStorage.getPropertyBookFilePath()); + } + + @Override + public void savePropertyBook(Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + propertyBookStorage.savePropertyBook(filePath); + } + +} diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/exercise/storage/UserPrefsStorage.java similarity index 57% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/seedu/exercise/storage/UserPrefsStorage.java index 29eef178dbc..f0cfb72a49e 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/exercise/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package seedu.exercise.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.model.ReadOnlyUserPrefs; +import seedu.exercise.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link seedu.exercise.model.UserPrefs}. */ public interface UserPrefsStorage { @@ -20,14 +20,16 @@ public interface UserPrefsStorage { /** * Returns UserPrefs data from storage. - * Returns {@code Optional.empty()} if storage file is not found. + * Returns {@code Optional.empty()} if storage file is not found. + * * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. + * @throws IOException if there was any problem when reading from the storage. */ Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link ReadOnlyUserPrefs} to the storage. + * * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/exercise/storage/bookstorage/JsonExerciseBookStorage.java b/src/main/java/seedu/exercise/storage/bookstorage/JsonExerciseBookStorage.java new file mode 100644 index 00000000000..0ae693287a1 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/bookstorage/JsonExerciseBookStorage.java @@ -0,0 +1,74 @@ +package seedu.exercise.storage.bookstorage; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.commons.util.FileUtil; +import seedu.exercise.commons.util.JsonUtil; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.storage.serializablebook.JsonSerializableExerciseBook; + +/** + * A class to access ExerciseBook data stored as a json file on the hard disk. + */ +public class JsonExerciseBookStorage implements ResourceBookStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonExerciseBookStorage.class); + + private Path filePath; + + public JsonExerciseBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getResourceBookFilePath() { + return this.filePath; + } + + @Override + public Optional> readResourceBook() throws DataConversionException { + return readResourceBook(filePath); + } + + @Override + public Optional> readResourceBook(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonExerciseBook = JsonUtil.readJsonFile( + filePath, JsonSerializableExerciseBook.class); + if (!jsonExerciseBook.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonExerciseBook.get().toModelType(Exercise.class, DEFAULT_EXERCISE_COMPARATOR)); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveResourceBook(ReadOnlyResourceBook exerciseBook) throws IOException { + saveResourceBook(exerciseBook, filePath); + } + + @Override + public void saveResourceBook(ReadOnlyResourceBook exerciseBook, Path filePath) throws IOException { + requireNonNull(exerciseBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableExerciseBook(exerciseBook), filePath); + } + +} diff --git a/src/main/java/seedu/exercise/storage/bookstorage/JsonRegimeBookStorage.java b/src/main/java/seedu/exercise/storage/bookstorage/JsonRegimeBookStorage.java new file mode 100644 index 00000000000..da0783378a2 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/bookstorage/JsonRegimeBookStorage.java @@ -0,0 +1,73 @@ +package seedu.exercise.storage.bookstorage; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_REGIME_COMPARATOR; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.commons.util.FileUtil; +import seedu.exercise.commons.util.JsonUtil; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.storage.serializablebook.JsonSerializableRegimeBook; + +/** + * A class to access RegimeBook data stored as a json file on the hard disk. + */ +public class JsonRegimeBookStorage implements ResourceBookStorage { + private static final Logger logger = LogsCenter.getLogger(JsonRegimeBookStorage.class); + + private Path filePath; + + public JsonRegimeBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getResourceBookFilePath() { + return this.filePath; + } + + @Override + public Optional> readResourceBook() throws DataConversionException { + return readResourceBook(filePath); + } + + @Override + public Optional> readResourceBook(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonRegimeBook = JsonUtil.readJsonFile( + filePath, JsonSerializableRegimeBook.class); + + if (!jsonRegimeBook.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonRegimeBook.get().toModelType(Regime.class, DEFAULT_REGIME_COMPARATOR)); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveResourceBook(ReadOnlyResourceBook regimeBook) throws IOException { + saveResourceBook(regimeBook, filePath); + } + + @Override + public void saveResourceBook(ReadOnlyResourceBook regimeBook, Path filePath) throws IOException { + requireNonNull(regimeBook); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableRegimeBook(regimeBook), filePath); + } +} diff --git a/src/main/java/seedu/exercise/storage/bookstorage/JsonScheduleBookStorage.java b/src/main/java/seedu/exercise/storage/bookstorage/JsonScheduleBookStorage.java new file mode 100644 index 00000000000..a0ed3fa4116 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/bookstorage/JsonScheduleBookStorage.java @@ -0,0 +1,74 @@ +package seedu.exercise.storage.bookstorage; + +import static java.util.Objects.requireNonNull; +import static seedu.exercise.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_SCHEDULE_COMPARATOR; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.commons.util.FileUtil; +import seedu.exercise.commons.util.JsonUtil; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.storage.serializablebook.JsonSerializableScheduleBook; + +/** + * A class to access ScheduleBook data stored as a json file on the hard disk. + */ +public class JsonScheduleBookStorage implements ResourceBookStorage { + + private static final Logger logger = LogsCenter.getLogger(JsonScheduleBookStorage.class); + + private Path filePath; + + public JsonScheduleBookStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getResourceBookFilePath() { + return this.filePath; + } + + @Override + public Optional> readResourceBook() throws DataConversionException { + return readResourceBook(filePath); + } + + @Override + public Optional> readResourceBook(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonScheduleBook = JsonUtil.readJsonFile( + filePath, JsonSerializableScheduleBook.class); + + if (!jsonScheduleBook.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonScheduleBook.get().toModelType(Schedule.class, DEFAULT_SCHEDULE_COMPARATOR)); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveResourceBook(ReadOnlyResourceBook scheduleBook) throws IOException { + saveResourceBook(scheduleBook, filePath); + } + + @Override + public void saveResourceBook(ReadOnlyResourceBook scheduleBook, Path filePath) throws IOException { + requireAllNonNull(scheduleBook, filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableScheduleBook(scheduleBook), filePath); + } +} diff --git a/src/main/java/seedu/exercise/storage/bookstorage/ResourceBookStorage.java b/src/main/java/seedu/exercise/storage/bookstorage/ResourceBookStorage.java new file mode 100644 index 00000000000..33d94a1824c --- /dev/null +++ b/src/main/java/seedu/exercise/storage/bookstorage/ResourceBookStorage.java @@ -0,0 +1,49 @@ +package seedu.exercise.storage.bookstorage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.exercise.commons.exceptions.DataConversionException; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Resource; + +/** + * API of a ResourceBookStorage. + */ +public interface ResourceBookStorage { + + /** + * Retrieves the file path of the ResourceBook data file. + */ + Path getResourceBookFilePath(); + + /** + * Reads the resource data from storage. + * Returns {@code Optional.Empty()} if storage file is not found. + * + * @throws DataConversionException if the data in storage is not found. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional> readResourceBook() throws DataConversionException, IOException; + + /** + * Works similarly to {@link ResourceBookStorage#readResourceBook()}. + * However, this reads the data in the given {@code Path}. + */ + Optional> readResourceBook(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@code ReadOnlyResourceBook} to the storage. + * + * @param resourceBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveResourceBook(ReadOnlyResourceBook resourceBook) throws IOException; + + /** + * Works similarly to {@link ResourceBookStorage#saveResourceBook(ReadOnlyResourceBook)}. + * However, this stores the data in the given {@code Path}. + */ + void saveResourceBook(ReadOnlyResourceBook resourceBook, Path filePath) throws IOException; +} diff --git a/src/main/java/seedu/exercise/storage/resource/JsonAdaptedExercise.java b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedExercise.java new file mode 100644 index 00000000000..e7b36174628 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedExercise.java @@ -0,0 +1,138 @@ +package seedu.exercise.storage.resource; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.property.Calories; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.property.Quantity; +import seedu.exercise.model.property.Unit; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.storage.JsonAdaptedMuscle; + +/** + * Jackson-friendly version of {@link Exercise}. + */ +public class JsonAdaptedExercise extends JsonAdaptedResource { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Exercise's %s field is missing!"; + + private final String name; + private final String date; + private final String calories; + private final String quantity; + private final String unit; + private final List muscles = new ArrayList<>(); + private final Map customProperties = new TreeMap<>(); + + /** + * Constructs a {@code JsonAdaptedExercise} with the given exercise details. + */ + @JsonCreator + public JsonAdaptedExercise(@JsonProperty("name") String name, @JsonProperty("date") String date, + @JsonProperty("calories") String calories, @JsonProperty("quantity") String quantity, + @JsonProperty("unit") String unit, + @JsonProperty("muscles") List muscles, + @JsonProperty("customProperties") Map customProperties) { + this.name = name; + this.date = date; + this.calories = calories; + this.quantity = quantity; + this.unit = unit; + if (muscles != null) { + this.muscles.addAll(muscles); + } + if (customProperties != null) { + this.customProperties.putAll(customProperties); + } + } + + /** + * Converts a given {@code Exercise} into this class for Jackson use. + */ + public JsonAdaptedExercise(Exercise source) { + name = source.getName().fullName; + date = source.getDate().toString(); + calories = source.getCalories().toString(); + quantity = source.getQuantity().toString(); + unit = source.getUnit().unit; + muscles.addAll(source.getMuscles().stream() + .map(JsonAdaptedMuscle::new) + .collect(Collectors.toList())); + customProperties.putAll(source.getCustomPropertiesMap()); + } + + /** + * Converts this Jackson-friendly adapted exercise object into the model's {@code Exercise} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted exercise. + */ + public Exercise toModelType() throws IllegalValueException { + final List personMuscles = new ArrayList<>(); + for (JsonAdaptedMuscle muscle : muscles) { + personMuscles.add(muscle.toModelType()); + } + + if (name == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Name.class.getSimpleName())); + } + if (!Name.isValidName(name)) { + throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); + } + final Name modelName = new Name(name); + + if (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Date.class.getSimpleName())); + } + if (!Date.isValidDate(date)) { + throw new IllegalValueException(Date.MESSAGE_CONSTRAINTS); + } + final Date modelDate = new Date(date); + + if (calories == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Calories.class.getSimpleName())); + } + if (!Calories.isValidCalories(calories)) { + throw new IllegalValueException(Calories.MESSAGE_CONSTRAINTS); + } + final Calories modelCalories = new Calories(calories); + + if (quantity == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Quantity.class.getSimpleName())); + } + if (!Quantity.isValidQuantity(quantity)) { + throw new IllegalValueException(Quantity.MESSAGE_CONSTRAINTS); + } + final Quantity modelQuantity = new Quantity(quantity); + + if (unit == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Unit.class.getSimpleName())); + } + if (!Unit.isValidUnit(unit)) { + throw new IllegalValueException(Unit.MESSAGE_CONSTRAINTS); + } + final Unit modelUnit = new Unit(unit); + + final Set modelMuscles = new HashSet<>(personMuscles); + final Map modelCustomProperties = new TreeMap<>(customProperties); + return new Exercise(modelName, modelDate, modelCalories, modelQuantity, modelUnit, modelMuscles, + modelCustomProperties); + } + +} diff --git a/src/main/java/seedu/exercise/storage/resource/JsonAdaptedRegime.java b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedRegime.java new file mode 100644 index 00000000000..048a44d3c4d --- /dev/null +++ b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedRegime.java @@ -0,0 +1,61 @@ +package seedu.exercise.storage.resource; + +import static seedu.exercise.model.resource.ResourceComparator.DEFAULT_EXERCISE_COMPARATOR; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.SortedUniqueResourceList; +import seedu.exercise.model.property.Name; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; + +/** + * Jackson-friendly version of {@link Regime}. + */ +public class JsonAdaptedRegime extends JsonAdaptedResource { + + private final String name; + private final List exercises = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedRegime} with the given regime details. + */ + public JsonAdaptedRegime(@JsonProperty("name") String name, + @JsonProperty("exercises") List exercises) { + this.name = name; + if (exercises != null) { + this.exercises.addAll(exercises); + } + } + + /** + * Converts a given {@code Regime} into this class for Jackson use. + */ + public JsonAdaptedRegime(Regime source) { + name = source.getRegimeName().toString(); + exercises.addAll(source.getRegimeExercises().asUnmodifiableObservableList().stream() + .map(JsonAdaptedExercise::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted regime object into the model's {@code Regime} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted regime. + */ + public Regime toModelType() throws IllegalValueException { + final SortedUniqueResourceList modelExercises = + new SortedUniqueResourceList<>(DEFAULT_EXERCISE_COMPARATOR); + for (JsonAdaptedExercise exercise : exercises) { + modelExercises.add(exercise.toModelType()); + } + + final Name modelName = new Name(name); + return new Regime(modelName, modelExercises); + } +} diff --git a/src/main/java/seedu/exercise/storage/resource/JsonAdaptedResource.java b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedResource.java new file mode 100644 index 00000000000..7d989b4cea2 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedResource.java @@ -0,0 +1,17 @@ +package seedu.exercise.storage.resource; + +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.resource.Resource; + +/** + * Represents a Jackson-friendly version of {@code Resource}. + */ +public abstract class JsonAdaptedResource { + + /** + * Converts the given {@code JsonAdaptedResource} into the model's {@code Resource} object. + * + * @throws IllegalValueException if there were any violations in the data constraints + */ + public abstract T toModelType() throws IllegalValueException; +} diff --git a/src/main/java/seedu/exercise/storage/resource/JsonAdaptedSchedule.java b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedSchedule.java new file mode 100644 index 00000000000..fb995c1ca82 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/resource/JsonAdaptedSchedule.java @@ -0,0 +1,55 @@ +package seedu.exercise.storage.resource; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Schedule; + +/** + * Jackson-friendly version of {@link Schedule}. + */ +public class JsonAdaptedSchedule extends JsonAdaptedResource { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Schedule's %s field is missing!"; + + private final JsonAdaptedRegime regime; + private final String date; + + @JsonCreator + public JsonAdaptedSchedule(@JsonProperty("regime") JsonAdaptedRegime regime, @JsonProperty("date") String date) { + this.date = date; + this.regime = regime; + } + + public JsonAdaptedSchedule(Schedule schedule) { + this.date = schedule.getDate().toString(); + this.regime = new JsonAdaptedRegime(schedule.getRegime()); + } + + /** + * Converts this Jackson-friendly adapted schedule object into the model's {@code Schedule} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted schedule. + */ + public Schedule toModelType() throws IllegalValueException { + if (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Date.class.getSimpleName())); + } + if (!Date.isValidDate(date)) { + throw new IllegalValueException(Date.MESSAGE_CONSTRAINTS); + } + final Date modelDate = new Date(date); + + if (regime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Regime.class.getSimpleName())); + } + + Regime modelRegime = regime.toModelType(); + return new Schedule(modelRegime, modelDate); + } +} diff --git a/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableExerciseBook.java b/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableExerciseBook.java new file mode 100644 index 00000000000..35c07cd81e0 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableExerciseBook.java @@ -0,0 +1,36 @@ +package seedu.exercise.storage.serializablebook; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.storage.resource.JsonAdaptedExercise; + +/** + * An Immutable ExerciseBook that is serializable to JSON format. + */ +@JsonRootName(value = "exercisebook") +public class JsonSerializableExerciseBook extends SerializableResourceBook { + + /** + * Constructs a {@code JsonSerializableExerciseBook} with the given persons. + */ + @JsonCreator + public JsonSerializableExerciseBook(@JsonProperty("jsonResources") List exercises) { + super(exercises); + } + + /** + * Converts a given {@code ReadOnlyResourceBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableExerciseBook}. + */ + public JsonSerializableExerciseBook(ReadOnlyResourceBook source) { + super(source, JsonAdaptedExercise.class); + } + +} diff --git a/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableRegimeBook.java b/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableRegimeBook.java new file mode 100644 index 00000000000..301ea0f8069 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableRegimeBook.java @@ -0,0 +1,36 @@ +package seedu.exercise.storage.serializablebook; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.storage.resource.JsonAdaptedRegime; + +/** + * An Immutable RegimeBook that is serializable to JSON format. + */ +@JsonRootName(value = "regimebook") +public class JsonSerializableRegimeBook extends SerializableResourceBook { + + /** + * Constructs a {@code JsonSerializableRegimeBook} with the given persons. + */ + @JsonCreator + public JsonSerializableRegimeBook(@JsonProperty("jsonResources") List regimes) { + super(regimes); + } + + /** + * Converts a given {@code ReadOnlyResourceBook} into this class for Jackson use. + * + * @param source future changes to this will not affect the created {@code JsonSerializableRegimeBook}. + */ + public JsonSerializableRegimeBook(ReadOnlyResourceBook source) { + super(source, JsonAdaptedRegime.class); + } + +} diff --git a/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableScheduleBook.java b/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableScheduleBook.java new file mode 100644 index 00000000000..aed70d46eb4 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/serializablebook/JsonSerializableScheduleBook.java @@ -0,0 +1,30 @@ +package seedu.exercise.storage.serializablebook; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonRootName; + +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Schedule; +import seedu.exercise.storage.resource.JsonAdaptedSchedule; + +/** + * An Immutable ScheduleBook that is serializable to JSON format. + */ +@JsonRootName(value = "schedulebook") +public class JsonSerializableScheduleBook extends SerializableResourceBook { + + + @JsonCreator + public JsonSerializableScheduleBook(@JsonProperty("jsonResources") List schedules) { + super(schedules); + } + + public JsonSerializableScheduleBook(ReadOnlyResourceBook source) { + super(source, JsonAdaptedSchedule.class); + } + + +} diff --git a/src/main/java/seedu/exercise/storage/serializablebook/SerializableResourceBook.java b/src/main/java/seedu/exercise/storage/serializablebook/SerializableResourceBook.java new file mode 100644 index 00000000000..de9f1c6f1a8 --- /dev/null +++ b/src/main/java/seedu/exercise/storage/serializablebook/SerializableResourceBook.java @@ -0,0 +1,63 @@ +package seedu.exercise.storage.serializablebook; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.exceptions.IllegalValueException; +import seedu.exercise.model.ReadOnlyResourceBook; +import seedu.exercise.model.resource.Resource; +import seedu.exercise.storage.resource.JsonAdaptedResource; + +/** + * An immutable ResourceBook that is serializable to JSON format. + * In particular, this resource book can be extended to create an immutable ResourceBook that holds + * any {@code JsonAdaptedResource} of type {@code T}. + */ +public abstract class SerializableResourceBook, U extends Resource> { + + public static final String MESSAGE_DUPLICATE_RESOURCE = "The list has duplicate exercises/regimes/schedules."; + + private static final Logger logger = LogsCenter.getLogger(SerializableResourceBook.class); + + private final List jsonResources = new ArrayList<>(); + + public SerializableResourceBook(List jsonResources) { + this.jsonResources.addAll(jsonResources); + } + + public SerializableResourceBook(ReadOnlyResourceBook source, Class clazz) { + jsonResources + .addAll(source.getSortedResourceList() + .stream() + .map(U::toJsonType) + .map(clazz::cast) + .collect(Collectors.toList())); + } + + /** + * Converts the Jackson-friendly {@code SerializableResourceBook} into the model's {@code ReadOnlyResourceBook}. + * + * @throws IllegalValueException if there are any violations in the data constraints. + */ + public ReadOnlyResourceBook toModelType(Class clazz, Comparator comparator) throws IllegalValueException { + ReadOnlyResourceBook resourceBook = new ReadOnlyResourceBook<>(comparator); + for (JsonAdaptedResource jsonResource : jsonResources) { + U resourceModel = clazz.cast(jsonResource.toModelType()); + if (resourceBook.hasResource(resourceModel)) { + logger.info("The list to convert has duplicates."); + throw new IllegalValueException(MESSAGE_DUPLICATE_RESOURCE); + } + resourceBook.addResource(resourceModel); + } + return resourceBook; + } + + public void setList(List data) { + jsonResources.addAll(data); + } + +} diff --git a/src/main/java/seedu/exercise/ui/BarChartPanel.java b/src/main/java/seedu/exercise/ui/BarChartPanel.java new file mode 100644 index 00000000000..24c67d09879 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/BarChartPanel.java @@ -0,0 +1,65 @@ +package seedu.exercise.ui; + +import java.util.ArrayList; + +import javafx.fxml.FXML; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.layout.Region; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.ui.util.ChartUtil; + +/** + * An UI for bar chart. + */ +public class BarChartPanel extends UiPart { + private static final String FXML = "BarChartPanel.fxml"; + private static final String DEFAULT_EXERCISES = "Exercises"; + private Statistic statistic; + + @FXML + private CategoryAxis xAxis; + @FXML + private NumberAxis yAxis; + @FXML + private BarChart barChart; + + public BarChartPanel(Statistic statistic) { + super(FXML); + this.statistic = statistic; + display(); + } + + /** + * Set data for bar chart to be displayed. + */ + private void display() { + String category = statistic.getCategory(); + String startDate = statistic.getStartDate().toString(); + String endDate = statistic.getEndDate().toString(); + ArrayList properties = statistic.getProperties(); + ArrayList values = statistic.getValues(); + + barChart.setAnimated(false); + barChart.layout(); + + xAxis.setLabel(DEFAULT_EXERCISES); + yAxis.setLabel(ChartUtil.labelFormatter(statistic.getCategory())); + + XYChart.Series series = new XYChart.Series<>(); + + int size = properties.size(); + for (int i = 0; i < size; i++) { + String property = ChartUtil.propertyFormatter(properties.get(i)); + series.getData().add(new XYChart.Data<>(property, values.get(i))); + } + + barChart.setLegendVisible(false); + barChart.setTitle(ChartUtil.lineAndBarChartTitleFormatter(category, startDate, endDate)); + barChart.getData().add(series); + + ChartUtil.installToolTipXyChart(series.getData()); + } +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/exercise/ui/CommandBox.java similarity index 82% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/seedu/exercise/ui/CommandBox.java index 7d76e691f52..a2bd5546bbd 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/exercise/ui/CommandBox.java @@ -1,12 +1,12 @@ -package seedu.address.ui; +package seedu.exercise.ui; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import seedu.exercise.logic.commands.CommandResult; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.logic.parser.exceptions.ParseException; /** * The UI component that is responsible for receiving user command inputs. @@ -28,6 +28,14 @@ public CommandBox(CommandExecutor commandExecutor) { commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); } + public void requestFocus() { + commandTextField.requestFocus(); + } + + void clearText() { + commandTextField.setText(""); + } + /** * Handles the Enter button pressed event. */ @@ -35,7 +43,7 @@ public CommandBox(CommandExecutor commandExecutor) { private void handleCommandEntered() { try { commandExecutor.execute(commandTextField.getText()); - commandTextField.setText(""); + clearText(); } catch (CommandException | ParseException e) { setStyleToIndicateCommandFailure(); } @@ -69,7 +77,7 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see seedu.exercise.logic.Logic#execute(String) */ CommandResult execute(String commandText) throws CommandException, ParseException; } diff --git a/src/main/java/seedu/exercise/ui/CustomPropertiesWindow.java b/src/main/java/seedu/exercise/ui/CustomPropertiesWindow.java new file mode 100644 index 00000000000..c3f27fbf1e8 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/CustomPropertiesWindow.java @@ -0,0 +1,87 @@ +package seedu.exercise.ui; + +import java.util.logging.Logger; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.stage.Stage; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.model.property.PropertyBook; +import seedu.exercise.model.property.custom.CustomProperty; + +/** + * UI component to display all previously defined custom properties. + */ +public class CustomPropertiesWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(CustomPropertiesWindow.class); + private static final String FXML = "CustomPropertiesWindow.fxml"; + + @FXML + private TableView customPropertiesTable; + + @FXML + private TableColumn nameColumn; + + @FXML + private TableColumn prefixColumn; + + @FXML + private TableColumn parameterColumn; + + /** + * Creates a new custom properties window. + * + * @param root Stage to use as the root of the CustomPropertiesWindow. + */ + public CustomPropertiesWindow(Stage root) { + super(FXML, root); + } + + public CustomPropertiesWindow() { + this(new Stage()); + } + + /** + * Displays the custom properties defined by the user. + */ + public void show() { + logger.fine("Displaying custom properties defined by the user."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Returns true if the window is currently being displayed. + */ + public boolean isShowing() { + return getRoot().isShowing(); + } + + public void focus() { + getRoot().requestFocus(); + } + + public void hide() { + getRoot().hide(); + } + + /** + * Helps to populate the table with the data for users to view. + */ + public void initialiseTable() { + ObservableList observableList = PropertyBook.getInstance() + .getObservableCustomProperties(); + customPropertiesTable.setItems(observableList); + nameColumn.setCellValueFactory(cp -> + new ReadOnlyObjectWrapper<>(cp.getValue().getFullName())); + prefixColumn.setCellValueFactory(cp -> + new ReadOnlyObjectWrapper<>(cp.getValue().getPrefix().getPrefixName())); + parameterColumn.setCellValueFactory(cp -> + new ReadOnlyObjectWrapper<>(cp.getValue().getParameterType().getParameterName())); + } + +} diff --git a/src/main/java/seedu/exercise/ui/CustomPropertyCard.java b/src/main/java/seedu/exercise/ui/CustomPropertyCard.java new file mode 100644 index 00000000000..1dac2efd02d --- /dev/null +++ b/src/main/java/seedu/exercise/ui/CustomPropertyCard.java @@ -0,0 +1,24 @@ +package seedu.exercise.ui; + +import static seedu.exercise.ui.util.LabelUtil.setLabelTooltip; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; + +/** + * Represents a UI component that displays a custom property information. + */ +public class CustomPropertyCard extends UiPart { + private static final String FXML = "CustomPropertyCard.fxml"; + + @FXML + private Label propertyAndValue; + + public CustomPropertyCard(String customPropertyDisplayResult) { + super(FXML); + this.propertyAndValue.setText(customPropertyDisplayResult); + setLabelTooltip(propertyAndValue); + } + +} diff --git a/src/main/java/seedu/exercise/ui/CustomPropertyListPanel.java b/src/main/java/seedu/exercise/ui/CustomPropertyListPanel.java new file mode 100644 index 00000000000..b9464b4ed15 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/CustomPropertyListPanel.java @@ -0,0 +1,47 @@ +package seedu.exercise.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; + +/** + * Represents a list panel that contains all of the custom property information of an exercise. + */ +public class CustomPropertyListPanel extends UiPart { + private static final String FXML = "CustomPropertyListPanel.fxml"; + + private final ObservableList customPropertiesList; + + @FXML + private ListView customPropertiesListView; + + public CustomPropertyListPanel(ObservableList customPropertiesList) { + super(FXML); + this.customPropertiesList = customPropertiesList; + customPropertiesListView.setItems(customPropertiesList); + customPropertiesListView.setCellFactory(listView -> new CustomPropertyListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a custom property value using a + * {@code CustomPropertyCard}. + */ + class CustomPropertyListViewCell extends ListCell { + + @Override + protected void updateItem(String customPropertyDisplayResult, boolean empty) { + super.updateItem(customPropertyDisplayResult, empty); + + if (empty || customPropertyDisplayResult == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new CustomPropertyCard(customPropertyDisplayResult).getRoot()); + } + } + } + + +} diff --git a/src/main/java/seedu/exercise/ui/ExerciseInfoPanel.java b/src/main/java/seedu/exercise/ui/ExerciseInfoPanel.java new file mode 100644 index 00000000000..8442b9f69ce --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ExerciseInfoPanel.java @@ -0,0 +1,94 @@ +package seedu.exercise.ui; + +import static seedu.exercise.ui.util.LabelUtil.setLabelTooltip; + +import java.util.Comparator; +import java.util.Set; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import seedu.exercise.model.property.Muscle; +import seedu.exercise.model.resource.Exercise; + +/** + * An UI component that displays information of a {@code Exercise}. + */ +public class ExerciseInfoPanel extends UiPart { + + private static final String FXML = "ExerciseInfoPanel.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + private Exercise exercise; + + @FXML + private AnchorPane cardPane; + + // Exercise information + @FXML + private Label name; + @FXML + private Label date; + @FXML + private Label calories; + @FXML + private Label quantityAndUnit; + @FXML + private FlowPane muscles; + @FXML + private StackPane customProperties; + + private CustomPropertyListPanel customPropertiesList; + + public ExerciseInfoPanel(Exercise exercise) { + super(FXML); + this.exercise = exercise; + name.setText(exercise.getName().fullName); + date.setText(exercise.getDate().toString()); + calories.setText(exercise.getCalories().toString() + " kcal"); + quantityAndUnit.setText(exercise.getQuantity().toString() + " " + exercise.getUnit().unit); + setMuscleTags(exercise.getMuscles()); + customPropertiesList = new CustomPropertyListPanel(exercise.getObservableCustomPropertiesList()); + customProperties.getChildren().add(customPropertiesList.getRoot()); + setLabelTooltip(name); + setLabelTooltip(calories); + setLabelTooltip(quantityAndUnit); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExerciseInfoPanel)) { + return false; + } + + // state check + ExerciseInfoPanel card = (ExerciseInfoPanel) other; + return exercise.equals(card.exercise); + } + + private void setMuscleTags(Set musclesSet) { + if (musclesSet.isEmpty()) { + muscles.getChildren().add(new Label("N/A")); + } else { + exercise.getMuscles().stream() + .sorted(Comparator.comparing(muscle -> muscle.muscleName)) + .forEach(muscle -> muscles.getChildren().add(new Label(muscle.muscleName))); + } + } +} diff --git a/src/main/java/seedu/exercise/ui/ExerciseListCard.java b/src/main/java/seedu/exercise/ui/ExerciseListCard.java new file mode 100644 index 00000000000..f7716bc38fe --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ExerciseListCard.java @@ -0,0 +1,79 @@ +package seedu.exercise.ui; + +import static seedu.exercise.ui.util.LabelUtil.setLabelTooltip; + +import java.util.Comparator; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.exercise.model.resource.Exercise; + +/** + * An UI component that displays information of a {@code Exercise}. + */ +public class ExerciseListCard extends UiPart { + + private static final String FXML = "ExerciseListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Exercise exercise; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label date; + @FXML + private Label calories; + @FXML + private Label quantityAndUnit; + + @FXML + private FlowPane tags; + + public ExerciseListCard(Exercise exercise, int displayedIndex) { + super(FXML); + this.exercise = exercise; + id.setText(displayedIndex + ". "); + name.setText(exercise.getName().fullName); + date.setText(exercise.getDate().toString()); + calories.setText(exercise.getCalories().toString() + " kcal"); + quantityAndUnit.setText(exercise.getQuantity().toString() + " " + exercise.getUnit().unit); + exercise.getMuscles().stream() + .sorted(Comparator.comparing(muscle -> muscle.muscleName)) + .forEach(muscle -> tags.getChildren().add(new Label(muscle.muscleName))); + + setLabelTooltip(name); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExerciseListCard)) { + return false; + } + + // state check + ExerciseListCard card = (ExerciseListCard) other; + return id.getText().equals(card.id.getText()) + && exercise.equals(card.exercise); + } +} diff --git a/src/main/java/seedu/exercise/ui/ExerciseListPanel.java b/src/main/java/seedu/exercise/ui/ExerciseListPanel.java new file mode 100644 index 00000000000..0ce9054cc1a --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ExerciseListPanel.java @@ -0,0 +1,63 @@ +package seedu.exercise.ui; + +import static java.util.Objects.requireNonNull; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import seedu.exercise.model.resource.Exercise; + +/** + * Panel containing the list of exercises. + */ +public class ExerciseListPanel extends ResourceListPanel { + + private static final String FXML = "ExerciseListPanel.fxml"; + + @FXML + private ListView exerciseListView; + + @FXML + private Label exerciseTitle; + + public ExerciseListPanel(ObservableList exerciseList) { + super(FXML, exerciseList); + exerciseListView.setItems(exerciseList); + exerciseListView.setCellFactory(listView -> new ExerciseListViewCell()); + exerciseListView.getFocusModel().focusedItemProperty().addListener(getDefaultListViewListener()); + } + + public ListView getResourceListView() { + return exerciseListView; + } + + public void setPanelTitleText(String title) { + requireNonNull(title); + exerciseTitle.setText(title); + } + + @Override + public void resetListSelection() { + exerciseListView.getSelectionModel().clearSelection(); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Exercise} using a {@code ExerciseInfoPanel}. + */ + class ExerciseListViewCell extends ListCell { + @Override + protected void updateItem(Exercise exercise, boolean empty) { + super.updateItem(exercise, empty); + + if (empty || exercise == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ExerciseListCard(exercise, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/exercise/ui/HelpWindow.java similarity index 91% rename from src/main/java/seedu/address/ui/HelpWindow.java rename to src/main/java/seedu/exercise/ui/HelpWindow.java index 9a665915949..363f9c26836 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/exercise/ui/HelpWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.exercise.ui; import java.util.logging.Logger; @@ -8,14 +8,15 @@ import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; +import seedu.exercise.commons.core.LogsCenter; /** * Controller for a help page */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = + "https://github.com/AY1920S1-CS2103T-T09-2/main/blob/master/docs/UserGuide.adoc"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/exercise/ui/InfoDisplayPanel.java b/src/main/java/seedu/exercise/ui/InfoDisplayPanel.java new file mode 100644 index 00000000000..a8719d9e9e9 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/InfoDisplayPanel.java @@ -0,0 +1,61 @@ +package seedu.exercise.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.model.resource.Exercise; +import seedu.exercise.model.resource.Regime; +import seedu.exercise.model.resource.Resource; +import seedu.exercise.model.resource.Schedule; + +/** + * Placeholder Panel for specific information displayed in center panel. + */ +public class InfoDisplayPanel extends UiPart { + + public static final String DEFAULT_MESSAGE = "Select an exercise/regime/schedule to display its info."; + + private static final Logger logger = LogsCenter.getLogger(InfoDisplayPanel.class); + private static final String FXML = "InfoDisplayPanel.fxml"; + + @FXML + private StackPane infoPanelPlaceholder; + + public InfoDisplayPanel() { + super(FXML); + } + + /** + * Updates the information displayed in the center panel of application. + */ + public void update(Resource resource) { + infoPanelPlaceholder.getChildren().clear(); + if (resource instanceof Exercise) { + infoPanelPlaceholder.getChildren().add(new ExerciseInfoPanel((Exercise) resource).getRoot()); + logCurrentInfoShown("exercise"); + } else if (resource instanceof Regime) { + infoPanelPlaceholder.getChildren().add(new RegimeInfoPanel((Regime) resource).getRoot()); + logCurrentInfoShown("regime"); + } else if (resource instanceof Schedule) { + infoPanelPlaceholder.getChildren().add(new ScheduleInfoPanel((Schedule) resource).getRoot()); + logCurrentInfoShown("schedule"); + } + } + + /** + * Updates the information displayed in the center panel of application to show the default message. + */ + public void showDefaultMessage() { + infoPanelPlaceholder.getChildren().clear(); + infoPanelPlaceholder.getChildren().add(new Label(DEFAULT_MESSAGE)); + logCurrentInfoShown("default message"); + } + + private void logCurrentInfoShown(String infoShown) { + logger.info("Info panel displaying " + infoShown); + } +} diff --git a/src/main/java/seedu/exercise/ui/LeftRightPanel.java b/src/main/java/seedu/exercise/ui/LeftRightPanel.java new file mode 100644 index 00000000000..3bcd8c6ec5e --- /dev/null +++ b/src/main/java/seedu/exercise/ui/LeftRightPanel.java @@ -0,0 +1,53 @@ +package seedu.exercise.ui; + +import javafx.fxml.FXML; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import seedu.exercise.model.resource.Regime; + +/** + * Controller for panel which has two text area stacked horizontally. + */ +public class LeftRightPanel extends UiPart { + + private static final String FXML = "LeftRightPanel.fxml"; + private static final String LEFT_PANEL_TITLE_TEXT = "Scheduled Regime: %1$s"; + private static final String RIGHT_PANEL_TITLE_TEXT = "Conflicting Regime: %1$s"; + + private ExerciseListPanel leftPanel; + private ExerciseListPanel rightPanel; + + @FXML + private StackPane leftPanelPlaceholder; + + @FXML + private StackPane rightPanelPlaceholder; + + public LeftRightPanel() { + super(FXML); + } + + public void setLeftPanel(Regime scheduleRegime) { + leftPanel = new ExerciseListPanel(scheduleRegime.getRegimeExercises().asUnmodifiableObservableList()); + leftPanel.setPanelTitleText(String.format(LEFT_PANEL_TITLE_TEXT, + scheduleRegime.getRegimeName().toString())); + leftPanelPlaceholder.getChildren().add(leftPanel.getRoot()); + } + + public void setRightPanel(Regime conflictingRegime) { + rightPanel = new ExerciseListPanel(conflictingRegime.getRegimeExercises().asUnmodifiableObservableList()); + rightPanel.setPanelTitleText(String.format(RIGHT_PANEL_TITLE_TEXT, + conflictingRegime.getRegimeName().toString())); + rightPanelPlaceholder.getChildren().add(rightPanel.getRoot()); + } + + /** + * Clears text for both left and right text areas + */ + public void clearAll() { + leftPanel = null; + rightPanel = null; + leftPanelPlaceholder.getChildren().clear(); + rightPanelPlaceholder.getChildren().clear(); + } +} diff --git a/src/main/java/seedu/exercise/ui/LineChartPanel.java b/src/main/java/seedu/exercise/ui/LineChartPanel.java new file mode 100644 index 00000000000..bc4e60dae23 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/LineChartPanel.java @@ -0,0 +1,65 @@ +package seedu.exercise.ui; + +import java.util.ArrayList; + +import javafx.fxml.FXML; +import javafx.scene.chart.CategoryAxis; +import javafx.scene.chart.LineChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.layout.Region; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.ui.util.ChartUtil; + +/** + * An UI for line chart. + */ +public class LineChartPanel extends UiPart { + + private static final String FXML = "LineChartPanel.fxml"; + private static final String DEFAULT_DATE = "Date"; + private Statistic statistic; + + @FXML + private CategoryAxis xAxis; + @FXML + private NumberAxis yAxis; + @FXML + private LineChart lineChart; + + public LineChartPanel(Statistic statistic) { + super(FXML); + this.statistic = statistic; + display(); + } + + /** + * Set data for line chart to be displayed. + */ + private void display() { + String category = statistic.getCategory(); + String startDate = statistic.getStartDate().toString(); + String endDate = statistic.getEndDate().toString(); + ArrayList dates = statistic.getProperties(); + ArrayList values = statistic.getValues(); + + lineChart.setAnimated(false); + lineChart.layout(); + + xAxis.setLabel(DEFAULT_DATE); + yAxis.setLabel(ChartUtil.labelFormatter(category)); + + XYChart.Series series = new XYChart.Series<>(); + + int size = dates.size(); + for (int i = 0; i < size; i++) { + series.getData().add(new XYChart.Data<>(dates.get(i), values.get(i))); + } + + lineChart.setLegendVisible(false); + lineChart.setTitle(ChartUtil.lineAndBarChartTitleFormatter(category, startDate, endDate)); + lineChart.getData().add(series); + + ChartUtil.installToolTipXyChart(series.getData()); + } +} diff --git a/src/main/java/seedu/exercise/ui/ListResourceType.java b/src/main/java/seedu/exercise/ui/ListResourceType.java new file mode 100644 index 00000000000..bed98f86a9f --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ListResourceType.java @@ -0,0 +1,19 @@ +package seedu.exercise.ui; + +/** + * Encapsulates the different list of resource types to be displayed in GUI. + */ +public enum ListResourceType { + NULL, + EXERCISE, + REGIME, + SCHEDULE, + SUGGESTION; + + public static final String LIST_TYPE_EXERCISE = "exercise"; + public static final String LIST_TYPE_REGIME = "regime"; + public static final String LIST_TYPE_SCHEDULE = "schedule"; + public static final String LIST_TYPE_SUGGESTION = "suggestion"; + public static final String LIST_RESOURCE_TYPE_CONSTRAINTS = + "List type should be one of the following: exercise, regime, schedule or suggestion"; +} diff --git a/src/main/java/seedu/exercise/ui/MainWindow.java b/src/main/java/seedu/exercise/ui/MainWindow.java new file mode 100644 index 00000000000..58e1cc66317 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/MainWindow.java @@ -0,0 +1,400 @@ +package seedu.exercise.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.core.index.Index; +import seedu.exercise.logic.Logic; +import seedu.exercise.logic.commands.CommandResult; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.logic.parser.exceptions.ParseException; +import seedu.exercise.model.resource.Resource; +import seedu.exercise.ui.util.ChartUtil; + +/** + * The Main Window. Provides the basic application layout containing + * a menu bar and space where other JavaFX elements can be placed. + */ +public class MainWindow extends UiPart { + + private static final String FXML = "MainWindow.fxml"; + + private final Logger logger = LogsCenter.getLogger(getClass()); + + private Stage primaryStage; + private Logic logic; + + // Independent Ui parts residing in this Ui container + private ResultDisplay resultDisplay; + private HelpWindow helpWindow; + private ResolveWindow resolveWindow; + private CustomPropertiesWindow customPropertiesWindow; + private ResourceListPanel exerciseListPanel; + private ResourceListPanel regimeListPanel; + private ResourceListPanel scheduleListPanel; + private ResourceListPanel suggestionListPanel; + private InfoDisplayPanel infoDisplayPanel; + private StatsDisplayPanel statsDisplayPanel; + + @FXML + private StackPane commandBoxPlaceholder; + + @FXML + private StackPane resultDisplayPlaceholder; + + @FXML + private TabPane resourceListPanelPlaceholder; + + @FXML + private Tab exerciseListTabPlaceholder; + + @FXML + private Tab regimeListTabPlaceholder; + + @FXML + private Tab scheduleListTabPlaceholder; + + @FXML + private Tab suggestionListTabPlaceholder; + + @FXML + private StackPane infoDisplayPanelPlaceholder; + + @FXML + private ImageView logoImageView; + + @FXML + private StackPane chartPlaceholder; + + @FXML + private StackPane statsPlaceholder; + + public MainWindow(Stage primaryStage, Logic logic) { + super(FXML, primaryStage); + + // Set dependencies + this.primaryStage = primaryStage; + this.logic = logic; + + // Configure the UI + setWindowDefaultSize(logic.getGuiSettings()); + primaryStage.setTitle("ExerHealth"); + + helpWindow = new HelpWindow(); + } + + public Stage getPrimaryStage() { + return primaryStage; + } + + /** + * Fills up all the placeholders of this window. + */ + void fillInnerParts() { + Image imageView = new Image(getClass().getResource("/images/logo_no_bg_eH.png").toExternalForm()); + logoImageView.setImage(imageView); + + resultDisplay = new ResultDisplay(); + resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + + resolveWindow = new ResolveWindow(logic, resultDisplay); + + customPropertiesWindow = new CustomPropertiesWindow(); + + exerciseListPanel = new ExerciseListPanel(logic.getSortedExerciseList()); + exerciseListTabPlaceholder = new Tab(); + exerciseListTabPlaceholder.setContent(exerciseListPanel.getResourceListView()); + + regimeListPanel = new RegimeListPanel(logic.getSortedRegimeList()); + regimeListTabPlaceholder = new Tab(); + regimeListTabPlaceholder.setContent(regimeListPanel.getResourceListView()); + + scheduleListPanel = new ScheduleListPanel(logic.getSortedScheduleList()); + scheduleListTabPlaceholder = new Tab(); + scheduleListTabPlaceholder.setContent(scheduleListPanel.getResourceListView()); + + suggestionListPanel = new SuggestionListPanel(logic.getSuggestedExerciseList()); + suggestionListTabPlaceholder = new Tab(); + suggestionListTabPlaceholder.setContent(suggestionListPanel.getResourceListView()); + + resourceListPanelPlaceholder.getTabs().add(exerciseListTabPlaceholder); + resourceListPanelPlaceholder.getTabs().add(regimeListTabPlaceholder); + resourceListPanelPlaceholder.getTabs().add(scheduleListTabPlaceholder); + resourceListPanelPlaceholder.getTabs().add(suggestionListTabPlaceholder); + + chartPlaceholder.getChildren().add(new LineChartPanel(logic.getStatistic()).getRoot()); + Label totalAndAverage = new Label(getTotalAndAverage()); + statsPlaceholder.getChildren().add(totalAndAverage); + + CommandBox commandBox = new CommandBox(this::executeCommand); + commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); + commandBox.requestFocus(); + + infoDisplayPanel = new InfoDisplayPanel(); + infoDisplayPanelPlaceholder.getChildren().add(infoDisplayPanel.getRoot()); + + initListeners(); + displayInitialList(); + } + + /** + * Sets the chart type in chartPlaceholder according to user input. + */ + private void setChart() { + Statistic statistic = logic.getStatistic(); + String chart = statistic.getChart(); + chartPlaceholder.getChildren().clear(); + if (chart.equals("barchart")) { + chartPlaceholder.getChildren().add(new BarChartPanel(statistic).getRoot()); + } else if (chart.equals("linechart")) { + chartPlaceholder.getChildren().add(new LineChartPanel(statistic).getRoot()); + } else { + chartPlaceholder.getChildren().add(new PieChartPanel(statistic).getRoot()); + } + } + + private String getTotalAndAverage() { + Statistic statistic = logic.getStatistic(); + return ChartUtil.totalFormatter(statistic.getCategory(), statistic.getTotal()) + "\n" + + ChartUtil.averageFormatter(statistic.getCategory(), statistic.getAverage()); + } + + private void setStats() { + statsPlaceholder.getChildren().clear(); + Label totalAndAverage = new Label(getTotalAndAverage()); + statsPlaceholder.getChildren().add(totalAndAverage); + } + + private void displayInitialList() { + changeTab(scheduleListTabPlaceholder); + infoDisplayPanel.showDefaultMessage(); + } + + /** + * Initialises a listener for each list panel on the left of the window + */ + private void initListeners() { + exerciseListPanel.setOnItemSelectListener(getOnItemSelectListener()); + regimeListPanel.setOnItemSelectListener(getOnItemSelectListener()); + scheduleListPanel.setOnItemSelectListener(getOnItemSelectListener()); + suggestionListPanel.setOnItemSelectListener(getOnItemSelectListener()); + resolveWindow.setOnResolveSuccessListener(getResolveSuccessListener()); + + logger.info("Listeners for main window set"); + } + + private ResolveWindow.OnResolveSuccessListener getResolveSuccessListener() { + return result -> { + updateResourceListTab(result, -1); + resultDisplay.setFeedbackToUser(result.getFeedbackToUser()); + }; + } + + private ResourceListPanel.OnItemSelectListener getOnItemSelectListener() { + return new ResourceListPanel.OnItemSelectListener() { + @Override + public void onItemSelect(Resource selected) { + infoDisplayPanel.update(selected); + } + }; + } + + /** + * Sets the default size based on {@code guiSettings}. + */ + private void setWindowDefaultSize(GuiSettings guiSettings) { + primaryStage.setHeight(guiSettings.getWindowHeight()); + primaryStage.setWidth(guiSettings.getWindowWidth()); + if (guiSettings.getWindowCoordinates() != null) { + primaryStage.setX(guiSettings.getWindowCoordinates().getX()); + primaryStage.setY(guiSettings.getWindowCoordinates().getY()); + } + } + + /** + * Executes the command and returns the result. + * + * @see seedu.exercise.logic.Logic#execute(String) + */ + private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + try { + CommandResult commandResult = logic.execute(commandText); + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + setChart(); + setStats(); + resetResourceListTabs(); + + shouldShowWindowsBasedOnCommandResult(commandResult); + shouldExitAppBasedOnCommandResult(commandResult); + updateResourceListTab(commandResult, -1); // Negative index means nothing is selected. + + return commandResult; + } catch (CommandException | ParseException e) { + logger.info("Invalid command: " + commandText); + resultDisplay.setFeedbackToUser(e.getMessage()); + throw e; + } + } + + /** + * Resets the selection models of the resource list panels on the left of the GUI. + */ + private void resetResourceListTabs() { + exerciseListPanel.resetListSelection(); + regimeListPanel.resetListSelection(); + scheduleListPanel.resetListSelection(); + suggestionListPanel.resetListSelection(); + } + + /** + * Checks if a secondary window should be shown based on the command results. + * Method will show the windows if it is to be shown. + */ + private void shouldShowWindowsBasedOnCommandResult(CommandResult commandResult) { + if (commandResult.isShowHelp()) { + handleHelp(); + } + + if (commandResult.isShowResolve()) { + handleResolve(); + } + + if (commandResult.isShowCustomProperties()) { + handleViewCustom(); + } + + if (commandResult.isSelectResource()) { + handleSelectResource(commandResult); + } + } + + private void shouldExitAppBasedOnCommandResult(CommandResult commandResult) { + if (commandResult.isExit()) { + handleExit(); + } + } + + /** + * Opens the help window or focuses on it if it's already opened. + */ + @FXML + public void handleHelp() { + if (!helpWindow.isShowing()) { + helpWindow.show(); + } else { + helpWindow.focus(); + } + } + + /** + * Opens the resolve window and blocks all events until closed. + */ + @FXML + private void handleResolve() { + resolveWindow.setLeftRightPanel(); + if (resolveWindow.isShowing()) { + resolveWindow.focus(); + } else { + resolveWindow.show(); + } + } + + /** + * Opens the custom properties window or focuses on it if it's already opened. + */ + @FXML + private void handleViewCustom() { + customPropertiesWindow.initialiseTable(); + if (!customPropertiesWindow.isShowing()) { + customPropertiesWindow.show(); + } else { + customPropertiesWindow.focus(); + } + } + + /** + * Selects the resource at the given index of the desired resource list. + */ + @FXML + private void handleSelectResource(CommandResult commandResult) { + Index selectedIndex = commandResult.getSelectedIndex().get(); + updateResourceListTab(commandResult, selectedIndex.getZeroBased()); + } + + void show() { + primaryStage.show(); + } + + /** + * Closes the application. + */ + @FXML + private void handleExit() { + GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), + (int) primaryStage.getX(), (int) primaryStage.getY()); + logic.setGuiSettings(guiSettings); + helpWindow.hide(); + resolveWindow.hideAndClearPanels(); + customPropertiesWindow.hide(); + primaryStage.hide(); + } + + /** + * Checks if a the resource list has to change based on the {@code CommandResult} + */ + private void updateResourceListTab(CommandResult commandResult, int index) { + logger.info("Changing resource list panel to show " + commandResult.getShowListResourceType()); + switch (commandResult.getShowListResourceType()) { + case NULL: + //no change to GUI + return; + case EXERCISE: + handleShowResourceList(exerciseListTabPlaceholder); + exerciseListPanel.selectGivenIndex(index); + return; + case REGIME: + handleShowResourceList(regimeListTabPlaceholder); + regimeListPanel.selectGivenIndex(index); + return; + case SCHEDULE: + handleShowResourceList(scheduleListTabPlaceholder); + scheduleListPanel.selectGivenIndex(index); + return; + case SUGGESTION: + handleShowResourceList(suggestionListTabPlaceholder); + suggestionListPanel.selectGivenIndex(index); + return; + default: + throw new AssertionError(ListResourceType.LIST_RESOURCE_TYPE_CONSTRAINTS); + } + } + + private void changeTab(Tab tab) { + resourceListPanelPlaceholder.getSelectionModel().select(tab); + } + + /** + * Updates the GUI to show the resource list tab and refresh info display panel if the tab did change. + */ + private void handleShowResourceList(Tab resourceListTabPlaceholder) { + if (!(isResourceListPanelShown(resourceListTabPlaceholder))) { + infoDisplayPanel.showDefaultMessage(); + } + resourceListPanelPlaceholder.getSelectionModel().select(resourceListTabPlaceholder); + } + + private boolean isResourceListPanelShown(Tab resourceListPlaceholder) { + return resourceListPanelPlaceholder.getSelectionModel().getSelectedItem().equals(resourceListPlaceholder); + } +} diff --git a/src/main/java/seedu/exercise/ui/PieChartPanel.java b/src/main/java/seedu/exercise/ui/PieChartPanel.java new file mode 100644 index 00000000000..9809f1b81aa --- /dev/null +++ b/src/main/java/seedu/exercise/ui/PieChartPanel.java @@ -0,0 +1,57 @@ +package seedu.exercise.ui; + +import java.util.ArrayList; + +import javafx.fxml.FXML; +import javafx.scene.chart.PieChart; +import javafx.scene.layout.Region; +import seedu.exercise.logic.commands.statistic.Statistic; +import seedu.exercise.ui.util.ChartUtil; + +/** + * An UI for pie chart. + */ +public class PieChartPanel extends UiPart { + private static final String FXML = "PieChartPanel.fxml"; + + private Statistic statistic; + + @FXML + private PieChart pieChart; + + public PieChartPanel(Statistic statistic) { + super(FXML); + this.statistic = statistic; + display(); + } + + /** + * Set data for pie chart to be displayed. + */ + private void display() { + String category = statistic.getCategory(); + String startDate = statistic.getStartDate().toString(); + String endDate = statistic.getEndDate().toString(); + ArrayList properties = statistic.getProperties(); + ArrayList values = statistic.getValues(); + + int size = properties.size(); + if (size == 0) { + PieChart.Data slice = new PieChart.Data("No exercise data found", 1); + pieChart.getData().add(slice); + } + + for (int i = 0; i < size; i++) { + double percentage = Statistic.percentage(values.get(i), statistic.getTotal()); + String property = ChartUtil.propertyFormatter(properties.get(i)); + String propertyWithPercentage = ChartUtil.percentageFormatter(property, percentage); + PieChart.Data slice = new PieChart.Data(propertyWithPercentage, values.get(i)); + pieChart.getData().add(slice); + } + + ChartUtil.installToolTipPieChart(pieChart.getData()); + + pieChart.setLegendVisible(false); + pieChart.setTitle(ChartUtil.pieChartTitleFormatter(category, startDate, endDate)); + } +} diff --git a/src/main/java/seedu/exercise/ui/RegimeCard.java b/src/main/java/seedu/exercise/ui/RegimeCard.java new file mode 100644 index 00000000000..c8549fd9246 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/RegimeCard.java @@ -0,0 +1,37 @@ +package seedu.exercise.ui; + +import static seedu.exercise.ui.util.LabelUtil.setLabelTooltip; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.exercise.model.resource.Regime; + +/** + * An UI component that displays informatino of a {@code Regime}. + */ +public class RegimeCard extends UiPart { + private static final String FXML = "RegimeCard.fxml"; + + private final Regime regime; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label name; + @FXML + private Label exercises; + + public RegimeCard(Regime regime, int displayedIndex) { + super(FXML); + this.regime = regime; + id.setText(displayedIndex + ". "); + name.setText(regime.getRegimeName().toString()); + exercises.setText(regime.toString()); + + setLabelTooltip(name); + } +} diff --git a/src/main/java/seedu/exercise/ui/RegimeInfoPanel.java b/src/main/java/seedu/exercise/ui/RegimeInfoPanel.java new file mode 100644 index 00000000000..50a43fc1f22 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/RegimeInfoPanel.java @@ -0,0 +1,44 @@ +package seedu.exercise.ui; + +import static seedu.exercise.ui.util.LabelUtil.setLabelTooltip; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import seedu.exercise.model.resource.Regime; + +/** + * Information panel for regime type resources + */ +public class RegimeInfoPanel extends UiPart { + + private static final String FXML = "RegimeInfoPanel.fxml"; + private static final String EXERCISE_LIST_PANEL_TEXT = "List of Exercises"; + private static final String REGIME_NAME_TEXT = "Regime %1$s"; + private static final String TOTAL_CALORIE_COUNT_TEXT = "Total Calories: %1$d"; + + private Regime regime; + + @FXML + private Label regimeName; + + @FXML + private Label totalCalorieCount; + + @FXML + private StackPane exerciseListPanelPlaceholder; + + public RegimeInfoPanel(Regime regime) { + super(FXML); + + this.regimeName.setText(String.format(REGIME_NAME_TEXT, regime.getRegimeName().toString())); + this.totalCalorieCount.setText(String.format(TOTAL_CALORIE_COUNT_TEXT, regime.getTotalCalorieCount())); + + ExerciseListPanel exerciseListPanel = new ExerciseListPanel( + regime.getRegimeExercises().asUnmodifiableObservableList()); + exerciseListPanel.setPanelTitleText(EXERCISE_LIST_PANEL_TEXT); + exerciseListPanelPlaceholder.getChildren().add(exerciseListPanel.getRoot()); + setLabelTooltip(regimeName); + } +} diff --git a/src/main/java/seedu/exercise/ui/RegimeListPanel.java b/src/main/java/seedu/exercise/ui/RegimeListPanel.java new file mode 100644 index 00000000000..73af844ca1b --- /dev/null +++ b/src/main/java/seedu/exercise/ui/RegimeListPanel.java @@ -0,0 +1,51 @@ +package seedu.exercise.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import seedu.exercise.model.resource.Regime; + +/** + * Panel containing list of regimes. + */ +public class RegimeListPanel extends ResourceListPanel { + private static final String FXML = "RegimeListPanel.fxml"; + + @FXML + private ListView regimeListView; + + public RegimeListPanel(ObservableList regimeList) { + super(FXML, regimeList); + regimeListView.setItems(regimeList); + regimeListView.setCellFactory(listView -> new RegimeListViewCell()); + regimeListView.getFocusModel().focusedItemProperty().addListener(getDefaultListViewListener()); + } + + @Override + public void resetListSelection() { + regimeListView.getSelectionModel().clearSelection(); + } + + @Override + public ListView getResourceListView() { + return regimeListView; + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Regime} using a {@code RegimeCard}. + */ + class RegimeListViewCell extends ListCell { + @Override + protected void updateItem(Regime regime, boolean empty) { + super.updateItem(regime, empty); + + if (empty || regime == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new RegimeCard(regime, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/exercise/ui/ResolveWindow.java b/src/main/java/seedu/exercise/ui/ResolveWindow.java new file mode 100644 index 00000000000..9075e2ff86d --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ResolveWindow.java @@ -0,0 +1,198 @@ +package seedu.exercise.ui; + +import java.util.logging.Logger; + +import javafx.event.Event; +import javafx.fxml.FXML; +import javafx.scene.layout.StackPane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import seedu.exercise.commons.core.GuiSettings; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.logic.Logic; +import seedu.exercise.logic.commands.CommandResult; +import seedu.exercise.logic.commands.ResolveCommand; +import seedu.exercise.logic.commands.exceptions.CommandException; +import seedu.exercise.logic.parser.exceptions.ParseException; + +/** + * Controller for resolving schedule conflicts. + *

+ * This window acts as a visual feedback + * for the conflicting schedules. Provides a left right panel for display of + * the schedules and a command box. Window will block all events until it is + * closed or resolved. + *

+ */ +public class ResolveWindow extends UiPart { + + private static final String FXML = "ResolveWindow.fxml"; + private static final String UNABLE_TO_CLOSE_WINDOW = "Please resolve your conflict using the resolve command.\n" + + "You are not allowed to close this window before resolving this conflict."; + private static final String INITIAL_HELP_MESSAGE = "Please resolve your conflict with the resolve command." + + ResolveCommand.MESSAGE_USAGE; + + private final Logger logger = LogsCenter.getLogger(getClass()); + private OnResolveSuccessListener resolveSuccessListener; + + private Logic logic; + + private LeftRightPanel leftRightPanel; + private CommandBox commandBox; + private ResultDisplay resultDisplay; + private ResultDisplay mainWindowDisplay; + private Stage parent; + + @FXML + private StackPane commandBoxPlaceHolder; + + @FXML + private StackPane resultDisplayPlaceHolder; + + @FXML + private StackPane leftRightPanelPlaceHolder; + + + public ResolveWindow(Stage root, Logic logic, ResultDisplay mainWindowDisplay) { + super(FXML, root); + parent = getRoot(); + this.logic = logic; + this.mainWindowDisplay = mainWindowDisplay; + + blockEvents(root); + fillInnerParts(); + + setWindowSize(logic.getGuiSettings()); + centerWindow(); + } + + public ResolveWindow(Logic logic, ResultDisplay mainWindowDisplay) { + this(new Stage(), logic, mainWindowDisplay); + } + + /** + * Shows the resolve window. + */ + public void show() { + parent.show(); + parent.centerOnScreen(); + } + + public boolean isShowing() { + return getRoot().isShowing(); + } + + /** + * Hides the window and clears the text in LeftRightPanel + * + * @see LeftRightPanel#clearAll() + */ + public void hideAndClearPanels() { + leftRightPanel.clearAll(); + resultDisplay.clearText(); + commandBox.clearText(); + parent.hide(); + } + + public void focus() { + getRoot().requestFocus(); + } + + public void setLeftRightPanel() { + logger.info("Resolve window showing scheduled " + + logic.getConflict().getScheduledName() + + ", conflicting " + + logic.getConflict().getConflictedName()); + resultDisplay.setFeedbackToUser(INITIAL_HELP_MESSAGE); + leftRightPanel.setLeftPanel(logic.getConflict().getScheduledRegime()); + leftRightPanel.setRightPanel(logic.getConflict().getConflictingRegime()); + } + + /** + * The user is not allowed to close the {@code ResolveWindow} using the exit button. + * All events are consumed and feedback is given to user to resolve the conflict. + */ + @FXML + private void handleCloseRequest(Event evt) { + evt.consume(); + resultDisplay.setFeedbackToUser(UNABLE_TO_CLOSE_WINDOW); + } + + private void setWindowSize(GuiSettings guiSettings) { + parent.setHeight(guiSettings.getWindowHeight()); + parent.setWidth(guiSettings.getWindowWidth()); + } + + private void centerWindow() { + parent.centerOnScreen(); + } + + private void fillInnerParts() { + fillLeftRightPanel(); + fillCommandBox(); + fillResultDisplay(); + } + + private void fillResultDisplay() { + resultDisplay = new ResultDisplay(); + resultDisplayPlaceHolder.getChildren().add(resultDisplay.getRoot()); + } + + private void fillLeftRightPanel() { + leftRightPanel = new LeftRightPanel(); + leftRightPanelPlaceHolder.getChildren().add(leftRightPanel.getRoot()); + } + + private void fillCommandBox() { + commandBox = new CommandBox(this::execute); + commandBoxPlaceHolder.getChildren().add(commandBox.getRoot()); + } + + private void blockEvents(Stage root) { + root.initModality(Modality.APPLICATION_MODAL); + } + + /** + * Executor for resolve window's command box. + *

+ * This executor will only allow resolve commands to be executed. + *

+ * @param commandText user input + * @return Result from executing a valid command + */ + private CommandResult execute(String commandText) throws CommandException, ParseException { + try { + CommandResult commandResult = logic.execute(commandText); + logger.info("Result: " + commandResult.getFeedbackToUser()); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + + if (!commandResult.isShowResolve()) { + informResolveSuccessListener(commandResult); + this.hideAndClearPanels(); + } + + return commandResult; + } catch (CommandException | ParseException e) { + logger.info("Invalid command: " + commandText); + resultDisplay.setFeedbackToUser(e.getMessage()); + throw e; + } + } + + void setOnResolveSuccessListener(OnResolveSuccessListener listener) { + this.resolveSuccessListener = listener; + } + + private void informResolveSuccessListener(CommandResult commandResult) { + if (resolveSuccessListener != null) { + resolveSuccessListener.onResolveSuccess(commandResult); + } + } + + /** + * Informs interested classes of scheduling conflict resolution being successful. + */ + interface OnResolveSuccessListener { + void onResolveSuccess(CommandResult result); + } +} diff --git a/src/main/java/seedu/exercise/ui/ResourceListPanel.java b/src/main/java/seedu/exercise/ui/ResourceListPanel.java new file mode 100644 index 00000000000..433fd7656d3 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ResourceListPanel.java @@ -0,0 +1,162 @@ +package seedu.exercise.ui; + +import static java.util.Objects.requireNonNull; + +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.exercise.model.resource.Resource; + +/** + * Panel containing the list of resources. + */ +public abstract class ResourceListPanel extends UiPart { + + private OnItemSelectListener itemSelectListener; + private ObservableList resourceList; + + public ResourceListPanel(String fxml, ObservableList resourceList) { + super(fxml); + this.resourceList = resourceList; + + setResourceListListener(); + } + + public void setOnItemSelectListener(OnItemSelectListener listener) { + requireNonNull(listener); + itemSelectListener = listener; + } + + void notifyOnSelectListener(Resource r) { + if (itemSelectListener != null) { + itemSelectListener.onItemSelect(r); + } + } + + /** + * Selects, focus and scrolls to the {@code index} in {@code resources} list on left panel. + *

+ * Method will additionally inform the {@link OnItemSelectListener} of this selection event + *

+ */ + void selectFocusAndScrollTo(ListView resources, int index) { + resources.getSelectionModel().select(index); + resources.getFocusModel().focus(index); + resources.scrollTo(index); + notifyOnSelectListener(resources.getSelectionModel().getSelectedItem()); + } + + ChangeListener getDefaultListViewListener() { + return new ChangeListener() { + @Override + public void changed(ObservableValue observableValue, Resource resource, Resource t1) { + //This is to handle mouse click events. + if (observableValue != null) { + notifyOnSelectListener(observableValue.getValue()); + } + } + }; + } + + private void setResourceListListener() { + resourceList.addListener(new ListChangeListener() { + /** + * Called after a change is made to the {@code resourceList}. + * + * It is important to note that the {@code change} reported + * may consist of one or more actual changes that can be iterated by + * the {@code next()} method. For our purposes, we are usually only + * interested in the first change event that is notified by the + * {@code ObservableList}. + * + * The basic CRUD that is implemented will rarely ever have + * multiple changes we are interested in. For reference, below listed are some + * basic expectations of CRUD in ExerHealth. + *

+ * C - create --> {@code change.wasAdded()} + *

+ *

+ * R - read --> {no methods are called} + *

+ *

+ * U - update --> {@code change.wasUpdated()} + *

+ *

+ * D - delete --> {@code change.wasRemoved()} + *

+ * + * Also, according to javadocs, there is a order to follow for calling + * changes of different types. This method adheres to the order provided + * in the javadocs for consistency sake. + *

+ * See + * + * ListChangeListener.Change + *

+ */ + @Override + public void onChanged(Change change) { + int index = -1; + while (change.next()) { + if (change.wasReplaced()) { + index = change.getFrom(); + break; + } else if (change.wasAdded()) { + index = change.getFrom(); + break; + } else if (change.wasRemoved()) { + index = change.getFrom(); + break; + } else if (change.wasUpdated()) { + index = change.getFrom(); + break; + } + } + if (index >= 0) { + selectGivenIndex(index); + } + } + }); + } + + /** + * Selects the {@code index} in the resource list view on the left panel. + *

+ * This method will cause the selected {@code index} to propagate to {@link OnItemSelectListener} + * so that any class listening out for selection changes can be informed of the selection. + * Classes interested in listening out for selection events should implement the + * {@link OnItemSelectListener#onItemSelect(Resource)} to be informed of the resource + * that has been selected. + *

+ */ + public void selectGivenIndex(int index) { + ListView resourceListView = getResourceListView(); + if (index >= 0) { + /* + An extremely hacky way to get the list to select, focus and scroll to the newly changed item. + Without this method, when any add/edit commands are supplied, the ListChangeListener attached to + ObservableList is called first without the list actually changing its structure. So when the index + is provided, the listview is not updated and thus cannot be focused on. + So the solution is to make this focusing operation be done at a slightly later time when the + list view has been updated to reflect the commands changes + */ + Platform.runLater(() -> selectFocusAndScrollTo(resourceListView, index)); + } + } + + public abstract void resetListSelection(); + + public abstract ListView getResourceListView(); + + /** + * Listener for item selection events in this {@code ResourceListPanel}. + */ + public interface OnItemSelectListener { + void onItemSelect(Resource selected); + } + +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/exercise/ui/ResultDisplay.java similarity index 87% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/seedu/exercise/ui/ResultDisplay.java index 7d98e84eedf..8288ea4efb6 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/exercise/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.exercise.ui; import static java.util.Objects.requireNonNull; @@ -25,4 +25,7 @@ public void setFeedbackToUser(String feedbackToUser) { resultDisplay.setText(feedbackToUser); } + void clearText() { + setFeedbackToUser(""); + } } diff --git a/src/main/java/seedu/exercise/ui/ScheduleCard.java b/src/main/java/seedu/exercise/ui/ScheduleCard.java new file mode 100644 index 00000000000..127f9c2b46a --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ScheduleCard.java @@ -0,0 +1,72 @@ +package seedu.exercise.ui; + +import static seedu.exercise.ui.util.LabelUtil.setLabelTooltip; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import seedu.exercise.model.property.Date; +import seedu.exercise.model.resource.Schedule; + +/** + * An UI component that displays information of a {@code Schedule}. + */ +public class ScheduleCard extends UiPart { + + private static final String FXML = "ScheduleListCard.fxml"; + private static final String DATE_PREAMBLE = "Scheduled on: "; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Schedule schedule; + + @FXML + private HBox cardPane; + @FXML + private Label name; + @FXML + private Label id; + @FXML + private Label date; + + public ScheduleCard(Schedule schedule, int displayedIndex) { + super(FXML); + this.schedule = schedule; + id.setText(displayedIndex + ". "); + name.setText(schedule.getRegime().getRegimeName().toString()); + date.setText(getDateStringForDisplay(schedule.getDate().toString())); + setLabelTooltip(name); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ScheduleCard)) { + return false; + } + + // state check + ScheduleCard card = (ScheduleCard) other; + return id.getText().equals(card.id.getText()) + && schedule.equals(card.schedule); + } + + /** + * Returns a string representation of {@code date} that is pretty printed. + */ + private String getDateStringForDisplay(String date) { + return DATE_PREAMBLE + date + "\n" + Date.prettyPrint(date); + } +} diff --git a/src/main/java/seedu/exercise/ui/ScheduleInfoPanel.java b/src/main/java/seedu/exercise/ui/ScheduleInfoPanel.java new file mode 100644 index 00000000000..7877d07aad6 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ScheduleInfoPanel.java @@ -0,0 +1,32 @@ +package seedu.exercise.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import seedu.exercise.model.resource.Schedule; + +/** + * Information panel for displaying schedule type information + */ +public class ScheduleInfoPanel extends UiPart { + + private static final String FXML = "ScheduleInfoPanel.fxml"; + private static final String DATE_DISPLAY_TEXT = "Scheduled on: %1$s"; + + @FXML + private Label dateOfSchedule; + + @FXML + private StackPane regimePanelPlaceholder; + + public ScheduleInfoPanel(Schedule schedule) { + super(FXML); + + dateOfSchedule.setText(String.format(DATE_DISPLAY_TEXT, + schedule.getDate().toString())); + + RegimeInfoPanel infoPanel = new RegimeInfoPanel(schedule.getRegime()); + regimePanelPlaceholder.getChildren().add(infoPanel.getRoot()); + } +} diff --git a/src/main/java/seedu/exercise/ui/ScheduleListPanel.java b/src/main/java/seedu/exercise/ui/ScheduleListPanel.java new file mode 100644 index 00000000000..d1cb7f75178 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/ScheduleListPanel.java @@ -0,0 +1,52 @@ +package seedu.exercise.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import seedu.exercise.model.resource.Schedule; + +/** + * Panel containing the list of schedules. + */ +public class ScheduleListPanel extends ResourceListPanel { + private static final String FXML = "ScheduleListPanel.fxml"; + + @FXML + private ListView scheduleListView; + + public ScheduleListPanel(ObservableList scheduleList) { + super(FXML, scheduleList); + scheduleListView.setItems(scheduleList); + scheduleListView.setCellFactory(listView -> new ScheduleListViewCell()); + scheduleListView.getFocusModel().focusedItemProperty().addListener(getDefaultListViewListener()); + } + + @Override + public void resetListSelection() { + scheduleListView.getSelectionModel().clearSelection(); + } + + @Override + public ListView getResourceListView() { + return scheduleListView; + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Schedule} using a {@code ScheduleCard}. + */ + class ScheduleListViewCell extends ListCell { + @Override + protected void updateItem(Schedule schedule, boolean empty) { + super.updateItem(schedule, empty); + + if (empty || schedule == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ScheduleCard(schedule, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/exercise/ui/StatsDisplayPanel.java b/src/main/java/seedu/exercise/ui/StatsDisplayPanel.java new file mode 100644 index 00000000000..b2b2b470ef5 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/StatsDisplayPanel.java @@ -0,0 +1,14 @@ +package seedu.exercise.ui; + +import javafx.scene.layout.Region; + +/** + * Placeholder Panel for Stats Display in right panel + */ +public class StatsDisplayPanel extends UiPart { + private static final String FXML = "StatsDisplayPanel.fxml"; + + public StatsDisplayPanel() { + super(FXML); + } +} diff --git a/src/main/java/seedu/exercise/ui/SuggestionListPanel.java b/src/main/java/seedu/exercise/ui/SuggestionListPanel.java new file mode 100644 index 00000000000..b163bf31e9e --- /dev/null +++ b/src/main/java/seedu/exercise/ui/SuggestionListPanel.java @@ -0,0 +1,55 @@ +package seedu.exercise.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import seedu.exercise.model.resource.Exercise; + +/** + * Panel containing the list of suggested exercises. + */ +public class SuggestionListPanel extends ResourceListPanel { + private static final String FXML = "SuggestionListPanel.fxml"; + + @FXML + private ListView suggestionListView; + + public SuggestionListPanel(ObservableList exerciseList) { + super(FXML, exerciseList); + suggestionListView.setItems(exerciseList); + suggestionListView.setCellFactory(listView -> new SuggestionListViewCell()); + suggestionListView.getFocusModel().focusedItemProperty().addListener(getDefaultListViewListener()); + } + + public ListView getSuggestionListView() { + return suggestionListView; + } + + @Override + public void resetListSelection() { + suggestionListView.getSelectionModel().clearSelection(); + } + + @Override + public ListView getResourceListView() { + return suggestionListView; + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Exercise} using a {@code ExerciseInfoPanel}. + */ + class SuggestionListViewCell extends ListCell { + @Override + protected void updateItem(Exercise exercise, boolean empty) { + super.updateItem(exercise, empty); + + if (empty || exercise == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ExerciseListCard(exercise, getIndex() + 1).getRoot()); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/seedu/exercise/ui/Ui.java similarity index 85% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/seedu/exercise/ui/Ui.java index 17aa0b494fe..a1307c51c68 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/seedu/exercise/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.exercise.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/exercise/ui/UiManager.java similarity index 91% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/seedu/exercise/ui/UiManager.java index 876621d79b9..bca7285cf55 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/exercise/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.exercise.ui; import java.util.logging.Logger; @@ -7,10 +7,10 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; +import seedu.exercise.MainApp; +import seedu.exercise.commons.core.LogsCenter; +import seedu.exercise.commons.util.StringUtil; +import seedu.exercise.logic.Logic; /** * The manager of the UI component. @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/logo_eH.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/exercise/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/seedu/exercise/ui/UiPart.java index fc820e01a9c..a99886e0a70 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/exercise/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package seedu.exercise.ui; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import seedu.exercise.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. diff --git a/src/main/java/seedu/exercise/ui/util/ChartUtil.java b/src/main/java/seedu/exercise/ui/util/ChartUtil.java new file mode 100644 index 00000000000..7fba3ecce7a --- /dev/null +++ b/src/main/java/seedu/exercise/ui/util/ChartUtil.java @@ -0,0 +1,144 @@ +package seedu.exercise.ui.util; + +import javafx.collections.ObservableList; +import javafx.scene.chart.PieChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.Tooltip; + +/** + * A class that contains common methods for ChartPanel. + */ +public class ChartUtil { + + public static final String TITLE_FORMAT = "%s (%s to %s)"; + + /** + * Install tooltip for bar chart and line chart. + */ + public static void installToolTipXyChart(ObservableList> data) { + data.stream().forEach(d -> { + Tooltip tooltip = new Tooltip(); + tooltip.setText(d.getXValue() + "\n" + d.getYValue()); + Tooltip.install(d.getNode(), tooltip); + }); + } + + /** + * Install tooltip for pie chart. + */ + public static void installToolTipPieChart(ObservableList data) { + data.stream().forEach(d -> { + Tooltip tooltip = new Tooltip(); + tooltip.setText(d.getName()); + Tooltip.install(d.getNode(), tooltip); + }); + } + + /** + * Returns the formatted title of line chart and bar chart. + */ + public static String lineAndBarChartTitleFormatter(String category, String startDate, String endDate) { + return changeFirstLetterToUpperCase(String.format(TITLE_FORMAT, category, startDate, endDate)); + } + + /** + * Returns the formatted title of pie chart. + */ + public static String pieChartTitleFormatter(String category, String startDate, String endDate) { + if (category.equals("exercise")) { + return changeFirstLetterToUpperCase(String.format("%s Frequency (%s to %s)", category, startDate, endDate)); + } else { + return changeFirstLetterToUpperCase(String.format(TITLE_FORMAT, category, startDate, endDate)); + } + } + + /** + * Returns the y-axis label of bar and line chart. + */ + public static String labelFormatter(String category) { + if (category.equals("exercise")) { + return "Frequency"; + } else { + return "kcal"; + } + } + + /** + * Returns the string with first letter changed to upper case. + */ + public static String changeFirstLetterToUpperCase(String string) { + return string.substring(0, 1).toUpperCase() + string.substring(1); + } + + /** + * If the string is too long, it will format string to first 10 character plus last 8 character. + */ + public static String propertyFormatter(String string) { + string = changeFirstLetterToUpperCase(string); + int length = string.length(); + if (length > 18) { + string = string.substring(0, 10) + "..." + string.substring(length - 8); + } + + return string; + } + + /** + * Returns the percentage of property for pie chart. + */ + public static String percentageFormatter(String property, double percentage) { + return String.format("%s \n[%.2f%%]", property, percentage); + } + + /** + * Returns the formatted string of total. + */ + public static String totalFormatter(String category, double total) { + String totalString = numberFormatter(total); + if (category.equals("exercise")) { + return String.format("Total: %s Exercise(s)", totalString); + } else { + return String.format("Total: %s kcal", totalString); + } + } + + /** + * Returns the formatted string of average. + */ + public static String averageFormatter(String category, double average) { + String averageString = numberFormatter(average); + if (category.equals("exercise")) { + return String.format("Average: %s Exercise(s)", averageString); + } else { + return String.format("Average: %s kcal", averageString); + } + } + + /** + * Change number to format of million, billion, trillion or quadrillion. + */ + public static String numberFormatter(double number) { + long million = 1000000L; + long billion = 1000000000L; + long trillion = 1000000000000L; + long quadrillion = 1000000000000000L; + + if (number > quadrillion) { + return String.format("%.2f quadrillion", number / quadrillion); + } + + if (number > trillion) { + return String.format("%.2f trillion", number / trillion); + } + + if (number > billion) { + return String.format("%.2f billion", number / billion); + } + + if (number > million) { + return String.format("%.2f million", number / million); + } + + return String.format("%.2f", number); + } +} diff --git a/src/main/java/seedu/exercise/ui/util/LabelUtil.java b/src/main/java/seedu/exercise/ui/util/LabelUtil.java new file mode 100644 index 00000000000..d85915c4e68 --- /dev/null +++ b/src/main/java/seedu/exercise/ui/util/LabelUtil.java @@ -0,0 +1,15 @@ +package seedu.exercise.ui.util; + +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; + +/** + * Represents a utility class that contains methods that acts on a {@code Label} object. + */ +public class LabelUtil { + + public static void setLabelTooltip(Label label) { + Tooltip tooltip = new Tooltip(label.getText()); + label.setTooltip(tooltip); + } +} diff --git a/src/main/resources/images/ExerHealth.png b/src/main/resources/images/ExerHealth.png new file mode 100644 index 00000000000..ea2d21f0b8a Binary files /dev/null and b/src/main/resources/images/ExerHealth.png differ diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png deleted file mode 100644 index 29810cf1fd9..00000000000 Binary files a/src/main/resources/images/address_book_32.png and /dev/null differ diff --git a/src/main/resources/images/calendar.png b/src/main/resources/images/calendar.png index 8b2bdf4f1c1..9c450daaecd 100644 Binary files a/src/main/resources/images/calendar.png and b/src/main/resources/images/calendar.png differ diff --git a/src/main/resources/images/calories.png b/src/main/resources/images/calories.png new file mode 100644 index 00000000000..441bed758e3 Binary files /dev/null and b/src/main/resources/images/calories.png differ diff --git a/src/main/resources/images/logo_eH.png b/src/main/resources/images/logo_eH.png new file mode 100644 index 00000000000..a01636f7fa7 Binary files /dev/null and b/src/main/resources/images/logo_eH.png differ diff --git a/src/main/resources/images/logo_eXerH.png b/src/main/resources/images/logo_eXerH.png new file mode 100644 index 00000000000..6e0fd65ee61 Binary files /dev/null and b/src/main/resources/images/logo_eXerH.png differ diff --git a/src/main/resources/images/logo_no_bg_eH.png b/src/main/resources/images/logo_no_bg_eH.png new file mode 100644 index 00000000000..e4f70ffe002 Binary files /dev/null and b/src/main/resources/images/logo_no_bg_eH.png differ diff --git a/src/main/resources/images/muscles.png b/src/main/resources/images/muscles.png new file mode 100644 index 00000000000..f6c8f3e4851 Binary files /dev/null and b/src/main/resources/images/muscles.png differ diff --git a/src/main/resources/images/stretching-exercises.png b/src/main/resources/images/stretching-exercises.png new file mode 100644 index 00000000000..86dc137f1d6 Binary files /dev/null and b/src/main/resources/images/stretching-exercises.png differ diff --git a/src/main/resources/view/BarChartPanel.fxml b/src/main/resources/view/BarChartPanel.fxml new file mode 100644 index 00000000000..90a54ae4e47 --- /dev/null +++ b/src/main/resources/view/BarChartPanel.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/Chart.css b/src/main/resources/view/Chart.css new file mode 100644 index 00000000000..a5f68f1fa02 --- /dev/null +++ b/src/main/resources/view/Chart.css @@ -0,0 +1,5 @@ +.axis { + -fx-stroke: black; + -fx-fill: black; + -fx-tick-label-fill: black; +} diff --git a/src/main/resources/view/CustomPropertiesWindow.fxml b/src/main/resources/view/CustomPropertiesWindow.fxml new file mode 100644 index 00000000000..f15c1d30d5b --- /dev/null +++ b/src/main/resources/view/CustomPropertiesWindow.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/CustomPropertyCard.fxml b/src/main/resources/view/CustomPropertyCard.fxml new file mode 100644 index 00000000000..db220e853b1 --- /dev/null +++ b/src/main/resources/view/CustomPropertyCard.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/CustomPropertyListPanel.fxml b/src/main/resources/view/CustomPropertyListPanel.fxml new file mode 100644 index 00000000000..968ca9232b6 --- /dev/null +++ b/src/main/resources/view/CustomPropertyListPanel.fxml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/view/CustomTheme.css b/src/main/resources/view/CustomTheme.css new file mode 100644 index 00000000000..6196795098c --- /dev/null +++ b/src/main/resources/view/CustomTheme.css @@ -0,0 +1,498 @@ +.background { + -fx-background-color: derive(#ffffff, 20%); + background-color: #9cd8eb; /* Used in the default.html file */ +} + +.label { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: #383838; + -fx-opacity: 0.75; +} + +.label-bright { + -fx-font-size: 11pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: white; + -fx-opacity: 1; +} + +.label-header { + -fx-font-size: 32pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 1; +} + +.text-field { + -fx-font-size: 12pt; + -fx-font-family: "Segoe UI Semibold"; +} + +.tab-pane { + -fx-padding: 0 0 0 1; +} + +.tab-pane .tab-header-area { + -fx-padding: 0 0 0 0; + -fx-min-height: 0; + -fx-max-height: 0; +} + +.table-view { + -fx-base: #1d1d1d; + -fx-control-inner-background: #1d1d1d; + -fx-background-color: #1d1d1d; + -fx-table-cell-border-color: transparent; + -fx-table-header-border-color: transparent; + -fx-padding: 5; +} + +.table-view .column-header-background { + -fx-background-color: transparent; +} + +.table-view .column-header, .table-view .filler { + -fx-size: 35; + -fx-border-width: 0 0 1 0; + -fx-background-color: transparent; + -fx-border-color: + transparent + transparent + derive(-fx-base, 80%) + transparent; + -fx-border-insets: 0 10 1 0; +} + +.table-view .column-header .label { + -fx-font-size: 20pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-alignment: center-left; + -fx-opacity: 1; +} + +.table-view:focused .table-row-cell:filled:focused:selected { + -fx-background-color: -fx-focus-color; +} + +.split-pane:horizontal .split-pane-divider { + -fx-background-color: derive(#1d1d1d, 20%); + -fx-border-color: transparent transparent transparent #bababa; +} + +.split-pane { + -fx-border-radius: 1; + -fx-border-width: 1; + -fx-background-color: derive(#1d1d1d, 20%); +} + +.list-view { + -fx-background-insets: 0; + -fx-padding: 0; + -fx-background-color: rgb(255, 255, 255); + -fx-background-radius: 10; + -fx-border-radius: 10; +} + +.list-cell { + -fx-border-radius: 10; + -fx-background-radius: 10; + -fx-label-padding: 0 0 0 0; + -fx-graphic-text-gap : 0; + -fx-padding: 0 0 0 0; + -fx-background-color: rgb(229, 229, 229); +} + +.list-cell:filled:even { + -fx-border-color: black black black black; +} + +.list-cell:filled:odd { + -fx-border-color: black black black black; +} + +.list-cell:filled:selected { + -fx-background-color: #f9f9f9; + -fx-border-color: #383838; +} + +.list-cell:filled:selected #cardPane { + -fx-border-color: #84ceeb; + -fx-border-width: 5; + -fx-effect: dropShadow(gaussian , #84ceeb, 10, 0.5,0,0 ); + +} + +.list-cell:filled:disabled #cardPane { + -fx-opacity: 0.3; +} + +.list-cell .label { + -fx-text-fill: #000000; +} + +.list-cell:empty { + -fx-opacity: 0; +} + +.cell_big_label { + -fx-font-family: "Segoe UI Semibold"; + -fx-font-size: 16px; + -fx-text-fill: #eb3fc4; +} + +.cell_small_label { + -fx-font-family: "Segoe UI"; + -fx-font-size: 16px; + -fx-text-fill: #383838; +} + +.stack-pane { + -fx-background-color: #ffffff; +} + +.pane-with-border { + -fx-background-color: rgb(249, 249, 249); + -fx-border-color: transparent transparent transparent #bababa; + -fx-border-top-width: 1px; +} + +.status-bar { + -fx-background-color: derive(#1d1d1d, 30%); +} + +.result-display { + -fx-background-color: #bababa; + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: black; +} + +.result-display .label { + -fx-text-fill: #bababa !important; +} + +.left-right-panel > TextArea { + -fx-border-color: derive(#1d1d1d, 70%); + -fx-border-radius: 5; + -fx-border-width: 1pt; +} + +.left-right-panel > TextArea:focused { + -fx-border-color: derive(#1d1d1d, 70%), derive(#3e7b91, 40%), derive(#3e7b91, 30%), derive(#3e7b91, 10%); + -fx-border-width: 1pt; + -fx-border-insets: 2, 3, 4, 5; +} + +.left-right-panel * { + -fx-background-color: transparent; + -fx-font-family: "Segoe UI Semilight"; + -fx-font-size: 13pt; + -fx-text-fill: black; +} + +.status-bar .label { + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-padding: 4px; + -fx-pref-height: 30px; +} + +.status-bar-with-border { + -fx-background-color: derive(#1d1d1d, 30%); + -fx-border-color: derive(#1d1d1d, 25%); + -fx-border-width: 1px; +} + +.status-bar-with-border .label { + -fx-text-fill: white; +} + +.grid-pane { + -fx-background-color: derive(#1d1d1d, 30%); + -fx-border-color: derive(#1d1d1d, 30%); + -fx-border-width: 1px; +} + +.grid-pane .stack-pane { + -fx-background-color: derive(#1d1d1d, 30%); +} + +.context-menu { + -fx-background-color: derive(#1d1d1d, 50%); +} + +.context-menu .label { + -fx-text-fill: white; +} + +.menu-bar { + -fx-background-color: derive(#1d1d1d, 20%); +} + +.menu-bar .label { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: white; + -fx-opacity: 0.9; +} + +.menu .left-container { + -fx-background-color: black; +} + +/* + * Metro style Push Button + * Author: Pedro Duque Vieira + * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ + */ +.button { + -fx-padding: 5 22 5 22; + -fx-border-color: #e2e2e2; + -fx-border-width: 2; + -fx-background-radius: 0; + -fx-background-color: #1d1d1d; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #d8d8d8; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + +.button:hover { + -fx-background-color: #3a3a3a; +} + +.button:pressed, .button:default:hover:pressed { + -fx-background-color: white; + -fx-text-fill: #1d1d1d; +} + +.button:focused { + -fx-border-color: white, white; + -fx-border-width: 1, 1; + -fx-border-style: solid, segments(1, 1); + -fx-border-radius: 0, 0; + -fx-border-insets: 1 1 1 1, 0; +} + +.button:disabled, .button:default:disabled { + -fx-opacity: 0.4; + -fx-background-color: #1d1d1d; + -fx-text-fill: white; +} + +.button:default { + -fx-background-color: -fx-focus-color; + -fx-text-fill: #ffffff; +} + +.button:default:hover { + -fx-background-color: derive(-fx-focus-color, 30%); +} + +.dialog-pane { + -fx-background-color: #1d1d1d; +} + +.dialog-pane > *.button-bar > *.container { + -fx-background-color: #1d1d1d; +} + +.dialog-pane > *.label.content { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: white; +} + +.dialog-pane:header *.header-panel { + -fx-background-color: derive(#1d1d1d, 25%); +} + +.dialog-pane:header *.header-panel *.label { + -fx-font-size: 18px; + -fx-font-style: italic; + -fx-fill: white; + -fx-text-fill: white; +} + +.scroll-bar { + -fx-background-color: derive(#bababa, 20%); + -fx-background-radius: 10; + -fx-border-radius: 10; +} + +.scroll-bar .thumb { + -fx-background-color: derive(#737373, 50%); + -fx-background-insets: 3 3 0 0; + -fx-padding: 2 2 2 2; +} + +.scroll-bar .increment-button, .scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-padding: 0 0 0 0; +} + +.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { + -fx-shape: " "; +} + +.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { + -fx-padding: 1 8 1 8; +} + +.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { + -fx-padding: 8 1 8 1; +} + +.commandBox { + -fx-background-color: linear-gradient(to top, #5680e9, #84ceeb, #5ab9ea); +} + +.resultDisplayBox { + -fx-background-color: linear-gradient(to bottom, #5680e9, #84ceeb, #5ab9ea); +} + +.tab-pane { + -fx-tab-max-height: 0; +} + +.tab-pane .tab { + -fx-background-color: derive(#eb3fc4, 20%); +} + +.tab-pane .tab-header-area .tab-header-background { + visibility: hidden; + -fx-background-color: transparent; + -fx-padding: -20 0 0 0; +} + +.resourceListPanelBox { + -fx-background-color: rgb(249, 249, 249); + -fx-border-color: #ffffff #bababa #ffffff #ffffff; + -fx-border-top-width: 0px; +} + +.logoBox { + -fx-background-color: linear-gradient(to bottom, #5680e9, #84ceeb, #5ab9ea); +} + +.chartBox { + -fx-border-color: #f9f9f9 #f9f9f9 #f9f9f9 transparent; +} + +.chartBoxBorder { + -fx-background-color: rgb(0, 253, 119); +} + +#cardPane { + -fx-background-color: transparent; + -fx-border-width: 0; +} + +#commandTypeLabel { + -fx-font-size: 11px; + -fx-text-fill: #F70D1A; +} + +#commandTextField { + -fx-background-color: #f9f9f9 #bababa transparent #bababa; + -fx-background-insets: 0; + -fx-border-color: #383838 #383838 #383838 #383838; + -fx-border-insets: 0; + -fx-border-width: 1; + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: black; +} + +#filterField, #personListPanel, #personWebpage { + -fx-effect: innershadow(gaussian, black, 10, 0, 0, 0); +} + +#resultDisplay { + -fx-background-color: #bababa; + -fx-border-color: #383838; + -fx-background-radius: 0; + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: black; +} + +#resultDisplay .content{ + -fx-background-color: transparent, #bababa, transparent, #bababa; + -fx-background-radius: 0; + -fx-text-fill: white; +} + +#tags { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#tags .label { + -fx-text-fill: white; + -fx-background-color: #3e7b91; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 10; +} + +#muscles { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#muscles .label { + -fx-text-fill: white; + -fx-background-color: #3e7b91; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 18; +} + +.infoPanel_name { + -fx-font-size: 28; + -fx-font-weight: bold; + -fx-text-fill: black; + -fx-wrap-text: true; + -fx-pref-width: 500; + -fx-pref-height: 10; +} + +.infoPanel_properties { + -fx-font-size: 24; + -fx-font-weight: bold; + -fx-text-fill: black; +} + +.infoPanel_customProperties_label { + -fx-font-size: 18; + -fx-font-weight: bold; + -fx-text-fill: black; +} + +.infoPanel_customProperties_listView { + -fx-border-color: #FFFFFF; + -fx-border-radius: 10; + -fx-background-radius: 10 +} + +.customProperties_cell { + -fx-font-size: 18; + -fx-font-weight: bold; + -fx-text-fill: black; +} + +.cardView { + -fx-border-radius: 10; + -fx-background-radius: 10; +} + +.tooltip { + -fx-font-size: 12; + -fx-font-family: "Segoe UI Semibold"; + -fx-wrap-text: true; +} diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..0e63329ede8 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -88,15 +88,19 @@ } .list-view { + -fx-border-radius: 10; + -fx-background-radius: 10; -fx-background-insets: 0; -fx-padding: 0; -fx-background-color: derive(#1d1d1d, 20%); } .list-cell { - -fx-label-padding: 0 0 0 0; + -fx-background-radius: 10; + -fx-border-radius: 10; + -fx-label-padding: 5 5 5 5; -fx-graphic-text-gap : 0; - -fx-padding: 0 0 0 0; + -fx-padding: 5 5 5 5; } .list-cell:filled:even { @@ -157,6 +161,25 @@ -fx-text-fill: black !important; } +.left-right-panel > TextArea { + -fx-border-color: derive(#1d1d1d, 70%); + -fx-border-radius: 5; + -fx-border-width: 1pt; +} + +.left-right-panel > TextArea:focused { + -fx-border-color: derive(#1d1d1d, 70%), derive(#3e7b91, 40%), derive(#3e7b91, 30%), derive(#3e7b91, 10%); + -fx-border-width: 1pt; + -fx-border-insets: 2, 3, 4, 5; +} + +.left-right-panel * { + -fx-background-color: transparent; + -fx-font-family: "Segoe UI Semilight"; + -fx-font-size: 13pt; + -fx-text-fill: white; +} + .status-bar .label { -fx-font-family: "Segoe UI Light"; -fx-text-fill: white; diff --git a/src/main/resources/view/ExerciseInfoPanel.fxml b/src/main/resources/view/ExerciseInfoPanel.fxml new file mode 100644 index 00000000000..32cf1222789 --- /dev/null +++ b/src/main/resources/view/ExerciseInfoPanel.fxml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExerciseListCard.fxml b/src/main/resources/view/ExerciseListCard.fxml new file mode 100644 index 00000000000..1593a10f8fb --- /dev/null +++ b/src/main/resources/view/ExerciseListCard.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExerciseListPanel.fxml b/src/main/resources/view/ExerciseListPanel.fxml new file mode 100644 index 00000000000..7962c9d1f91 --- /dev/null +++ b/src/main/resources/view/ExerciseListPanel.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index fa0fb54d9f4..082df19d4e8 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -9,31 +9,31 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/InfoDisplayPanel.fxml b/src/main/resources/view/InfoDisplayPanel.fxml new file mode 100644 index 00000000000..8144d29eb02 --- /dev/null +++ b/src/main/resources/view/InfoDisplayPanel.fxml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/view/LeftRightPanel.fxml b/src/main/resources/view/LeftRightPanel.fxml new file mode 100644 index 00000000000..428793912fe --- /dev/null +++ b/src/main/resources/view/LeftRightPanel.fxml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/main/resources/view/LineChartPanel.fxml b/src/main/resources/view/LineChartPanel.fxml new file mode 100644 index 00000000000..d819f6fbca5 --- /dev/null +++ b/src/main/resources/view/LineChartPanel.fxml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index a431648f6c0..0594746c9bf 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,58 +3,89 @@ - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
+ +
+ + + + +
+
+
+ + + + + + + + + + + +
+ + + + + +
+ + + + + + + + + +
+
+
+
diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml deleted file mode 100644 index 8836d323cc5..00000000000 --- a/src/main/resources/view/PersonListPanel.fxml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/view/PieChartPanel.fxml b/src/main/resources/view/PieChartPanel.fxml new file mode 100644 index 00000000000..232a118c242 --- /dev/null +++ b/src/main/resources/view/PieChartPanel.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/view/RegimeCard.fxml b/src/main/resources/view/RegimeCard.fxml new file mode 100644 index 00000000000..f30f870c25d --- /dev/null +++ b/src/main/resources/view/RegimeCard.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/RegimeInfoPanel.fxml b/src/main/resources/view/RegimeInfoPanel.fxml new file mode 100644 index 00000000000..5e56c4a00be --- /dev/null +++ b/src/main/resources/view/RegimeInfoPanel.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/RegimeListPanel.fxml b/src/main/resources/view/RegimeListPanel.fxml new file mode 100644 index 00000000000..5c6437771fe --- /dev/null +++ b/src/main/resources/view/RegimeListPanel.fxml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/src/main/resources/view/ResolveWindow.fxml b/src/main/resources/view/ResolveWindow.fxml new file mode 100644 index 00000000000..e849a3ac469 --- /dev/null +++ b/src/main/resources/view/ResolveWindow.fxml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..8f44fd233d3 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -4,6 +4,6 @@ -