diff --git a/.github/workflows/maven-publish-linux.yml b/.github/workflows/maven-publish-linux.yml new file mode 100644 index 0000000..62d94bc --- /dev/null +++ b/.github/workflows/maven-publish-linux.yml @@ -0,0 +1,35 @@ +name: Maven Package for Linux + +#on: +# release: +# types: [created] +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 23 + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'oracle' + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn install -s $GITHUB_WORKSPACE/settings.xml + env: + GITHUB_TOKEN: ${{ github.token }} + + # this is nasty! the publish fails as the pom already exists but we only really want to artefact published, so we try to ignore the error + # this is also why the full build is performed in the previous step as we do want that to fail if there are other errors in the build + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml || echo "Ignoring non-zero result" + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/maven-publish-windows.yml b/.github/workflows/maven-publish-windows.yml new file mode 100644 index 0000000..9e604a7 --- /dev/null +++ b/.github/workflows/maven-publish-windows.yml @@ -0,0 +1,34 @@ +name: Maven Package for Windows + +#on: +# release: +# types: [created] +on: [push] + +jobs: + build: + runs-on: windows-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 23 + uses: actions/setup-java@v4 + with: + java-version: '23' + distribution: 'oracle' + server-id: github # Value of the distributionManagement/repository/id field of the pom.xml + settings-path: ${{ github.workspace }} # location for the settings.xml file + + - name: Build with Maven + run: mvn install -s ${{ github.workspace }}\settings.xml --file pom.xml + + # this is nasty! the publish fails as the pom already exists but we only really want to artefact published, so we try to ignore the error + # this is also why the full build is performed in the previous step as we do want that to fail if there are other errors in the build + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy -s ${{ github.workspace }}\settings.xml --file pom.xml || echo "Ignoring non-zero result" & exit 0 + env: + GITHUB_TOKEN: ${{ github.token }} + diff --git a/.idea/compiler.xml b/.idea/compiler.xml index db77b62..6d4e20b 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -10,6 +10,7 @@ + diff --git a/.idea/encodings.xml b/.idea/encodings.xml index eed87f6..6b233fd 100644 --- a/.idea/encodings.xml +++ b/.idea/encodings.xml @@ -3,6 +3,8 @@ + + @@ -11,6 +13,7 @@ + diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml index 2ac1362..1572c3c 100644 --- a/.idea/jarRepositories.xml +++ b/.idea/jarRepositories.xml @@ -2,14 +2,9 @@ - - - - + + + + + + + + \ No newline at end of file diff --git a/PowerWorkoutSchedule.xls b/PowerWorkoutSchedule.xls new file mode 100644 index 0000000..86ebb24 Binary files /dev/null and b/PowerWorkoutSchedule.xls differ diff --git a/README.md b/README.md index 6fe718d..3553b62 100755 --- a/README.md +++ b/README.md @@ -6,11 +6,21 @@ A suite of tools for creating Garmin FIT files for workouts, schedules and cours [Routes](./ROUTES.md) ## Building -The application is a multi-module Maven project. It uses jlink and jpackage to produce installers for Windows and MacOS. +The application is a multi-module Maven project. It uses jlink and jpackage to produce installers for Windows, MacOS and Linux. The resulting installer files can be found in `app/target/installer` after the maven build has completed on the target platform. -Alternatively the installers are checked into GitHub here: https://github.com/jpickup/GarminTools/tree/master/installer +The main class is `app/src/main/java/com/johnpickup/app/javafx/AppLauncher.java` if you want to run this from an IDE. -NOTE: for Windows the most recent version is available in a ZIP file as I've been unable to get compatible versions of -the various tools required to build an MSI. Hopefully this will be fixed in the near future. +## Installers +The project now uses GitHub actions to build Windows and Linux installations. These can be found in the project's packages, +here: https://github.com/jpickup/GarminTools/packages/ +Click on `com.johnpickup.garmin.bundle` and search for "windows.zip", "linux.zip" or "macos.zip", download and unpack. +I need to figure out a better way to link to these. + +Previously the were installers are checked directly into GitHub but once the size exceeded the limit this was no longer +suitable. + +NOTE: +For Windows the most recent version is available in a ZIP file as I've been unable to get compatible versions of +the various tools required to build an MSI. diff --git a/WORKOUTS.md b/WORKOUTS.md index 5df99ed..147836c 100755 --- a/WORKOUTS.md +++ b/WORKOUTS.md @@ -28,16 +28,18 @@ Workouts are defined as a series of steps, where a step is either a period of ti that the step end when the lap button is pressed. For example 10 minutes or 400 metres. A step can also have a target such as a pace or a heart rate. Steps are written as text, for example: -| Workout | Description | -|--------------------|-------------------------------------------------------------------------------| -| `1mi` | A step of 1 mile | -| `400m` | A step of 400 metres | -| `Open` | An open step - ends when lap button pressed | +| Workout | Description | +|---------------------|-------------------------------------------------------------------------------| +| `1mi` | A step of 1 mile | +| `400m` | A step of 400 metres | +| `Open` | An open step - ends when lap button pressed | | `100m@4:00-5:00/km` | A step of 100 metres with a target pace between 4 and 5 minutes per kilometre | -| `1mi>6mph` | 1 mile at a pace faster than 6 miles per hour | +| `1mi>6mph` | 1 mile at a pace faster than 6 miles per hour | | `Open@8:00-9:00/mi` | An open step with a pace target | -| `800m@Z3` | 800 metres in heart rate Zone 3 | -| `400m@160-180bpm` | 400 metres wth a heart rate between 160 and 180 beats per minute | +| `800m@Z3` | 800 metres in heart rate Zone 3 | +| `400m@160-180bpm` | 400 metres wth a heart rate between 160 and 180 beats per minute | +| `30:00@PZ4` | 30 minutes at power zone 4 | +| '20km@300-400W` | 20km at a power between 300 and 400 watts | ### Sequences of Steps Steps can also be strung together with a `+` character, repeated using `*n` and grouped using brackets, for example: @@ -59,10 +61,10 @@ More examples can be found in `ExampleWorkoutSchedule.xls` ## Units ### Distance | Unit | Description | Example | -| ---- | ----------- |---------| -| m | Metre | 400m | -| km | Kilometre | 5km | -| mi | Mile | 26.2mi | +|------|-------------|---------| +| m | Metre | 400m | +| km | Kilometre | 5km | +| mi | Mile | 26.2mi | ### Intensity Valid values for the intensity are: @@ -97,6 +99,17 @@ Examples: | 120-130bpm | between 120 and 130 beats per minute | | Z3 | Zone 3 | +### Power ranges +These are similar to heart rate ranges where the units are watts (W) and the zones use a "PZ" prefix and a +number between 1 and 7. + +Examples: + +| Power range | Description | +|-------------|---------------------------| +| 300-400W | between 300 and 400 watts | +| PZ4 | Power zone 4 | + ## Excel workbook The app expects the input workbook to have specific named sheets and specific column headers within these. Any additional sheets or columns are ignored. @@ -107,32 +120,31 @@ sheets or columns are ignored. | Workout | Named workouts in the workout language described above | | Schedule | A schedule for workouts for specific dates | -| Sheet | Column | Description | -| ---- |-------------|-------------------------------------------------------------------------------------------| -| Pace | Name | The name for a pace | -| Pace | Value | The pace defined in workout language | -| Workout | Name | The name for a workout | -| Workout | Description | The workout language definition, may include named paces defined in the pace sheet | -| Workout | Sport | The sport that this workout is for (if not present running is the default) | -| Schedule | Date | The date for a specific workout | -| Schedule | Workout | Either defined in the workout language or the name a named workout from the Workout sheet | -| Schedule | Sport | The sport that this workout is for (if not present running is the default) | +| Sheet | Column | Description | +|----------|-------------|--------------------------------------------------------------------------------------------| +| Pace | Name | The name for a pace | +| Pace | Value | The pace defined in workout language | +| Workout | Name | The name for a workout | +| Workout | Description | The workout language definition, may include named paces defined in the pace sheet | +| Workout | Sport | The sport that this workout is for (if not present running is the default) | +| Schedule | Date | The date for a specific workout | +| Schedule | Workout | Either defined in the workout language or the name a named workout from the Workout sheet | +| Schedule | Sport | The sport that this workout is for (if not present running is the default) | ## Sport types The sport type case be specified as a value in the spreadsheet under the Sport heading. The values supported are: -| Sport | Description | -| ----- | ----------------------------- | -| Running | Running of any type | -| Road running | Road running | -| Trail running | Trail running | -| Cycling | Cycling of any type | -| Road cycling | Road cycling | -| MTB | Mountain biking | -| Swimming | Pool swimming | -| Open water | Open water swimming | +| Sport | Description | +|---------------|-------------------------------| +| Running | Running of any type | +| Road running | Road running | +| Trail running | Trail running | +| Cycling | Cycling of any type | +| Road cycling | Road cycling | +| MTB | Mountain biking | +| Swimming | Pool swimming | +| Open water | Open water swimming | ## ANTLR4 Grammar The grammar for the language is defined in `grammar/Workout.g4`, which is an ANTLR4 grammar that is used to generate Java code. - diff --git a/app/build_app_debian.sh b/app/build_app_debian.sh new file mode 100755 index 0000000..2028fd7 --- /dev/null +++ b/app/build_app_debian.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# ------ ENVIRONMENT -------------------------------------------------------- +# The script depends on various environment variables to exist in order to +# run properly. The java version we want to use, the location of the java +# binaries (java home), and the project version as defined inside the pom.xml +# file, e.g. 1.0-SNAPSHOT. +# +# PROJECT_VERSION: version used in pom.xml, e.g. 1.0-SNAPSHOT +# APP_VERSION: the application version, e.g. 1.0.0, shown in "about" dialog + +JAVA_VERSION=23 +MAIN_JAR="app-$PROJECT_VERSION.jar" + +# Set desired installer type: "dmg", "pkg", "deb", "app-image". +INSTALLER_TYPE=app-image + +echo "java home: $JAVA_HOME" +echo "project version: $PROJECT_VERSION" +echo "app version: $APP_VERSION" +echo "main JAR file: $MAIN_JAR" +echo "Java home : $JAVA_HOME" + +# ------ SETUP DIRECTORIES AND FILES ---------------------------------------- +# Remove previously generated java runtime and installers. Copy all required +# jar files into the input/libs folder. +rm -rfd ./target/java-runtime/ +rm -rfd target/installer/ + +mkdir -p target/installer/input/libs/ + +cp target/libs/* target/installer/input/libs/ +cp target/${MAIN_JAR} target/installer/input/libs/ + +# ------ REQUIRED MODULES --------------------------------------------------- +# Use jlink to detect all modules that are required to run the application. +# Starting point for the jdep analysis is the set of jars being used by the +# application. + +echo "detecting required modules" +$JAVA_HOME/bin/jdeps \ + -v \ + --multi-release ${JAVA_VERSION} \ + --ignore-missing-deps \ + --class-path "target/installer/input/libs/*" \ + target/classes/com/johnpickup/app/javafx/MainForm.class +detected_modules=`$JAVA_HOME/bin/jdeps \ + -q \ + --multi-release ${JAVA_VERSION} \ + --ignore-missing-deps \ + --print-module-deps \ + --class-path "target/installer/input/libs/*" \ + target/classes/com/johnpickup/app/javafx/MainForm.class` +echo "detected modules: ${detected_modules}" + + +# ------ MANUAL MODULES ----------------------------------------------------- +# jdk.crypto.ec has to be added manually bound via --bind-services or +# otherwise HTTPS does not work. +# +# See: https://bugs.openjdk.java.net/browse/JDK-8221674 +# +# In addition we need jdk.localedata if the application is localized. +# This can be reduced to the actually needed locales via a jlink parameter, +# e.g., --include-locales=en,de. +# +# Don't forget the leading ','! + +manual_modules=,jdk.crypto.ec,jdk.localedata +echo "manual modules: ${manual_modules}" + +# ------ RUNTIME IMAGE ------------------------------------------------------ +# Use the jlink tool to create a runtime image for our application. We are +# doing this in a separate step instead of letting jlink do the work as part +# of the jpackage tool. This approach allows for finer configuration and also +# works with dependencies that are not fully modularized, yet. +echo "creating java runtime image" +$JAVA_HOME/bin/jlink \ + --strip-native-commands \ + --no-header-files \ + --bind-services \ + --no-man-pages \ + --strip-debug \ + --add-modules "${detected_modules}${manual_modules}" \ + --include-locales=en,de \ + --output target/java-runtime + +# ------ PACKAGING ---------------------------------------------------------- +# In the end we will find the package inside the target/installer directory. +echo "Creating installer of type $INSTALLER_TYPE" +$JAVA_HOME/bin/jpackage \ +--type $INSTALLER_TYPE \ +--dest target/installer/ \ +--input target/installer/input/libs \ +--name garmintools \ +--main-class com.johnpickup.app.javafx.AppLauncher \ +--main-jar ${MAIN_JAR} \ +--java-options -Xmx2048m \ +--java-options '--sun-misc-unsafe-memory-access=allow' \ +--runtime-image target/java-runtime \ +--app-version ${APP_VERSION} \ +--vendor "John Pickup" \ +--copyright "Copyright © 2025 John Pickup" \ + +DEB_PKG_ROOT=target/deb/garmintools +rm -rf ${DEB_PKG_ROOT} +mkdir -p ${DEB_PKG_ROOT}/opt +mkdir -p ${DEB_PKG_ROOT}/usr/bin +mkdir -p ${DEB_PKG_ROOT}/DEBIAN +cp src/main/resources/control ${DEB_PKG_ROOT}/DEBIAN/ +mv target/installer/garmintools ${DEB_PKG_ROOT}/opt/ +cd ${DEB_PKG_ROOT}/usr/bin || exit +ln -s ../../opt/garmintools/bin/garmintools garmintools +cd - || exit +cd target/deb || exit +dpkg-deb -b garmintools +cd - || exit +cp target/deb/garmintools*deb ../installer \ No newline at end of file diff --git a/app/build_app_linux.sh b/app/build_app_linux.sh new file mode 100755 index 0000000..8d74cb0 --- /dev/null +++ b/app/build_app_linux.sh @@ -0,0 +1,121 @@ +#!/bin/bash + +# ------ ENVIRONMENT -------------------------------------------------------- +# The script depends on various environment variables to exist in order to +# run properly. The java version we want to use, the location of the java +# binaries (java home), and the project version as defined inside the pom.xml +# file, e.g. 1.0-SNAPSHOT. +# +# PROJECT_VERSION: version used in pom.xml, e.g. 1.0-SNAPSHOT +# APP_VERSION: the application version, e.g. 1.0.0, shown in "about" dialog + +JAVA_VERSION=23 +MAIN_JAR="app-$PROJECT_VERSION.jar" + +# Set desired installer type: "dmg", "pkg", "deb", "app-image". +INSTALLER_TYPE=app-image + +echo "java home: $JAVA_HOME" +echo "project version: $PROJECT_VERSION" +echo "app version: $APP_VERSION" +echo "main JAR file: $MAIN_JAR" +echo "Java home : $JAVA_HOME" + +# ------ SETUP DIRECTORIES AND FILES ---------------------------------------- +# Remove previously generated java runtime and installers. Copy all required +# jar files into the input/libs folder. +rm -rfd ./target/java-runtime/ +rm -rfd target/installer/ + +mkdir -p target/installer/input/libs/ + +cp target/libs/* target/installer/input/libs/ +cp target/${MAIN_JAR} target/installer/input/libs/ + +# ------ REQUIRED MODULES --------------------------------------------------- +# Use jlink to detect all modules that are required to run the application. +# Starting point for the jdep analysis is the set of jars being used by the +# application. + +echo "detecting required modules" +$JAVA_HOME/bin/jdeps \ + -v \ + --multi-release ${JAVA_VERSION} \ + --ignore-missing-deps \ + --class-path "target/installer/input/libs/*" \ + target/classes/com/johnpickup/app/javafx/MainForm.class +detected_modules=`$JAVA_HOME/bin/jdeps \ + -q \ + --multi-release ${JAVA_VERSION} \ + --ignore-missing-deps \ + --print-module-deps \ + --class-path "target/installer/input/libs/*" \ + target/classes/com/johnpickup/app/javafx/MainForm.class` +echo "detected modules: ${detected_modules}" + + +# ------ MANUAL MODULES ----------------------------------------------------- +# jdk.crypto.ec has to be added manually bound via --bind-services or +# otherwise HTTPS does not work. +# +# See: https://bugs.openjdk.java.net/browse/JDK-8221674 +# +# In addition we need jdk.localedata if the application is localized. +# This can be reduced to the actually needed locales via a jlink parameter, +# e.g., --include-locales=en,de. +# +# Don't forget the leading ','! + +manual_modules=,jdk.crypto.ec,jdk.localedata +echo "manual modules: ${manual_modules}" + +# ------ RUNTIME IMAGE ------------------------------------------------------ +# Use the jlink tool to create a runtime image for our application. We are +# doing this in a separate step instead of letting jlink do the work as part +# of the jpackage tool. This approach allows for finer configuration and also +# works with dependencies that are not fully modularized, yet. +echo "creating java runtime image" +$JAVA_HOME/bin/jlink \ + --strip-native-commands \ + --no-header-files \ + --bind-services \ + --no-man-pages \ + --strip-debug \ + --add-modules "${detected_modules}${manual_modules}" \ + --include-locales=en,de \ + --output target/java-runtime + +# ------ PACKAGING ---------------------------------------------------------- +# In the end we will find the package inside the target/installer directory. +echo "Creating installer of type $INSTALLER_TYPE" +$JAVA_HOME/bin/jpackage \ +--type $INSTALLER_TYPE \ +--dest target/installer/ \ +--input target/installer/input/libs \ +--name garmintools \ +--main-class com.johnpickup.app.javafx.AppLauncher \ +--main-jar ${MAIN_JAR} \ +--java-options -Xmx2048m \ +--java-options '--sun-misc-unsafe-memory-access=allow' \ +--runtime-image target/java-runtime \ +--app-version ${APP_VERSION} \ +--vendor "John Pickup" \ +--copyright "Copyright © 2025 John Pickup" \ + +cd target || exit +curl -L https://github.com/AppImage/AppImageKit/releases/download/10/appimagetool-x86_64.AppImage -o appimagetool.AppImage +chmod +x appimagetool.AppImage +./appimagetool.AppImage --appimage-extract +export PATH=./squashfs-root/usr/bin/:${PATH} + +APP_DIR=GarminTools.AppDir/ +mkdir -p ${APP_DIR} +cp ../src/main/resources/GarminTools.desktop ${APP_DIR} +cp ../src/main/resources/garmintools.png ${APP_DIR} +cp -r installer/garmintools/* ${APP_DIR} +cd ${APP_DIR} || exit +ln -s bin/garmintools AppRun +cd .. || exit +appimagetool ${APP_DIR} garmintools-x86_64.AppImage +cp garmintools-x86_64.AppImage ../installer/ +cp garmintools-x86_64.AppImage ../../installer/ \ No newline at end of file diff --git a/app/build_app_mac.sh b/app/build_app_mac.sh index 1c55090..f793c8d 100755 --- a/app/build_app_mac.sh +++ b/app/build_app_mac.sh @@ -106,4 +106,7 @@ $JAVA_HOME/bin/jpackage \ --vendor "John Pickup" \ --copyright "Copyright © 2023 John Pickup" \ --mac-package-identifier com.johnpickup.garmintools.app \ ---mac-package-name ACME \ No newline at end of file +--mac-package-name GarminTools + +cp target/installer/GarminTools*.pkg ../installer/ + diff --git a/app/build_app_windows.bat b/app/build_app_windows.bat index 8889e09..e777800 100644 --- a/app/build_app_windows.bat +++ b/app/build_app_windows.bat @@ -97,3 +97,11 @@ rem --win-dir-chooser ^ rem --win-shortcut ^ rem --win-per-user-install ^ rem --win-menu +echo Current dir +cd +echo Listing output +dir target\installer\ +echo Copying installer output +xcopy /S /Y /F target\installer\* ..\installer\ +echo Listing of copied files +dir ..\installer\ \ No newline at end of file diff --git a/app/pom.xml b/app/pom.xml index 8c15e9f..cc82b04 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -6,14 +6,16 @@ com.johnpickup.garmin GarminTools - 1.0-SNAPSHOT + 1.1 app + true 1.0.0 1.6.0 + 3.7.1 24.0.1 2.18.2 2.14.0 @@ -217,15 +219,51 @@ - - build-mac + unix + + + unix + + + + + + exec-maven-plugin + org.codehaus.mojo + ${exec.maven.plugin.version} + + + Build Native Linux App + install + + exec + + + + + ${project.basedir} + ./build_app_linux.sh + + + ${client.version} + + + ${project.version} + + + + + + + + + build-mac mac - @@ -260,11 +298,9 @@ build-windows - windows - @@ -296,13 +332,5 @@ - - - - - local-repo - file://${basedir}/../local-repo - - \ No newline at end of file diff --git a/app/src/main/java/com/johnpickup/app/GarminScheduleGenerator.java b/app/src/main/java/com/johnpickup/app/GarminScheduleGenerator.java index b2cc1d5..8ffb608 100755 --- a/app/src/main/java/com/johnpickup/app/GarminScheduleGenerator.java +++ b/app/src/main/java/com/johnpickup/app/GarminScheduleGenerator.java @@ -52,7 +52,7 @@ public void generate(File inputFile, File outputDir) throws IOException { workoutSaver.save(converter.getTrainingSchedule(), scheduleFile); log.info("Saved workout schedule as {}", scheduleFile.getPath()); } - catch (RuntimeException e) { + catch (Exception e) { log.error("Error converting {}", inputFile.getPath()); log.error(e.getMessage()); } diff --git a/app/src/main/java/com/johnpickup/app/converter/CustomPowerConverter.java b/app/src/main/java/com/johnpickup/app/converter/CustomPowerConverter.java new file mode 100755 index 0000000..81faeb1 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/CustomPowerConverter.java @@ -0,0 +1,24 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.garmin.common.unit.CustomPowerTarget; +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.common.unit.PowerUnit; +import com.johnpickup.garmin.parser.Power; +import com.johnpickup.garmin.parser.PowerRange; + +public class CustomPowerConverter implements PowerConverter { + @Override + public PowerTarget convert(Power power) { + PowerRange powerRange = (PowerRange) power; + PowerUnit unit; + switch (powerRange.getUnit()) { + case WATTS: + unit = PowerUnit.WATTS; + break; + default: + throw new RuntimeException("Unknown power unit: " + powerRange.getUnit()); + } + + return new CustomPowerTarget(powerRange.getMinimum(), powerRange. getMaximum(), unit); + } +} diff --git a/app/src/main/java/com/johnpickup/app/converter/DistancePowerStepConverter.java b/app/src/main/java/com/johnpickup/app/converter/DistancePowerStepConverter.java new file mode 100755 index 0000000..cc4783c --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/DistancePowerStepConverter.java @@ -0,0 +1,28 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.app.garmin.workout.DistancePowerWorkoutStep; +import com.johnpickup.app.garmin.workout.WorkoutStep; +import com.johnpickup.garmin.common.unit.Distance; +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.parser.DistancePowerStep; +import com.johnpickup.garmin.parser.Step; + +/** + * Convert independent pace steps into the Garmin equivalent + */ +public class DistancePowerStepConverter implements StepConverter { + @Override + public WorkoutStep convert(Step step) { + DistancePowerStep distancePowerStep = (DistancePowerStep)step; + + Distance d = new Distance( + distancePowerStep.getDistance().getQuantity(), + DiatanceUnitConverter.convert(distancePowerStep.getDistance().getUnit())); + + PowerTarget powerTarget = PowerConverterFactory.getInstance() + .getPowerConverter(distancePowerStep.getPower()) + .convert(distancePowerStep.getPower()); + + return new DistancePowerWorkoutStep(StepIntensityConverter.convert(step.getStepIntensity()), d, powerTarget); + } +} diff --git a/app/src/main/java/com/johnpickup/app/converter/OpenPowerStepConverter.java b/app/src/main/java/com/johnpickup/app/converter/OpenPowerStepConverter.java new file mode 100755 index 0000000..0dc6056 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/OpenPowerStepConverter.java @@ -0,0 +1,23 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.app.garmin.workout.OpenPowerWorkoutStep; +import com.johnpickup.app.garmin.workout.WorkoutStep; +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.parser.OpenPowerStep; +import com.johnpickup.garmin.parser.Step; + +/** + * Convert independent pace steps into the Garmin equivalent + */ +public class OpenPowerStepConverter implements StepConverter { + @Override + public WorkoutStep convert(Step step) { + OpenPowerStep openPowerStep = (OpenPowerStep)step; + + PowerTarget powerTarget = PowerConverterFactory.getInstance() + .getPowerConverter(openPowerStep.getPower()) + .convert(openPowerStep.getPower()); + + return new OpenPowerWorkoutStep(StepIntensityConverter.convert(step.getStepIntensity()), powerTarget); + } +} diff --git a/app/src/main/java/com/johnpickup/app/converter/PowerConverter.java b/app/src/main/java/com/johnpickup/app/converter/PowerConverter.java new file mode 100755 index 0000000..4c27e00 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/PowerConverter.java @@ -0,0 +1,13 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.parser.Power; + +/** + * Interface that pace converters must implement. + * One converter will be implemented for each sub-type of Pace and will emit a corresponding + * instance of a Garmin PaceTarget + */ +public interface PowerConverter { + PowerTarget convert(Power power); +} diff --git a/app/src/main/java/com/johnpickup/app/converter/PowerConverterFactory.java b/app/src/main/java/com/johnpickup/app/converter/PowerConverterFactory.java new file mode 100755 index 0000000..0b1cae0 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/PowerConverterFactory.java @@ -0,0 +1,36 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.garmin.parser.Power; +import com.johnpickup.garmin.parser.PowerRange; +import com.johnpickup.garmin.parser.PowerZone; + +import java.util.HashMap; +import java.util.Map; + +/** + * Factory that returns the correct converter instance based on the type of heart rate target object passed in + */ +public class PowerConverterFactory { + private static PowerConverterFactory instance; + private final Map converters = new HashMap<>(); + + private PowerConverterFactory() { + register(new ZonePowerConverter(), PowerZone.class); + register(new CustomPowerConverter(), PowerRange.class); + } + + public void register(PowerConverter converter, Class aClass) { + converters.put(aClass, converter); + } + + public static PowerConverterFactory getInstance() { + if (instance == null) { + instance = new PowerConverterFactory(); + } + return instance; + } + + public PowerConverter getPowerConverter(Power power) { + return converters.get(power.getClass()); + } +} diff --git a/app/src/main/java/com/johnpickup/app/converter/StepConverterFactory.java b/app/src/main/java/com/johnpickup/app/converter/StepConverterFactory.java index 41280bb..a35d9e6 100755 --- a/app/src/main/java/com/johnpickup/app/converter/StepConverterFactory.java +++ b/app/src/main/java/com/johnpickup/app/converter/StepConverterFactory.java @@ -10,18 +10,21 @@ */ public class StepConverterFactory { private static StepConverterFactory instance; - private Map converters = new HashMap<>(); + private final Map converters = new HashMap<>(); private StepConverterFactory() { register(new DistanceStepConverter(), DistanceStep.class); register(new DistancePaceStepConverter(), DistancePaceStep.class); register(new DistanceHeartRateStepConverter(), DistanceHeartRateStep.class); + register(new DistancePowerStepConverter(), DistancePowerStep.class); register(new TimeStepConverter(), TimeStep.class); register(new TimePaceStepConverter(), TimePaceStep.class); register(new TimeHeartRateStepConverter(), TimeHeartRateStep.class); + register(new TimePowerStepConverter(), TimePowerStep.class); register(new OpenStepConverter(), OpenStep.class); register(new OpenPaceStepConverter(), OpenPaceStep.class); register(new OpenHeartRateStepConverter(), OpenHeartRateStep.class); + register(new OpenPowerStepConverter(), OpenPowerStep.class); register(new RepeatingStepsConverter(), RepeatingSteps.class); } diff --git a/app/src/main/java/com/johnpickup/app/converter/TimePowerStepConverter.java b/app/src/main/java/com/johnpickup/app/converter/TimePowerStepConverter.java new file mode 100755 index 0000000..63fb141 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/TimePowerStepConverter.java @@ -0,0 +1,26 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.app.garmin.workout.TimePowerWorkoutStep; +import com.johnpickup.app.garmin.workout.WorkoutStep; +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.common.unit.Time; +import com.johnpickup.garmin.parser.Step; +import com.johnpickup.garmin.parser.TimePowerStep; + +/** + * Convert independent pace steps into the Garmin equivalent + */ +public class TimePowerStepConverter implements StepConverter { + @Override + public WorkoutStep convert(Step step) { + TimePowerStep timePowerStep = (TimePowerStep)step; + + Time t = new Time(timePowerStep.getTime().asDouble() * 60); + + PowerTarget powerTarget = PowerConverterFactory.getInstance() + .getPowerConverter(timePowerStep.getPower()) + .convert(timePowerStep.getPower()); + + return new TimePowerWorkoutStep(StepIntensityConverter.convert(step.getStepIntensity()), t, powerTarget); + } +} diff --git a/app/src/main/java/com/johnpickup/app/converter/WorkoutConverter.java b/app/src/main/java/com/johnpickup/app/converter/WorkoutConverter.java index f3937f5..6ff7914 100755 --- a/app/src/main/java/com/johnpickup/app/converter/WorkoutConverter.java +++ b/app/src/main/java/com/johnpickup/app/converter/WorkoutConverter.java @@ -27,6 +27,9 @@ public class WorkoutConverter { sportMap.put(com.johnpickup.garmin.parser.Sport.SWIMMING, Sport.SWIMMING); sportMap.put(com.johnpickup.garmin.parser.Sport.POOL_SWIMMING, Sport.SWIMMING); sportMap.put(com.johnpickup.garmin.parser.Sport.OPEN_WATER_SWIMMING, Sport.SWIMMING); + sportMap.put(com.johnpickup.garmin.parser.Sport.CARDIO, Sport.TRAINING); + sportMap.put(com.johnpickup.garmin.parser.Sport.STRENGTH, Sport.TRAINING); + sportMap.put(com.johnpickup.garmin.parser.Sport.HIIT, Sport.HIIT); } private static final Map subSportMap = new HashMap<>(); @@ -38,6 +41,9 @@ public class WorkoutConverter { subSportMap.put(com.johnpickup.garmin.parser.Sport.SWIMMING, SubSport.LAP_SWIMMING); subSportMap.put(com.johnpickup.garmin.parser.Sport.POOL_SWIMMING, SubSport.LAP_SWIMMING); subSportMap.put(com.johnpickup.garmin.parser.Sport.OPEN_WATER_SWIMMING, SubSport.OPEN_WATER); + subSportMap.put(com.johnpickup.garmin.parser.Sport.CARDIO, SubSport.CARDIO_TRAINING); + subSportMap.put(com.johnpickup.garmin.parser.Sport.STRENGTH, SubSport.STRENGTH_TRAINING); + subSportMap.put(com.johnpickup.garmin.parser.Sport.HIIT, SubSport.HIIT); } public com.johnpickup.app.garmin.workout.Workout convert(Workout workout) { diff --git a/app/src/main/java/com/johnpickup/app/converter/WorkoutScheduleConverter.java b/app/src/main/java/com/johnpickup/app/converter/WorkoutScheduleConverter.java index 8cad0f9..a564b65 100755 --- a/app/src/main/java/com/johnpickup/app/converter/WorkoutScheduleConverter.java +++ b/app/src/main/java/com/johnpickup/app/converter/WorkoutScheduleConverter.java @@ -13,6 +13,7 @@ * Created by john on 12/01/2017. */ public class WorkoutScheduleConverter { + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(WorkoutScheduleConverter.class); private final Map namedPaces = new HashMap<>(); private final List garminWorkouts = new ArrayList<>(); private final TrainingSchedule trainingSchedule = new TrainingSchedule(); @@ -25,27 +26,37 @@ public WorkoutScheduleConverter() { public void convert(WorkoutSchedule workoutSchedule) { init(); + log.debug("Converting paces"); for (Map.Entry namedPace : workoutSchedule.getPaces().entrySet()) { Pace pace = namedPace.getValue(); + log.debug("Pace: {}", pace); namedPaces.put(namedPace.getKey(), PaceConverterFactory.getInstance().getPaceConverter(pace).convert(pace)); } + log.debug("Done converting paces"); WorkoutConverter workoutConverter = new WorkoutConverter(); + log.debug("Converting workouts"); for (Map.Entry workoutEntry : workoutSchedule.getWorkouts().entrySet()) { + log.debug("Converting {} ", workoutEntry); com.johnpickup.app.garmin.workout.Workout garminWorkout = workoutConverter.convert(workoutEntry.getValue()); garminWorkout.setName(workoutEntry.getKey()); + log.debug("Workout: {}", garminWorkout); garminWorkouts.add(garminWorkout); workoutMap.put(workoutEntry.getValue(), garminWorkout); } + log.debug("Done converting workouts"); + log.debug("Converting schedules"); for (ScheduledWorkout scheduledWorkout: workoutSchedule.getSchedule()) { Workout workout = scheduledWorkout.getWorkout(); com.johnpickup.app.garmin.workout.Workout garminWorkout = workoutMap.get(workout); com.johnpickup.app.garmin.schedule.ScheduledWorkout garminScheduledWorkout = new com.johnpickup.app.garmin.schedule.ScheduledWorkout(garminWorkout, scheduledWorkout.getDate()); + log.debug("Workout schedule: {}", garminScheduledWorkout); trainingSchedule.addScheduledWorkout(garminScheduledWorkout); } + log.debug("Done converting schedules"); } diff --git a/app/src/main/java/com/johnpickup/app/converter/ZonePowerConverter.java b/app/src/main/java/com/johnpickup/app/converter/ZonePowerConverter.java new file mode 100755 index 0000000..df245e3 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/converter/ZonePowerConverter.java @@ -0,0 +1,14 @@ +package com.johnpickup.app.converter; + +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.common.unit.ZonePowerTarget; +import com.johnpickup.garmin.parser.Power; +import com.johnpickup.garmin.parser.PowerZone; + +public class ZonePowerConverter implements PowerConverter { + @Override + public PowerTarget convert(Power power) { + PowerZone powerZone = (PowerZone) power; + return new ZonePowerTarget(powerZone.getZoneNumber()); + } +} diff --git a/app/src/main/java/com/johnpickup/app/excel/ExcelUtils.java b/app/src/main/java/com/johnpickup/app/excel/ExcelUtils.java index b6d2d3f..f8658c3 100644 --- a/app/src/main/java/com/johnpickup/app/excel/ExcelUtils.java +++ b/app/src/main/java/com/johnpickup/app/excel/ExcelUtils.java @@ -29,6 +29,9 @@ public static Sport readSportValue(Row row, Integer index) { case "SWIMMING", "SWIM" -> Sport.SWIMMING; case "POOL SWIMMING", "POOL" -> Sport.POOL_SWIMMING; case "OPEN WATER SWIMMING", "OPEN WATER" -> Sport.OPEN_WATER_SWIMMING; + case "CARDIO" -> Sport.CARDIO; + case "STRENGTH" -> Sport.STRENGTH; + case "HIIT" -> Sport.HIIT; default -> Sport.RUNNING; }; } diff --git a/app/src/main/java/com/johnpickup/app/excel/ScheduleSheetReader.java b/app/src/main/java/com/johnpickup/app/excel/ScheduleSheetReader.java index d360ed9..0f204db 100755 --- a/app/src/main/java/com/johnpickup/app/excel/ScheduleSheetReader.java +++ b/app/src/main/java/com/johnpickup/app/excel/ScheduleSheetReader.java @@ -44,7 +44,7 @@ public List readSchedule(Sheet sheet, Map wor private ScheduledWorkout readScheduledWorkout(Row row, Map workouts) { Cell dateCell = row.getCell(dateIndex); Cell workoutCell = row.getCell(workoutIndex); - if (dateCell != null && workoutCell != null) { + if (dateCell != null && workoutCell != null && dateCell.getDateCellValue() != null) { Date date = dateCell.getDateCellValue(); LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); String value = workoutCell.getStringCellValue(); diff --git a/app/src/main/java/com/johnpickup/app/garmin/schedule/ScheduledWorkout.java b/app/src/main/java/com/johnpickup/app/garmin/schedule/ScheduledWorkout.java index b524fb0..41834f4 100755 --- a/app/src/main/java/com/johnpickup/app/garmin/schedule/ScheduledWorkout.java +++ b/app/src/main/java/com/johnpickup/app/garmin/schedule/ScheduledWorkout.java @@ -35,4 +35,12 @@ public Workout getWorkout() { public LocalDate getDate() { return this.date; } + + @Override + public String toString() { + return "ScheduledWorkout{" + + "workout=" + workout + + ", date=" + date + + '}'; + } } diff --git a/app/src/main/java/com/johnpickup/app/garmin/workout/DistancePowerWorkoutStep.java b/app/src/main/java/com/johnpickup/app/garmin/workout/DistancePowerWorkoutStep.java new file mode 100755 index 0000000..d771a3c --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/garmin/workout/DistancePowerWorkoutStep.java @@ -0,0 +1,65 @@ +package com.johnpickup.app.garmin.workout; + +import com.garmin.fit.Intensity; +import com.garmin.fit.WktStepDuration; +import com.garmin.fit.WktStepTarget; +import com.garmin.fit.WorkoutStepMesg; +import com.johnpickup.garmin.common.unit.Distance; +import com.johnpickup.garmin.common.unit.PowerTarget; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Simple workout that lasts a specific distance with a heart rate target + */ +public class DistancePowerWorkoutStep extends WorkoutStep { + private final Distance distance; + private final PowerTarget powerTarget; + + public DistancePowerWorkoutStep(Intensity intensity, Distance distance, PowerTarget powerTarget) { + super(intensity); + this.distance = distance; + this.powerTarget = powerTarget; + } + + @Override + public String getName() { + return distance.toString() + " " + powerTarget.toString(); + } + + @Override + public List generateWorkoutSteps() { + WorkoutStepMesg step = new WorkoutStepMesg(); + step.setIntensity(intensity); + step.setDurationType(WktStepDuration.DISTANCE); + step.setDurationDistance(distance.toGarminDistance()); + step.setTargetType(WktStepTarget.POWER); + step.setTargetValue(powerTarget.getTargetValue()); + step.setMessageIndex(generateWorkoutStepIndex()); + step.setCustomTargetValueLow(powerTarget.getGarminLow()); + step.setCustomTargetValueHigh(powerTarget.getGarminHigh()); + step.setNotes(nameWithIntensity()); + + return Collections.singletonList(step); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DistancePowerWorkoutStep that = (DistancePowerWorkoutStep) o; + return Objects.equals(distance, that.distance) && Objects.equals(powerTarget, that.powerTarget); + } + + @Override + public int hashCode() { + return Objects.hash(distance, powerTarget); + } + + protected boolean canEqual(final Object other) { + return other instanceof DistancePowerWorkoutStep; + } + +} diff --git a/app/src/main/java/com/johnpickup/app/garmin/workout/OpenPowerWorkoutStep.java b/app/src/main/java/com/johnpickup/app/garmin/workout/OpenPowerWorkoutStep.java new file mode 100755 index 0000000..2d477af --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/garmin/workout/OpenPowerWorkoutStep.java @@ -0,0 +1,61 @@ +package com.johnpickup.app.garmin.workout; + +import com.garmin.fit.Intensity; +import com.garmin.fit.WktStepDuration; +import com.garmin.fit.WktStepTarget; +import com.garmin.fit.WorkoutStepMesg; +import com.johnpickup.garmin.common.unit.PowerTarget; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Simple workout step that's open with a heart rate target + */ +public class OpenPowerWorkoutStep extends WorkoutStep { + private final PowerTarget powerTarget; + + public OpenPowerWorkoutStep(Intensity intensity, PowerTarget powerTarget) { + super(intensity); + this.powerTarget = powerTarget; + } + + @Override + public String getName() { + return "Open " + powerTarget.toString(); + } + + @Override + public List generateWorkoutSteps() { + WorkoutStepMesg step = new WorkoutStepMesg(); + step.setIntensity(intensity); + step.setDurationType(WktStepDuration.OPEN); + step.setTargetType(WktStepTarget.POWER); + step.setTargetValue(powerTarget.getTargetValue()); + step.setMessageIndex(generateWorkoutStepIndex()); + step.setCustomTargetValueLow(powerTarget.getGarminLow()); + step.setCustomTargetValueHigh(powerTarget.getGarminHigh()); + step.setNotes(nameWithIntensity()); + + return Collections.singletonList(step); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OpenPowerWorkoutStep that = (OpenPowerWorkoutStep) o; + return Objects.equals(powerTarget, that.powerTarget); + } + + @Override + public int hashCode() { + return Objects.hash(powerTarget); + } + + protected boolean canEqual(final Object other) { + return other instanceof OpenPowerWorkoutStep; + } + +} diff --git a/app/src/main/java/com/johnpickup/app/garmin/workout/TimePowerWorkoutStep.java b/app/src/main/java/com/johnpickup/app/garmin/workout/TimePowerWorkoutStep.java new file mode 100755 index 0000000..b4ef161 --- /dev/null +++ b/app/src/main/java/com/johnpickup/app/garmin/workout/TimePowerWorkoutStep.java @@ -0,0 +1,64 @@ +package com.johnpickup.app.garmin.workout; + +import com.garmin.fit.Intensity; +import com.garmin.fit.WktStepDuration; +import com.garmin.fit.WktStepTarget; +import com.garmin.fit.WorkoutStepMesg; +import com.johnpickup.garmin.common.unit.PowerTarget; +import com.johnpickup.garmin.common.unit.Time; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Simple workout that lasts a specific distance with a pace target + */ +public class TimePowerWorkoutStep extends WorkoutStep { + private final Time time; + private final PowerTarget powerTarget; + + public TimePowerWorkoutStep(Intensity intensity, Time time, PowerTarget powerTarget) { + super(intensity); + this.time = time; + this.powerTarget = powerTarget; + } + + @Override + public String getName() { + return time.toString() + " " + powerTarget.toString(); + } + + @Override + public List generateWorkoutSteps() { + WorkoutStepMesg step = new WorkoutStepMesg(); + step.setIntensity(intensity); + step.setDurationType(WktStepDuration.TIME); + step.setDurationDistance(time.toGarminTime()); + step.setTargetType(WktStepTarget.POWER); + step.setTargetValue(powerTarget.getTargetValue()); + step.setMessageIndex(generateWorkoutStepIndex()); + step.setCustomTargetValueLow(powerTarget.getGarminLow()); + step.setCustomTargetValueHigh(powerTarget.getGarminHigh()); + step.setNotes(nameWithIntensity()); + return Collections.singletonList(step); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimePowerWorkoutStep that = (TimePowerWorkoutStep) o; + return Objects.equals(time, that.time) && Objects.equals(powerTarget, that.powerTarget); + } + + @Override + public int hashCode() { + return Objects.hash(time, powerTarget); + } + + protected boolean canEqual(final Object other) { + return other instanceof TimePowerWorkoutStep; + } + +} diff --git a/app/src/main/java/com/johnpickup/app/javafx/AppLauncher.java b/app/src/main/java/com/johnpickup/app/javafx/AppLauncher.java index 4314a2c..2d1e0ab 100644 --- a/app/src/main/java/com/johnpickup/app/javafx/AppLauncher.java +++ b/app/src/main/java/com/johnpickup/app/javafx/AppLauncher.java @@ -1,7 +1,13 @@ package com.johnpickup.app.javafx; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class AppLauncher { + private static final Logger log = LoggerFactory.getLogger(AppLauncher.class); + public static void main(String[] args) { + Thread.setDefaultUncaughtExceptionHandler((t, e) -> log.error("Unhandled error", e)); MainForm.main(args); } } diff --git a/app/src/main/resources/GarminTools.desktop b/app/src/main/resources/GarminTools.desktop new file mode 100644 index 0000000..9814ac5 --- /dev/null +++ b/app/src/main/resources/GarminTools.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=garmintools +Exec=bin/garmintools +Comment=A suite of tools for Garmin watches +Icon=garmintools +Terminal=false +Categories=Utility; diff --git a/app/src/main/resources/control b/app/src/main/resources/control new file mode 100644 index 0000000..3d02b04 --- /dev/null +++ b/app/src/main/resources/control @@ -0,0 +1,8 @@ +Package: garmintools +Architecture: all +Version: 1.0.0 +Section: misc +Maintainer: John Pickup +Priority: optional +Standards-Version: 4.7.0 +Description: Garmin Tools diff --git a/app/src/main/resources/garmintools.png b/app/src/main/resources/garmintools.png new file mode 100755 index 0000000..e598efc Binary files /dev/null and b/app/src/main/resources/garmintools.png differ diff --git a/bundle/pom.xml b/bundle/pom.xml new file mode 100644 index 0000000..87e6dda --- /dev/null +++ b/bundle/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + com.johnpickup.garmin + GarminTools + 1.1 + + + bundle + + + false + 1.6.0 + 3.7.1 + 1.0.0 + + + + + ${project.groupId} + app + ${project.version} + + + + + + unix + + + unix + + + + + + maven-assembly-plugin + ${maven.assembly.plugin.version} + + + src/assembly/linux.xml + + + + + make-linux-assembly + install + + single + + + + + + + + + + build-mac + + mac + + + + + maven-assembly-plugin + ${maven.assembly.plugin.version} + + + src/assembly/macos.xml + + + + + make-macos-assembly + install + + single + + + + + + + + + + build-windows + + windows + + + + + maven-assembly-plugin + ${maven.assembly.plugin.version} + + + src/assembly/windows.xml + + + + + make-windows-assembly + install + + single + + + + + + + + + + + + local-repo + file://${basedir}/../local-repo + + + \ No newline at end of file diff --git a/bundle/src/assembly/linux.xml b/bundle/src/assembly/linux.xml new file mode 100644 index 0000000..3bcf5ca --- /dev/null +++ b/bundle/src/assembly/linux.xml @@ -0,0 +1,16 @@ + + linux + + zip + + + + ${project.basedir}/../installer + / + + *.AppImage + + + + \ No newline at end of file diff --git a/bundle/src/assembly/macos.xml b/bundle/src/assembly/macos.xml new file mode 100644 index 0000000..c286355 --- /dev/null +++ b/bundle/src/assembly/macos.xml @@ -0,0 +1,16 @@ + + macos + + zip + + + + ${project.basedir}/../installer + / + + *.pkg + + + + \ No newline at end of file diff --git a/bundle/src/assembly/windows.xml b/bundle/src/assembly/windows.xml new file mode 100644 index 0000000..85c5a12 --- /dev/null +++ b/bundle/src/assembly/windows.xml @@ -0,0 +1,16 @@ + + windows + + zip + + + + ${project.basedir}/../installer/GarminTools + / + + **/* + + + + \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index d39d608..065c9f0 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -6,7 +6,7 @@ com.johnpickup.garmin GarminTools - 1.0-SNAPSHOT + 1.1 common @@ -15,6 +15,7 @@ ${java.version} ${java.version} UTF-8 + true diff --git a/common/src/main/java/com/johnpickup/garmin/common/unit/CustomPowerTarget.java b/common/src/main/java/com/johnpickup/garmin/common/unit/CustomPowerTarget.java new file mode 100755 index 0000000..aa8b8aa --- /dev/null +++ b/common/src/main/java/com/johnpickup/garmin/common/unit/CustomPowerTarget.java @@ -0,0 +1,52 @@ +package com.johnpickup.garmin.common.unit; + +import java.util.Objects; + +/** + * Power target - a minimum and maximum power in watts + */ +public class CustomPowerTarget extends PowerTarget { + private final Power maxPower; + private final Power minPower; + + public CustomPowerTarget(long min, long max, PowerUnit unit) { + this.minPower = new Power(min, unit); + this.maxPower = new Power(max, unit); + } + + @Override + public String toString() { + return minPower.toValueString() + "-" + maxPower; + } + + public Long getGarminLow() { + if (minPower.toGarminPower() < maxPower.toGarminPower()) + return minPower.toGarminPower(); + else + return maxPower.toGarminPower(); + } + + public Long getGarminHigh() { + if (minPower.toGarminPower() < maxPower.toGarminPower()) + return maxPower.toGarminPower(); + else + return minPower.toGarminPower(); + } + + @Override + public Long getTargetValue() { + return 0L; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + CustomPowerTarget that = (CustomPowerTarget) o; + return Objects.equals(maxPower, that.maxPower) && Objects.equals(minPower, that.minPower); + } + + @Override + public int hashCode() { + return Objects.hash(maxPower, minPower); + } +} diff --git a/common/src/main/java/com/johnpickup/garmin/common/unit/Power.java b/common/src/main/java/com/johnpickup/garmin/common/unit/Power.java new file mode 100755 index 0000000..e4f1ce9 --- /dev/null +++ b/common/src/main/java/com/johnpickup/garmin/common/unit/Power.java @@ -0,0 +1,47 @@ +package com.johnpickup.garmin.common.unit; + +import java.util.Objects; + +/** + * Encapsulation of custom power value with human-readable toString plus a conversion to Garmin units + */ +public class Power { + private final long value; + private final PowerUnit unit; + + public Power(long value, PowerUnit unit) { + this.value = value; + this.unit = unit; + } + + @Override + public String toString() { + return String.format("%s%s", toValueString(), unit.getShortName()); + } + + public Long toGarminPower() { + // Garmin power units are in watts offset by 1000 + // 0 – 1000% reserved for functional threshold power (FTP) + return switch (unit) { + case WATTS -> value + 1000; + }; + } + + public String toValueString() { + return switch (unit) { + case WATTS -> String.format("%d", value); + }; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Power power = (Power) o; + return value == power.value && unit == power.unit; + } + + @Override + public int hashCode() { + return Objects.hash(value, unit); + } +} diff --git a/common/src/main/java/com/johnpickup/garmin/common/unit/PowerTarget.java b/common/src/main/java/com/johnpickup/garmin/common/unit/PowerTarget.java new file mode 100755 index 0000000..2ced0be --- /dev/null +++ b/common/src/main/java/com/johnpickup/garmin/common/unit/PowerTarget.java @@ -0,0 +1,13 @@ +package com.johnpickup.garmin.common.unit; + +/** + * Power target - either a zone or a custom minimum and maximum power (in subclasses) + */ +public abstract class PowerTarget { + + public abstract Long getGarminLow(); + + public abstract Long getGarminHigh(); + + public abstract Long getTargetValue(); +} diff --git a/common/src/main/java/com/johnpickup/garmin/common/unit/PowerUnit.java b/common/src/main/java/com/johnpickup/garmin/common/unit/PowerUnit.java new file mode 100755 index 0000000..8fa6f3f --- /dev/null +++ b/common/src/main/java/com/johnpickup/garmin/common/unit/PowerUnit.java @@ -0,0 +1,28 @@ +package com.johnpickup.garmin.common.unit; + +public enum PowerUnit { + WATTS("watts","W"); + + private final String description; + + final String shortName; + + PowerUnit(String description, String shortName) { + this.description = description; + this.shortName = shortName; + } + + @Override + public String toString() { + return description; + } + + public String getDescription() { + return this.description; + } + + public String getShortName() { + return this.shortName; + } +} + diff --git a/common/src/main/java/com/johnpickup/garmin/common/unit/ZonePowerTarget.java b/common/src/main/java/com/johnpickup/garmin/common/unit/ZonePowerTarget.java new file mode 100755 index 0000000..c11bb05 --- /dev/null +++ b/common/src/main/java/com/johnpickup/garmin/common/unit/ZonePowerTarget.java @@ -0,0 +1,29 @@ +package com.johnpickup.garmin.common.unit; + +public class ZonePowerTarget extends PowerTarget { + private final Long zone; + + public ZonePowerTarget(Long zone) { + this.zone = zone; + } + + @Override + public Long getGarminLow() { + return 0L; + } + + @Override + public Long getGarminHigh() { + return 0L; + } + + @Override + public Long getTargetValue() { + return zone; + } + + @Override + public String toString() { + return "PZ" + zone; + } +} diff --git a/gpx/pom.xml b/gpx/pom.xml index d48bb01..b90cebf 100644 --- a/gpx/pom.xml +++ b/gpx/pom.xml @@ -6,7 +6,7 @@ com.johnpickup.garmin GarminTools - 1.0-SNAPSHOT + 1.1 gpx @@ -15,6 +15,7 @@ ${java.version} ${java.version} UTF-8 + true diff --git a/installer/GarminTools-1.0.0.msi b/installer/GarminTools-1.0.0.msi deleted file mode 100644 index 9d502f5..0000000 Binary files a/installer/GarminTools-1.0.0.msi and /dev/null differ diff --git a/installer/GarminTools-1.0.0.pkg b/installer/GarminTools-1.0.0.pkg deleted file mode 100644 index dd365ca..0000000 --- a/installer/GarminTools-1.0.0.pkg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71a6552012b04f0e96f8d0b1bd75e6c589ff9b1412b603d51e89beb15f28de67 -size 101283204 diff --git a/installer/GarminTools.zip b/installer/GarminTools.zip deleted file mode 100644 index d514d8d..0000000 Binary files a/installer/GarminTools.zip and /dev/null differ diff --git a/parser/pom.xml b/parser/pom.xml index fef9692..8654e6d 100644 --- a/parser/pom.xml +++ b/parser/pom.xml @@ -6,7 +6,7 @@ com.johnpickup.garmin GarminTools - 1.0-SNAPSHOT + 1.1 parser @@ -15,6 +15,7 @@ ${java.version} ${java.version} UTF-8 + true diff --git a/parser/src/main/antlr4/Workout.g4 b/parser/src/main/antlr4/Workout.g4 index d99830c..4079186 100755 --- a/parser/src/main/antlr4/Workout.g4 +++ b/parser/src/main/antlr4/Workout.g4 @@ -34,18 +34,24 @@ step returns [Step value] | distance_pace_intensity_step {$value = $distance_pace_intensity_step.value;} | distance_hr_step {$value = $distance_hr_step.value;} | distance_hr_intensity_step {$value = $distance_hr_intensity_step.value;} + | distance_power_step {$value = $distance_power_step.value;} + | distance_power_intensity_step {$value = $distance_power_intensity_step.value;} | time_step {$value = $time_step.value;} | time_intensity_step {$value = $time_intensity_step.value;} | time_pace_step {$value = $time_pace_step.value;} | time_pace_intensity_step {$value = $time_pace_intensity_step.value;} | time_hr_step {$value = $time_hr_step.value;} | time_hr_intensity_step {$value = $time_hr_intensity_step.value;} + | time_power_step {$value = $time_power_step.value;} + | time_power_intensity_step {$value = $time_power_intensity_step.value;} | open_step {$value = $open_step.value;} | open_intensity_step {$value = $open_intensity_step.value;} | open_pace_step {$value = $open_pace_step.value;} | open_pace_intensity_step {$value = $open_pace_intensity_step.value;} | open_hr_step {$value = $open_hr_step.value;} | open_hr_intensity_step {$value = $open_hr_intensity_step.value;} + | open_power_step {$value = $open_power_step.value;} + | open_power_intensity_step {$value = $open_power_intensity_step.value;} | repeating_steps {$value = $repeating_steps.value;} ; @@ -79,6 +85,16 @@ distance_hr_intensity_step returns [DistanceHeartRateStep value] | distance '@' hr_zone PIPE intensity {$value = new DistanceHeartRateStep($intensity.value, $distance.value, $hr_zone.value);} ; +distance_power_step returns [DistancePowerStep value] + : distance '@' power_range {$value = new DistancePowerStep($distance.value, $power_range.value);} + | distance '@' power_zone {$value = new DistancePowerStep($distance.value, $power_zone.value);} + ; + +distance_power_intensity_step returns [DistancePowerStep value] + : distance '@' power_range PIPE intensity {$value = new DistancePowerStep($intensity.value, $distance.value, $power_range.value);} + | distance '@' power_zone PIPE intensity {$value = new DistancePowerStep($intensity.value, $distance.value, $power_zone.value);} + ; + time_step returns [TimeStep value] : time {$value = new TimeStep($time.value);} ; @@ -109,6 +125,16 @@ time_hr_intensity_step returns [TimeHeartRateStep value] | time '@' hr_zone PIPE intensity {$value = new TimeHeartRateStep($intensity.value, $time.value, $hr_zone.value);} ; +time_power_step returns [TimePowerStep value] + : time '@' power_range {$value = new TimePowerStep($time.value, $power_range.value);} + | time '@' power_zone {$value = new TimePowerStep($time.value, $power_zone.value);} + ; + +time_power_intensity_step returns [TimePowerStep value] + : time '@' power_range PIPE intensity {$value = new TimePowerStep($intensity.value, $time.value, $power_range.value);} + | time '@' power_zone PIPE intensity {$value = new TimePowerStep($intensity.value, $time.value, $power_zone.value);} + ; + open_step returns [OpenStep value] : open {$value = new OpenStep();} ; @@ -139,6 +165,16 @@ open_hr_intensity_step returns [OpenHeartRateStep value] | open '@' hr_zone PIPE intensity {$value = new OpenHeartRateStep($intensity.value, $hr_zone.value);} ; +open_power_step returns [OpenPowerStep value] + : open '@' power_range {$value = new OpenPowerStep($power_range.value);} + | open '@' power_zone {$value = new OpenPowerStep($power_zone.value);} + ; + +open_power_intensity_step returns [OpenPowerStep value] + : open '@' power_range PIPE intensity {$value = new OpenPowerStep($intensity.value, $power_range.value);} + | open '@' power_zone PIPE intensity {$value = new OpenPowerStep($intensity.value, $power_zone.value);} + ; + repeating_steps returns [RepeatingSteps value] : '(' s=stepList {$value = new RepeatingSteps($s.steps);} @@ -175,17 +211,40 @@ hr_unit returns [HeartRateUnit value] hr_zone returns [HeartRateZone value] : 'Z1' {$value = HeartRateZone.Z1;} + | 'HZ1' {$value = HeartRateZone.Z1;} | 'z1' {$value = HeartRateZone.Z1;} | 'Z2' {$value = HeartRateZone.Z2;} + | 'HZ2' {$value = HeartRateZone.Z2;} | 'z2' {$value = HeartRateZone.Z2;} | 'Z3' {$value = HeartRateZone.Z3;} + | 'HZ3' {$value = HeartRateZone.Z3;} | 'z3' {$value = HeartRateZone.Z3;} | 'Z4' {$value = HeartRateZone.Z4;} + | 'HZ4' {$value = HeartRateZone.Z4;} | 'z4' {$value = HeartRateZone.Z4;} | 'Z5' {$value = HeartRateZone.Z5;} + | 'HZ5' {$value = HeartRateZone.Z5;} | 'z5' {$value = HeartRateZone.Z5;} ; +power_range returns [Power value] + : p1=cardinal '-' p2=cardinal power_unit {$value = new PowerRange($p1.value, $p2.value, $power_unit.value);} + ; + +power_unit returns [PowerUnit value] + : 'W' {$value = PowerUnit.WATTS;} + ; + +power_zone returns [PowerZone value] + : 'PZ1' {$value = PowerZone.PZ1;} + | 'PZ2' {$value = PowerZone.PZ2;} + | 'PZ3' {$value = PowerZone.PZ3;} + | 'PZ4' {$value = PowerZone.PZ4;} + | 'PZ5' {$value = PowerZone.PZ5;} + | 'PZ6' {$value = PowerZone.PZ6;} + | 'PZ7' {$value = PowerZone.PZ7;} + ; + time returns [Time value] : DIGIT + COLON DIGIT DIGIT {$value = Time.parseTime($text);} ; diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/DistancePowerStep.java b/parser/src/main/java/com/johnpickup/garmin/parser/DistancePowerStep.java new file mode 100755 index 0000000..9d843a9 --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/DistancePowerStep.java @@ -0,0 +1,50 @@ +package com.johnpickup.garmin.parser; + +import java.util.Objects; + +public class DistancePowerStep extends Step { + private final Distance distance; + private final Power power; + + public DistancePowerStep(Distance distance, Power power) { + super(); + this.distance = distance; + this.power = power; + } + + public DistancePowerStep(StepIntensity stepIntensity, Distance distance, Power power) { + super(stepIntensity); + this.distance = distance; + this.power = power; + } + @Override + public String toString() { + return distance + "@" + power + (stepIntensity==null?"":("|"+stepIntensity)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DistancePowerStep that = (DistancePowerStep) o; + return Objects.equals(distance, that.distance) && Objects.equals(power, that.power) + && Objects.equals(stepIntensity, that.stepIntensity); + } + + @Override + public int hashCode() { + return Objects.hash(distance, power); + } + + protected boolean canEqual(final Object other) { + return other instanceof DistancePowerStep; + } + + public Distance getDistance() { + return this.distance; + } + + public Power getPower() { + return this.power; + } +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/OpenPowerStep.java b/parser/src/main/java/com/johnpickup/garmin/parser/OpenPowerStep.java new file mode 100755 index 0000000..2e7abfe --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/OpenPowerStep.java @@ -0,0 +1,44 @@ +package com.johnpickup.garmin.parser; + +import java.util.Objects; + +public class OpenPowerStep extends Step { + private final Power power; + + public OpenPowerStep(Power power) { + super(); + this.power = power; + } + + public OpenPowerStep(StepIntensity stepIntensity, Power power) { + super(stepIntensity); + this.power = power; + } + + @Override + public String toString() { + return "Open@" + power + (stepIntensity==null?"":("|"+stepIntensity)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OpenPowerStep that = (OpenPowerStep) o; + return Objects.equals(power, that.power) + && Objects.equals(stepIntensity, that.stepIntensity); + } + + @Override + public int hashCode() { + return Objects.hash(power); + } + + protected boolean canEqual(final Object other) { + return other instanceof OpenPowerStep; + } + + public Power getPower() { + return this.power; + } +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/Power.java b/parser/src/main/java/com/johnpickup/garmin/parser/Power.java new file mode 100755 index 0000000..9cc4091 --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/Power.java @@ -0,0 +1,4 @@ +package com.johnpickup.garmin.parser; + +public interface Power { +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/PowerRange.java b/parser/src/main/java/com/johnpickup/garmin/parser/PowerRange.java new file mode 100755 index 0000000..a0a936c --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/PowerRange.java @@ -0,0 +1,49 @@ +package com.johnpickup.garmin.parser; + +import java.util.Objects; + +public class PowerRange implements Power { + private final int minimum; + private final int maximum; + private final PowerUnit unit; + + public PowerRange(int minimum, int maximum, PowerUnit unit) { + this.minimum = minimum; + this.maximum = maximum; + this.unit = unit; + } + + @Override + public String toString() { + return String.format("%s-%s%s", minimum, maximum, unit); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PowerRange that = (PowerRange) o; + return minimum == that.minimum && maximum == that.maximum && unit == that.unit; + } + + @Override + public int hashCode() { + return Objects.hash(minimum, maximum, unit); + } + + protected boolean canEqual(final Object other) { + return other instanceof PowerRange; + } + + public int getMinimum() { + return this.minimum; + } + + public int getMaximum() { + return this.maximum; + } + + public PowerUnit getUnit() { + return this.unit; + } +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/PowerUnit.java b/parser/src/main/java/com/johnpickup/garmin/parser/PowerUnit.java new file mode 100755 index 0000000..e60ab0f --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/PowerUnit.java @@ -0,0 +1,13 @@ +package com.johnpickup.garmin.parser; + +public enum PowerUnit { + WATTS; + + @Override + public String toString() { + switch (this) { + case WATTS: return "W"; + default: return super.toString(); + } + } +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/PowerZone.java b/parser/src/main/java/com/johnpickup/garmin/parser/PowerZone.java new file mode 100755 index 0000000..f528823 --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/PowerZone.java @@ -0,0 +1,56 @@ +package com.johnpickup.garmin.parser; + +import java.util.Objects; + +public class PowerZone implements Power { + private final Zone zone; + private final long zoneNumber; + private PowerZone(Zone zone, long zoneNumber) { + this.zone = zone; + this.zoneNumber = zoneNumber; + } + + public static PowerZone PZ1 = new PowerZone(Zone.PZ1, 1); + public static PowerZone PZ2 = new PowerZone(Zone.PZ2, 2); + public static PowerZone PZ3 = new PowerZone(Zone.PZ3, 3); + public static PowerZone PZ4 = new PowerZone(Zone.PZ4, 4); + public static PowerZone PZ5 = new PowerZone(Zone.PZ5, 5); + public static PowerZone PZ6 = new PowerZone(Zone.PZ6, 6); + public static PowerZone PZ7 = new PowerZone(Zone.PZ7, 7); + + @Override + public String toString() { + return zone.name(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PowerZone that = (PowerZone) o; + return zoneNumber == that.zoneNumber && zone == that.zone; + } + + @Override + public int hashCode() { + return Objects.hash(zone, zoneNumber); + } + + protected boolean canEqual(final Object other) { + return other instanceof PowerZone; + } + + public long getZoneNumber() { + return this.zoneNumber; + } + + enum Zone { + PZ1, + PZ2, + PZ3, + PZ4, + PZ5, + PZ6, + PZ7 + } +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/Sport.java b/parser/src/main/java/com/johnpickup/garmin/parser/Sport.java index 9d84f00..605bb6a 100644 --- a/parser/src/main/java/com/johnpickup/garmin/parser/Sport.java +++ b/parser/src/main/java/com/johnpickup/garmin/parser/Sport.java @@ -9,5 +9,8 @@ public enum Sport { MTB, SWIMMING, POOL_SWIMMING, - OPEN_WATER_SWIMMING + OPEN_WATER_SWIMMING, + CARDIO, + STRENGTH, + HIIT } diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/TimePowerStep.java b/parser/src/main/java/com/johnpickup/garmin/parser/TimePowerStep.java new file mode 100755 index 0000000..37f063c --- /dev/null +++ b/parser/src/main/java/com/johnpickup/garmin/parser/TimePowerStep.java @@ -0,0 +1,51 @@ +package com.johnpickup.garmin.parser; + +import java.util.Objects; + +public class TimePowerStep extends Step { + private final Time time; + private final Power power; + + public TimePowerStep(Time time, Power power) { + super(); + this.time = time; + this.power = power; + } + + public TimePowerStep(StepIntensity stepIntensity, Time time, Power power) { + super(stepIntensity); + this.time = time; + this.power = power; + } + + @Override + public String toString() { + return time + "@" + power + (stepIntensity==null?"":("|"+stepIntensity)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TimePowerStep that = (TimePowerStep) o; + return Objects.equals(time, that.time) && Objects.equals(power, that.power) + && Objects.equals(stepIntensity, that.stepIntensity); + } + + @Override + public int hashCode() { + return Objects.hash(time, power); + } + + protected boolean canEqual(final Object other) { + return other instanceof TimePowerStep; + } + + public Time getTime() { + return this.time; + } + + public Power getPower() { + return this.power; + } +} diff --git a/parser/src/main/java/com/johnpickup/garmin/parser/Workout.java b/parser/src/main/java/com/johnpickup/garmin/parser/Workout.java index b494d0b..69fd9d3 100755 --- a/parser/src/main/java/com/johnpickup/garmin/parser/Workout.java +++ b/parser/src/main/java/com/johnpickup/garmin/parser/Workout.java @@ -61,5 +61,4 @@ public int hashCode() { protected boolean canEqual(final Object other) { return other instanceof Workout; } - } diff --git a/pom.xml b/pom.xml index 69cc987..3122645 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.johnpickup.garmin GarminTools - 1.0-SNAPSHOT + 1.1 pom GarminTools @@ -14,6 +14,7 @@ parser gpx app + bundle @@ -28,8 +29,9 @@ 2.3.1 4.7.1 5.2.2 - 1.4.12 + 1.5.13 ${project.basedir}/lib/fit.jar + true @@ -113,4 +115,12 @@ file://${basedir}/local-repo + + + + github + GitHub Apache Maven Packages + https://maven.pkg.github.com/jpickup/GarminTools + + \ No newline at end of file