Compare commits

..

10 Commits

Author SHA1 Message Date
Dennis Guse 91b379c328 Release: v4.28.3 2026-08-31 20:07:23 +02:00
Dennis Guse e28a84fc88 Allow to disable segment creation (max recording distance).
Fixes #2389.
2026-08-28 07:07:06 +02:00
Dennis Guse c2d8d06204 Merge remote-tracking branch 'weblate/main' 2026-08-28 06:47:06 +02:00
José Rebelo 7ca871c699 Bugfix: Respect declared namespace prefixes 2026-08-26 15:21:06 +01:00
Dennis Guse 337c50eb91 Cleanup: qName. 2026-08-26 07:34:22 +02:00
Dany Mestas 2ff8d21897 GPX: match activity types case-insensitively on import. 2026-08-25 22:23:57 +02:00
Dennis Guse 2d4e987812 GPX: export moving speed in summary. 2026-08-24 17:26:11 +02:00
CI/CD Bot by pstorch fb928ccc45 chore(deps): update dependency com.android.tools.build:gradle to v9.3.2 2026-08-24 03:34:20 +00:00
Priit Jõerüüt 25a2f978c4 Translated using Weblate (Estonian)
Currently translated at 100.0% (554 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/et/
2026-07-29 03:59:43 +00:00
jimkats 36c3885dce Translated using Weblate (Greek)
Currently translated at 70.3% (390 of 554 strings)

Translation: OpenTracks/opentracks.src-main-res-values-strings-xml--main
Translate-URL: https://translate.codeberg.org/projects/opentracks/opentracks-src-main-res-values-strings-xml-main/el/
2026-07-29 03:59:43 +00:00
13 changed files with 499 additions and 124 deletions
+3 -3
View File
@@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:9.3.1'
classpath 'com.android.tools.build:gradle:9.3.2'
}
}
@@ -47,8 +47,8 @@ android {
defaultConfig {
applicationId 'de.dennisguse.opentracks'
versionCode 6741
versionName "v4.28.2"
versionCode 6751
versionName "v4.28.3"
buildConfigField "String", "VERSION_NAME_FULL", "\"${getVersionName()}\""
@@ -0,0 +1,10 @@
v4.28.3: OpenTracks
Changes:
- Settings: allow to disable segment creation (max recording distance)
- Importer: match activity type case-insensitive
- GPX: export moving speed in summary
Bugfix:
- GPX: importer must respect namespace prefixes
@@ -0,0 +1,78 @@
package de.dennisguse.opentracks.data.models;
import static org.junit.Assert.assertEquals;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Locale;
@RunWith(AndroidJUnit4.class)
public class ActivityTypeTest {
@Test
public void findBy_id() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("trail running"));
}
@Test
public void findBy_idOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
assertEquals(activityType, ActivityType.findBy(activityType.getId()));
}
}
@Test
public void findBy_synonymsOfEveryActivityType() {
for (ActivityType activityType : ActivityType.values()) {
for (String synonym : activityType.synonyms) {
assertEquals(activityType, ActivityType.findBy(synonym));
assertEquals(activityType, ActivityType.findBy(synonym.toUpperCase(Locale.ROOT).replace(' ', '_')));
}
}
}
@Test
public void findBy_ignoresCase() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("RUNNING"));
assertEquals(ActivityType.ATV, ActivityType.findBy("atv"));
}
@Test
public void findBy_ignoresSeparators() {
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUNNING"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("mountain-biking"));
assertEquals(ActivityType.CROSS_COUNTRY_SKIING, ActivityType.findBy("cross_country_skiing"));
}
@Test
public void findBy_ignoresSurroundingAndRepeatedWhitespace() {
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy(" Mountain Biking "));
}
@Test
public void findBy_synonym() {
assertEquals(ActivityType.RUNNING, ActivityType.findBy("treadmill"));
assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUN"));
assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("MOUNTAIN_BIKE"));
assertEquals(ActivityType.SWIMMING_OPEN, ActivityType.findBy("SWIMMING_OPENWATER"));
assertEquals(ActivityType.BIKING, ActivityType.findBy("bike"));
}
@Test
public void findBy_unknown() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy("hovercraft"));
}
@Test
public void findBy_null() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(null));
}
@Test
public void findBy_blank() {
assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(" "));
}
}
@@ -161,6 +161,143 @@ public class GPXTrackImporterTest {
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_pwd() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_pwr);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_with_power_gpxpx() throws IOException {
// given
XMLImporter importer = new XMLImporter(new GPXTrackImporter(context, trackImporter));
InputStream inputStream = InstrumentationRegistry.getInstrumentation().getContext().getResources().openRawResource(de.dennisguse.opentracks.test.R.raw.gpx_with_power_gpxpx);
// when
// 1. import
importTrackId = importer.importFile(inputStream).get(0);
// then
// 2. track
Track importedTrack = contentProviderUtils.getTrack(importTrackId);
assertNotNull(importedTrack);
assertEquals("", importedTrack.activityTypeLocalized());
assertEquals("", importedTrack.description());
assertEquals("gpx with power", importedTrack.name());
assertEquals(ActivityType.UNKNOWN, importedTrack.activityType());
// 3. trackpoints
List<TrackPoint> importedTrackPoints = TestDataUtil.getTrackPoints(contentProviderUtils, importTrackId);
assertEquals(3, importedTrackPoints.size());
TrackPointAssert a = new TrackPointAssert();
a.assertEquals(List.of(
new TrackPoint(null, TrackPoint.Type.SEGMENT_START_AUTOMATIC,
new Position(
Instant.parse("2021-09-07T22:10:19Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
null),
null,
null,
null,
null,
new Power(100f),
null
),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:11:07Z"),
30.14184657, -40.38670089, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7976524233818054)),
null,
null,
null,
null,
new Power(110f),
null),
new TrackPoint(null, TrackPoint.Type.TRACKPOINT,
new Position(
Instant.parse("2021-09-07T22:12:00Z"),
30.14185982, -40.3863038, null,
Altitude.WGS84.of(-5), null,
null,
Speed.of(0.7224021553993225)),
null,
null,
null,
null,
new Power(150f),
null)
), importedTrackPoints);
}
@LargeTest
@Test
public void gpx_speed_no_namespace() throws IOException {
+2 -1
View File
@@ -28,10 +28,11 @@ xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/
<opentracks:typeTranslated><![CDATA[unknown]]></opentracks:typeTranslated>
<gpxtrkx:TrackStatsExtension>
<gpxtrkx:Distance>0.0</gpxtrkx:Distance>
<gpxtrkx:TimerTime>0</gpxtrkx:TimerTime>
<gpxtrkx:TotalElapsedTime>0</gpxtrkx:TotalElapsedTime>
<gpxtrkx:MovingTime>0</gpxtrkx:MovingTime>
<gpxtrkx:StoppedTime>0</gpxtrkx:StoppedTime>
<gpxtrkx:MaxSpeed>0.0</gpxtrkx:MaxSpeed>
<gpxtrkx:MovingSpeed>0.0</gpxtrkx:MovingSpeed>
</gpxtrkx:TrackStatsExtension>
</extensions>
<trkseg>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>100</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>110</gpxpx:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<gpxpx:PowerInWatts>150</gpxpx:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<gpx version="1.1" creator="Cruiser" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pwr="http://www.garmin.com/xmlschemas/PowerExtension/v1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<trk>
<name>gpx with power</name>
<trkseg>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:10:19Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>100</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14184657" lon="-40.38670089">
<ele>-5</ele>
<time>2021-09-07T22:11:07Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>110</pwr:PowerInWatts>
</extensions>
</trkpt>
<trkpt lat="30.14185982" lon="-40.3863038">
<ele>-5</ele>
<time>2021-09-07T22:12:00Z</time>
<hdop>4</hdop>
<extensions>
<pwr:PowerInWatts>150</pwr:PowerInWatts>
</extensions>
</trkpt>
</trkseg>
</trk>
</gpx>
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import de.dennisguse.opentracks.R;
@@ -14,13 +15,13 @@ public enum ActivityType {
AIRPLANE("airplane", R.string.activity_type_airplane, R.drawable.ic_activity_flight_24dp, true),
ATV("ATV", R.string.activity_type_atv, R.drawable.ic_activity_drive_24dp, true),
BIKING("biking", R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true),
BIKING("biking", List.of("bike", "indoor cycling"), R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true),
BLIMP("blimp", R.string.activity_type_blimp, ActivityIcon.ICON_UNKNOWN, true),
BOAT("boat", R.string.activity_type_boat, R.drawable.ic_activity_boat_24dp, true),
CLIMBING("climbing", R.string.activity_type_climbing, R.drawable.ic_activity_climbing_24dp, false),
COMMERCIAL_AIRPLANE("commercial airplane", R.string.activity_type_commercial_airplane, R.drawable.ic_activity_flight_24dp, true),
CROSS_COUNTRY_SKIING("cross-country skiing", R.string.activity_type_cross_country_skiing, R.drawable.ic_activity_skiing_24dp, true),
CYCLING("cycling", R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true),
CYCLING("cycling", List.of("cycle"), R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true),
DIRT_BIKE("dirt bike", R.string.activity_type_dirt_bike, R.drawable.ic_activity_mtb_24dp, true),
DONKEY_BACK_RIDING("donkey back riding", R.string.activity_type_donkey_back_riding, ActivityIcon.ICON_UNKNOWN, true),
DRIVING("driving", R.string.activity_type_driving, R.drawable.ic_activity_drive_24dp, true),
@@ -30,21 +31,21 @@ public enum ActivityType {
FERRY("ferry", R.string.activity_type_ferry, R.drawable.ic_activity_boat_24dp, true),
FRISBEE("frisbee", R.string.activity_type_frisbee, ActivityIcon.ICON_UNKNOWN, true),
GLIDING("gliding", R.string.activity_type_gliding, ActivityIcon.ICON_UNKNOWN, true),
GRAVEL_BIKING("gravel biking", R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true),
GRAVEL_BIKING("gravel biking", List.of("gravel bike"), R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true),
HANG_GLIDING("hang gliding", R.string.activity_type_hang_gliding, ActivityIcon.ICON_UNKNOWN, true),
HELICOPTER("helicopter", R.string.activity_type_helicopter, ActivityIcon.ICON_UNKNOWN, true),
HIKING("hiking", R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
HIKING("hiking", List.of("hike"), R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false),
HORSE_BACK_RIDING("horse back riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true),
HOT_AIR_BALLOON("hot air balloon", R.string.activity_type_hot_air_balloon, ActivityIcon.ICON_UNKNOWN, true),
ICE_SAILING("ice sailing", R.string.activity_type_ice_sailing, ActivityIcon.ICON_UNKNOWN, true),
INLINE_SKATING("inline skating", R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true),
INLINE_SKATING("inline skating", List.of("inline skate"), R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true),
KAYAKING("kayaking", R.string.activity_type_kayaking, R.drawable.ic_activity_kayaking_24dp, true),
KITE_SURFING("kite surfing", R.string.activity_type_kite_surfing, ActivityIcon.ICON_UNKNOWN, true),
LAND_SAILING("land sailing", R.string.activity_type_land_sailing, ActivityIcon.ICON_UNKNOWN, true),
MIXED_TYPE("mixed type", R.string.activity_type_mixed_type, ActivityIcon.ICON_UNKNOWN, true),
MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_motorbike_24dp, true),
MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true),
MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
MOUNTAIN_BIKING("mountain biking", List.of("mountain bike"), R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true),
OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true),
PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true),
@@ -53,46 +54,57 @@ public enum ActivityType {
RC_BOAT("RC boat", R.string.activity_type_rc_boat, R.drawable.ic_activity_boat_24dp, true),
RC_HELICOPTER("RC helicopter", R.string.activity_type_rc_helicopter, ActivityIcon.ICON_UNKNOWN, true),
RIDING("riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true),
ROAD_BIKING("road biking", R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true),
ROAD_BIKING("road biking", List.of("road bike"), R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true),
ROLLER_SKIING("roller skiing", R.string.activity_type_roller_skiing, ActivityIcon.ICON_UNKNOWN, true),
ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.ICON_UNKNOWN, true),
RUNNING("running", R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false),
RUNNING("running", List.of("run", "treadmill"), R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false),
SAILING("sailing", R.string.activity_type_sailing, R.drawable.ic_activity_sailing_24dp, true),
KICKSCOOTER("kickscooter", R.string.activity_type_kickscooter, R.drawable.ic_activity_scooter_24dp, true),
SEAPLANE("seaplane", R.string.activity_type_seaplane, R.drawable.ic_activity_flight_24dp, true),
SKATE_BOARDING("skateboarding", R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true),
SKATE_BOARDING("skateboarding", List.of("skateboard"), R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true),
SKATING("skating", R.string.activity_type_skating, ActivityIcon.ICON_UNKNOWN, true),
SKIING("skiing", R.string.activity_type_skiing, R.drawable.ic_activity_skiing_24dp, true),
SKY_JUMPING("sky jumping", R.string.activity_type_sky_jumping, ActivityIcon.ICON_UNKNOWN, true),
SLED("sled", R.string.activity_type_sled, ActivityIcon.ICON_UNKNOWN, true),
SNOW_BOARDING("snowboarding", R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true),
SNOW_BOARDING("snowboarding", List.of("snowboard"), R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true),
SNOW_SHOEING("snow shoeing", R.string.activity_type_snow_shoeing, ActivityIcon.ICON_UNKNOWN, true),
SPEED_WALKING("speed walking", R.string.activity_type_speed_walking, R.drawable.ic_activity_walk_24dp, false),
STREET_RUNNING("street running", R.string.activity_type_street_running, R.drawable.ic_activity_run_24dp, false),
SURFING("surfing", R.string.activity_type_surfing, ActivityIcon.ICON_UNKNOWN, true),
TRACK_CYCLING("track cycling", R.string.activity_type_track_cycling, R.drawable.ic_activity_bike_24dp, true),
TRACK_RUNNING("track running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRACK_RUNNING("track running", List.of("track run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIL_HIKING("trail hiking", R.string.activity_type_trail_hiking, R.drawable.ic_activity_walk_24dp, false),
TRAIL_RUNNING("trail running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIL_RUNNING("trail running", List.of("trail run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false),
TRAIN("train", R.string.activity_type_train, ActivityIcon.ICON_UNKNOWN, true),
ULTIMATE_FRISBEE("ultimate frisbee", R.string.activity_type_ultimate_frisbee, ActivityIcon.ICON_UNKNOWN, true),
WAKEBOARDING("wakeboarding", R.string.activity_type_wakeboarding, ActivityIcon.ICON_UNKNOWN, true),
WALKING("walking", R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false),
WALKING("walking", List.of("walk"), R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false),
WATER_SKIING("water skiing", R.string.activity_type_water_skiing, ActivityIcon.ICON_UNKNOWN, true),
WIND_SURFING("wind surfing", R.string.activity_type_wind_surfing, ActivityIcon.ICON_UNKNOWN, true),
SWIMMING("swimming", R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false),
SWIMMING_OPEN("swimming in open water", R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false),
SWIMMING("swimming", List.of("swim"), R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false),
SWIMMING_OPEN("swimming in open water", List.of("swimming openwater", "open water swimming"), R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false),
WORKOUT("workout", R.string.activity_type_workout, R.drawable.ic_activity_workout_24dp, false),
UNKNOWN("unknown", R.string.activity_type_unknown, ActivityIcon.ICON_UNKNOWN, true);
final String id;
/**
* Spellings used by other applications that {@link #normalize(String)} cannot map onto {@link #id}.
* Already normalized.
*/
final List<String> synonyms;
final int iconDrawableId;
final boolean showSpeedPreferred;
final int localizedStringId;
ActivityType(String id, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) {
this(id, List.of(), localizedStringId, iconDrawableId, showSpeedPreferred);
}
ActivityType(String id, List<String> synonyms, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) {
this.id = id;
this.synonyms = synonyms;
this.localizedStringId = localizedStringId;
this.iconDrawableId = iconDrawableId;
this.showSpeedPreferred = showSpeedPreferred;
@@ -121,12 +133,39 @@ public enum ActivityType {
.toList();
}
/**
* Lowercases and reduces "_", "-" and whitespace runs to a single space.
* Lowercasing is locale-independent: under a Turkish locale the I of "RUNNING" would otherwise
* lowercase to a dotless i and stop matching "running".
*/
@NonNull
private static String normalize(String activityTypeId) {
if (activityTypeId == null) {
return "";
}
return activityTypeId.toLowerCase(Locale.ROOT)
.replaceAll("[\\s_-]+", " ")
.trim();
}
/**
* Resolves an activity type as written by another application; GPX's type element is free text.
*/
@NonNull
public static ActivityType findBy(String activityTypeId) {
return Arrays.stream(ActivityType.values()).filter(
it -> it.id.equals(activityTypeId)
).findFirst()
.orElse(ActivityType.UNKNOWN);
String normalized = normalize(activityTypeId);
if (normalized.isEmpty()) {
return ActivityType.UNKNOWN;
}
return Arrays.stream(ActivityType.values())
.filter(it -> normalize(it.id).equals(normalized))
.findFirst()
.orElseGet(() -> Arrays.stream(ActivityType.values())
.filter(it -> it.synonyms.contains(normalized))
.findFirst()
.orElse(ActivityType.UNKNOWN));
}
@NonNull
@@ -282,10 +282,11 @@ public class GPXTrackExporter implements TrackExporter {
Statistics trackStatistics = track.statistics();
printWriter.println("<gpxtrkx:TrackStatsExtension>");
printWriter.println("<gpxtrkx:Distance>" + trackStatistics.totalDistance().toM() + "</gpxtrkx:Distance>");
printWriter.println("<gpxtrkx:TimerTime>" + trackStatistics.totalDuration().getSeconds() + "</gpxtrkx:TimerTime>");
printWriter.println("<gpxtrkx:TotalElapsedTime>" + trackStatistics.totalDuration().getSeconds() + "</gpxtrkx:TotalElapsedTime>");
printWriter.println("<gpxtrkx:MovingTime>" + trackStatistics.movingDuration().getSeconds() + "</gpxtrkx:MovingTime>");
printWriter.println("<gpxtrkx:StoppedTime>" + trackStatistics.getStoppedTime().getSeconds() + "</gpxtrkx:StoppedTime>");
printWriter.println("<gpxtrkx:MaxSpeed>" + trackStatistics.maxSpeed().toMPS() + "</gpxtrkx:MaxSpeed>");
printWriter.println("<gpxtrkx:MovingSpeed>" + trackStatistics.getAverageMovingSpeed().toMPS() + "</gpxtrkx:MovingSpeed>");
if (trackStatistics.altitudeGainLoss() != null) {
printWriter.println("<gpxtrkx:Ascent>" + trackStatistics.altitudeGainLoss().gain_m() + "</gpxtrkx:Ascent>");
printWriter.println("<gpxtrkx:Descent>" + trackStatistics.altitudeGainLoss().loss_m() + "</gpxtrkx:Descent>");
@@ -74,28 +74,44 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_TYPE_LOCALIZED = "opentracks:typeTranslated";
private static final String TAG_MARKER = "wpt";
private static final String TAG_ID = "opentracks:trackid";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
private static final String TAG_EXTENSION_SPEED = "gpxtpx:speed";
/**
* Often speed is exported without the proper namespace.
*/
private static final String TAG_EXTENSION_SPEED_COMPAT = "speed";
private static final String TAG_EXTENSION_HEARTRATE = "gpxtpx:hr";
private static final String TAG_EXTENSION_TEMPERATURE = "gpxtpx:atemp";
private static final String TAG_EXTENSION_CADENCE = "gpxtpx:cad";
private static final String TAG_EXTENSION_POWER = "pwr:PowerInWatts";
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";
/**
* Garmin Trackpoint Extensions.
*/
private static final String NS_GARMIN_TRACKPOINT_EXTENSION_V1 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1";
private static final String NS_GARMIN_TRACKPOINT_EXTENSION_V2 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v2";
private static final String TAG_GARMIN_TPX_EXTENSION_SPEED = "speed";
private static final String TAG_GARMIN_TPX_EXTENSION_HEARTRATE = "hr";
private static final String TAG_GARMIN_TPX_EXTENSION_TEMPERATURE = "atemp";
private static final String TAG_GARMIN_TPX_EXTENSION_CADENCE = "cad";
/**
* Garmin Trackpoint Extensions.
*/
private static final String NS_GARMIN_POWER_EXTENSION_V1 = "http://www.garmin.com/xmlschemas/PowerExtension/v1";
private static final String TAG_GARMIN_PWR_EXTENSION_POWER = "PowerInWatts";
/**
* OpenTracks Extensions.
*/
private static final String NS_OPENTRACKS = "http://opentracksapp.com/xmlschemas/v1";
private static final String TAG_OPENTRACKS_EXTENSION_TRACK_ID = "trackid";
private static final String TAG_OPENTRACKS_EXTENSION_TYPE_LOCALIZED = "typeTranslated";
private static final String TAG_OPENTRACKS_EXTENSION_GAIN = "gain";
private static final String TAG_OPENTRACKS_EXTENSION_LOSS = "loss";
private static final String TAG_OPENTRACKS_EXTENSION_DISTANCE = "distance";
private static final String TAG_OPENTRACKS_EXTENSION_ACCURACY_HORIZONTAL = "accuracy_horizontal";
private static final String TAG_OPENTRACKS_EXTENSION_ACCURACY_VERTICAL = "accuracy_vertical";
private Locator locator;
private final Context context;
@@ -145,8 +161,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) {
switch (tag) {
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (qName) {
case TAG_MARKER:
onMarkerStart(attributes);
break;
@@ -168,108 +184,128 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void endElement(String uri, String localName, String tag) {
switch (tag) {
case TAG_GPX -> onFileEnd();
case TAG_MARKER -> onMarkerEnd();
case TAG_TRACK -> {
if (activityTypeLocalized == null) {
// Backward compatibility: up v4.9.1 as <type> contained localized content.
activityTypeLocalized = activityType;
public void endElement(String uri, String localName, String qName) {
if (NS_GARMIN_TRACKPOINT_EXTENSION_V1.equals(uri) || NS_GARMIN_TRACKPOINT_EXTENSION_V2.equals(uri)) {
switch (localName) {
case TAG_GARMIN_TPX_EXTENSION_SPEED -> {
if (content != null) {
speed = content.trim();
}
}
trackImporter.setTrackData(context, name, uuid, description, activityTypeLocalized, activityType, zoneOffset);
zoneOffset = null;
}
case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
case TAG_TRACK_POINT -> {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint != null) currentSegment.add(trackPoint);
}
case TAG_NAME -> {
if (content != null) {
name = content.trim();
case TAG_GARMIN_TPX_EXTENSION_HEARTRATE -> {
if (content != null) {
heartrate = content.trim();
}
}
}
case TAG_DESCRIPTION -> {
if (content != null) {
description = content.trim();
case TAG_GARMIN_TPX_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
}
}
}
case TAG_TYPE -> { //Track or Marker/WPT
if (content != null) {
// In older version this might be localized content.
activityType = content.trim();
markerType = content.trim();
case TAG_GARMIN_TPX_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
}
}
default -> Log.w(TAG, "Unknown Garmin trackpoint extension tag " + localName);
}
case TAG_TYPE_LOCALIZED -> {
if (content != null) {
activityTypeLocalized = content.trim();
} else if (NS_GARMIN_POWER_EXTENSION_V1.equals(uri)) {
//noinspection SwitchStatementWithTooFewBranches
switch (localName) {
case TAG_GARMIN_PWR_EXTENSION_POWER -> {
if (content != null) {
power = content.trim();
}
}
default -> Log.w(TAG, "Unknown Garmin power extension tag " + localName);
}
case TAG_TIME -> {
if (content != null) {
time = content.trim();
} else if (NS_OPENTRACKS.equals(uri)) {
switch (localName) {
case TAG_OPENTRACKS_EXTENSION_TRACK_ID -> {
if (content != null) {
uuid = content.trim();
}
}
}
case TAG_ALTITUDE -> {
if (content != null) {
altitude = content.trim();
case TAG_OPENTRACKS_EXTENSION_TYPE_LOCALIZED -> {
if (content != null) {
activityTypeLocalized = content.trim();
}
}
}
case TAG_EXTENSION_SPEED, TAG_EXTENSION_SPEED_COMPAT -> {
if (content != null) {
speed = content.trim();
case TAG_OPENTRACKS_EXTENSION_GAIN -> {
if (content != null) {
gain = content.trim();
}
}
}
case TAG_EXTENSION_HEARTRATE -> {
if (content != null) {
heartrate = content.trim();
case TAG_OPENTRACKS_EXTENSION_LOSS -> {
if (content != null) {
loss = content.trim();
}
}
}
case TAG_EXTENSION_TEMPERATURE -> {
if (content != null) {
temperature = content.trim();
case TAG_OPENTRACKS_EXTENSION_DISTANCE -> {
if (content != null) {
sensorDistance = content.trim();
}
}
}
case TAG_EXTENSION_CADENCE -> {
if (content != null) {
cadence = content.trim();
case TAG_OPENTRACKS_EXTENSION_ACCURACY_HORIZONTAL -> {
if (content != null) {
accuracyHorizontal = content.trim();
}
}
}
case TAG_EXTENSION_POWER -> {
if (content != null) {
power = content.trim();
case TAG_OPENTRACKS_EXTENSION_ACCURACY_VERTICAL -> {
if (content != null) {
accuracyVertical = content.trim();
}
}
default -> Log.w(TAG, "Unknown OpenTracks extension tag " + localName);
}
case TAG_ID -> {
if (content != null) {
uuid = content.trim();
} else {
switch (qName) {
case TAG_GPX -> onFileEnd();
case TAG_MARKER -> onMarkerEnd();
case TAG_TRACK -> {
if (activityTypeLocalized == null) {
// Backward compatibility: up v4.9.1 as <type> contained localized content.
activityTypeLocalized = activityType;
}
trackImporter.setTrackData(context, name, uuid, description, activityTypeLocalized, activityType, zoneOffset);
zoneOffset = null;
}
}
case TAG_EXTENSION_GAIN -> {
if (content != null) {
gain = content.trim();
case TAG_TRACK_SEGMENT -> onTrackSegmentEnd();
case TAG_TRACK_POINT -> {
TrackPoint trackPoint = createTrackPoint();
if (trackPoint != null) currentSegment.add(trackPoint);
}
}
case TAG_EXTENSION_LOSS -> {
if (content != null) {
loss = content.trim();
case TAG_NAME -> {
if (content != null) {
name = content.trim();
}
}
}
case TAG_EXTENSION_DISTANCE -> {
if (content != null) {
sensorDistance = content.trim();
case TAG_DESCRIPTION -> {
if (content != null) {
description = content.trim();
}
}
}
case TAG_EXTENSION_ACCURACY_HORIZONTAL -> {
if (content != null) {
accuracyHorizontal = content.trim();
case TAG_TYPE -> { //Track or Marker/WPT
if (content != null) {
// In older version this might be localized content.
activityType = content.trim();
markerType = content.trim();
}
}
}
case TAG_EXTENSION_ACCURACY_VERTICAL -> {
if (content != null) {
accuracyVertical = content.trim();
case TAG_TIME -> {
if (content != null) {
time = content.trim();
}
}
case TAG_ALTITUDE -> {
if (content != null) {
altitude = content.trim();
}
}
case TAG_EXTENSION_SPEED_COMPAT -> {
if (content != null) {
speed = content.trim();
}
}
}
}
@@ -146,8 +146,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes) throws SAXException {
switch (tag) {
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (qName) {
case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
// Note that a track is contained in a Placemark, calling onMarkerStart will clear various track variables like name, typeLocalized, and description.
onMarkerStart();
@@ -168,8 +168,8 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
switch (tag) {
public void endElement(String uri, String localName, String qName) throws SAXException {
switch (qName) {
case TAG_KML -> onFileEnd();
case TAG_PLACEMARK, TAG_PHOTO_OVERLAY ->
// Note that a track is contained in a Placemark, calling onMarkerEnd is save since markerType is not set for a track.
@@ -539,6 +539,7 @@ public class PreferencesUtils {
String[] entries = new String[entryValues.length];
final int maxRecordingDistanceDefault = Integer.parseInt(resources.getString(R.string.max_recording_distance_default));
final int maxRecordingDistanceOff = Integer.parseInt(resources.getString(R.string.max_recording_distance_off));
UnitSystem unitSystem = getUnitSystem();
DistanceFormatter formatter = DistanceFormatter.Builder()
@@ -555,6 +556,9 @@ public class PreferencesUtils {
case METRIC, IMPERIAL_METER -> {
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_meter_recommended, value);
}
if (value == maxRecordingDistanceOff) {
entries[i] = resources.getString(R.string.value_off);
} else {
entries[i] = displayValue;
}
@@ -562,6 +566,9 @@ public class PreferencesUtils {
case IMPERIAL_FEET, NAUTICAL_IMPERIAL -> {
if (value == maxRecordingDistanceDefault) {
entries[i] = resources.getString(R.string.value_integer_feet_recommended, (int) distance.toFT());
}
if (value == maxRecordingDistanceOff) {
entries[i] = resources.getString(R.string.value_off);
} else {
entries[i] = displayValue;
}
+2
View File
@@ -81,7 +81,9 @@
<string name="max_recording_distance_key" translatable="false">maxRecordingDistance</string>
<string name="max_recording_distance_default" translatable="false">200</string>
<string name="max_recording_distance_off" translatable="false">2147483647</string><!-- MAX_INT -->
<string-array name="max_recording_distance_values">
<item>@string/max_recording_distance_off</item>
<item>50</item>
<item>100</item>
<item>@string/max_recording_distance_default</item>