forked from upstream-mirrors/OpenTracks
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7f7e3fca1 | |||
| a52beffef5 | |||
| e6f6d61b84 | |||
| 4a167c9335 | |||
| 16857a9968 | |||
| 094a20c379 | |||
| 0d3cf4ce0d | |||
| 8a2d1c42a8 | |||
| a5b44175a3 | |||
| db5c9479df | |||
| 16f238f34b | |||
| a57b5cd6ee | |||
| 5e54cb8fda | |||
| 0934e7f10c | |||
| 6741ef4d9f | |||
| f2ac1edc4b | |||
| b44bab1ab5 | |||
| 574bd38a35 | |||
| 374959f340 | |||
| 3815ee6c59 | |||
| 5e4924a5e1 | |||
| 8123104f39 | |||
| a9e0c4916d | |||
| 58803c975a | |||
| d7dc59e547 | |||
| 5cf8b67954 | |||
| fb70486bdc | |||
| 18b21a6a3e | |||
| 71288cd9be | |||
| 220545da9f | |||
| 43e9c42915 | |||
| 28944eff07 | |||
| 92f9f94423 | |||
| 8ae268fab2 | |||
| 346264025b | |||
| 0a810e7123 | |||
| 02f3fc1940 | |||
| dd1c5667a0 | |||
| 0f489f1cc9 | |||
| a07f11589b | |||
| 209a320433 | |||
| 8ab9ed0e31 | |||
| 80ca4e647a | |||
| a8de9cb216 | |||
| 12c21b7940 | |||
| 4c5ff546a4 | |||
| 2e0835b2ec | |||
| 18ea8fea87 | |||
| 02a69c8817 | |||
| cab70d665c | |||
| c42f833703 | |||
| 39838232b5 | |||
| 2b346b3f18 | |||
| 7c5115fc56 | |||
| cb71c40350 | |||
| 22bde4af3c |
@@ -9,7 +9,7 @@ jobs:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '11'
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
- name: Archive
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: OpenTracks-nightly.apk
|
||||
path: ./build/outputs/apk/nightly/*.apk
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
|
||||
@@ -10,20 +10,20 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
api-level: [ 23, 30 ]
|
||||
api-level: [ 24, 30 ]
|
||||
target: [ default ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-java@v2
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
|
||||
- name: run tests
|
||||
uses: reactivecircus/android-emulator-runner@v2.23.0
|
||||
uses: reactivecircus/android-emulator-runner@v2.24.0
|
||||
with:
|
||||
api-level: ${{ matrix.api-level }}
|
||||
target: ${{ matrix.target }}
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: Archive
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code-coverage-report
|
||||
path: build/reports/androidTests/
|
||||
|
||||
@@ -60,7 +60,6 @@ captures/
|
||||
.idea/encodings.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
.idea/libraries/
|
||||
.idea/misc.xml
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/shelf/
|
||||
|
||||
@@ -59,6 +59,7 @@ We do not support type of movement (i.e., walking vs running).
|
||||
|
||||
* Polar Stride
|
||||
* Stryd (incl. power)
|
||||
* Wahoo Tickr X (Cadence appears to be reported in SPM rather than RPM)
|
||||
|
||||
## Tested Barometric Sensor Smartphones
|
||||
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "de.dennisguse.opentracks"
|
||||
versionCode 4773
|
||||
versionName "v3.26.0"
|
||||
versionCode 4816
|
||||
versionName "v3.27.0"
|
||||
|
||||
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
|
||||
|
||||
@@ -130,7 +130,7 @@ dependencies {
|
||||
androidTestImplementation 'androidx.test:rules:1.4.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.4.0'
|
||||
androidTestImplementation 'org.mockito:mockito-android:4.5.1'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.4.1'
|
||||
}
|
||||
|
||||
@@ -29,4 +29,26 @@
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element name="accuracy_horizontal"
|
||||
type="xsd:float">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation xml:lang="en">
|
||||
Horizontal accuracy in meters of the current TrackPoint; 68% chance that the actual
|
||||
location is
|
||||
within this radius around the measured location.
|
||||
Only used in GPX.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
<xsd:element name="accuracy_vertical"
|
||||
type="xsd:float">
|
||||
<xsd:annotation>
|
||||
<xsd:documentation xml:lang="en">
|
||||
Vertical accuracy in meters of the current TrackPoint; 68% chance that the actual
|
||||
location is
|
||||
within this radius around the measured location.
|
||||
Only used in GPX.
|
||||
</xsd:documentation>
|
||||
</xsd:annotation>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
@@ -0,0 +1,4 @@
|
||||
v3.26.1: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Contextual action bar didn't overlap toolbar in night theme.
|
||||
@@ -0,0 +1,17 @@
|
||||
v3.27.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Voice annoucements: add 100m, 200m, and 500m
|
||||
- Interval: add 100m and 500m
|
||||
- Clock shows now also seconds
|
||||
- GPX, KML, KMZ: export and import horizontal accuracy
|
||||
- GPX: export cumulative distance via clustrust extension
|
||||
- CSV: basic export (TrackPoints only; not for backup)
|
||||
- Workaround for Tickr X as it reports SPM instead of RPM
|
||||
|
||||
Bugfixes:
|
||||
- Adhere to recording distance interval;
|
||||
TrackPoints were recorded although they were too close
|
||||
- Prevent crash on stop recording (race condition)
|
||||
- Prevent crash when using multiple sensors (race condition)
|
||||
- Prevent crash when opening OpenTracks via notification
|
||||
@@ -0,0 +1,6 @@
|
||||
v3.28.0: OpenTracks
|
||||
|
||||
Changes:
|
||||
- Configurable filename for export/sharing (default: {starttime}_{name}.{suffix})
|
||||
- Vertical GPS accuracy is now stored and can be exported/imported
|
||||
(API level >= 26).
|
||||
@@ -0,0 +1,11 @@
|
||||
v3.25.0: OpenTracks
|
||||
|
||||
Veranderingen:
|
||||
- GPS is nu optioneel incl. waarschuwing GPS uitgeschakeld
|
||||
- PublicAPI: andere apps kunnen nu opgenomen gegevens ontvangen als ze een opname starten (geïmplementeerd voor Gadgetbridge)
|
||||
- Nieuwe activiteiten: zwemmen, buiten zwemmen, en training
|
||||
- Verwijderd: share pic functie
|
||||
|
||||
Bugfix:
|
||||
- UI: snelheid/hoogte met wisselingen tussen 0,0 en de werkelijke waarde
|
||||
- Tracks met emoji's kunnen worden geëxporteerd
|
||||
@@ -0,0 +1,5 @@
|
||||
v3.25.1: OpenTracks
|
||||
|
||||
Verbetering:
|
||||
- Automatische export: hopelijk voorkomen van stille mislukkingen
|
||||
- Automatische export: voorkom crash
|
||||
@@ -0,0 +1,8 @@
|
||||
v3.26.0: OpenTracks
|
||||
|
||||
Wijzigingen:
|
||||
- Gesproken meldingen zijn instelbaar
|
||||
- Gesproken meldingen van hartslag
|
||||
|
||||
Bugfix:
|
||||
- Crash due to race condition
|
||||
@@ -0,0 +1,4 @@
|
||||
v3.26.1: OpenTracks
|
||||
|
||||
Bugfix:
|
||||
- Contextuele actie balk overlapte de werkbalk niet in het nachtthema.
|
||||
@@ -1,11 +1,22 @@
|
||||
OpenTracks is een sport tracking applicatie die uw privacy volledig respecteert.
|
||||
Een sport tracker maatje met respect voor privacy.
|
||||
|
||||
Kenmerken:
|
||||
* Sport tracking: volg uw sport en outdoor activiteiten
|
||||
* Foto's en markeringen: markeer interessante locaties tijdens het volgen
|
||||
* Hoogte winst en verlies: via barometrische sensor
|
||||
* Hoogte weergegeven in EGM2008 (boven gemiddeld zeeniveau); geëxporteerd als WGS84
|
||||
* Spraakaankondigingen
|
||||
* Ondersteunt Bluetooth LE-sensoren: hartslag, snelheid en afstand (fietsen), cadans (fietsen), en vermogensmeter (fietsen)
|
||||
* Exporteer gegevens: exporteer tracks als KMZ (incl. foto's), KML, of GPX
|
||||
* Geen toegang tot internet: Internet wordt niet gebruikt
|
||||
Sport en outdoor activiteiten geven geluk, vaardigheid en zelfvertrouwen.
|
||||
|
||||
Geef je gezondheid waarde door de conditie bij te houden.
|
||||
Registreert terwijl je hardloopt of wandelt, en geeft je een fietscomputer met een groter scherm voor op de fiets.
|
||||
Markeer interessante locaties langs de weg met foto's.
|
||||
Bewaar geregistreerde statistieken tot in detail voor analyse.
|
||||
Deel alleen de gegevens die je aan anderen wilt laten zien.
|
||||
|
||||
* Spraakaankondigingen.
|
||||
* Ondersteunt Bluetooth LE sensoren: hartslag, snelheid en afstand (fietsen), cadans (fietsen), en vermogensmeter (fietsen).
|
||||
* Hoogtewinst en -verlies: via barometrische sensor.
|
||||
* Hoogte weergegeven in EGM2008 (boven gemiddeld zeeniveau); geëxporteerd als WGS84.
|
||||
* Gegevens exporteren als tracks, hetzij als KMZ (incl. foto's), KML, of GPX.
|
||||
* Geen Internet toegang of extra toegangsrechten.
|
||||
* Donker en licht thema, met inachtneming van de systeeminstellingen.
|
||||
* Geen advertenties.
|
||||
|
||||
Libre software / Vrije Software / Open Source
|
||||
wat betekent dat je de broncode mag gebruiken, bestuderen, veranderen en delen.
|
||||
Gelicenseerd Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
Een sport tracker die uw privacy volledig respecteert.
|
||||
Een sport tracker maatje met respect voor privacy.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
v3.27.0: OpenTracks
|
||||
|
||||
Mudanças:
|
||||
- Anúncios de voz: adicionado 100m, 200m e 500m
|
||||
- Intervalo: adicionado 100m e 500m
|
||||
- O relógio mostra agora também os segundos
|
||||
- GPX, KML, KMZ: uma maior precisão horizontal de exportação e importação
|
||||
- GPX: exporta a distância acumulada via extensão clustrust
|
||||
- CSV: exportação básica (apenas TrackPoints; não para backup)
|
||||
- Solução alternativa para o Tickr X, pois relata SPM em vez de RPM
|
||||
|
||||
Correções de bugs:
|
||||
- Foi Aderido ao intervalo de distância de gravação; Os TrackPoints foram registrados embora estivessem muito próximos
|
||||
- Prevenir travamento ao parar de gravar (condição de corrida)
|
||||
- Evite acidentes ao usar vários sensores (condição de corrida)
|
||||
- Evite travamentos ao abrir o OpenTracks via notificação
|
||||
@@ -1 +1 @@
|
||||
Um rastreador esportivo que respeita totalmente a sua privacidade.
|
||||
Um rastreador esportivo que respeita a sua privacidade.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
v3.26.0: OpenTracks
|
||||
|
||||
Изменения:
|
||||
- Голосовые оповещения теперь настраиваются
|
||||
- Голосовые оповещения о частоте сердечных сокращений
|
||||
|
||||
Исправление:
|
||||
- Вылет из-за состояния гонки
|
||||
@@ -0,0 +1,4 @@
|
||||
v3.26.1: OpenTracks
|
||||
|
||||
Исправление:
|
||||
- Контекстная панель действий не перекрывала панель инструментов в ночной теме.
|
||||
@@ -1,11 +1,22 @@
|
||||
OpenTracks - это приложение для спортивного слежения, которое полностью уважает вашу конфиденциальность.
|
||||
Спортивный трекер уважающий вашу приватность.
|
||||
|
||||
Особенности:
|
||||
* Спортивный трекинг: отслеживайте свои занятия спортом и активный отдых.
|
||||
* Фотографии и маркеры: отмечайте интересные места во время отслеживания.
|
||||
* Набор и потеря высоты: через барометрический датчик.
|
||||
* Высота отображается в EGM2008 (над средним уровнем моря); экспортируется в WGS84.
|
||||
* Голосовые уведомления
|
||||
* Поддержка датчиков Bluetooth LE: пульс, скорость и расстояние (велоспорт), каденция (велоспорт) и измеритель мощности (велоспорт)
|
||||
* Экспорт данных: экспорт треков в формате KMZ (включая фотографии), KML или GPX.
|
||||
* Нет доступа к Интернету: Интернет не используется
|
||||
Спорт и иные активности дарят радость, мастерство и уверенность в себе.
|
||||
|
||||
Цените свое здоровье, следя за тренировками.
|
||||
Записывает, как вы бегаете или идете, и предоставляет вам функционал велокомпьютера с большим экраном для езды на велосипеде.
|
||||
Отмечайте интересные места на своем пути с помощью фотографий.
|
||||
Храните записанные данные в мельчайших подробностях для анализа.
|
||||
Делитесь только теми данными, которые хотите, чтобы другие имели доступ к ним.
|
||||
|
||||
* Голосовые уведомления.
|
||||
* Поддержка датчиков Bluetooth LE: пульс, скорость и дистанция (для велосипеда), частота вращения педалей (для велосипеда) и измеритель мощности (для велосипеда).
|
||||
* Набор и потеря высоты: с помощью барометрического датчика.
|
||||
* Высота указана в EGM2008 (над средним уровнем моря); экспортируется как WGS84.
|
||||
* Экспорт данных в виде треков формата KMZ (включая фотографии), KML или GPX.
|
||||
* Не требует соединения с Интернетом и лишних разрешений.
|
||||
* Темная и светлая темы с учетом системных настроек.
|
||||
* Без рекламы.
|
||||
|
||||
Libre software / Свободное программное обеспечение / Открытый исходный код
|
||||
означает, что вы можете использовать, изучать, изменять и делиться исходным кодом.
|
||||
Лицензия Apache 2.0
|
||||
|
||||
@@ -1 +1 @@
|
||||
Спортивный трекер, который полностью соблюдает вашу конфиденциальность.
|
||||
Спортивный трекер, уважающий вашу конфиденциальность.
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+2
-3
@@ -1,6 +1,5 @@
|
||||
#Wed Jul 28 23:10:13 CEST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,67 +17,101 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@@ -106,80 +140,95 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
Vendored
+3
-18
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,21 +64,6 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
@@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
+8
-7
@@ -5,12 +5,13 @@ import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
public class PreferenceMetricUnitRule implements TestRule {
|
||||
private final boolean metricUnits;
|
||||
public class PreferenceUnitSystemRule implements TestRule {
|
||||
private final UnitSystem unit;
|
||||
|
||||
public PreferenceMetricUnitRule(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
public PreferenceUnitSystemRule(UnitSystem unit) {
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -18,13 +19,13 @@ public class PreferenceMetricUnitRule implements TestRule {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
final boolean previousMetricUnits = PreferencesUtils.isMetricUnits();
|
||||
final UnitSystem previousUnitSystem = PreferencesUtils.getUnitSystem();
|
||||
|
||||
try {
|
||||
PreferencesUtils.setMetricUnits(metricUnits);
|
||||
PreferencesUtils.setUnit(unit);
|
||||
base.evaluate();
|
||||
} finally {
|
||||
PreferencesUtils.setMetricUnits(previousMetricUnits);
|
||||
PreferencesUtils.setUnit(previousUnitSystem);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class TimezoneRule implements TestRule {
|
||||
|
||||
private final TimeZone mTimeZone;
|
||||
private TimeZone mDeviceTimeZone;
|
||||
|
||||
|
||||
public TimezoneRule(@NonNull TimeZone timeZone) {
|
||||
mTimeZone = timeZone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(Statement base, Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
try {
|
||||
mDeviceTimeZone = TimeZone.getDefault();
|
||||
TimeZone.setDefault(mTimeZone);
|
||||
base.evaluate();
|
||||
} finally {
|
||||
TimeZone.setDefault(mDeviceTimeZone);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@@ -26,7 +27,7 @@ public class ChartPointTest {
|
||||
statistics.setTotalTime(Duration.ofSeconds(1000));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, false);
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), false, UnitSystem.IMPERIAL);
|
||||
|
||||
// then
|
||||
assertEquals(1000000, (long) point.getTimeOrDistance());
|
||||
@@ -39,7 +40,7 @@ public class ChartPointTest {
|
||||
statistics.setTotalDistance(Distance.of(1000));
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, true);
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(0), Altitude.EGM2008.of(0), true, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(1, (long) point.getTimeOrDistance());
|
||||
@@ -51,7 +52,7 @@ public class ChartPointTest {
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(50, point.getAltitude(), 0.01);
|
||||
@@ -65,7 +66,7 @@ public class ChartPointTest {
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, true);
|
||||
ChartPoint point = new ChartPoint(statistics, TrackStubUtils.createDefaultTrackPoint(), Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertNull(point.getHeartRate());
|
||||
@@ -84,7 +85,7 @@ public class ChartPointTest {
|
||||
TrackStatistics statistics = new TrackStatistics();
|
||||
|
||||
// when
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, true);
|
||||
ChartPoint point = new ChartPoint(statistics, trackPoint, Speed.of(10), Altitude.EGM2008.of(50), false, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(100.0, point.getHeartRate(), 0.01);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DistanceFormatterTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void testFormatDistance_metric() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.build(context);
|
||||
|
||||
// A large number in metric
|
||||
assertEquals("5.00 km", formatter.formatDistance(Distance.of(5000)));
|
||||
// A small number in metric
|
||||
assertEquals("100.00 m", formatter.formatDistance(Distance.of(100)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatDistance_imperial() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.IMPERIAL)
|
||||
.build(context);
|
||||
|
||||
// A large number in imperial
|
||||
assertEquals("3.11 mi", formatter.formatDistance(Distance.of(5000)));
|
||||
// A small number in imperial
|
||||
assertEquals("328.08 ft", formatter.formatDistance(Distance.of(100)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SpeedFormatterTest {
|
||||
|
||||
private final Context context = ApplicationProvider.getApplicationContext();
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_pace_metric() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
|
||||
assertEquals("4:59", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
assertEquals("5:00", formatter.getSpeedParts(Speed.of(3.33)).first);
|
||||
|
||||
assertEquals("min/km", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_pace_imperial() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.IMPERIAL)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
|
||||
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
// assertEquals("TODO", formatter.getSpeedParts(Speed.of(3.33)).first);
|
||||
|
||||
assertEquals("min/mi", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_metric() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(context);
|
||||
|
||||
assertEquals("11.9", formatter.getSpeedParts(Speed.of(3.31)).first);
|
||||
assertEquals("km/h", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts_speed_imperial() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.IMPERIAL)
|
||||
.setReportSpeedOrPace(true)
|
||||
.build(context);
|
||||
|
||||
assertEquals("7.5", formatter.getSpeedParts(Speed.of(3.34)).first);
|
||||
assertEquals("mph", formatter.getSpeedParts(Speed.zero()).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatSpeed() {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder()
|
||||
.setDecimalCount(2)
|
||||
.setUnit(UnitSystem.METRIC)
|
||||
.setReportSpeedOrPace(false)
|
||||
.build(context);
|
||||
|
||||
assertEquals("4:59 min/km", formatter.formatSpeed(Speed.of(3.34)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package de.dennisguse.opentracks.io.file;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TimezoneRule;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
public class TrackFilenameGeneratorTest {
|
||||
|
||||
@Rule
|
||||
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<String[]> data() {
|
||||
return Arrays.asList(new String[][]{
|
||||
{"{uuid}_{name}", "0000fee0_Best Track.gpx" },
|
||||
{"{name}_{uuid}", "Best Track_0000fee0.gpx" },
|
||||
{"{time}_{name}", "2020-02-02T02_02_02Z_Best Track.gpx" },
|
||||
{ApplicationProvider.getApplicationContext().getString(R.string.export_filename_format_default), "2020-02-02T02_02_02Z_Best Track.gpx" },
|
||||
});
|
||||
}
|
||||
|
||||
private final TrackFilenameGenerator subject;
|
||||
private final String expected;
|
||||
|
||||
public TrackFilenameGeneratorTest(String template, String expected) {
|
||||
this.subject = new TrackFilenameGenerator(template);
|
||||
this.expected = expected;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilenameTemplate() {
|
||||
// given
|
||||
Track track = new Track();
|
||||
track.setName("Best Track");
|
||||
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
|
||||
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
|
||||
|
||||
// when
|
||||
String filename = subject.format(track, TrackFileFormat.GPX);
|
||||
|
||||
// then
|
||||
assertEquals(expected, filename);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package de.dennisguse.opentracks.io.file;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
|
||||
//TODO Merge with TrackFilenameGeneratorTest whenever Junit5 gets available.
|
||||
//https://github.com/android/android-test/issues/224
|
||||
@RunWith(Parameterized.class)
|
||||
public class TrackFilenameGeneratorTest2 {
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static Collection<String> data() {
|
||||
return Arrays.asList(
|
||||
"{name}_{starime}",
|
||||
"{name",
|
||||
"name}");
|
||||
}
|
||||
|
||||
private final TrackFilenameGenerator subject;
|
||||
|
||||
public TrackFilenameGeneratorTest2(String template) {
|
||||
this.subject = new TrackFilenameGenerator(template);
|
||||
}
|
||||
|
||||
@Test(expected = TrackFilenameGenerator.TemplateInvalidException.class)
|
||||
public void testFilenameTemplate() {
|
||||
// given
|
||||
Track track = new Track();
|
||||
track.setName("Best Track");
|
||||
track.setUuid(UUID.fromString("0000fee0-0000-1000-8000-00805f9b34fb"));
|
||||
track.getTrackStatistics().setStartTime(Instant.parse("2020-02-02T02:02:02Z"));
|
||||
|
||||
// when
|
||||
String filename = subject.format(track, TrackFileFormat.GPX);
|
||||
}
|
||||
}
|
||||
+68
-16
@@ -9,6 +9,7 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -16,28 +17,36 @@ import androidx.preference.PreferenceManager;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.rule.ServiceTestRule;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TimezoneRule;
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
@@ -67,12 +76,18 @@ import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExportImportTest {
|
||||
|
||||
private static final String TAG = ExportImportTest.class.getSimpleName();
|
||||
|
||||
@Rule
|
||||
public final ServiceTestRule mServiceRule = ServiceTestRule.withTimeout(5, TimeUnit.SECONDS);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
//For csv_export_only() as we the timezone is hardcoded in the expectation.
|
||||
@Rule
|
||||
public TimezoneRule timezoneRule = new TimezoneRule(TimeZone.getTimeZone("Europe/Berlin"));
|
||||
|
||||
@BeforeClass
|
||||
public static void preSetUp() {
|
||||
// Prepare looper for Android's message queue
|
||||
@@ -132,7 +147,7 @@ public class ExportImportTest {
|
||||
|
||||
Distance sensorDistance = Distance.of(10); // recording distance interval
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 15, 10, 1);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:03Z", 3, 14, 10, 13, 15, 10, 1);
|
||||
service.insertMarker("Marker 1", "Marker 1 category", "Marker 1 desc", null);
|
||||
|
||||
// A sensor-only TrackPoint
|
||||
@@ -143,7 +158,7 @@ public class ExportImportTest {
|
||||
trackPointCreator.setClock("2020-02-02T02:02:05Z");
|
||||
mockBLESensorData(trackPointCreator, 5f, Distance.of(2), 66f, 3f, 50f); // Distance will be added to next TrackPoint
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:05Z", 3, 14.001, 10, 13, 15, 10, 0);
|
||||
service.insertMarker("Marker 2", "Marker 2 category", "Marker 2 desc", null);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:06Z");
|
||||
@@ -153,11 +168,11 @@ public class ExportImportTest {
|
||||
trackPointCreator.setClock("2020-02-02T02:02:20Z");
|
||||
service.resumeCurrentTrack();
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:21Z", 3, 14.002, 10, 13, 15, 10, 0);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:22Z", 3, 16, 10, 13, 15, 10, 0);
|
||||
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 15, 10, 0);
|
||||
sendLocation(trackPointCreator, "2020-02-02T02:02:23Z", 3, 16.001, 10, 27, 15, 10, 0);
|
||||
|
||||
trackPointCreator.setClock("2020-02-02T02:02:24Z");
|
||||
trackPointCreator.setRemoteSensorManager(new BluetoothRemoteSensorManager(context, trackPointCreator));
|
||||
@@ -172,7 +187,7 @@ public class ExportImportTest {
|
||||
track = contentProviderUtils.getTrack(trackId);
|
||||
trackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, trackId);
|
||||
markers = contentProviderUtils.getMarkers(trackId);
|
||||
assertEquals(11, trackPoints.size());
|
||||
assertEquals(10, trackPoints.size());
|
||||
assertEquals(2, markers.size());
|
||||
}
|
||||
|
||||
@@ -206,8 +221,7 @@ public class ExportImportTest {
|
||||
assertEquals(track.getIcon(), importedTrack.getIcon());
|
||||
|
||||
// 2. trackpoints
|
||||
TrackPointAssert a = new TrackPointAssert()
|
||||
.noAccuracy();
|
||||
TrackPointAssert a = new TrackPointAssert();
|
||||
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
a.assertEquals(trackPoints, actual);
|
||||
|
||||
@@ -320,8 +334,7 @@ public class ExportImportTest {
|
||||
// Therefore, the track segmentation is changes.
|
||||
|
||||
TrackPointAssert a = new TrackPointAssert()
|
||||
.setDelta(0.05)
|
||||
.noAccuracy(); // speed is not fully
|
||||
.setDelta(0.05); // speed is not fully
|
||||
List<TrackPoint> actual = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
|
||||
a.assertEquals(List.of(
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:03Z"))
|
||||
@@ -330,7 +343,8 @@ public class ExportImportTest {
|
||||
.setAltitude(10)
|
||||
.setSpeed(Speed.of(15))
|
||||
.setAltitudeLoss(1f)
|
||||
.setAltitudeGain(1f),
|
||||
.setAltitudeGain(1f)
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:05Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(14.001)
|
||||
@@ -341,21 +355,24 @@ public class ExportImportTest {
|
||||
.setSensorDistance(Distance.of(12))
|
||||
.setHeartRate(66f)
|
||||
.setPower(50f)
|
||||
.setCadence(3f),
|
||||
.setCadence(3f)
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:21Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(14.002)
|
||||
.setAltitude(10)
|
||||
.setAltitudeLoss(0f)
|
||||
.setAltitudeGain(0f)
|
||||
.setSpeed(Speed.of(15)),
|
||||
.setSpeed(Speed.of(15))
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_AUTOMATIC, Instant.parse("2020-02-02T02:02:22Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(16)
|
||||
.setAltitude(10)
|
||||
.setAltitudeLoss(0f)
|
||||
.setAltitudeGain(0f)
|
||||
.setSpeed(Speed.of(15)),
|
||||
.setSpeed(Speed.of(15))
|
||||
.setHorizontalAccuracy(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse("2020-02-02T02:02:23Z"))
|
||||
.setLatitude(3)
|
||||
.setLongitude(16.001)
|
||||
@@ -363,6 +380,7 @@ public class ExportImportTest {
|
||||
.setAltitudeLoss(0f)
|
||||
.setAltitudeGain(0f)
|
||||
.setSpeed(Speed.of(15))
|
||||
.setHorizontalAccuracy(Distance.of(10))
|
||||
), actual);
|
||||
|
||||
// 3. trackstatistics
|
||||
@@ -423,6 +441,37 @@ public class ExportImportTest {
|
||||
assertNull(trackImported);
|
||||
}
|
||||
|
||||
@Ignore(value = "TODO Fails on CI; works on API24 and API30 locally")
|
||||
@LargeTest
|
||||
@Test
|
||||
public void csv_export_only() throws TimeoutException, IOException {
|
||||
setUp();
|
||||
|
||||
// given
|
||||
Track track = contentProviderUtils.getTrack(trackId);
|
||||
|
||||
TrackExporter trackExporter = TrackFileFormat.CSV.createTrackExporter(context);
|
||||
|
||||
// when
|
||||
// 1. export
|
||||
trackExporter.writeTrack(track, context.getContentResolver().openOutputStream(tmpFileUri));
|
||||
contentProviderUtils.deleteTrack(context, trackId);
|
||||
|
||||
// then
|
||||
InputStream expected = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.debug.test.R.raw.csv_export);
|
||||
String expectedText = new BufferedReader(new InputStreamReader(expected, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
|
||||
InputStream actual = context.getContentResolver().openInputStream(tmpFileUri);
|
||||
String actualText = new BufferedReader(new InputStreamReader(actual, StandardCharsets.UTF_8))
|
||||
.lines()
|
||||
.collect(Collectors.joining("\n"));
|
||||
|
||||
assertEquals(expectedText, actualText);
|
||||
}
|
||||
|
||||
private void assertMarkers() {
|
||||
assertEquals(markers.size(), contentProviderUtils.getMarkers(importTrackId).size());
|
||||
|
||||
@@ -469,7 +518,7 @@ public class ExportImportTest {
|
||||
return sensorDataSet;
|
||||
}
|
||||
});
|
||||
trackPointCreator.onChange(null);
|
||||
trackPointCreator.onChange(new SensorDataSet());
|
||||
}
|
||||
|
||||
private void mockAltitudeChange(TrackPointCreator trackPointCreator, float altitudeGain) {
|
||||
@@ -478,11 +527,14 @@ public class ExportImportTest {
|
||||
altitudeSumManager.setAltitudeLoss_m(altitudeGain);
|
||||
}
|
||||
|
||||
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float speed, float altitude, float altitudeGain) {
|
||||
private void sendLocation(TrackPointCreator trackPointCreator, String time, double latitude, double longitude, float accuracy, float verticalAccuracy, float speed, float altitude, float altitudeGain) {
|
||||
Location location = new Location("mock");
|
||||
location.setLatitude(latitude);
|
||||
location.setLongitude(longitude);
|
||||
location.setAccuracy(accuracy);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
location.setVerticalAccuracyMeters(verticalAccuracy);
|
||||
}
|
||||
location.setSpeed(speed);
|
||||
location.setAltitude(altitude);
|
||||
|
||||
|
||||
+7
-14
@@ -8,8 +8,6 @@ import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
|
||||
public class TrackPointAssert {
|
||||
|
||||
private boolean assertAccuracy = true;
|
||||
|
||||
private double delta = 0.001;
|
||||
|
||||
public TrackPointAssert() {
|
||||
@@ -45,13 +43,13 @@ public class TrackPointAssert {
|
||||
Assert.assertEquals(expected.getSpeed().toMPS(), actual.getSpeed().toMPS(), delta);
|
||||
}
|
||||
|
||||
if (assertAccuracy) {
|
||||
Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
|
||||
if (expected.hasHorizontalAccuracy()) {
|
||||
Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
|
||||
}
|
||||
} else {
|
||||
Assert.assertFalse(actual.hasHorizontalAccuracy());
|
||||
Assert.assertEquals(expected.hasHorizontalAccuracy(), actual.hasHorizontalAccuracy());
|
||||
if (expected.hasHorizontalAccuracy()) {
|
||||
Assert.assertEquals(expected.getHorizontalAccuracy().toM(), actual.getHorizontalAccuracy().toM(), delta);
|
||||
}
|
||||
Assert.assertEquals(expected.hasVerticalAccuracy(), actual.hasVerticalAccuracy());
|
||||
if (expected.hasVerticalAccuracy()) {
|
||||
Assert.assertEquals(expected.getVerticalAccuracy().toM(), actual.getVerticalAccuracy().toM(), delta);
|
||||
}
|
||||
|
||||
Assert.assertEquals(expected.hasSensorDistance(), actual.hasSensorDistance());
|
||||
@@ -96,11 +94,6 @@ public class TrackPointAssert {
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPointAssert noAccuracy() {
|
||||
this.assertAccuracy = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackPointAssert setDelta(double delta) {
|
||||
this.delta = delta;
|
||||
return this;
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@@ -48,7 +49,7 @@ public class TrackRecordingServiceNotificationManagerTest {
|
||||
.thenReturn(notificationCompatBuilder);
|
||||
|
||||
TrackRecordingServiceNotificationManager subject = new TrackRecordingServiceNotificationManager(notificationManager, notificationCompatBuilder);
|
||||
subject.setMetricUnits(true);
|
||||
subject.setUnitSystem(UnitSystem.METRIC);
|
||||
|
||||
// when
|
||||
subject.updateTrackPoint(context, trackStatisticsMock, trackPointMock, Distance.of(100));
|
||||
|
||||
+50
-4
@@ -612,6 +612,55 @@ public class TrackRecordingServiceTestRecording {
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
public void testRecording_gpsOnly_recordingDistance_movement_non_idle() {
|
||||
// given
|
||||
String startTime = "2020-02-02T02:02:02Z";
|
||||
TrackPointCreator trackPointCreator = service.getTrackPointCreator();
|
||||
trackPointCreator.setClock(startTime);
|
||||
Track.Id trackId = service.startNewTrack();
|
||||
trackPointCreator.setAltitudeSumManager(altitudeSumManager);
|
||||
|
||||
// when
|
||||
String gps1 = "2020-02-02T02:02:03Z";
|
||||
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps1, 45.0, 35.0, 1, 15);
|
||||
|
||||
// when - will be ignored
|
||||
String gps2 = "2020-02-02T02:02:04Z";
|
||||
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps2, 45.0, 35.0, 1, 15);
|
||||
|
||||
// when
|
||||
String gps3 = "2020-02-02T02:02:05Z";
|
||||
TrackRecordingServiceTestUtils.sendGPSLocation(trackPointCreator, gps3, 45.0001, 35.0, 1, 15);
|
||||
|
||||
// when
|
||||
String stopTime = "2020-02-02T02:02:12Z";
|
||||
trackPointCreator.setClock(stopTime);
|
||||
service.endCurrentTrack();
|
||||
|
||||
// then
|
||||
new TrackPointAssert().assertEquals(List.of(
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, Instant.parse(startTime)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps1))
|
||||
.setLatitude(45)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f)
|
||||
.setSpeed(Speed.of(15)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
|
||||
.setLatitude(45)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f)
|
||||
.setSpeed(Speed.of(15)),
|
||||
new TrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL, Instant.parse(stopTime))
|
||||
.setAltitudeGain(0f)
|
||||
.setAltitudeLoss(0f)
|
||||
), TestDataUtil.getTrackPoints(contentProviderUtils, trackId));
|
||||
}
|
||||
|
||||
@MediumTest
|
||||
@Test
|
||||
@@ -796,15 +845,12 @@ public class TrackRecordingServiceTestRecording {
|
||||
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor3))
|
||||
.setSpeed(Speed.of(5))
|
||||
.setSensorDistance(Distance.of(10)),
|
||||
new TrackPoint(TrackPoint.Type.SENSORPOINT, Instant.parse(sensor5)) //TODO No need to store this TrackPoint, data could be merged into the next one
|
||||
.setSpeed(Speed.of(5))
|
||||
.setSensorDistance(Distance.of(4)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps3))
|
||||
.setLatitude(45.001)
|
||||
.setLongitude(35)
|
||||
.setHorizontalAccuracy(Distance.of(1))
|
||||
.setSpeed(Speed.of(5))
|
||||
.setSensorDistance(Distance.of(0)),
|
||||
.setSensorDistance(Distance.of(4.0)),
|
||||
new TrackPoint(TrackPoint.Type.TRACKPOINT, Instant.parse(gps4))
|
||||
.setLatitude(45.001)
|
||||
.setLongitude(35)
|
||||
|
||||
+10
-9
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
||||
@@ -54,7 +55,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, null, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Speed 18.4 kilometers per hour.", announcement);
|
||||
@@ -76,7 +77,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour.", announcement);
|
||||
@@ -92,7 +93,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, null, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 20.00 kilometers. 1 hour 5 minutes 10 seconds. Pace 3 minutes 15 seconds per kilometer.", announcement);
|
||||
@@ -114,7 +115,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, false, lastInterval, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, false, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Pace 1 minute 10 seconds per kilometer. Lap time 1 minute 10 seconds per kilometer.", announcement);
|
||||
@@ -130,7 +131,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, null, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Speed 11.4 miles per hour.", announcement);
|
||||
@@ -152,7 +153,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, true, lastInterval, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, true, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Speed 31.8 miles per hour. Lap speed 31.8 miles per hour.", announcement);
|
||||
@@ -168,7 +169,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
stats.setTotalAltitudeGain(6000f);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, null, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, null, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 12.43 miles. 1 hour 5 minutes 10 seconds. Pace 5 minutes 15 seconds per mile.", announcement);
|
||||
@@ -190,7 +191,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
}
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, false, false, lastInterval, null).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.IMPERIAL, false, lastInterval, null).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 8.83 miles. 16 minutes 39 seconds. Pace 1 minute 53 seconds per mile. Lap time 1 minute 53 seconds per mile.", announcement);
|
||||
@@ -215,7 +216,7 @@ public class VoiceAnnouncementUtilsTest {
|
||||
SensorStatistics sensorStatistics = new SensorStatistics(HeartRate.of(180f), HeartRate.of(180f), null, null, null);
|
||||
|
||||
// when
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, true, true, lastInterval, sensorStatistics).toString();
|
||||
String announcement = VoiceAnnouncementUtils.getAnnouncement(context, stats, UnitSystem.METRIC, true, lastInterval, sensorStatistics).toString();
|
||||
|
||||
// then
|
||||
assertEquals("Total distance 14.21 kilometers. 16 minutes 39 seconds. Speed 51.2 kilometers per hour. Lap speed 51.2 kilometers per hour. Average heart rate 180 bpm. Current heart rate 133 bpm.", announcement);
|
||||
|
||||
@@ -22,7 +22,6 @@ import org.mockito.junit.MockitoJUnitRunner;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
@@ -62,7 +61,7 @@ public class GPSHandlerTest {
|
||||
locationHandler.onLocationChanged(createLocation(45f, 35f, 3, 5, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class));
|
||||
verify(trackPointCreator, times(1)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +77,7 @@ public class GPSHandlerTest {
|
||||
locationHandler.onLocationChanged(createLocation(latitude, 35f, 3, 5, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class));
|
||||
verify(trackPointCreator, times(0)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +93,7 @@ public class GPSHandlerTest {
|
||||
|
||||
// then
|
||||
// no newTrackPoint called
|
||||
verify(trackPointCreator, times(0)).onNewTrackPoint(any(TrackPoint.class));
|
||||
verify(trackPointCreator, times(0)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,7 +106,7 @@ public class GPSHandlerTest {
|
||||
locationHandler.onLocationChanged(createLocation(99.0, 35.0, Long.MAX_VALUE, 15, System.currentTimeMillis()));
|
||||
|
||||
// then
|
||||
verify(trackPointCreator, times(1)).onNewTrackPoint(any(TrackPoint.class));
|
||||
verify(trackPointCreator, times(1)).onChange(any(Location.class));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,8 +47,6 @@ import java.util.TimeZone;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.LocaleRule;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
|
||||
/**
|
||||
* Tests for {@link StringUtils}.
|
||||
@@ -91,21 +89,6 @@ public class StringUtilsTest {
|
||||
assertEquals("100:00:00", StringUtils.formatElapsedTimeWithHour(Duration.ofMillis(360000000)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#formatDistance(android.content.Context, Distance, boolean)}.
|
||||
*/
|
||||
@Test
|
||||
public void testFormatDistance() {
|
||||
// A large number in metric
|
||||
assertEquals("5.00 km", StringUtils.formatDistance(context, Distance.of(5000), true));
|
||||
// A large number in imperial
|
||||
assertEquals("3.11 mi", StringUtils.formatDistance(context, Distance.of(5000), false));
|
||||
// A small number in metric
|
||||
assertEquals("100.00 m", StringUtils.formatDistance(context, Distance.of(100), true));
|
||||
// A small number in imperial
|
||||
assertEquals("328.08 ft", StringUtils.formatDistance(context, Distance.of(100), false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link StringUtils#formatCData(String)}.
|
||||
*/
|
||||
@@ -183,27 +166,6 @@ public class StringUtilsTest {
|
||||
assertEquals("1.0", StringUtils.formatDecimal(0.99, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpeedParts() {
|
||||
assertEquals("4:59", StringUtils.getSpeedParts(context, Speed.of(3.34), true, false).first);
|
||||
assertEquals("5:00", StringUtils.getSpeedParts(context, Speed.of(3.33), true, false).first);
|
||||
|
||||
assertEquals("11.9", StringUtils.getSpeedParts(context, Speed.of(3.31), true, true).first);
|
||||
assertEquals("7.5", StringUtils.getSpeedParts(context, Speed.of(3.34), false, true).first);
|
||||
|
||||
assertEquals("min/km", StringUtils.getSpeedParts(context, Speed.zero(), true, false).second);
|
||||
assertEquals("min/mi", StringUtils.getSpeedParts(context, Speed.zero(), false, false).second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatSpeed() {
|
||||
assertEquals("4:59 min/km", StringUtils.formatSpeed(context, Speed.of(3.34), true, false));
|
||||
assertEquals("8:02 min/mi", StringUtils.formatSpeed(context, Speed.of(3.34), false, false));
|
||||
|
||||
assertEquals("12.0 km/h", StringUtils.formatSpeed(context, Speed.of(3.34), true, true));
|
||||
assertEquals("7.5 mph", StringUtils.formatSpeed(context, Speed.of(3.34), false, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormatDateTodayRelative() {
|
||||
// given
|
||||
|
||||
+3
-2
@@ -23,6 +23,7 @@ import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
||||
@@ -61,7 +62,7 @@ public class StatisticDataBuilderTest extends TestCase {
|
||||
layout.addField(context.getString(R.string.stats_custom_layout_max_speed_key), context.getString(R.string.stats_max_speed), false, true, false);
|
||||
|
||||
// when
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(statisticDataList.size(), 3);
|
||||
@@ -100,7 +101,7 @@ public class StatisticDataBuilderTest extends TestCase {
|
||||
layout.addField(context.getString(R.string.stats_custom_layout_power_key), context.getString(R.string.stats_sensors_power), true, true, false);
|
||||
|
||||
// when
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, true);
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(context, recordingDataMock, layout, UnitSystem.METRIC);
|
||||
|
||||
// then
|
||||
assertEquals(statisticDataList.size(), 18);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#time,trackpoint_type,latitude,longitude,altitude,accuracy_horizontal,accuracy_vertical,speed,altitude_gain,altitude_loss,sensor_distance,heartrate,cadence,power
|
||||
"2020-02-02T03:02:02+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:02:03+01:00","TRACKPOINT",3,3,10,10,,54,1,1,,,,
|
||||
"2020-02-02T03:02:04+01:00","SENSORPOINT",,,,,,54,1,1,10,66,3,50
|
||||
"2020-02-02T03:02:05+01:00","TRACKPOINT",3,3,10,10,,18,0,0,2,66,3,50
|
||||
"2020-02-02T03:02:06+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:02:20+01:00","SEGMENT_START_MANUAL",,,,,,,,,,,,
|
||||
"2020-02-02T03:02:21+01:00","TRACKPOINT",3,3,10,10,,54,0,0,,,,
|
||||
"2020-02-02T03:02:22+01:00","SEGMENT_START_AUTOMATIC",3,3,10,10,,54,0,0,,,,
|
||||
"2020-02-02T03:02:23+01:00","TRACKPOINT",3,3,10,10,,54,0,0,,,,
|
||||
"2020-02-02T03:02:24+01:00","SEGMENT_END_MANUAL",,,,,,,,,,,,
|
||||
|
@@ -4,8 +4,6 @@ import android.app.Application;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
/**
|
||||
@@ -28,9 +26,7 @@ public class Startup extends Application {
|
||||
PreferencesUtils.initPreferences(this, getResources());
|
||||
// Set default values of preferences on first start.
|
||||
PreferencesUtils.resetPreferences(this, false);
|
||||
if (PreferencesUtils.getString( R.string.stats_units_key, "").equals("")) {
|
||||
PreferencesUtils.setMetricUnits(!Locale.US.equals(Locale.getDefault()));
|
||||
}
|
||||
PreferencesUtils.applyDefaultUnit();
|
||||
PreferencesUtils.applyNightMode();
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.databinding.TrackListBinding;
|
||||
@@ -58,6 +59,7 @@ import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.services.handlers.GpsStatusValue;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.SettingsActivity;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.share.ShareUtils;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.AggregatedStatisticsActivity;
|
||||
import de.dennisguse.opentracks.ui.aggregatedStatistics.ConfirmDeleteDialogFragment;
|
||||
@@ -87,7 +89,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack();
|
||||
|
||||
// Preferences
|
||||
private boolean metricUnits = true;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
|
||||
private GpsStatusValue gpsStatusValue = TrackRecordingService.STATUS_GPS_DEFAULT;
|
||||
private RecordingStatus recordingStatus = TrackRecordingService.STATUS_DEFAULT;
|
||||
@@ -111,7 +113,7 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
|
||||
private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits();
|
||||
unitSystem = PreferencesUtils.getUnitSystem();
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -190,7 +192,9 @@ public class TrackListActivity extends AbstractTrackDeleteActivity implements Co
|
||||
int iconId = TrackIconUtils.getIconDrawable(icon);
|
||||
String name = cursor.getString(nameIndex);
|
||||
String totalTime = StringUtils.formatElapsedTime(Duration.ofMillis(cursor.getLong(totalTimeIndex)));
|
||||
String totalDistance = StringUtils.formatDistance(TrackListActivity.this, Distance.of(cursor.getDouble(totalDistanceIndex)), metricUnits);
|
||||
String totalDistance = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(TrackListActivity.this).formatDistance(Distance.of(cursor.getDouble(totalDistanceIndex)));
|
||||
int markerCount = cursor.getInt(markerCountIndex);
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
int startTimeOffset = cursor.getInt(startTimeOffsetIndex);
|
||||
|
||||
@@ -39,6 +39,7 @@ import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatisticsUpdater;
|
||||
|
||||
@@ -77,9 +78,9 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits();
|
||||
if (metricUnits != viewBinding.chartView.getMetricUnits()) {
|
||||
viewBinding.chartView.setMetricUnits(metricUnits);
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
if (unitSystem != viewBinding.chartView.getUnitSystem()) {
|
||||
viewBinding.chartView.setUnitSystem(unitSystem);
|
||||
runOnUiThread(() -> {
|
||||
if (isResumed()) {
|
||||
viewBinding.chartView.requestLayout();
|
||||
@@ -189,7 +190,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
|
||||
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull TrackStatistics trackStatistics, Speed smoothedSpeed, Altitude smoothedAltitude) {
|
||||
if (isResumed()) {
|
||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getMetricUnits());
|
||||
ChartPoint point = new ChartPoint(trackStatistics, trackPoint, smoothedSpeed, smoothedAltitude, chartByDistance, viewBinding.chartView.getUnitSystem());
|
||||
pendingPoints.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
|
||||
public class ChartPoint {
|
||||
@@ -28,20 +29,20 @@ public class ChartPoint {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, boolean metricUnits) {
|
||||
public ChartPoint(@NonNull TrackStatistics trackStatistics, @NonNull TrackPoint trackPoint, Speed smoothedSpeed, Altitude smoothedAltitude, boolean chartByDistance, UnitSystem unitSystem) {
|
||||
if (chartByDistance) {
|
||||
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(metricUnits);
|
||||
timeOrDistance = trackStatistics.getTotalDistance().toKM_Miles(unitSystem);
|
||||
} else {
|
||||
timeOrDistance = trackStatistics.getTotalTime().toMillis();
|
||||
}
|
||||
|
||||
if (smoothedAltitude != null) {
|
||||
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(metricUnits);
|
||||
altitude = Distance.of(smoothedAltitude.toM()).toM_FT(unitSystem);
|
||||
}
|
||||
|
||||
if (smoothedSpeed != null) {
|
||||
speed = smoothedSpeed.to(metricUnits);
|
||||
pace = smoothedSpeed.toPace(metricUnits).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
|
||||
speed = smoothedSpeed.to(unitSystem);
|
||||
pace = smoothedSpeed.toPace(unitSystem).toMillis() * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN;
|
||||
}
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
heartRate = (double) trackPoint.getHeartRate().getBPM();
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.ExtremityMonitor;
|
||||
|
||||
/**
|
||||
@@ -217,11 +218,15 @@ abstract class ChartValueSeries {
|
||||
return maxMarkerValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title id.
|
||||
*/
|
||||
int getTitleId(boolean metricUnits) {
|
||||
return metricUnits ? metricTitleId : imperialTitleId;
|
||||
int getTitleId(UnitSystem unitSystem) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return metricTitleId;
|
||||
case IMPERIAL:
|
||||
return imperialTitleId;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.ExtremityMonitor;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
@@ -116,7 +117,7 @@ public class ChartView extends View {
|
||||
private int effectiveHeight = 0;
|
||||
|
||||
private boolean chartByDistance = false;
|
||||
private boolean metricUnits = true;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
private boolean reportSpeed = true;
|
||||
private boolean showPointer = false;
|
||||
|
||||
@@ -371,17 +372,12 @@ public class ChartView extends View {
|
||||
this.chartByDistance = chartByDistance;
|
||||
}
|
||||
|
||||
public boolean getMetricUnits() {
|
||||
return metricUnits;
|
||||
public UnitSystem getUnitSystem() {
|
||||
return unitSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets metric units.
|
||||
*
|
||||
* @param value true to use metric units
|
||||
*/
|
||||
public void setMetricUnits(boolean value) {
|
||||
metricUnits = value;
|
||||
public void setUnitSystem(UnitSystem value) {
|
||||
unitSystem = value;
|
||||
}
|
||||
|
||||
public boolean getReportSpeed() {
|
||||
@@ -677,7 +673,7 @@ public class ChartView extends View {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
count++;
|
||||
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
|
||||
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
|
||||
Paint paint = chartValueSeries.getTitlePaint();
|
||||
int x = (int) (0.5 * width) + getScrollX();
|
||||
int y = topBorder - spacer - (lines - count) * (lineHeight + spacer);
|
||||
@@ -696,7 +692,7 @@ public class ChartView extends View {
|
||||
for (ChartValueSeries chartValueSeries : seriesList) {
|
||||
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
|
||||
lines++;
|
||||
String title = getContext().getString(chartValueSeries.getTitleId(metricUnits));
|
||||
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
|
||||
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
|
||||
if (rect.height() > lineHeight) {
|
||||
lineHeight = rect.height();
|
||||
@@ -731,7 +727,14 @@ public class ChartView extends View {
|
||||
private String getXAxisLabel() {
|
||||
Context context = getContext();
|
||||
if (chartByDistance) {
|
||||
return metricUnits ? context.getString(R.string.unit_kilometer) : context.getString(R.string.unit_mile);
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return context.getString(R.string.unit_kilometer);
|
||||
case IMPERIAL:
|
||||
return context.getString(R.string.unit_mile);
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
} else {
|
||||
return context.getString(R.string.description_time);
|
||||
}
|
||||
@@ -986,7 +989,7 @@ public class ChartView extends View {
|
||||
|
||||
private double getMarkerXValue(Marker marker) {
|
||||
if (chartByDistance) {
|
||||
return marker.getLength().toKM_Miles(metricUnits);
|
||||
return marker.getLength().toKM_Miles(unitSystem);
|
||||
} else {
|
||||
return marker.getDuration().toMillis();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class CachedTrackPointsIndexes {
|
||||
final int timeIndex;
|
||||
final int altitudeIndex;
|
||||
final int accuracyIndex;
|
||||
final int accuracyVerticalIndex;
|
||||
final int speedIndex;
|
||||
final int bearingIndex;
|
||||
final int sensorHeartRateIndex;
|
||||
@@ -32,6 +33,7 @@ class CachedTrackPointsIndexes {
|
||||
timeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.TIME);
|
||||
altitudeIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE);
|
||||
accuracyIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.HORIZONTAL_ACCURACY);
|
||||
accuracyVerticalIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.VERTICAL_ACCURACY);
|
||||
speedIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SPEED);
|
||||
bearingIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.BEARING);
|
||||
sensorHeartRateIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.SENSOR_HEARTRATE);
|
||||
|
||||
@@ -573,6 +573,9 @@ public class ContentProviderUtils {
|
||||
if (!cursor.isNull(indexes.accuracyIndex)) {
|
||||
trackPoint.setHorizontalAccuracy(Distance.of(cursor.getFloat(indexes.accuracyIndex)));
|
||||
}
|
||||
if (!cursor.isNull(indexes.accuracyVerticalIndex)) {
|
||||
trackPoint.setVerticalAccuracy(Distance.of(cursor.getFloat(indexes.accuracyVerticalIndex)));
|
||||
}
|
||||
if (!cursor.isNull(indexes.speedIndex)) {
|
||||
trackPoint.setSpeed(Speed.of(cursor.getFloat(indexes.speedIndex)));
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = CustomSQLiteOpenHelper.class.getSimpleName();
|
||||
|
||||
private static final int DATABASE_VERSION = 33;
|
||||
private static final int DATABASE_VERSION = 34;
|
||||
|
||||
public CustomSQLiteOpenHelper(Context context) {
|
||||
this(context, ((Startup) context.getApplicationContext()).getDatabaseName());
|
||||
@@ -91,6 +91,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 33:
|
||||
upgradeFrom32to33(db);
|
||||
break;
|
||||
case 34:
|
||||
upgradeFrom33to34(db);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: upgrade to " + toVersion);
|
||||
@@ -133,6 +136,9 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
case 32:
|
||||
downgradeFrom33to32(db);
|
||||
break;
|
||||
case 33:
|
||||
downgradeFrom34to33(db);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented: downgrade to " + toVersion);
|
||||
}
|
||||
@@ -206,7 +212,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE tracks ADD COLUMN uuid BLOB");
|
||||
try (Cursor cursor = db.query("tracks", new String[]{"_id"}, null, null, null, null, null)) {
|
||||
try (Cursor cursor = db.query("tracks", new String[]{"_id" }, null, null, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
|
||||
do {
|
||||
@@ -396,7 +402,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.beginTransaction();
|
||||
|
||||
// TrackPoints
|
||||
//TODO That does not restore deleted trackpoints
|
||||
// Info: this does not restore deleted trackpoints
|
||||
db.execSQL("UPDATE trackpoints SET latitude = 200 * 1E6, longitude = NULL WHERE type = -2");
|
||||
db.execSQL("UPDATE trackpoints SET latitude = 100 * 1E6, longitude = NULL WHERE type = 1");
|
||||
|
||||
@@ -481,7 +487,7 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
ZoneRules zoneRules = ZoneOffset.systemDefault().getRules();
|
||||
|
||||
try (Cursor cursor = db.query("tracks", new String[]{"_id", "starttime"}, null, null, null, null, null)) {
|
||||
try (Cursor cursor = db.query("tracks", new String[]{"_id", "starttime" }, null, null, null, null, null)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
int trackIdIndex = cursor.getColumnIndexOrThrow("_id");
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow("starttime");
|
||||
@@ -513,6 +519,33 @@ public class CustomSQLiteOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("CREATE UNIQUE INDEX tracks_uuid_index ON tracks(uuid)");
|
||||
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add accuracy_vertical
|
||||
*/
|
||||
private void upgradeFrom33to34(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE trackpoints ADD COLUMN accuracy_vertical FLOAT");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
private void downgradeFrom34to33(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
|
||||
db.execSQL("ALTER TABLE trackpoints RENAME TO trackpoints_old");
|
||||
db.execSQL("CREATE TABLE trackpoints (_id INTEGER PRIMARY KEY AUTOINCREMENT, trackid INTEGER NOT NULL, longitude INTEGER, latitude INTEGER, time INTEGER, elevation FLOAT, accuracy FLOAT, speed FLOAT, bearing FLOAT, sensor_heartrate FLOAT, sensor_cadence FLOAT, sensor_power FLOAT, elevation_gain FLOAT, elevation_loss FLOAT, type TEXT CHECK(type IN (-2, -1, 0, 1)), sensor_distance FLOAT, FOREIGN KEY (trackid) REFERENCES tracks(_id) ON UPDATE CASCADE ON DELETE CASCADE)");
|
||||
db.execSQL("INSERT INTO trackpoints SELECT _id, trackid, longitude, latitude, time, elevation, accuracy, speed, bearing, sensor_heartrate, sensor_cadence, sensor_power, elevation_gain, elevation_gain, type, sensor_distance FROM trackpoints_old");
|
||||
db.execSQL("DROP TABLE trackpoints_old");
|
||||
|
||||
db.execSQL("CREATE INDEX trackpoints_trackid_index ON trackpoints(trackid)");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
|
||||
private static final int URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA = 6;
|
||||
private static final int URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES = 7;
|
||||
|
||||
private static final int URI_SHARE_PICTURE = 8;
|
||||
private static final int URI_CSV = 8;
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
private static final String TRACKID_DELIMITER = "_";
|
||||
@@ -66,6 +65,7 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_AND_SENSORDATA);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES.getName() + "/*/*", URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES);
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY_PACKAGE, TracksColumns.TABLE_NAME + "/" + TrackFileFormat.CSV.getName() + "/*/*", URI_CSV);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -146,6 +146,9 @@ public class ShareContentProvider extends CustomContentProvider {
|
||||
case URI_KMZ_WITH_TRACKDETAIL_SENSORDATA_AND_PICTURES:
|
||||
return TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES;
|
||||
|
||||
case URI_CSV:
|
||||
return TrackFileFormat.CSV;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Could not derive TrackFileFormat from Uri " + uri);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
public class Distance {
|
||||
|
||||
public static Distance of(double distance_m) {
|
||||
@@ -14,6 +17,14 @@ public class Distance {
|
||||
return of(Float.parseFloat(distance_m));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Distance ofOrNull(Double distance_m) {
|
||||
if (distance_m == null) {
|
||||
return null;
|
||||
}
|
||||
return of(distance_m);
|
||||
}
|
||||
|
||||
public static Distance ofMile(double distance_mile) {
|
||||
return of(distance_mile * UnitConversions.MI_TO_M);
|
||||
}
|
||||
@@ -34,11 +45,14 @@ public class Distance {
|
||||
return of(0.1 * distance_dm);
|
||||
}
|
||||
|
||||
public static Distance one(boolean metricUnit) {
|
||||
if (metricUnit) {
|
||||
return Distance.ofKilometer(1);
|
||||
} else {
|
||||
return Distance.ofMile(1);
|
||||
public static Distance one(UnitSystem unitSystem) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return Distance.ofKilometer(1);
|
||||
case IMPERIAL:
|
||||
return Distance.ofMile(1);
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +114,26 @@ public class Distance {
|
||||
return toKM() * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
public double toKM_Miles(boolean metricUnit) {
|
||||
return metricUnit ? toKM() : toMI();
|
||||
public double toKM_Miles(UnitSystem unitSystem) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return toKM();
|
||||
case IMPERIAL:
|
||||
return toMI();
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
public double toM_FT(boolean metricUnit) {
|
||||
return metricUnit ? toM() : toFT();
|
||||
public double toM_FT(UnitSystem unitSystem) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return toM();
|
||||
case IMPERIAL:
|
||||
return toFT();
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class DistanceFormatter {
|
||||
|
||||
private final Resources resources;
|
||||
|
||||
private final int decimalCount;
|
||||
|
||||
private final UnitSystem unitSystem;
|
||||
|
||||
private DistanceFormatter(Resources resources, int decimalCount, UnitSystem unitSystem) {
|
||||
this.resources = resources;
|
||||
this.decimalCount = decimalCount;
|
||||
this.unitSystem = unitSystem;
|
||||
assert unitSystem != null;
|
||||
}
|
||||
|
||||
public String formatDistance(Distance distance) {
|
||||
if (distance.isInvalid()) {
|
||||
return resources.getString(R.string.value_unknown);
|
||||
}
|
||||
|
||||
Pair<String, String> distanceParts = getDistanceParts(distance);
|
||||
|
||||
return resources.getString(R.string.distance_with_unit, distanceParts.first, distanceParts.second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted distance with unit.
|
||||
*
|
||||
* @return the formatted distance (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public Pair<String, String> getDistanceParts(Distance distance) {
|
||||
if (distance.isInvalid()) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return new Pair<>(null, resources.getString(R.string.unit_meter));
|
||||
case IMPERIAL:
|
||||
return new Pair<>(null, resources.getString(R.string.unit_feet));
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
if (distance.greaterThan(Distance.of(500))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toKM(), decimalCount), resources.getString(R.string.unit_kilometer));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toM(), decimalCount), resources.getString(R.string.unit_meter));
|
||||
}
|
||||
case IMPERIAL:
|
||||
if (distance.greaterThan(Distance.ofMile(0.5))) {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toMI(), decimalCount), resources.getString(R.string.unit_mile));
|
||||
} else {
|
||||
return new Pair<>(StringUtils.formatDecimal(distance.toFT(), decimalCount), resources.getString(R.string.unit_feet));
|
||||
}
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder Builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private int decimalCount;
|
||||
|
||||
private UnitSystem unitSystem;
|
||||
|
||||
public Builder() {
|
||||
decimalCount = 2;
|
||||
}
|
||||
|
||||
public Builder setDecimalCount(int decimalCount) {
|
||||
this.decimalCount = decimalCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnit(@Nullable UnitSystem unitSystem) {
|
||||
this.unitSystem = unitSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DistanceFormatter build(Resources resource) {
|
||||
return new DistanceFormatter(resource, decimalCount, unitSystem);
|
||||
}
|
||||
|
||||
public DistanceFormatter build(Context context) {
|
||||
return build(context.getResources());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
public class Speed {
|
||||
|
||||
@@ -92,35 +93,37 @@ public class Speed {
|
||||
return toKMH() * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
public Duration toPace(boolean metricUnit) {
|
||||
public Duration toPace(UnitSystem unitSystem) {
|
||||
if (isZero()) {
|
||||
return Duration.ofSeconds(0);
|
||||
}
|
||||
|
||||
double distance = speed_mps * (metricUnit ? UnitConversions.M_TO_KM : UnitConversions.M_TO_MI);
|
||||
double distance = speed_mps;
|
||||
switch (unitSystem) { //TODO Can we use to(UnitSystem)?
|
||||
case METRIC:
|
||||
distance *= UnitConversions.M_TO_KM;
|
||||
break;
|
||||
case IMPERIAL:
|
||||
distance *= UnitConversions.M_TO_MI;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
return Duration.ofSeconds(Math.round(1 / distance));
|
||||
}
|
||||
|
||||
public double to(boolean metricUnit) {
|
||||
return to(metricUnit ? Unit.KMH : Unit.MPH);
|
||||
}
|
||||
|
||||
public double to(Unit unit) {
|
||||
switch (unit) {
|
||||
case KMH:
|
||||
public double to(UnitSystem unitSystem) {
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
return toKMH();
|
||||
case MPH:
|
||||
case IMPERIAL:
|
||||
return toMPH();
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
public enum Unit {
|
||||
KMH,
|
||||
MPH,
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class SpeedFormatter {
|
||||
|
||||
private final Resources resources;
|
||||
|
||||
private final int decimalCount;
|
||||
|
||||
private final UnitSystem unitSystem;
|
||||
|
||||
private final boolean reportSpeedOrPace;
|
||||
|
||||
private SpeedFormatter(Resources resources, int decimalCount, UnitSystem unitSystem, boolean reportSpeedOrPace) {
|
||||
this.resources = resources;
|
||||
this.decimalCount = decimalCount;
|
||||
this.unitSystem = unitSystem;
|
||||
this.reportSpeedOrPace = reportSpeedOrPace;
|
||||
}
|
||||
|
||||
public String formatSpeed(Speed speed) {
|
||||
Pair<String, String> distanceParts = getSpeedParts(speed);
|
||||
|
||||
return resources.getString(R.string.speed_with_unit, distanceParts.first, distanceParts.second);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the formatted speed with unit.
|
||||
*
|
||||
* @return the formatted speed (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public Pair<String, String> getSpeedParts(Speed speed) {
|
||||
int unitId;
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
unitId = reportSpeedOrPace ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
|
||||
break;
|
||||
case IMPERIAL:
|
||||
unitId = reportSpeedOrPace ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
String unitString = resources.getString(unitId);
|
||||
|
||||
if (speed == null) {
|
||||
speed = Speed.zero();
|
||||
}
|
||||
|
||||
if (reportSpeedOrPace) {
|
||||
return new Pair<>(StringUtils.formatDecimal(speed.to(unitSystem), 1), unitString);
|
||||
}
|
||||
|
||||
int pace = (int) speed.toPace(unitSystem).getSeconds();
|
||||
|
||||
int minutes = pace / 60;
|
||||
int seconds = pace % 60;
|
||||
return new Pair<>(resources.getString(R.string.time, minutes, seconds), unitString);
|
||||
}
|
||||
|
||||
public static Builder Builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private int decimalCount;
|
||||
|
||||
private UnitSystem unitSystem;
|
||||
|
||||
private boolean reportSpeedOrPace;
|
||||
|
||||
public Builder() {
|
||||
decimalCount = 2;
|
||||
reportSpeedOrPace = true;
|
||||
}
|
||||
|
||||
public Builder setDecimalCount(int decimalCount) {
|
||||
this.decimalCount = decimalCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUnit(@NonNull UnitSystem unitSystem) {
|
||||
this.unitSystem = unitSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setReportSpeedOrPace(boolean reportSpeedOrPace) {
|
||||
this.reportSpeedOrPace = reportSpeedOrPace;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpeedFormatter build(Resources resource) {
|
||||
return new SpeedFormatter(resource, decimalCount, unitSystem, reportSpeedOrPace);
|
||||
}
|
||||
|
||||
public SpeedFormatter build(Context context) {
|
||||
return build(context.getResources());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import android.location.Location;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -44,6 +45,7 @@ public class TrackPoint {
|
||||
private Double latitude;
|
||||
private Double longitude;
|
||||
private Distance horizontalAccuracy;
|
||||
private Distance verticalAccuracy;
|
||||
private Altitude altitude;
|
||||
private Speed speed;
|
||||
private Float bearing;
|
||||
@@ -205,6 +207,9 @@ public class TrackPoint {
|
||||
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
|
||||
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
|
||||
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
this.verticalAccuracy = location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null;
|
||||
}
|
||||
|
||||
//TODO Should we copy the bearing?
|
||||
return this;
|
||||
@@ -309,6 +314,19 @@ public class TrackPoint {
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasVerticalAccuracy() {
|
||||
return verticalAccuracy != null;
|
||||
}
|
||||
|
||||
public Distance getVerticalAccuracy() {
|
||||
return verticalAccuracy;
|
||||
}
|
||||
|
||||
public TrackPoint setVerticalAccuracy(Distance horizontalAccuracy) {
|
||||
this.verticalAccuracy = horizontalAccuracy;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
if (hasSensorDistance()) {
|
||||
|
||||
@@ -46,6 +46,7 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
String TIME = "time";
|
||||
String ALTITUDE = "elevation";
|
||||
String HORIZONTAL_ACCURACY = "accuracy";
|
||||
String VERTICAL_ACCURACY = "accuracy_vertical";
|
||||
String SPEED = "speed";
|
||||
String BEARING = "bearing";
|
||||
String SENSOR_HEARTRATE = "sensor_heartrate";
|
||||
@@ -79,6 +80,7 @@ public interface TrackPointsColumns extends BaseColumns {
|
||||
+ ALTITUDE_LOSS + " FLOAT, "
|
||||
+ TYPE + " TEXT CHECK(type IN (-2, -1, 0, 1, 2)), "
|
||||
+ SENSOR_DISTANCE + " FLOAT, "
|
||||
+ VERTICAL_ACCURACY + " FLOAT, "
|
||||
+ "FOREIGN KEY (" + TRACKID + ") REFERENCES " + TracksColumns.TABLE_NAME + "(" + TracksColumns._ID + ") ON UPDATE CASCADE ON DELETE CASCADE"
|
||||
+ ")";
|
||||
|
||||
|
||||
@@ -37,9 +37,12 @@ import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackRecordedActivity;
|
||||
import de.dennisguse.opentracks.adapters.StatisticsAdapter;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.StatisticsRecordedBinding;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
@@ -79,7 +82,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
private StatisticsRecordedBinding viewBinding;
|
||||
|
||||
private boolean preferenceMetricUnits;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
private boolean preferenceReportSpeed;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
@@ -87,7 +90,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
|
||||
unitSystem = PreferencesUtils.getUnitSystem();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.stats_rate_key, key) && track != null) {
|
||||
@@ -186,7 +189,9 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
TrackStatistics trackStatistics = track.getTrackStatistics();
|
||||
// Set total distance
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(getContext(), trackStatistics.getTotalDistance(), preferenceMetricUnits);
|
||||
Pair<String, String> parts = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext()).getDistanceParts(trackStatistics.getTotalDistance());
|
||||
|
||||
viewBinding.statsDistanceValue.setText(parts.first);
|
||||
viewBinding.statsDistanceUnit.setText(parts.second);
|
||||
@@ -204,11 +209,12 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
viewBinding.statsTotalTimeValue.setText(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()));
|
||||
}
|
||||
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(preferenceReportSpeed).build(getContext());
|
||||
// Set average speed/pace
|
||||
{
|
||||
viewBinding.statsAverageSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_speed : R.string.stats_average_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageSpeed(), preferenceMetricUnits, preferenceReportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getAverageSpeed());
|
||||
viewBinding.statsAverageSpeedValue.setText(parts.first);
|
||||
viewBinding.statsAverageSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -217,7 +223,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
{
|
||||
viewBinding.statsMaxSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_max_speed : R.string.stats_fastest_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getMaxSpeed(), preferenceMetricUnits, preferenceReportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getMaxSpeed());
|
||||
viewBinding.statsMaxSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMaxSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -226,7 +232,7 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
{
|
||||
viewBinding.statsMovingSpeedLabel.setText(preferenceReportSpeed ? R.string.stats_average_moving_speed : R.string.stats_average_moving_pace);
|
||||
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(getContext(), trackStatistics.getAverageMovingSpeed(), preferenceMetricUnits, preferenceReportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(trackStatistics.getAverageMovingSpeed());
|
||||
viewBinding.statsMovingSpeedValue.setText(parts.first);
|
||||
viewBinding.statsMovingSpeedUnit.setText(parts.second);
|
||||
}
|
||||
@@ -238,11 +244,11 @@ public class StatisticsRecordedFragment extends Fragment {
|
||||
|
||||
Pair<String, String> parts;
|
||||
|
||||
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeGain_m, preferenceMetricUnits);
|
||||
parts = StringUtils.getAltitudeParts(getContext(), altitudeGain_m, unitSystem);
|
||||
viewBinding.statsAltitudeGainValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeGainUnit.setText(parts.second);
|
||||
|
||||
parts = StringUtils.getAltitudeChangeParts(getContext(), altitudeLoss_m, preferenceMetricUnits);
|
||||
parts = StringUtils.getAltitudeParts(getContext(), altitudeLoss_m, unitSystem);
|
||||
viewBinding.statsAltitudeLossValue.setText(parts.first);
|
||||
viewBinding.statsAltitudeLossUnit.setText(parts.second);
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticData;
|
||||
import de.dennisguse.opentracks.viewmodels.StatisticsDataModel;
|
||||
@@ -55,14 +56,14 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
private StatisticsDataModel viewModel;
|
||||
private LiveData<List<StatisticData>> statisticsLiveData;
|
||||
|
||||
private boolean preferenceMetricUnits;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
boolean updateUInecessary = false;
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
|
||||
updateUInecessary = true;
|
||||
preferenceMetricUnits = PreferencesUtils.isMetricUnits();
|
||||
unitSystem = PreferencesUtils.getUnitSystem();
|
||||
}
|
||||
|
||||
if (PreferencesUtils.isKey(R.string.stats_custom_layouts_key, key) || PreferencesUtils.isKey(R.string.stats_custom_layout_selected_layout_key, key)) {
|
||||
@@ -157,7 +158,7 @@ public class StatisticsRecordingFragment extends Fragment {
|
||||
|
||||
private void updateUI() {
|
||||
if (isResumed()) {
|
||||
viewModel.update(recordingData, layout, preferenceMetricUnits);
|
||||
viewModel.update(recordingData, layout, unitSystem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.io.file.exporter.CSVTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.GPXTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KMLTrackExporter;
|
||||
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
|
||||
@@ -100,6 +101,23 @@ public enum TrackFileFormat {
|
||||
public String getExtension() {
|
||||
return "gpx";
|
||||
}
|
||||
},
|
||||
|
||||
CSV {
|
||||
@Override
|
||||
public TrackExporter createTrackExporter(Context context) {
|
||||
return new CSVTrackExporter(new ContentProviderUtils(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMimeType() {
|
||||
return "text/csv";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return "csv";
|
||||
}
|
||||
};
|
||||
|
||||
private static final String MIME_KMZ = "application/vnd.google-earth.kmz";
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package de.dennisguse.opentracks.io.file;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class TrackFilenameGenerator {
|
||||
|
||||
public static final String UUID_KEY = "{uuid}";
|
||||
public static final String TRACKNAME_KEY = "{name}";
|
||||
public static final String CATEGORY_KEY = "{category}";
|
||||
public static final String STARTTIME_KEY = "{time}";
|
||||
|
||||
public static String getAllOptions() {
|
||||
return Stream.of(UUID_KEY, TRACKNAME_KEY, CATEGORY_KEY, STARTTIME_KEY)
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
private final String template;
|
||||
|
||||
public TrackFilenameGenerator(@NonNull String template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
public String format(@NonNull Track track, @NonNull TrackFileFormat trackFileFormat) {
|
||||
Map<String, String> values = new HashMap<>();
|
||||
|
||||
values.put(UUID_KEY, track.getUuid().toString().substring(0, 8));
|
||||
values.put(TRACKNAME_KEY, track.getName());
|
||||
values.put(CATEGORY_KEY, track.getCategory());
|
||||
values.put(STARTTIME_KEY, track.getStartTime().toString());
|
||||
|
||||
return FileUtils.sanitizeFileName(format(template, values)) + "." + trackFileFormat.getExtension();
|
||||
}
|
||||
|
||||
private static String format(String template, Map<String, String> values) {
|
||||
StringBuilder templateCompiler = new StringBuilder(template);
|
||||
List<String> valueList = new ArrayList<>();
|
||||
|
||||
Matcher keyMatcher = Pattern
|
||||
.compile("\\{(\\w+)\\}")
|
||||
.matcher(template);
|
||||
|
||||
while (keyMatcher.find()) {
|
||||
String key = keyMatcher.group();
|
||||
|
||||
if (!values.containsKey(key)) {
|
||||
throw new TemplateInvalidException(key);
|
||||
}
|
||||
|
||||
int index = templateCompiler.indexOf(key);
|
||||
if (index != -1) {
|
||||
templateCompiler.replace(index, index + key.length(), "%s");
|
||||
valueList.add(values.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
String templateCompiled = templateCompiler.toString();
|
||||
if (templateCompiled.contains("{") || templateCompiled.contains("}")) {
|
||||
throw new TemplateInvalidException(template);
|
||||
}
|
||||
|
||||
return String.format(templateCompiled, valueList.toArray());
|
||||
}
|
||||
|
||||
public String getTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
try {
|
||||
getExample();
|
||||
return !template.isEmpty();
|
||||
} catch (TemplateInvalidException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getExample() {
|
||||
Track track = new Track();
|
||||
track.setName("Berlin");
|
||||
track.setUuid(UUID.fromString("fefefefefe-0000-1000-8000-00805f9b34fb"));
|
||||
track.getTrackStatistics().setStartTime(Instant.ofEpochMilli(0));
|
||||
|
||||
return format(track, TrackFileFormat.GPX);
|
||||
}
|
||||
|
||||
public static class TemplateInvalidException extends RuntimeException {
|
||||
public TemplateInvalidException(String invalidTemplate) {
|
||||
super(invalidTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package de.dennisguse.opentracks.io.file.exporter;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Function;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Exports the {@link TrackPoint} into a CSV.
|
||||
* * decimal separator: .
|
||||
* * column separator: ,
|
||||
* <p>
|
||||
* NOTE:
|
||||
* * {@link Track} data is not exported.
|
||||
* * {@link Marker} data is not exported.
|
||||
*/
|
||||
public class CSVTrackExporter implements TrackExporter {
|
||||
|
||||
private static final String TAG = CSVTrackExporter.class.getSimpleName();
|
||||
|
||||
private static final NumberFormat ALTITUDE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat SPEED_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat DISTANCE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat HEARTRATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat CADENCE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat POWER_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
|
||||
static {
|
||||
ALTITUDE_FORMAT.setMaximumFractionDigits(1);
|
||||
ALTITUDE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
COORDINATE_FORMAT.setMaximumFractionDigits(6);
|
||||
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
|
||||
COORDINATE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
SPEED_FORMAT.setMaximumFractionDigits(2);
|
||||
SPEED_FORMAT.setGroupingUsed(false);
|
||||
|
||||
HEARTRATE_FORMAT.setMaximumFractionDigits(0);
|
||||
HEARTRATE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
CADENCE_FORMAT.setMaximumFractionDigits(0);
|
||||
CADENCE_FORMAT.setGroupingUsed(false);
|
||||
|
||||
POWER_FORMAT.setMaximumFractionDigits(0);
|
||||
POWER_FORMAT.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private final ContentProviderUtils contentProviderUtils;
|
||||
|
||||
private PrintWriter printWriter;
|
||||
|
||||
public CSVTrackExporter(ContentProviderUtils contentProviderUtils) {
|
||||
this.contentProviderUtils = contentProviderUtils;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track track, @NonNull OutputStream outputStream) {
|
||||
return writeTrack(new Track[]{track}, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean writeTrack(Track[] tracks, @NonNull OutputStream outputStream) {
|
||||
List<Column> columns = Collections.unmodifiableList(Arrays.asList(
|
||||
new Column("time", null),
|
||||
new Column("trackpoint_type", t -> quote(t.getType().name())),
|
||||
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
|
||||
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getLatitude()) : ""),
|
||||
new Column("altitude", t -> t.hasAltitude() ? COORDINATE_FORMAT.format(t.getAltitude().toM()) : ""),
|
||||
new Column("accuracy_horizontal", t -> t.hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.getHorizontalAccuracy().toM()) : ""),
|
||||
new Column("accuracy_vertical", t -> t.hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.getVerticalAccuracy().toM()) : ""),
|
||||
|
||||
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
|
||||
new Column("altitude_gain", t -> t.hasAltitudeGain() ? DISTANCE_FORMAT.format(t.getAltitudeGain()) : ""),
|
||||
new Column("altitude_loss", t -> t.hasAltitudeLoss() ? DISTANCE_FORMAT.format(t.getAltitudeLoss()) : ""),
|
||||
new Column("sensor_distance", t -> t.hasSensorDistance() ? DISTANCE_FORMAT.format(t.getSensorDistance().toM()) : ""),
|
||||
new Column("heartrate", t -> t.hasHeartRate() ? HEARTRATE_FORMAT.format(t.getHeartRate().getBPM()) : ""),
|
||||
new Column("cadence", t -> t.hasCadence() ? CADENCE_FORMAT.format(t.getCadence().getRPM()) : ""),
|
||||
new Column("power", t -> t.hasPower() ? ALTITUDE_FORMAT.format(t.getPower().getW()) : "")
|
||||
));
|
||||
|
||||
try {
|
||||
prepare(outputStream);
|
||||
|
||||
boolean headerWritten = false;
|
||||
|
||||
for (Track track : tracks) {
|
||||
columns.get(0).extractor = t -> quote(StringUtils.formatDateTimeIso8601(t.getTime(), track.getZoneOffset()));
|
||||
|
||||
if (!headerWritten) {
|
||||
writeHeader(columns);
|
||||
headerWritten = true;
|
||||
}
|
||||
|
||||
writeTrackPoints(columns, track);
|
||||
}
|
||||
|
||||
close();
|
||||
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Thread interrupted", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTrackPoints(List<Column> columns, Track track) throws InterruptedException {
|
||||
try (TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null)) {
|
||||
while (trackPointIterator.hasNext()) {
|
||||
if (Thread.interrupted()) throw new InterruptedException();
|
||||
|
||||
TrackPoint trackPoint = trackPointIterator.next();
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
case SEGMENT_START_MANUAL:
|
||||
case SEGMENT_END_MANUAL:
|
||||
case SEGMENT_START_AUTOMATIC:
|
||||
case SENSORPOINT:
|
||||
case TRACKPOINT:
|
||||
writeTrackPoint(columns, trackPoint);
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void writeHeader(List<Column> columns) {
|
||||
if (printWriter != null) {
|
||||
String columnNames = columns.stream().map(c -> c.columnName)
|
||||
.reduce((s, s2) -> s + "," + s2)
|
||||
.orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||
printWriter.println("#" + columnNames);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeTrackPoint(List<Column> columns, TrackPoint trackPoint) {
|
||||
if (printWriter != null) {
|
||||
String columnNames = columns.stream().map(c -> c.extractor.apply(trackPoint))
|
||||
.reduce((s, s2) -> s + "," + s2)
|
||||
.orElseThrow(() -> new RuntimeException("No columns defined"));
|
||||
printWriter.println(columnNames);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Column {
|
||||
final String columnName;
|
||||
Function<TrackPoint, String> extractor;
|
||||
|
||||
Column(String columnName, Function<TrackPoint, String> extractor) {
|
||||
this.columnName = columnName;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
}
|
||||
|
||||
private static String quote(String content) {
|
||||
return '"' + content + '"';
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import java.util.stream.Stream;
|
||||
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
@@ -128,6 +129,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
private void writeTrackPoints(Track track) throws InterruptedException {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
Distance trackDistance = Distance.of(0);
|
||||
|
||||
LinkedList<TrackPoint> sensorPoints = new LinkedList<>();
|
||||
|
||||
@@ -159,7 +161,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
|
||||
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
|
||||
sensorPoints.clear();
|
||||
break;
|
||||
case SENSORPOINT:
|
||||
@@ -172,7 +174,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
wroteSegment = true;
|
||||
}
|
||||
|
||||
writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints);
|
||||
trackDistance = trackDistance.plus(writeTrackPoint(track.getZoneOffset(), trackPoint, sensorPoints, trackDistance));
|
||||
sensorPoints.clear();
|
||||
break;
|
||||
default:
|
||||
@@ -225,6 +227,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
printWriter.println("xmlns:opentracks=\"http://opentracksapp.com/xmlschemas/v1\"");
|
||||
printWriter.println("xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v2\"");
|
||||
printWriter.println("xmlns:gpxtrkx=\"http://www.garmin.com/xmlschemas/TrackStatsExtension/v1\"");
|
||||
printWriter.println("xmlns:cluetrust=\"http://www.cluetrust.com/Schemas/\"");
|
||||
printWriter.println("xmlns:pwr=\"http://www.garmin.com/xmlschemas/PowerExtension/v1\"");
|
||||
printWriter.println("xsi:schemaLocation=" +
|
||||
"\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"
|
||||
@@ -232,6 +235,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
+ " http://www.garmin.com/xmlschemas/TrackPointExtension/v2 https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd"
|
||||
+ " http://www.garmin.com/xmlschemas/PowerExtension/v1 https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd"
|
||||
+ " http://www.garmin.com/xmlschemas/TrackStatsExtension/v1"
|
||||
+ " http://www.cluetrust.com/Schemas http://www.cluetrust.com/Schemas/gpxdata10.xsd"
|
||||
+ " http://opentracksapp.com/xmlschemas/v1 http://opentracksapp.com/xmlschemas/OpenTracks_v1.xsd\">");
|
||||
}
|
||||
}
|
||||
@@ -312,7 +316,9 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
printWriter.println("</trkseg>");
|
||||
}
|
||||
|
||||
public void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints) {
|
||||
public Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
|
||||
Distance cumulativeDistance = null;
|
||||
|
||||
if (printWriter != null) {
|
||||
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getLatitude(), trackPoint.getLongitude()) + ">");
|
||||
@@ -352,9 +358,17 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
trackPointExtensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
|
||||
}
|
||||
|
||||
Double cumulativeDistance = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null);
|
||||
if (trackPoint.hasHorizontalAccuracy()) {
|
||||
trackPointExtensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
|
||||
}
|
||||
if (trackPoint.hasVerticalAccuracy()) {
|
||||
trackPointExtensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
|
||||
}
|
||||
|
||||
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null));
|
||||
if (cumulativeDistance != null) {
|
||||
trackPointExtensionContent += ("<opentracks:distance>" + ALTITUDE_FORMAT.format(cumulativeDistance) + "</opentracks:distance>\n");
|
||||
trackPointExtensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
|
||||
trackPointExtensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
|
||||
}
|
||||
|
||||
if (!trackPointExtensionContent.isEmpty()) {
|
||||
@@ -366,6 +380,10 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
|
||||
printWriter.println("</trkpt>");
|
||||
}
|
||||
if (cumulativeDistance != null) {
|
||||
return cumulativeDistance;
|
||||
}
|
||||
return Distance.of(0);
|
||||
}
|
||||
|
||||
private Double cumulateSensorData(TrackPoint trackPoint, List<TrackPoint> sensorPoints, Function<TrackPoint, Double> map) {
|
||||
|
||||
@@ -65,6 +65,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
public static final String EXTENDED_DATA_TYPE_POWER = "power";
|
||||
public static final String EXTENDED_DATA_TYPE_ALTITUDE_GAIN = "elevation_gain";
|
||||
public static final String EXTENDED_DATA_TYPE_ALTITUDE_LOSS = "elevation_loss";
|
||||
public static final String EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL = "accuracy_horizontal";
|
||||
public static final String EXTENDED_DATA_TYPE_ACCURACY_VERTICAL = "accuracy_vertical";
|
||||
|
||||
private static final String MARKER_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
|
||||
private static final String TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
|
||||
@@ -88,6 +90,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
private final List<Float> heartRateList = new ArrayList<>();
|
||||
private final List<Float> altitudeGainList = new ArrayList<>();
|
||||
private final List<Float> altitudeLossList = new ArrayList<>();
|
||||
private final List<Float> accuracyHorizontal = new ArrayList<>();
|
||||
private final List<Float> accuracyVertical = new ArrayList<>();
|
||||
|
||||
public KMLTrackExporter(Context context, boolean exportPhotos) {
|
||||
this.context = context;
|
||||
@@ -333,6 +337,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
heartRateList.clear();
|
||||
altitudeGainList.clear();
|
||||
altitudeLossList.clear();
|
||||
accuracyHorizontal.clear();
|
||||
accuracyVertical.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,6 +368,12 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
if (altitudeLossList.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(altitudeLossList, EXTENDED_DATA_TYPE_ALTITUDE_LOSS);
|
||||
}
|
||||
if (accuracyHorizontal.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(accuracyHorizontal, EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL);
|
||||
}
|
||||
if (accuracyVertical.stream().anyMatch(Objects::nonNull)) {
|
||||
writeSimpleArrayData(accuracyVertical, EXTENDED_DATA_TYPE_ACCURACY_VERTICAL);
|
||||
}
|
||||
printWriter.println("</SchemaData>");
|
||||
printWriter.println("</ExtendedData>");
|
||||
printWriter.println("</gx:Track>");
|
||||
@@ -387,6 +399,8 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
|
||||
altitudeGainList.add(trackPoint.hasAltitudeGain() ? trackPoint.getAltitudeGain() : null);
|
||||
altitudeLossList.add(trackPoint.hasAltitudeLoss() ? trackPoint.getAltitudeLoss() : null);
|
||||
accuracyHorizontal.add(trackPoint.hasHorizontalAccuracy() ? (float) trackPoint.getHorizontalAccuracy().toM() : null);
|
||||
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private static final String TAG_EXTENSION_GAIN = "opentracks:gain";
|
||||
private static final String TAG_EXTENSION_LOSS = "opentracks:loss";
|
||||
private static final String TAG_EXTENSION_DISTANCE = "opentracks:distance";
|
||||
|
||||
private static final String TAG_EXTENSION_ACCURACY_HORIZONTAL = "opentracks:accuracy_horizontal";
|
||||
private static final String TAG_EXTENSION_ACCURACY_VERTICAL = "opentracks:accuracy_vertical";
|
||||
private Locator locator;
|
||||
|
||||
private final Context context;
|
||||
@@ -106,12 +107,14 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private String heartrate;
|
||||
private String cadence;
|
||||
private String power;
|
||||
protected String markerType;
|
||||
protected String photoUrl;
|
||||
protected String uuid;
|
||||
protected String gain;
|
||||
protected String loss;
|
||||
protected String sensorDistance;
|
||||
private String markerType;
|
||||
private String photoUrl;
|
||||
private String uuid;
|
||||
private String gain;
|
||||
private String loss;
|
||||
private String sensorDistance;
|
||||
private String accuracyHorizontal;
|
||||
private String accuracyVertical;
|
||||
|
||||
private final LinkedList<TrackPoint> currentSegment = new LinkedList<>();
|
||||
|
||||
@@ -240,6 +243,16 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
sensorDistance = content.trim();
|
||||
}
|
||||
break;
|
||||
case TAG_EXTENSION_ACCURACY_HORIZONTAL:
|
||||
if (content != null) {
|
||||
accuracyHorizontal = content.trim();
|
||||
}
|
||||
break;
|
||||
case TAG_EXTENSION_ACCURACY_VERTICAL:
|
||||
if (content != null) {
|
||||
accuracyVertical = content.trim();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
content = "";
|
||||
@@ -342,6 +355,20 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", sensorDistance)), e);
|
||||
}
|
||||
}
|
||||
if (accuracyHorizontal != null) {
|
||||
try {
|
||||
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse accuracy_horizontal: %s", sensorDistance)), e);
|
||||
}
|
||||
}
|
||||
if (accuracyVertical != null) {
|
||||
try {
|
||||
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse accuracy_vertical: %s", sensorDistance)), e);
|
||||
}
|
||||
}
|
||||
|
||||
return trackPoint;
|
||||
}
|
||||
@@ -357,6 +384,8 @@ public class GpxTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
loss = null;
|
||||
|
||||
sensorDistance = null;
|
||||
accuracyHorizontal = null;
|
||||
accuracyVertical = null;
|
||||
power = null;
|
||||
heartrate = null;
|
||||
cadence = null;
|
||||
|
||||
@@ -89,6 +89,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private final ArrayList<Float> sensorPowerList = new ArrayList<>();
|
||||
private final ArrayList<Float> altitudeGainList = new ArrayList<>();
|
||||
private final ArrayList<Float> altitudeLossList = new ArrayList<>();
|
||||
private final ArrayList<Float> accuracyHorizontal = new ArrayList<>();
|
||||
private final ArrayList<Float> accuracyVertical = new ArrayList<>();
|
||||
|
||||
private final ArrayList<Marker> markers = new ArrayList<>();
|
||||
|
||||
@@ -294,6 +296,8 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
sensorPowerList.clear();
|
||||
altitudeGainList.clear();
|
||||
altitudeLossList.clear();
|
||||
accuracyHorizontal.clear();
|
||||
accuracyVertical.clear();
|
||||
}
|
||||
|
||||
private void onTrackSegmentEnd() {
|
||||
@@ -333,6 +337,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
if (i < altitudeLossList.size()) {
|
||||
trackPoint.setAltitudeLoss(altitudeLossList.get(i));
|
||||
}
|
||||
if (i < accuracyHorizontal.size() && accuracyHorizontal.get(i) != null) {
|
||||
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal.get(i)));
|
||||
}
|
||||
if (i < accuracyVertical.size() && accuracyVertical.get(i) != null) {
|
||||
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical.get(i)));
|
||||
}
|
||||
|
||||
// Update TrackPoint type for START / STOP.
|
||||
TrackPoint.Type type = trackPoint.getType();
|
||||
@@ -430,6 +440,12 @@ public class KmlTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS:
|
||||
altitudeLossList.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL:
|
||||
accuracyHorizontal.add(value);
|
||||
break;
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL:
|
||||
accuracyVertical.add(value);
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Data from extended data " + extendedDataType + " is not (yet) supported.");
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@ public abstract class BluetoothConnectionManager<DataType> {
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
|
||||
Log.d(TAG, "Received data from " + gatt.getDevice().getAddress());
|
||||
|
||||
SensorData<DataType> sensorData = parsePayload(gatt.getDevice().getName(), gatt.getDevice().getAddress(), characteristic);
|
||||
if (sensorData != null) {
|
||||
Log.d(TAG, "Decoded data from " + gatt.getDevice().getAddress() + ": " + sensorData);
|
||||
|
||||
@@ -165,6 +165,11 @@ public class BluetoothUtils {
|
||||
index = 3;
|
||||
if (valueLength - index >= 1) {
|
||||
cadence = Cadence.of(characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, index));
|
||||
|
||||
// Hacky workaround as the Wahoo Tickr X provides cadence in SPM (steps per minute) in violation to the standard.
|
||||
if (sensorName != null && sensorName.startsWith("TICKR X")) {
|
||||
cadence = Cadence.of(cadence.getRPM() / 2);
|
||||
}
|
||||
}
|
||||
|
||||
index = 4;
|
||||
|
||||
@@ -98,14 +98,14 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
}
|
||||
|
||||
void pause(TrackPointCreator trackPointCreator) {
|
||||
insertTrackPoint(trackPointCreator.createSegmentEnd());
|
||||
insertTrackPoint(trackPointCreator.createSegmentEnd(), true);
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void end(TrackPointCreator trackPointCreator) {
|
||||
TrackPoint segmentEnd = trackPointCreator.createSegmentEnd();
|
||||
insertTrackPoint(segmentEnd);
|
||||
insertTrackPoint(segmentEnd, true);
|
||||
|
||||
trackId = null;
|
||||
trackStatisticsUpdater = null;
|
||||
@@ -172,12 +172,12 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
|
||||
// Always insert the first segment location
|
||||
if (lastStoredTrackPoint == null) {
|
||||
insertTrackPoint(trackPoint);
|
||||
insertTrackPoint(trackPoint, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
|
||||
insertTrackPoint(trackPoint);
|
||||
insertTrackPoint(trackPoint, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -195,19 +195,19 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
|
||||
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
insertTrackPoint(trackPoint);
|
||||
insertTrackPoint(trackPoint, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (distanceToLastStoredTrackPoint.greaterOrEqualThan(recordingDistanceInterval)
|
||||
&& trackPoint.isMoving()) {
|
||||
insertTrackPoint(trackPoint);
|
||||
insertTrackPoint(trackPoint, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackPoint.isMoving() != lastStoredTrackPoint.isMoving()) {
|
||||
// Moving from non-moving to moving or vice versa; required to compute moving time correctly.
|
||||
insertTrackPoint(trackPoint);
|
||||
insertTrackPoint(trackPoint, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -221,8 +221,8 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
return trackStatisticsUpdater.getTrackStatistics();
|
||||
}
|
||||
|
||||
private void insertTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (lastTrackPoint != null) {
|
||||
private void insertTrackPoint(@NonNull TrackPoint trackPoint, boolean storeLastTrackPointIfUseful) {
|
||||
if (storeLastTrackPointIfUseful && lastTrackPoint != null) {
|
||||
if (lastStoredTrackPoint != null && lastTrackPoint.getTime().equals(lastStoredTrackPoint.getTime())) {
|
||||
// Do not insert if inserted already
|
||||
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid trackId point time.");
|
||||
@@ -231,8 +231,8 @@ class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChang
|
||||
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
|
||||
trackPoint.minusCumulativeSensorData(lastTrackPoint);
|
||||
}
|
||||
lastTrackPoint = null;
|
||||
}
|
||||
lastTrackPoint = null;
|
||||
|
||||
insertTrackPointHelper(trackPoint);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
private static final String TAG = TrackRecordingService.class.getSimpleName();
|
||||
|
||||
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofMillis(100);
|
||||
private static final Duration RECORDING_DATA_UPDATE_INTERVAL = Duration.ofSeconds(1);
|
||||
|
||||
public static final RecordingStatus STATUS_DEFAULT = RecordingStatus.notRecording();
|
||||
public static final RecordingData NOT_RECORDING = new RecordingData(null, null, null);
|
||||
@@ -120,11 +120,12 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
trackPointCreator.stop();
|
||||
trackPointCreator = null;
|
||||
|
||||
handler.removeCallbacksAndMessages(null); //Some tests do not finish the recording completely
|
||||
handler = null;
|
||||
|
||||
trackPointCreator.stop();
|
||||
trackPointCreator = null;
|
||||
trackRecordingManager.stop();
|
||||
trackRecordingManager = null;
|
||||
|
||||
@@ -312,7 +313,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
|
||||
private void showNotification(boolean isGpsStarted) {
|
||||
if (isRecording()) {
|
||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this));
|
||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setRecording(this, recordingStatus.getTrackId()));
|
||||
}
|
||||
if (!isRecording() && isGpsStarted) {
|
||||
startForeground(TrackRecordingServiceNotificationManager.NOTIFICATION_ID, notificationManager.setGPSonlyStarted(this));
|
||||
|
||||
+21
-11
@@ -9,19 +9,24 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.TrackListActivity;
|
||||
import de.dennisguse.opentracks.TrackRecordedActivity;
|
||||
import de.dennisguse.opentracks.TrackRecordingActivity;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Manages the content of the notification shown by {@link TrackRecordingService}.
|
||||
@@ -38,7 +43,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
|
||||
private boolean previousLocationWasAccurate = true;
|
||||
|
||||
private Boolean metricUnits = null;
|
||||
private UnitSystem unitSystem = null;
|
||||
|
||||
TrackRecordingServiceNotificationManager(Context context) {
|
||||
PreferencesUtils.registerOnSharedPreferenceChangeListener(this);
|
||||
@@ -79,8 +84,12 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
|
||||
void updateTrackPoint(Context context, TrackStatistics trackStatistics, TrackPoint trackPoint, Distance recordingGpsAccuracy) {
|
||||
String formattedAccuracy = context.getString(R.string.value_none);
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context);
|
||||
if (trackPoint.hasHorizontalAccuracy()) {
|
||||
formattedAccuracy = StringUtils.formatDistance(context, trackPoint.getHorizontalAccuracy(), metricUnits);
|
||||
formattedAccuracy = formatter.formatDistance(trackPoint.getHorizontalAccuracy());
|
||||
|
||||
boolean currentLocationWasAccurate = trackPoint.getHorizontalAccuracy().lessThan(recordingGpsAccuracy);
|
||||
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
|
||||
@@ -88,17 +97,18 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
previousLocationWasAccurate = currentLocationWasAccurate;
|
||||
}
|
||||
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, StringUtils.formatDistance(context, trackStatistics.getTotalDistance(), metricUnits)));
|
||||
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, StringUtils.formatSpeed(context, trackPoint.getSpeed(), metricUnits, true)));
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(trackStatistics.getTotalDistance())));
|
||||
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(trackPoint.getSpeed());
|
||||
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
|
||||
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
|
||||
updateNotification();
|
||||
|
||||
notificationBuilder.setOnlyAlertOnce(true);
|
||||
}
|
||||
|
||||
Notification setRecording(Context context) {
|
||||
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class);
|
||||
|
||||
Notification setRecording(Context context, @NonNull Track.Id trackId) {
|
||||
Intent intent = IntentUtils.newIntent(context, TrackRecordingActivity.class)
|
||||
.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId);
|
||||
int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
pendingIntentFlags |= PendingIntent.FLAG_IMMUTABLE;
|
||||
@@ -140,8 +150,8 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
void setUnitSystem(UnitSystem unitSystem) {
|
||||
this.unitSystem = unitSystem;
|
||||
}
|
||||
|
||||
private void updateNotification() {
|
||||
@@ -155,7 +165,7 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key)) {
|
||||
setMetricUnits(PreferencesUtils.isMetricUnits());
|
||||
setUnitSystem(PreferencesUtils.getUnitSystem());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import de.dennisguse.opentracks.data.TrackPointIterator;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
||||
@@ -165,12 +166,12 @@ public class VoiceAnnouncement {
|
||||
}
|
||||
|
||||
|
||||
boolean isMetricUnits = PreferencesUtils.isMetricUnits();
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
boolean isReportSpeed = PreferencesUtils.isReportSpeed(track.getCategory());
|
||||
|
||||
//TODO Do not load all trackpoints for every announcement
|
||||
TrackPointIterator trackPointIterator = contentProviderUtils.getTrackPointLocationIterator(track.getId(), null);
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(isMetricUnits));
|
||||
IntervalStatistics intervalStatistics = new IntervalStatistics(Distance.one(unitSystem));
|
||||
intervalStatistics.addTrackPoints(trackPointIterator);
|
||||
IntervalStatistics.Interval lastInterval = intervalStatistics.getLastInterval();
|
||||
SensorStatistics sensorStatistics = null;
|
||||
@@ -178,7 +179,7 @@ public class VoiceAnnouncement {
|
||||
sensorStatistics = contentProviderUtils.getSensorStats(track.getId());
|
||||
}
|
||||
|
||||
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), isMetricUnits, isReportSpeed, lastInterval, sensorStatistics);
|
||||
Spannable announcement = VoiceAnnouncementUtils.getAnnouncement(context, track.getTrackStatistics(), unitSystem, isReportSpeed, lastInterval, sensorStatistics);
|
||||
|
||||
// We don't care about the utterance id. It is supplied here to force onUtteranceCompleted to be called.
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null, "not used");
|
||||
|
||||
+34
-12
@@ -16,6 +16,7 @@ import java.time.Duration;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.intervals.IntervalStatistics;
|
||||
@@ -25,15 +26,37 @@ class VoiceAnnouncementUtils {
|
||||
private VoiceAnnouncementUtils() {
|
||||
}
|
||||
|
||||
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, boolean isMetricUnits, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||
static Spannable getAnnouncement(Context context, TrackStatistics trackStatistics, UnitSystem unitSystem, boolean isReportSpeed, @Nullable IntervalStatistics.Interval currentInterval, @Nullable SensorStatistics sensorStatistics) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
Distance distance = trackStatistics.getTotalDistance();
|
||||
Speed distancePerTime = trackStatistics.getAverageMovingSpeed();
|
||||
Speed currentDistancePerTime = currentInterval != null ? currentInterval.getSpeed() : null;
|
||||
int perUnitStringId = isMetricUnits ? R.string.voice_per_kilometer : R.string.voice_per_mile;
|
||||
|
||||
double distanceInUnit = distance.toKM_Miles(isMetricUnits);
|
||||
int distanceId = isMetricUnits ? R.plurals.voiceDistanceKilometers : R.plurals.voiceDistanceMiles;
|
||||
int perUnitStringId;
|
||||
int distanceId;
|
||||
int speedId;
|
||||
String unitDistanceTTS;
|
||||
String unitSpeedTTS;
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
perUnitStringId = R.string.voice_per_kilometer;
|
||||
distanceId = R.plurals.voiceDistanceKilometers;
|
||||
speedId = R.plurals.voiceSpeedKilometersPerHour;
|
||||
unitDistanceTTS = "kilometer";
|
||||
unitSpeedTTS = "kilometer per hour";
|
||||
break;
|
||||
case IMPERIAL:
|
||||
perUnitStringId = R.string.voice_per_mile;
|
||||
distanceId = R.plurals.voiceDistanceMiles;
|
||||
speedId = R.plurals.voiceSpeedMilesPerHour;
|
||||
unitDistanceTTS = "mile";
|
||||
unitSpeedTTS = "mile per hour";
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
double distanceInUnit = distance.toKM_Miles(unitSystem);
|
||||
|
||||
builder.append(context.getString(R.string.total_distance));
|
||||
long distanceIntegerPart = (long) distanceInUnit;
|
||||
@@ -41,7 +64,7 @@ class VoiceAnnouncementUtils {
|
||||
String distanceFractionalPart = String.format("%.2f", (distanceInUnit - distanceIntegerPart)).substring(2);
|
||||
// Units should always be english singular for TTS.
|
||||
// See https://developer.android.com/reference/android/text/style/TtsSpan?hl=en#TYPE_MEASURE
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, isMetricUnits ? "kilometer" : "mile");
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(distanceId, getQuantityCount(distanceInUnit), distanceInUnit), distanceIntegerPart, distanceFractionalPart, unitDistanceTTS);
|
||||
// Punctuation helps introduce natural pauses in TTS
|
||||
builder.append(".");
|
||||
if (distance.isZero()) {
|
||||
@@ -56,19 +79,18 @@ class VoiceAnnouncementUtils {
|
||||
}
|
||||
|
||||
if (isReportSpeed) {
|
||||
int speedId = isMetricUnits ? R.plurals.voiceSpeedKilometersPerHour : R.plurals.voiceSpeedMilesPerHour;
|
||||
double speedInUnit = distancePerTime.to(isMetricUnits);
|
||||
double speedInUnit = distancePerTime.to(unitSystem);
|
||||
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.speed));
|
||||
long speedIntegerPart = (long) speedInUnit;
|
||||
// Extract the decimal part
|
||||
String speedFractionalPart = String.format("%.1f", (speedInUnit - speedIntegerPart)).substring(2);
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, isMetricUnits ? "kilometer per hour" : "mile per hour");
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(speedInUnit), speedInUnit), speedIntegerPart, speedFractionalPart, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
|
||||
if (currentDistancePerTime != null) {
|
||||
double currentDistancePerTimeInUnit = currentDistancePerTime.to(isMetricUnits);
|
||||
double currentDistancePerTimeInUnit = currentDistancePerTime.to(unitSystem);
|
||||
|
||||
if (currentDistancePerTimeInUnit > 0) {
|
||||
|
||||
@@ -77,12 +99,12 @@ class VoiceAnnouncementUtils {
|
||||
long currentDistanceIntegerPart = (long) currentDistancePerTimeInUnit;
|
||||
// Extract the decimal part
|
||||
String currentDistanceFractionalPart = String.format("%.1f", (currentDistancePerTimeInUnit - currentDistanceIntegerPart)).substring(2);
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, isMetricUnits ? "kilometer per hour" : "mile per hour");
|
||||
appendDecimalUnit(builder, context.getResources().getQuantityString(speedId, getQuantityCount(currentDistancePerTimeInUnit), currentDistancePerTimeInUnit), currentDistanceIntegerPart, currentDistanceFractionalPart, unitSpeedTTS);
|
||||
builder.append(".");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Duration time = distancePerTime.toPace(isMetricUnits);
|
||||
Duration time = distancePerTime.toPace(unitSystem);
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.pace));
|
||||
appendDuration(context, builder, time);
|
||||
@@ -90,7 +112,7 @@ class VoiceAnnouncementUtils {
|
||||
.append(context.getString(perUnitStringId))
|
||||
.append(".");
|
||||
|
||||
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(isMetricUnits) : Duration.ofMillis(0);
|
||||
Duration currentTime = currentDistancePerTime != null ? currentDistancePerTime.toPace(unitSystem) : Duration.ofMillis(0);
|
||||
if (!currentTime.isZero()) {
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.lap_time));
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
@@ -109,25 +110,23 @@ public class GPSHandler implements LocationListener, GpsStatus.GpsStatusListener
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = new TrackPoint(location, trackPointCreator.createNow());
|
||||
boolean isAccurate = trackPoint.fulfillsAccuracy(thresholdHorizontalAccuracy);
|
||||
boolean isValid = LocationUtils.isValidLocation(location);
|
||||
|
||||
if (gpsStatus != null) {
|
||||
// Send each update to the status; please note that this TrackPoint is not stored.
|
||||
TrackPoint trackPoint = new TrackPoint(location, Instant.ofEpochMilli(location.getTime()));
|
||||
gpsStatus.onLocationChanged(trackPoint);
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
if (!LocationUtils.isValidLocation(location)) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAccurate) {
|
||||
if (!LocationUtils.fulfillsAccuracy(location, thresholdHorizontalAccuracy)) {
|
||||
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
trackPointCreator.onNewTrackPoint(trackPoint);
|
||||
trackPointCreator.onChange(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.dennisguse.opentracks.services.handlers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
@@ -47,7 +48,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
this.gpsHandler = gpsHandler;
|
||||
}
|
||||
|
||||
public void start(@NonNull Context context) {
|
||||
public synchronized void start(@NonNull Context context) {
|
||||
this.context = context;
|
||||
|
||||
gpsHandler.onStart(context);
|
||||
@@ -71,7 +72,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
gpsHandler.onStop();
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
public synchronized void reset() {
|
||||
if (remoteSensorManager == null || altitudeSumManager == null) {
|
||||
Log.d(TAG, "No recording running and no reset necessary.");
|
||||
return;
|
||||
@@ -80,7 +81,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
altitudeSumManager.reset();
|
||||
}
|
||||
|
||||
private SensorDataSet fill(TrackPoint trackPoint) {
|
||||
private SensorDataSet addSensorData(TrackPoint trackPoint) {
|
||||
if (!isStarted()) {
|
||||
Log.w(TAG, "Not started, should not be called.");
|
||||
return null;
|
||||
@@ -98,7 +99,7 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
public synchronized void stop() {
|
||||
gpsHandler.onStop();
|
||||
|
||||
if (remoteSensorManager != null) {
|
||||
@@ -114,16 +115,21 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
this.context = null;
|
||||
}
|
||||
|
||||
public synchronized void onChange(@NonNull Location location) {
|
||||
onNewTrackPoint(new TrackPoint(location, createNow()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Got a new TrackPoint from Bluetooth only; contains no GPS location.
|
||||
*/
|
||||
@Override
|
||||
public void onChange(SensorDataSet sensorDataSet) {
|
||||
public synchronized void onChange(@NonNull SensorDataSet unused) {
|
||||
onNewTrackPoint(new TrackPoint(TrackPoint.Type.SENSORPOINT, createNow()));
|
||||
}
|
||||
|
||||
public synchronized void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
fill(trackPoint);
|
||||
@VisibleForTesting
|
||||
public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
addSensorData(trackPoint);
|
||||
|
||||
boolean stored = service.newTrackPoint(trackPoint, gpsHandler.getThresholdHorizontalAccuracy());
|
||||
if (stored) {
|
||||
@@ -131,13 +137,13 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
}
|
||||
}
|
||||
|
||||
public TrackPoint createSegmentStartManual() {
|
||||
public synchronized TrackPoint createSegmentStartManual() {
|
||||
return TrackPoint.createSegmentStartManualWithTime(createNow());
|
||||
}
|
||||
|
||||
public TrackPoint createSegmentEnd() {
|
||||
public synchronized TrackPoint createSegmentEnd() {
|
||||
TrackPoint segmentEnd = TrackPoint.createSegmentEndWithTime(createNow());
|
||||
fill(segmentEnd);
|
||||
addSensorData(segmentEnd);
|
||||
reset();
|
||||
return segmentEnd;
|
||||
}
|
||||
@@ -158,11 +164,12 @@ public class TrackPointCreator implements BluetoothRemoteSensorManager.SensorDat
|
||||
currentTrackPoint.setLatitude(lastStoredTrackPointWithLocation.getLatitude());
|
||||
}
|
||||
|
||||
SensorDataSet sensorDataSet = fill(currentTrackPoint);
|
||||
SensorDataSet sensorDataSet = addSensorData(currentTrackPoint);
|
||||
|
||||
return new Pair<>(currentTrackPoint, sensorDataSet);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Instant createNow() {
|
||||
return Instant.now(clock);
|
||||
}
|
||||
|
||||
@@ -64,10 +64,23 @@ public class DefaultsSettingsFragment extends PreferenceFragmentCompat implement
|
||||
}
|
||||
|
||||
private void updateUnits() {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits();
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
|
||||
ListPreference statsRatePreferences = findPreference(getString(R.string.stats_rate_key));
|
||||
String[] entries = getResources().getStringArray(metricUnits ? R.array.stats_rate_metric_options : R.array.stats_rate_imperial_options);
|
||||
|
||||
int entriesId;
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
entriesId = R.array.stats_rate_metric_options;
|
||||
break;
|
||||
case IMPERIAL:
|
||||
entriesId = R.array.stats_rate_imperial_options;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
|
||||
String[] entries = getResources().getStringArray(entriesId);
|
||||
statsRatePreferences.setEntries(entries);
|
||||
|
||||
HackUtils.invalidatePreference(statsRatePreferences);
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.time.Duration;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
|
||||
public class GpsSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
@@ -17,30 +17,34 @@ public class GpsSettingsFragment extends PreferenceFragmentCompat {
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.settings_gps);
|
||||
|
||||
UnitSystem unitSystem = PreferencesUtils.getUnitSystem();
|
||||
|
||||
final DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
findPreference(getString(R.string.recording_distance_interval_key))
|
||||
.setSummaryProvider(
|
||||
preference -> {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits();
|
||||
Distance distance = PreferencesUtils.getRecordingDistanceInterval();
|
||||
return getString(R.string.settings_recording_location_frequency_summary, StringUtils.formatDistance(getContext(), distance, metricUnits));
|
||||
return getString(R.string.settings_recording_location_frequency_summary, formatter.formatDistance(distance));
|
||||
}
|
||||
);
|
||||
|
||||
findPreference(getString(R.string.max_recording_distance_key))
|
||||
.setSummaryProvider(
|
||||
preference -> {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits();
|
||||
Distance distance = PreferencesUtils.getMaxRecordingDistance();
|
||||
return getString(R.string.settings_recording_max_recording_distance_summary, StringUtils.formatDistance(getContext(), distance, metricUnits));
|
||||
return getString(R.string.settings_recording_max_recording_distance_summary, formatter.formatDistance(distance));
|
||||
}
|
||||
);
|
||||
|
||||
findPreference(getString(R.string.recording_gps_accuracy_key))
|
||||
.setSummaryProvider(
|
||||
preference -> {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits();
|
||||
Distance distance = PreferencesUtils.getThresholdHorizontalAccuracy();
|
||||
return getString(R.string.settings_recording_min_required_accuracy_summary, StringUtils.formatDistance(getContext(), distance, metricUnits));
|
||||
return getString(R.string.settings_recording_min_required_accuracy_summary, formatter.formatDistance(distance));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
@@ -12,16 +13,20 @@ import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||
import de.dennisguse.opentracks.util.IntentUtils;
|
||||
|
||||
public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
||||
|
||||
private static final String TAG = ImportExportSettingsFragment.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.settings_import_export);
|
||||
|
||||
setExportTrackFileFormatOptions();
|
||||
setExportDirectorySummary();
|
||||
setFilenameTemplate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,7 +50,8 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA_AND_PICTURES,
|
||||
TrackFileFormat.KMZ_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.KML_WITH_TRACKDETAIL_AND_SENSORDATA,
|
||||
TrackFileFormat.GPX
|
||||
TrackFileFormat.GPX,
|
||||
TrackFileFormat.CSV
|
||||
};
|
||||
String[] entries = new String[trackFileFormats.length];
|
||||
String[] entryValues = new String[trackFileFormats.length];
|
||||
@@ -76,4 +82,19 @@ public class ImportExportSettingsFragment extends PreferenceFragmentCompat {
|
||||
return directoryUri + (directory.canWrite() ? "" : getString(R.string.export_dir_not_writable));
|
||||
});
|
||||
}
|
||||
|
||||
private void setFilenameTemplate() {
|
||||
EditTextPreference preference = findPreference(getString(R.string.export_filename_format_key));
|
||||
preference.setOnBindEditTextListener(t -> {
|
||||
t.setHint(getString(R.string.export_filename_format_default));
|
||||
});
|
||||
preference.setDialogMessage(TrackFilenameGenerator.getAllOptions());
|
||||
|
||||
preference.setOnPreferenceChangeListener((p, newValue) -> new TrackFilenameGenerator(newValue.toString()).isValid());
|
||||
|
||||
preference.setSummaryProvider(p ->
|
||||
PreferencesUtils.getTrackFileformatGenerator()
|
||||
.getExample()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,16 +32,19 @@ import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.io.file.TrackFilenameGenerator;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.CsvLayoutUtils;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
@@ -193,19 +196,30 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.publicapi_dashboard_enabled_key, resources.getBoolean(R.bool.publicapi_dashboard_enabled_default));
|
||||
}
|
||||
|
||||
public static boolean isMetricUnits() {
|
||||
final String STATS_UNIT = resources.getString(R.string.stats_units_default);
|
||||
return STATS_UNIT.equals(getString(R.string.stats_units_key, STATS_UNIT));
|
||||
public static UnitSystem getUnitSystem() {
|
||||
final String STATS_UNIT_DEFAULT = resources.getString(R.string.stats_units_default);
|
||||
|
||||
final String VALUE = getString(R.string.stats_units_key, STATS_UNIT_DEFAULT);
|
||||
return Arrays.stream(UnitSystem.values())
|
||||
.filter(d -> VALUE.equals(getString(d.getPreferenceId(), STATS_UNIT_DEFAULT)))
|
||||
.findFirst()
|
||||
.orElse(UnitSystem.defaultUnitSystem()); //TODO This AGAIN defines the default
|
||||
}
|
||||
|
||||
public static void setMetricUnits(boolean metricUnits) {
|
||||
String unit;
|
||||
if (metricUnits) {
|
||||
unit = resources.getString(R.string.stats_units_metric);
|
||||
} else {
|
||||
unit = resources.getString(R.string.stats_units_imperial);
|
||||
public static void setUnit(UnitSystem unitSystem) {
|
||||
setString(R.string.stats_units_key, unitSystem.getPreferenceId());
|
||||
}
|
||||
|
||||
//TODO Check if actually needed or can be superseeded by a flexible default in getUnit()
|
||||
public static void applyDefaultUnit() {
|
||||
if (getString(R.string.stats_units_key, "").equals("")) {
|
||||
|
||||
if (!Locale.US.equals(Locale.getDefault())) {
|
||||
setUnit(UnitSystem.METRIC);
|
||||
} else {
|
||||
setUnit(UnitSystem.IMPERIAL);
|
||||
}
|
||||
}
|
||||
setString(R.string.stats_units_key, unit);
|
||||
}
|
||||
|
||||
public static boolean isReportSpeed(String category) {
|
||||
@@ -292,27 +306,32 @@ public class PreferencesUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result depends on isMetricUnits
|
||||
* @return Result depends on getUnitSystem
|
||||
*/
|
||||
public static Distance getVoiceAnnouncementDistance() {
|
||||
final float DEFAULT = Integer.parseInt(resources.getString(R.string.voice_announcement_distance_default));
|
||||
float value = getFloat(R.string.voice_announcement_distance_key, DEFAULT);
|
||||
return Distance.one(isMetricUnits()).multipliedBy(value);
|
||||
return Distance.one(getUnitSystem()).multipliedBy(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Result depends on isMetricUnits
|
||||
* @return Result depends on getUnitSystem
|
||||
*/
|
||||
static String[] getVoiceAnnouncementDistanceEntries() {
|
||||
String[] values = resources.getStringArray(R.array.voice_announcement_distance_values);
|
||||
String[] options = new String[values.length];
|
||||
boolean metricUnits = isMetricUnits();
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (resources.getString(R.string.announcement_off).equals(values[i])) {
|
||||
options[i] = resources.getString(R.string.value_off);
|
||||
} else {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
options[i] = resources.getString(metricUnits ? R.string.value_integer_kilometer : R.string.value_integer_mile, value);
|
||||
Distance distance = Distance.one(unitSystem).multipliedBy(Double.parseDouble(values[i]));
|
||||
options[i] = formatter.formatDistance(distance);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
@@ -349,26 +368,34 @@ public class PreferencesUtils {
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int recordingDistanceIntervalDefault = (int) getRecordingDistanceIntervalDefault().toM();
|
||||
boolean metricUnits = isMetricUnits();
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.setDecimalCount(0)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = resources.getString(R.string.value_integer_meter, value);
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
displayValue = resources.getString(R.string.value_integer_feet, feet);
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
Distance distance = Distance.of(1).multipliedBy(value);
|
||||
|
||||
String displayValue = formatter.formatDistance(distance);
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
if (value == recordingDistanceIntervalDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,26 +412,34 @@ public class PreferencesUtils {
|
||||
String[] entries = new String[entryValues.length];
|
||||
|
||||
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default));
|
||||
boolean metricUnits = isMetricUnits();
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = resources.getString(R.string.value_integer_meter, value);
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
displayValue = resources.getString(R.string.value_integer_feet, feet);
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
Distance distance = Distance.of(1).multipliedBy(value);
|
||||
|
||||
String displayValue = formatter.formatDistance(distance);
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
if (value == maxRecordingDistanceDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,33 +484,41 @@ public class PreferencesUtils {
|
||||
final int recordingGPSAccuracyExcellent = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_excellent));
|
||||
final int recordingGPSAccuracyPoor = Integer.parseInt(resources.getString(R.string.recording_gps_accuracy_poor));
|
||||
|
||||
boolean metricUnits = isMetricUnits();
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(resources);
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
int value = Integer.parseInt(entryValues[i]);
|
||||
String displayValue;
|
||||
if (metricUnits) {
|
||||
displayValue = resources.getString(R.string.value_integer_meter, value);
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_excellent_gps, value);
|
||||
} else if (value == recordingGPSAccuracyPoor) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_poor_gps, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
} else {
|
||||
int feet = (int) (value * UnitConversions.M_TO_FT);
|
||||
displayValue = resources.getString(R.string.value_integer_feet, feet);
|
||||
Distance distance = Distance.of(1).multipliedBy(value);
|
||||
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, feet);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, feet);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
String displayValue = formatter.formatDistance(distance);
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_excellent_gps, value);
|
||||
} else if (value == recordingGPSAccuracyPoor) {
|
||||
entries[i] = resources.getString(R.string.value_integer_meter_poor_gps, value);
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
if (value == recordingGPSAccuracyDefault) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
|
||||
} else if (value == recordingGPSAccuracyExcellent) {
|
||||
entries[i] = resources.getString(R.string.value_integer_feet_excellent_gps, (int) distance.toFT());
|
||||
} else {
|
||||
entries[i] = displayValue;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,24 +538,30 @@ public class PreferencesUtils {
|
||||
|
||||
final float idleSpeedDefault = Float.parseFloat(resources.getString(R.string.idle_speed_default));
|
||||
|
||||
boolean metricUnits = isMetricUnits();
|
||||
UnitSystem unitSystem = getUnitSystem();
|
||||
|
||||
for (int i = 0; i < entryValues.length; i++) {
|
||||
float value = Float.parseFloat(entryValues[i]);
|
||||
if (metricUnits) {
|
||||
if (value == idleSpeedDefault) {
|
||||
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
|
||||
}
|
||||
} else {
|
||||
double valueMPH = Speed.ofKMH(value).toMPH();
|
||||
|
||||
if (value == idleSpeedDefault) {
|
||||
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
|
||||
}
|
||||
switch (unitSystem) {
|
||||
case METRIC:
|
||||
if (value == idleSpeedDefault) {
|
||||
entries[i] = resources.getString(R.string.value_float_kilometer_hour_recommended, value);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_float_kilometer_hour, value);
|
||||
}
|
||||
break;
|
||||
case IMPERIAL:
|
||||
double valueMPH = Speed.ofKMH(value).toMPH();
|
||||
|
||||
if (value == idleSpeedDefault) {
|
||||
entries[i] = resources.getString(R.string.value_float_mile_hour_recommended, valueMPH);
|
||||
} else {
|
||||
entries[i] = resources.getString(R.string.value_float_mile_hour, valueMPH);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("Not implemented");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +574,16 @@ public class PreferencesUtils {
|
||||
return getBoolean(R.string.post_workout_export_enabled_key, INSTANT_POST_WORKOUT_EXPORT_DEFAULT) && isDefaultExportDirectoryUri();
|
||||
}
|
||||
|
||||
public static TrackFilenameGenerator getTrackFileformatGenerator() {
|
||||
String DEFAULT = getString(R.string.export_filename_format_default, null);
|
||||
TrackFilenameGenerator generator = new TrackFilenameGenerator(getString(R.string.export_filename_format_key, DEFAULT));
|
||||
if (generator.isValid()) {
|
||||
return generator;
|
||||
} else {
|
||||
return new TrackFilenameGenerator(DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
public static TrackFileFormat getExportTrackFileFormat() {
|
||||
final String TRACKFILEFORMAT_NAME_DEFAULT = getString(R.string.export_trackfileformat_default, null);
|
||||
String trackFileFormatName = getString(R.string.export_trackfileformat_key, TRACKFILEFORMAT_NAME_DEFAULT);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.dennisguse.opentracks.settings;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
|
||||
public enum UnitSystem {
|
||||
METRIC(R.string.stats_units_metric),
|
||||
IMPERIAL(R.string.stats_units_imperial);
|
||||
|
||||
private final int preferenceIdentifier;
|
||||
|
||||
UnitSystem(int preferenceIdentifier) {
|
||||
this.preferenceIdentifier = preferenceIdentifier;
|
||||
}
|
||||
|
||||
public int getPreferenceId() {
|
||||
return preferenceIdentifier;
|
||||
}
|
||||
|
||||
@Deprecated //TODO used to initialize before loading from preferences; should be loaded first
|
||||
public static UnitSystem defaultUnitSystem() {
|
||||
return METRIC;
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,10 @@ import java.time.ZoneId;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.UnitConversions;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
@@ -209,8 +211,8 @@ public class DescriptionGenerator {
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writePace(Speed speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
Pair<String, String> paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
|
||||
Pair<String, String> paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
|
||||
Pair<String, String> paceInMetrics = SpeedFormatter.Builder().setUnit(UnitSystem.METRIC).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
|
||||
Pair<String, String> paceInImperial = SpeedFormatter.Builder().setUnit(UnitSystem.IMPERIAL).setReportSpeedOrPace(false).build(context).getSpeedParts(speed);
|
||||
|
||||
String formattedPaceMetrics = paceInMetrics.first != null ? paceInMetrics.first : context.getString(R.string.value_unknown);
|
||||
String formattedPaceImperial = paceInImperial.first != null ? paceInImperial.first : context.getString(R.string.value_unknown);
|
||||
|
||||
@@ -15,8 +15,8 @@ import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.ShareContentProvider;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.io.file.TrackFileFormat;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
public class ShareUtils {
|
||||
|
||||
@@ -54,8 +54,9 @@ public class ShareUtils {
|
||||
continue;
|
||||
}
|
||||
|
||||
String trackName = FileUtils.sanitizeFileName(track.getName());
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, PreferencesUtils.getExportTrackFileFormat());
|
||||
TrackFileFormat format = PreferencesUtils.getExportTrackFileFormat();
|
||||
String trackName = PreferencesUtils.getTrackFileformatGenerator().format(track, format);
|
||||
Pair<Uri, String> uriAndMime = ShareContentProvider.createURI(trackId, trackName, format);
|
||||
|
||||
uris.add(uriAndMime.first);
|
||||
mime = uriAndMime.second;
|
||||
|
||||
+16
-7
@@ -13,7 +13,10 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
import de.dennisguse.opentracks.util.TrackIconUtils;
|
||||
|
||||
@@ -97,7 +100,7 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
private final TextView maxSpeedUnit;
|
||||
private final TextView maxSpeedLabel;
|
||||
|
||||
private boolean metricsUnits;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
private boolean reportSpeed;
|
||||
|
||||
public ViewHolder(View view) {
|
||||
@@ -118,15 +121,17 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
|
||||
public void setSpeed(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
|
||||
setCommonValues(aggregatedStatistic);
|
||||
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed());
|
||||
avgSpeed.setText(parts.first);
|
||||
avgSpeedUnit.setText(parts.second);
|
||||
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_speed));
|
||||
}
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getMaxSpeed());
|
||||
maxSpeed.setText(parts.first);
|
||||
maxSpeedUnit.setText(parts.second);
|
||||
maxSpeedLabel.setText(context.getString(R.string.stats_max_speed));
|
||||
@@ -134,16 +139,18 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
}
|
||||
|
||||
public void setPace(AggregatedStatistics.AggregatedStatistic aggregatedStatistic) {
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
|
||||
|
||||
setCommonValues(aggregatedStatistic);
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed(), metricsUnits, reportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getAverageMovingSpeed());
|
||||
avgSpeed.setText(parts.first);
|
||||
avgSpeedUnit.setText(parts.second);
|
||||
avgSpeedLabel.setText(context.getString(R.string.stats_average_moving_pace));
|
||||
}
|
||||
|
||||
{
|
||||
Pair<String, String> parts = StringUtils.getSpeedParts(context, aggregatedStatistic.getTrackStatistics().getMaxSpeed(), metricsUnits, reportSpeed);
|
||||
Pair<String, String> parts = formatter.getSpeedParts(aggregatedStatistic.getTrackStatistics().getMaxSpeed());
|
||||
maxSpeed.setText(parts.first);
|
||||
maxSpeedUnit.setText(parts.second);
|
||||
maxSpeedLabel.setText(R.string.stats_fastest_pace);
|
||||
@@ -155,13 +162,15 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
String category = aggregatedStatistic.getCategory();
|
||||
|
||||
reportSpeed = PreferencesUtils.isReportSpeed(category);
|
||||
metricsUnits = PreferencesUtils.isMetricUnits();
|
||||
unitSystem = PreferencesUtils.getUnitSystem();
|
||||
|
||||
sportIcon.setImageResource(getIcon(aggregatedStatistic));
|
||||
typeLabel.setText(category);
|
||||
numTracks.setText(StringUtils.valueInParentheses(String.valueOf(aggregatedStatistic.getCountTracks())));
|
||||
|
||||
Pair<String, String> parts = StringUtils.getDistanceParts(context, aggregatedStatistic.getTrackStatistics().getTotalDistance(), metricsUnits);
|
||||
Pair<String, String> parts = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context).getDistanceParts(aggregatedStatistic.getTrackStatistics().getTotalDistance());
|
||||
distance.setText(parts.first);
|
||||
distanceUnit.setText(parts.second);
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
@@ -20,11 +23,11 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
|
||||
private List<IntervalStatistics.Interval> intervalList;
|
||||
private final Context context;
|
||||
private final StackMode stackMode;
|
||||
private boolean metricUnits;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
private boolean isReportSpeed;
|
||||
|
||||
public IntervalStatisticsAdapter(Context context, StackMode stackMode, boolean metricUnits, boolean isReportSpeed) {
|
||||
this.metricUnits = metricUnits;
|
||||
public IntervalStatisticsAdapter(Context context, StackMode stackMode, UnitSystem unitSystem, boolean isReportSpeed) {
|
||||
this.unitSystem = unitSystem;
|
||||
this.context = context;
|
||||
this.stackMode = stackMode;
|
||||
this.isReportSpeed = isReportSpeed;
|
||||
@@ -54,12 +57,15 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
|
||||
} else {
|
||||
sumDistance = interval.getDistance().multipliedBy(nextPosition);
|
||||
}
|
||||
viewHolder.distance.setText(StringUtils.formatDistance(context, sumDistance, metricUnits));
|
||||
viewHolder.distance.setText(DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context).formatDistance(sumDistance));
|
||||
|
||||
viewHolder.rate.setText(StringUtils.formatSpeed(context, interval.getSpeed(), metricUnits, isReportSpeed));
|
||||
SpeedFormatter formatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(isReportSpeed).build(context);
|
||||
viewHolder.rate.setText(formatter.formatSpeed(interval.getSpeed()));
|
||||
|
||||
viewHolder.gain.setText(StringUtils.formatAltitudeChange(context, interval.getGain_m(), metricUnits));
|
||||
viewHolder.loss.setText(StringUtils.formatAltitudeChange(context, interval.getLoss_m(), metricUnits));
|
||||
viewHolder.gain.setText(StringUtils.formatAltitude(context, interval.getGain_m(), unitSystem));
|
||||
viewHolder.loss.setText(StringUtils.formatAltitude(context, interval.getLoss_m(), unitSystem));
|
||||
|
||||
}
|
||||
|
||||
@@ -71,8 +77,8 @@ public class IntervalStatisticsAdapter extends RecyclerView.Adapter<RecyclerView
|
||||
return intervalList.size();
|
||||
}
|
||||
|
||||
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, boolean metricUnits, boolean isReportSpeed) {
|
||||
this.metricUnits = metricUnits;
|
||||
public List<IntervalStatistics.Interval> swapData(List<IntervalStatistics.Interval> data, UnitSystem unitSystem, boolean isReportSpeed) {
|
||||
this.unitSystem = unitSystem;
|
||||
this.isReportSpeed = isReportSpeed;
|
||||
intervalList = data;
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
/**
|
||||
* This model is used to load intervals for a track.
|
||||
@@ -64,14 +65,14 @@ public class IntervalStatisticsModel extends AndroidViewModel {
|
||||
handler = null;
|
||||
}
|
||||
|
||||
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
|
||||
public MutableLiveData<List<IntervalStatistics.Interval>> getIntervalStats(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) {
|
||||
if (intervalsLiveData == null) {
|
||||
if (interval == null) {
|
||||
interval = IntervalOption.OPTION_1;
|
||||
}
|
||||
|
||||
intervalsLiveData = new MutableLiveData<>();
|
||||
distanceInterval = interval.getDistance(metricUnits);
|
||||
distanceInterval = interval.getDistance(unitSystem);
|
||||
intervalStatistics = new IntervalStatistics(distanceInterval);
|
||||
|
||||
loadIntervalStatistics(trackId);
|
||||
@@ -104,13 +105,13 @@ public class IntervalStatisticsModel extends AndroidViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
public void update(Track.Id trackId, boolean metricUnits, @Nullable IntervalOption interval) {
|
||||
public void update(Track.Id trackId, UnitSystem unitSystem, @Nullable IntervalOption interval) {
|
||||
if (interval == null) {
|
||||
interval = IntervalOption.OPTION_1;
|
||||
interval = IntervalOption.DEFAULT;
|
||||
}
|
||||
|
||||
lastTrackPointId = null;
|
||||
distanceInterval = interval.getDistance(metricUnits);
|
||||
distanceInterval = interval.getDistance(unitSystem);
|
||||
intervalStatistics = new IntervalStatistics(distanceInterval);
|
||||
loadIntervalStatistics(trackId);
|
||||
}
|
||||
@@ -119,6 +120,8 @@ public class IntervalStatisticsModel extends AndroidViewModel {
|
||||
* Intervals length this view model support.
|
||||
*/
|
||||
public enum IntervalOption {
|
||||
OPTION_0_1(0.1f),
|
||||
OPTION_0_5(0.5f),
|
||||
OPTION_1(1),
|
||||
OPTION_2(2),
|
||||
OPTION_3(3),
|
||||
@@ -128,25 +131,26 @@ public class IntervalStatisticsModel extends AndroidViewModel {
|
||||
OPTION_20(20),
|
||||
OPTION_50(50);
|
||||
|
||||
private final int multiplier;
|
||||
static IntervalOption DEFAULT = OPTION_1;
|
||||
|
||||
IntervalOption(int multiplier) {
|
||||
private final double multiplier;
|
||||
|
||||
IntervalOption(double multiplier) {
|
||||
this.multiplier = multiplier;
|
||||
}
|
||||
|
||||
public Distance getDistance(boolean metricUnits) {
|
||||
public Distance getDistance(UnitSystem unitSystem) {
|
||||
return Distance
|
||||
.one(metricUnits)
|
||||
.one(unitSystem)
|
||||
.multipliedBy(multiplier);
|
||||
}
|
||||
|
||||
public double getMultiplier() {
|
||||
return multiplier;
|
||||
}
|
||||
|
||||
public boolean sameMultiplier(IntervalOption intervalOption) {
|
||||
return intervalOption != null && this.multiplier == intervalOption.multiplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "" + multiplier; //TODO Somehow IntervalsFragment relies on a parsable Integer.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,15 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.databinding.IntervalListViewBinding;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
/**
|
||||
* A fragment to display the intervals from recorded track.
|
||||
@@ -39,7 +42,7 @@ public class IntervalsFragment extends Fragment {
|
||||
private IntervalStatisticsModel.IntervalOption selectedInterval;
|
||||
|
||||
private Track.Id trackId;
|
||||
private boolean metricUnits;
|
||||
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
|
||||
private IntervalStatisticsAdapter adapter;
|
||||
private ArrayAdapter<IntervalStatisticsModel.IntervalOption> intervalsAdapter;
|
||||
|
||||
@@ -49,7 +52,7 @@ public class IntervalsFragment extends Fragment {
|
||||
|
||||
protected final SharedPreferences.OnSharedPreferenceChangeListener sharedPreferenceChangeListener = (sharedPreferences, key) -> {
|
||||
if (PreferencesUtils.isKey(R.string.stats_units_key, key) || PreferencesUtils.isKey(R.string.stats_rate_key, key)) {
|
||||
updateIntervals(PreferencesUtils.isMetricUnits(), selectedInterval);
|
||||
updateIntervals(PreferencesUtils.getUnitSystem(), selectedInterval);
|
||||
if (intervalsAdapter != null) {
|
||||
intervalsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
@@ -59,10 +62,11 @@ public class IntervalsFragment extends Fragment {
|
||||
/**
|
||||
* Creates an instance of this class.
|
||||
*
|
||||
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
|
||||
* @param fromTopToBottom If true then the intervals are shown from top to bottom (the first interval on top). Otherwise the intervals are shown from bottom to top.
|
||||
* @return IntervalsFragment instance.
|
||||
*/
|
||||
public static Fragment newInstance(Track.Id trackId, boolean fromTopToBottom) {
|
||||
public static Fragment newInstance(@NonNull Track.Id trackId, boolean fromTopToBottom) {
|
||||
Objects.nonNull(trackId);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(FROM_TOP_TO_BOTTOM_KEY, fromTopToBottom);
|
||||
bundle.putParcelable(TRACK_ID_KEY, trackId);
|
||||
@@ -93,25 +97,30 @@ public class IntervalsFragment extends Fragment {
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Deprecated //TODO This method must be re-implemented.
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, metricUnits, isReportSpeed);
|
||||
adapter = new IntervalStatisticsAdapter(getContext(), stackModeListView, unitSystem, isReportSpeed);
|
||||
viewBinding.intervalList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
// TODO handle empty view: before we did viewBinding.intervalList.setEmptyView(viewBinding.intervalListEmptyView);
|
||||
viewBinding.intervalList.setAdapter(adapter);
|
||||
|
||||
final DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
intervalsAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, IntervalStatisticsModel.IntervalOption.values()) {
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
TextView v = (TextView) super.getView(position, convertView, parent);
|
||||
if (metricUnits) {
|
||||
v.setText(getContext().getString(R.string.value_integer_kilometer, Integer.parseInt(v.getText().toString())));
|
||||
} else {
|
||||
v.setText(getContext().getString(R.string.value_integer_mile, Integer.parseInt(v.getText().toString())));
|
||||
}
|
||||
|
||||
IntervalStatisticsModel.IntervalOption option = getItem(position);
|
||||
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
|
||||
v.setText(stringValue);
|
||||
return v;
|
||||
}
|
||||
|
||||
@@ -122,12 +131,15 @@ public class IntervalsFragment extends Fragment {
|
||||
};
|
||||
|
||||
viewBinding.intervalsDropdown.setAdapter(intervalsAdapter);
|
||||
viewBinding.intervalsDropdown.setOnItemClickListener((parent, view1, position, id) -> updateIntervals(metricUnits, IntervalStatisticsModel.IntervalOption.values()[position]));
|
||||
viewBinding.intervalsDropdown.setOnItemClickListener((parent, view1, position, id) -> {
|
||||
updateIntervals(unitSystem, IntervalStatisticsModel.IntervalOption.values()[position]);
|
||||
|
||||
viewBinding.intervalsDropdown.setText(
|
||||
getContext().getString(R.string.value_integer_kilometer, Integer.parseInt(selectedInterval != null ? selectedInterval.toString() : IntervalStatisticsModel.IntervalOption.values()[0].toString())),
|
||||
false
|
||||
);
|
||||
//TODO This duplicates the intervalAdapter code
|
||||
setIntervalsDropdownText();
|
||||
});
|
||||
|
||||
//TODO This duplicates the intervalAdapter code
|
||||
setIntervalsDropdownText();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -180,19 +192,32 @@ public class IntervalsFragment extends Fragment {
|
||||
}
|
||||
|
||||
viewBinding.intervalRate.setText(isReportSpeed ? getString(R.string.stats_speed) : getString(R.string.stats_pace));
|
||||
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, metricUnits, selectedInterval);
|
||||
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, metricUnits, isReportSpeed));
|
||||
LiveData<List<IntervalStatistics.Interval>> liveData = viewModel.getIntervalStats(trackId, unitSystem, selectedInterval);
|
||||
liveData.observe(getActivity(), intervalList -> adapter.swapData(intervalList, unitSystem, isReportSpeed));
|
||||
|
||||
setIntervalsDropdownText();
|
||||
}
|
||||
|
||||
private synchronized void updateIntervals(boolean metricUnits, IntervalStatisticsModel.IntervalOption selectedInterval) {
|
||||
boolean update = metricUnits != this.metricUnits
|
||||
private void setIntervalsDropdownText() {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(0)
|
||||
.setUnit(unitSystem)
|
||||
.build(getContext());
|
||||
|
||||
IntervalStatisticsModel.IntervalOption option = selectedInterval != null ? selectedInterval : IntervalStatisticsModel.IntervalOption.DEFAULT;
|
||||
String stringValue = formatter.formatDistance(option.getDistance(unitSystem));
|
||||
viewBinding.intervalsDropdown.setText(stringValue, false);
|
||||
}
|
||||
|
||||
private synchronized void updateIntervals(UnitSystem unitSystem, IntervalStatisticsModel.IntervalOption selectedInterval) {
|
||||
boolean update = unitSystem != this.unitSystem
|
||||
|| selectedInterval == null
|
||||
|| !selectedInterval.sameMultiplier(this.selectedInterval);
|
||||
this.metricUnits = metricUnits;
|
||||
this.unitSystem = unitSystem;
|
||||
this.selectedInterval = selectedInterval;
|
||||
|
||||
if (update && viewModel != null) {
|
||||
viewModel.update(trackId, this.metricUnits, this.selectedInterval);
|
||||
viewModel.update(trackId, this.unitSystem, this.selectedInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public class ExportUtils {
|
||||
}
|
||||
|
||||
private static Uri getExportDocumentFileUri(Context context, Track track, TrackFileFormat trackFileFormat, DocumentFile directory) {
|
||||
String exportFileName = getExportFileNameForTrack(track, trackFileFormat.getExtension());
|
||||
String exportFileName = PreferencesUtils.getTrackFileformatGenerator().format(track, trackFileFormat);
|
||||
Uri exportDocumentFileUri = findFile(context, directory.getUri(), exportFileName);
|
||||
if (exportDocumentFileUri == null) {
|
||||
final DocumentFile file = directory.createFile(trackFileFormat.getMimeType(), exportFileName);
|
||||
@@ -119,10 +119,6 @@ public class ExportUtils {
|
||||
return exportDocumentFileUri;
|
||||
}
|
||||
|
||||
private static String getExportFileNameForTrack(Track track, String trackFileFormatExtension) {
|
||||
return track.getUuid().toString().substring(0, 8) + "_" + FileUtils.sanitizeFileName(track.getName()) + "." + trackFileFormatExtension;
|
||||
}
|
||||
|
||||
private static Uri findFile(Context context, Uri directoryUri, String exportFileName) {
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(directoryUri, DocumentsContract.getDocumentId(directoryUri));
|
||||
|
||||
@@ -17,6 +17,8 @@ package de.dennisguse.opentracks.util;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
|
||||
/**
|
||||
* Utility class for decimating tracks at a given level of precision.
|
||||
*
|
||||
@@ -40,4 +42,11 @@ public class LocationUtils {
|
||||
&& Math.abs(location.getLatitude()) <= 90
|
||||
&& Math.abs(location.getLongitude()) <= 180;
|
||||
}
|
||||
|
||||
public static boolean fulfillsAccuracy(Location location, Distance thresholdHorizontalAccuracy) {
|
||||
return location.hasAccuracy() &&
|
||||
Distance.of(location.getAccuracy())
|
||||
.lessThan(thresholdHorizontalAccuracy);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,10 @@ import java.time.temporal.TemporalAccessor;
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
|
||||
/**
|
||||
* Various string manipulation methods.
|
||||
@@ -128,32 +129,6 @@ public class StringUtils {
|
||||
return TextUtils.split(value, ":").length == 2 ? "0:" + value : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the distance in meters.
|
||||
*
|
||||
* @param context the context
|
||||
* @param distance the distance
|
||||
* @param metricUnits true to use metric units. False to use imperial units
|
||||
*/
|
||||
public static String formatDistance(Context context, Distance distance, boolean metricUnits) {
|
||||
if (distance.isInvalid()) {
|
||||
return context.getString(R.string.value_unknown);
|
||||
}
|
||||
|
||||
Pair<String, String> distanceParts = getDistanceParts(context, distance, metricUnits);
|
||||
|
||||
return context.getString(R.string.distance_with_unit, distanceParts.first, distanceParts.second);
|
||||
}
|
||||
|
||||
public static String formatSpeed(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
|
||||
Pair<String, String> distanceParts = getSpeedParts(context, speed, metricUnits, reportSpeed);
|
||||
|
||||
return context.getString(R.string.speed_with_unit, distanceParts.first, distanceParts.second);
|
||||
}
|
||||
|
||||
private static String formatDecimal(double value) {
|
||||
return StringUtils.formatDecimal(value, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a decimal number while removing trailing zeros of the decimal part (if present).
|
||||
@@ -187,67 +162,6 @@ public class StringUtils {
|
||||
return context.getString(R.string.location_latitude_longitude, Location.convert(latitude, Location.FORMAT_DEGREES), Location.convert(longitude, Location.FORMAT_DEGREES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted distance with unit.
|
||||
*
|
||||
* @param context the context
|
||||
* @param distance the distance
|
||||
* @param metricUnits true to use metric unit
|
||||
* @return the formatted distance (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public static Pair<String, String> getDistanceParts(Context context, Distance distance, boolean metricUnits) {
|
||||
if (distance.isInvalid()) {
|
||||
return new Pair<>(null, context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet));
|
||||
}
|
||||
|
||||
if (metricUnits) {
|
||||
if (distance.greaterThan(Distance.of(500))) {
|
||||
return new Pair<>(formatDecimal(distance.toKM()), context.getString(R.string.unit_kilometer));
|
||||
} else {
|
||||
return new Pair<>(formatDecimal(distance.toM()), context.getString(R.string.unit_meter));
|
||||
}
|
||||
} else {
|
||||
if (distance.greaterThan(Distance.ofMile(0.5))) {
|
||||
return new Pair<>(formatDecimal(distance.toMI()), context.getString(R.string.unit_mile));
|
||||
} else {
|
||||
return new Pair<>(formatDecimal(distance.toFT()), context.getString(R.string.unit_feet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the formatted speed with unit.
|
||||
*
|
||||
* @param context the context
|
||||
* @param speed the speed
|
||||
* @param metricUnits true to use metric unit
|
||||
* @param reportSpeed true to report speed; false for pace
|
||||
* @return the formatted speed (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public static Pair<String, String> getSpeedParts(Context context, Speed speed, boolean metricUnits, boolean reportSpeed) {
|
||||
int unitId;
|
||||
if (metricUnits) {
|
||||
unitId = reportSpeed ? R.string.unit_kilometer_per_hour : R.string.unit_minute_per_kilometer;
|
||||
} else {
|
||||
unitId = reportSpeed ? R.string.unit_mile_per_hour : R.string.unit_minute_per_mile;
|
||||
}
|
||||
String unitString = context.getString(unitId);
|
||||
|
||||
if (speed == null) {
|
||||
speed = Speed.zero();
|
||||
}
|
||||
|
||||
if (reportSpeed) {
|
||||
return new Pair<>(StringUtils.formatDecimal(speed.to(metricUnits), 1), unitString);
|
||||
}
|
||||
|
||||
int pace = (int) speed.toPace(metricUnits).getSeconds();
|
||||
|
||||
int minutes = pace / 60;
|
||||
int seconds = pace % 60;
|
||||
return new Pair<>(context.getString(R.string.time, minutes, seconds), unitString);
|
||||
}
|
||||
|
||||
public static Pair<String, String> getHeartRateParts(Context context, HeartRate heartrate) {
|
||||
String value = context.getString(R.string.value_unknown);
|
||||
if (heartrate != null) {
|
||||
@@ -340,35 +254,18 @@ public class StringUtils {
|
||||
/**
|
||||
* @return the formatted altitude_m (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, boolean metricUnits) {
|
||||
String formattedValue = context.getString(R.string.value_unknown);
|
||||
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
|
||||
public static Pair<String, String> getAltitudeParts(Context context, Float altitude_m, UnitSystem unitSystem) {
|
||||
DistanceFormatter formatter = DistanceFormatter.Builder()
|
||||
.setDecimalCount(1)
|
||||
.setUnit(unitSystem)
|
||||
.build(context);
|
||||
|
||||
if (altitude_m != null) {
|
||||
double value = Distance.of(altitude_m).toM_FT(metricUnits);
|
||||
formattedValue = StringUtils.formatDecimal(value, 1);
|
||||
}
|
||||
|
||||
return new Pair<>(formattedValue, unit);
|
||||
Distance distance = altitude_m != null ? Distance.of(altitude_m) : Distance.of(Double.NaN); //TODO Refactor Double.NaN
|
||||
return formatter.getDistanceParts(distance);
|
||||
}
|
||||
|
||||
public static String formatAltitude(Context context, Float altitude_m, boolean metricUnits) {
|
||||
Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, metricUnits);
|
||||
|
||||
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the formatted altitudeDifference_m (or null) and it's unit as {@link Pair}
|
||||
*/
|
||||
public static Pair<String, String> getAltitudeChangeParts(Context context, Float altitudeDifference_m, boolean metricUnits) {
|
||||
String formattedValue = altitudeDifference_m != null ? String.valueOf((int) Distance.of(altitudeDifference_m).toM_FT(metricUnits)) : context.getString(R.string.value_unknown);
|
||||
String unit = context.getString(metricUnits ? R.string.unit_meter : R.string.unit_feet);
|
||||
return new Pair<>(formattedValue, unit);
|
||||
}
|
||||
|
||||
public static String formatAltitudeChange(Context context, Float altitude_m, boolean metricUnits) {
|
||||
Pair<String, String> altitudeParts = getAltitudeChangeParts(context, altitude_m, metricUnits);
|
||||
public static String formatAltitude(Context context, Float altitude_m, UnitSystem unitSystem) {
|
||||
Pair<String, String> altitudeParts = getAltitudeParts(context, altitude_m, unitSystem);
|
||||
|
||||
return context.getString(R.string.altitude_with_unit, altitudeParts.first, altitudeParts.second);
|
||||
}
|
||||
|
||||
@@ -12,10 +12,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.DistanceFormatter;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.SpeedFormatter;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.stats.SensorStatistics;
|
||||
import de.dennisguse.opentracks.stats.TrackStatistics;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.DataField;
|
||||
@@ -24,17 +27,17 @@ import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class StatisticDataBuilder {
|
||||
|
||||
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull Layout layout, boolean metricUnits) {
|
||||
public static List<StatisticData> fromRecordingData(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull Layout layout, UnitSystem unitSystem) {
|
||||
List<StatisticData> statisticDataList = layout.getFields().stream()
|
||||
.filter(DataField::isVisible)
|
||||
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), metricUnits))
|
||||
.map(field -> build(context, recordingData, field.getKey(), field.isPrimary(), unitSystem))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, metricUnits));
|
||||
statisticDataList.addAll(getSensorStatsDataIfNeeded(context, recordingData, statisticDataList, unitSystem));
|
||||
return statisticDataList;
|
||||
}
|
||||
|
||||
private static StatisticData build(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, boolean metricUnits) {
|
||||
private static StatisticData build(@NonNull Context context, @NonNull RecordingData recordingData, @NonNull String fieldKey, boolean isPrimary, UnitSystem unitSystem) {
|
||||
final TrackPoint latestTrackPoint = recordingData.getLatestTrackPoint();
|
||||
final SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
final TrackStatistics trackStatistics = recordingData.getTrackStatistics();
|
||||
@@ -46,6 +49,10 @@ public class StatisticDataBuilder {
|
||||
|
||||
final String sensorUnknown = context.getString(R.string.value_unknown);
|
||||
|
||||
SpeedFormatter speedFormatterPace = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(false).build(context);
|
||||
SpeedFormatter speedFormatterSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context);
|
||||
|
||||
|
||||
if (fieldKey.equals(context.getString(R.string.stats_custom_layout_total_time_key))) {
|
||||
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getTotalTime()), null);
|
||||
title = context.getString(R.string.stats_total_time);
|
||||
@@ -53,48 +60,51 @@ public class StatisticDataBuilder {
|
||||
valueAndUnit = new Pair<>(StringUtils.formatElapsedTime(trackStatistics.getMovingTime()), null);
|
||||
title = context.getString(R.string.stats_moving_time);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_distance_key))) {
|
||||
valueAndUnit = StringUtils.getDistanceParts(context, trackStatistics.getTotalDistance(), metricUnits);
|
||||
valueAndUnit = DistanceFormatter.Builder()
|
||||
.setUnit(unitSystem)
|
||||
.build(context).getDistanceParts(trackStatistics.getTotalDistance());
|
||||
title = context.getString(R.string.stats_distance);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_speed_key)) || fieldKey.equals(context.getString(R.string.stats_custom_layout_pace_key))) {
|
||||
boolean reportSpeed = fieldKey.equals("speed");
|
||||
title = reportSpeed ? context.getString(R.string.stats_speed) : context.getString(R.string.stats_pace);
|
||||
|
||||
SpeedFormatter localSpeedFormatter = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(reportSpeed).build(context);
|
||||
if (sensorDataSet != null && sensorDataSet.getSpeed() != null) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, sensorDataSet.getSpeed().first, metricUnits, reportSpeed);
|
||||
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.getSpeed().first);
|
||||
description = sensorDataSet.getSpeed().second;
|
||||
} else {
|
||||
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, speed, metricUnits, reportSpeed);
|
||||
valueAndUnit = localSpeedFormatter.getSpeedParts(speed);
|
||||
description = context.getString(R.string.description_speed_source_gps);
|
||||
}
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_speed_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageMovingSpeed(), metricUnits, true);
|
||||
valueAndUnit = speedFormatterSpeed.getSpeedParts(trackStatistics.getAverageMovingSpeed());
|
||||
title = context.getString(R.string.stats_average_moving_speed);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_speed_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageSpeed(), metricUnits, true);
|
||||
valueAndUnit = speedFormatterSpeed.getSpeedParts(trackStatistics.getAverageSpeed());
|
||||
title = context.getString(R.string.stats_average_speed);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_max_speed_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getMaxSpeed(), metricUnits, true);
|
||||
valueAndUnit = speedFormatterSpeed.getSpeedParts(trackStatistics.getMaxSpeed());
|
||||
title = context.getString(R.string.stats_max_speed);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_moving_pace_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageMovingSpeed(), metricUnits, false);
|
||||
valueAndUnit = speedFormatterPace.getSpeedParts(trackStatistics.getAverageMovingSpeed());
|
||||
title = context.getString(R.string.stats_average_moving_pace);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_average_pace_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getAverageSpeed(), metricUnits, false);
|
||||
valueAndUnit = speedFormatterPace.getSpeedParts(trackStatistics.getAverageSpeed());
|
||||
title = context.getString(R.string.stats_average_pace);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_fastest_pace_key))) {
|
||||
valueAndUnit = StringUtils.getSpeedParts(context, trackStatistics.getMaxSpeed(), metricUnits, true);
|
||||
valueAndUnit = speedFormatterPace.getSpeedParts(trackStatistics.getMaxSpeed());
|
||||
title = context.getString(R.string.stats_fastest_pace);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_altitude_key))) {
|
||||
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
|
||||
title = context.getString(R.string.stats_altitude);
|
||||
description = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? context.getString(latestTrackPoint.getAltitude().getLabelId()) : null;
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, metricUnits);
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, altitude, unitSystem);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_gain_key))) {
|
||||
valueAndUnit = StringUtils.getAltitudeChangeParts(context, trackStatistics.getTotalAltitudeGain(), metricUnits);
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, trackStatistics.getTotalAltitudeGain(), unitSystem);
|
||||
title = context.getString(R.string.stats_gain);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_loss_key))) {
|
||||
valueAndUnit = StringUtils.getAltitudeChangeParts(context, trackStatistics.getTotalAltitudeLoss(), metricUnits);
|
||||
valueAndUnit = StringUtils.getAltitudeParts(context, trackStatistics.getTotalAltitudeLoss(), unitSystem);
|
||||
title = context.getString(R.string.stats_loss);
|
||||
} else if (fieldKey.equals(context.getString(R.string.stats_custom_layout_coordinates_key))) {
|
||||
title = context.getString(R.string.stats_coordinates);
|
||||
@@ -153,20 +163,20 @@ public class StatisticDataBuilder {
|
||||
/**
|
||||
* Builds a list of StatisticData with sensors configured but not in statisticDataList.
|
||||
*/
|
||||
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, RecordingData recordingData, List<StatisticData> statisticDataList, boolean metricUnits) {
|
||||
private static List<StatisticData> getSensorStatsDataIfNeeded(Context context, RecordingData recordingData, List<StatisticData> statisticDataList, UnitSystem unitSystem) {
|
||||
List<StatisticData> sensorDataList = new ArrayList<>();
|
||||
SensorDataSet sensorDataSet = recordingData.getSensorDataSet();
|
||||
if (sensorDataSet == null) {
|
||||
return sensorDataList;
|
||||
}
|
||||
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_heart_rate))) && sensorDataSet.getHeartRate() != null) {
|
||||
sensorDataList.add(build(context, recordingData, "heart_rate", true, metricUnits));
|
||||
sensorDataList.add(build(context, recordingData, "heart_rate", true, unitSystem));
|
||||
}
|
||||
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_cadence))) && sensorDataSet.getCadence() != null) {
|
||||
sensorDataList.add(build(context, recordingData, "cadence", true, metricUnits));
|
||||
sensorDataList.add(build(context, recordingData, "cadence", true, unitSystem));
|
||||
}
|
||||
if (statisticDataList.stream().noneMatch(i -> i.getField().getTitle().equals(context.getString(R.string.stats_sensors_power))) && sensorDataSet.getCyclingPower() != null && sensorDataSet.getCyclingPower().hasValue()) {
|
||||
sensorDataList.add(build(context, recordingData, "power", true, metricUnits));
|
||||
sensorDataList.add(build(context, recordingData, "power", true, unitSystem));
|
||||
}
|
||||
|
||||
return sensorDataList;
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.services.RecordingData;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.ui.customRecordingLayout.Layout;
|
||||
|
||||
public class StatisticsDataModel extends AndroidViewModel {
|
||||
@@ -26,9 +27,9 @@ public class StatisticsDataModel extends AndroidViewModel {
|
||||
return statsData;
|
||||
}
|
||||
|
||||
public void update(RecordingData recordingData, Layout layout, boolean metricUnit) {
|
||||
public void update(RecordingData recordingData, Layout layout, UnitSystem unitSystem) {
|
||||
new Thread(() -> {
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, metricUnit);
|
||||
List<StatisticData> statisticDataList = StatisticDataBuilder.fromRecordingData(getApplication(), recordingData, layout, unitSystem);
|
||||
statsData.postValue(statisticDataList);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/TextAppearance.OpenTracks.PrimaryValue"
|
||||
android:value="@string/value_unknown"
|
||||
android:format12Hour="hh:mm:ss a"
|
||||
android:format24Hour="HH:mm:ss"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="00:00" />
|
||||
tools:text="00:00:00" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/stats_description_main"
|
||||
|
||||
@@ -258,16 +258,12 @@ limitations under the License.
|
||||
<string name="unit_minute_per_kilometer">د/كم</string>
|
||||
<string name="unit_minute_per_mile">دقيقة/ميل</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet">%1$d قدم</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d قدم (إشارة نظام تحديد المواقع ممتازة)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d قدم (موصى به)</string>
|
||||
<string name="value_integer_kilometer">%1$d كم</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d كم/ساعة</string>
|
||||
<string name="value_integer_meter">%1$d م</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d م (إشارة نظام تحديد المواقع ممتازة)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d م (إشارة نظام تحديد المواقع سيئة)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d م (موصى به)</string>
|
||||
<string name="value_integer_mile">%1$d ميل</string>
|
||||
<string name="value_integer_mile_hour">%1$d ميل/ساعة</string>
|
||||
<string name="value_integer_minute">%1$d دقيقة</string>
|
||||
<string name="value_integer_second">%1$d ث</string>
|
||||
|
||||
@@ -211,16 +211,12 @@
|
||||
<string name="unit_mile_per_hour">mi/h</string>
|
||||
<string name="unit_minute_per_kilometer">min/km</string>
|
||||
<string name="unit_minute_per_mile">min/mi</string>
|
||||
<string name="value_integer_feet">%1$d ft</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d ft (excellent GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d ft (recommended)</string>
|
||||
<string name="value_integer_kilometer">%1$d km</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d km/h</string>
|
||||
<string name="value_integer_meter">%1$d m</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d m (excellent GPS)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d m (poor GPS)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d m (recommended)</string>
|
||||
<string name="value_integer_mile">%1$d mi</string>
|
||||
<string name="value_integer_mile_hour">%1$d mph</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
|
||||
@@ -214,16 +214,12 @@
|
||||
<string name="unit_mile_per_hour">millas/hora</string>
|
||||
<string name="unit_minute_per_kilometer">min/km</string>
|
||||
<string name="unit_minute_per_mile">min/milla</string>
|
||||
<string name="value_integer_feet">%1$d pies</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d pies (señal de GPS excelente)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d pies (recomendado)</string>
|
||||
<string name="value_integer_kilometer">%1$d km</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d km/h</string>
|
||||
<string name="value_integer_meter">%1$d m</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d m (señal de GPS excelente)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d m (señal de GPS deficiente)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d m (recomendado)</string>
|
||||
<string name="value_integer_mile">%1$d millas</string>
|
||||
<string name="value_integer_mile_hour">%1$d mi/h</string>
|
||||
<string name="value_integer_minute">%1$d mín</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
@@ -537,4 +533,22 @@
|
||||
<string name="settings_public_api_dashboard_enabled_title">Transferencia automática de datos</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_off">Los datos grabados no se compartirán automáticamente con otras aplicaciones.</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_on">Una aplicación que ha iniciado una grabación puede también acceder a los datos grabados.</string>
|
||||
<string name="voice_per_kilometer">por kilometro</string>
|
||||
<string name="voice_per_mile">por milla</string>
|
||||
<string name="lap_time">Tiempo de vuelta</string>
|
||||
<string name="pace">Ritmo</string>
|
||||
<string name="lap_speed">Velocidad de vuelta</string>
|
||||
<string name="speed">Velocidad</string>
|
||||
<string name="total_distance">Distancia total</string>
|
||||
<plurals name="voiceDistanceKilometers">
|
||||
<item quantity="one">1 kilómetro</item>
|
||||
<item quantity="other">%1$.2f kilómetros</item>
|
||||
</plurals>
|
||||
<plurals name="voiceDistanceMiles">
|
||||
<item quantity="one">1 milla</item>
|
||||
<item quantity="other">%1$.2f millas</item>
|
||||
</plurals>
|
||||
<string name="average_heart_rate">Ritmo cardíaco promedio</string>
|
||||
<string name="current_heart_rate">Ritmo cardíaco actual</string>
|
||||
<string name="settings_announcements_report_heart_rate">Anunciar ritmo cardíaco</string>
|
||||
</resources>
|
||||
@@ -259,16 +259,12 @@ limitations under the License.
|
||||
<string name="unit_minute_per_kilometer">мин/км</string>
|
||||
<string name="unit_minute_per_mile">мин/мил</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet">%1$d фт</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d фт (отличен GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d фт (препоръчително)</string>
|
||||
<string name="value_integer_kilometer">%1$d км</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d км/ч</string>
|
||||
<string name="value_integer_meter">%1$d м</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d м (отличен GPS)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d м (слаб GPS)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d м (препоръчително)</string>
|
||||
<string name="value_integer_mile">%1$d мил</string>
|
||||
<string name="value_integer_mile_hour">%1$d мил/ч</string>
|
||||
<string name="value_integer_minute">%1$d мин</string>
|
||||
<string name="value_integer_second">%1$d сек</string>
|
||||
|
||||
@@ -257,16 +257,12 @@ limitations under the License.
|
||||
<string name="unit_minute_per_kilometer">min/km</string>
|
||||
<string name="unit_minute_per_mile">min/milles</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet">%1$d peus</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d peus (senyal de GPS excel·lent)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d peus (recomanat)</string>
|
||||
<string name="value_integer_kilometer">%1$d km</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d km/h</string>
|
||||
<string name="value_integer_meter">%1$d m</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d m (GPS amb bona senyal)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d m (GPS amb poca senyal)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d m (recomanat)</string>
|
||||
<string name="value_integer_mile">%1$d milles</string>
|
||||
<string name="value_integer_mile_hour">%1$d mi/h</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
|
||||
@@ -324,16 +324,12 @@ limitations under the License.
|
||||
<string name="unit_minute_per_kilometer">min/km</string>
|
||||
<string name="unit_minute_per_mile">min/míle</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet">%1$d stop</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d stop (vynikající signál GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d stop (doporučeno)</string>
|
||||
<string name="value_integer_kilometer">%1$d km</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d km/h</string>
|
||||
<string name="value_integer_meter">%1$d m</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d m (vynikající signál GPS)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d m (špatný signál GPS)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d m (doporučeno)</string>
|
||||
<string name="value_integer_mile">%1$d mil</string>
|
||||
<string name="value_integer_mile_hour">%1$d mil/h</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_second">%1$d s</string>
|
||||
@@ -589,9 +585,9 @@ limitations under the License.
|
||||
<string name="speed">Rychlost</string>
|
||||
<string name="total_distance">Celková vzdálenost</string>
|
||||
<plurals name="voiceDistanceKilometers">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="one"/>
|
||||
<item quantity="few"/>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="average_heart_rate">Průměrný srdeční tep</string>
|
||||
<string name="current_heart_rate">Aktuální srdeční tep</string>
|
||||
@@ -610,4 +606,10 @@ limitations under the License.
|
||||
<string name="settings_public_api_dashboard_enabled_title">Automatický přenos dat</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_on">Aplikace, která zahájila nahrávání, může také přistupovat k nahraným datům.</string>
|
||||
<string name="settings_public_api_dashboard_enabled_summary_off">Zaznamenaná data nebudou automaticky sdílena s jinými aplikacemi.</string>
|
||||
<string name="pace">Tempo</string>
|
||||
<plurals name="voiceDistanceMiles">
|
||||
<item quantity="one"></item>
|
||||
<item quantity="few"></item>
|
||||
<item quantity="other"></item>
|
||||
</plurals>
|
||||
</resources>
|
||||
@@ -329,16 +329,12 @@ Hvis GPS-enheden rapporterer unøjagtige data (f.eks. Placering, hastighed, høj
|
||||
<string name="unit_minute_per_kilometer">min./km</string>
|
||||
<string name="unit_minute_per_mile">min./mil</string>
|
||||
<!-- Value -->
|
||||
<string name="value_integer_feet">%1$d fod</string>
|
||||
<string name="value_integer_feet_excellent_gps">%1$d fod (fremragende GPS)</string>
|
||||
<string name="value_integer_feet_recommended">%1$d fod (anbefales)</string>
|
||||
<string name="value_integer_kilometer">%1$d km</string>
|
||||
<string name="value_integer_kilometer_hour">%1$d km/t</string>
|
||||
<string name="value_integer_meter">%1$d m</string>
|
||||
<string name="value_integer_meter_excellent_gps">%1$d m (fremragende GPS)</string>
|
||||
<string name="value_integer_meter_poor_gps">%1$d m (dårlig GPS)</string>
|
||||
<string name="value_integer_meter_recommended">%1$d m (anbefalet)</string>
|
||||
<string name="value_integer_mile">%1$d mil</string>
|
||||
<string name="value_integer_mile_hour">%1$d mil/t</string>
|
||||
<string name="value_integer_minute">%1$d min</string>
|
||||
<string name="value_integer_second">%1$d sek</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user