forked from upstream-mirrors/OpenTracks
TrackPoint is now a record.
This commit is contained in:
@@ -278,7 +278,7 @@ public class TrackRecordingActivity extends AbstractActivity implements ChooseAc
|
||||
Intent intent = IntentUtils
|
||||
.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId)
|
||||
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.getPosition().toLocation());
|
||||
.putExtra(MarkerEditActivity.EXTRA_LOCATION, trackPoint.position().toLocation());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ import de.dennisguse.opentracks.data.TrackDataHub;
|
||||
import de.dennisguse.opentracks.data.models.Statistics;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
|
||||
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.databinding.ChartBinding;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.data.statistics.SegmentStatisticUpdater;
|
||||
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
|
||||
|
||||
/**
|
||||
* A fragment to display track chart to the user.
|
||||
@@ -204,7 +204,7 @@ public class ChartFragment extends Fragment implements TrackDataHub.Listener {
|
||||
|
||||
public void onSampledInTrackPoint(@NonNull TrackPoint trackPoint, @NonNull Statistics trackStatistics) {
|
||||
if (isResumed()) {
|
||||
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.getSpeed(), chartByDistance, viewBinding.chartView.getUnitSystem());
|
||||
ChartPoint point = ChartPoint.create(trackStatistics, trackPoint, trackPoint.position().speed(), chartByDistance, viewBinding.chartView.getUnitSystem());
|
||||
pendingPoints.add(point);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ public record ChartPoint(
|
||||
chartByDistance
|
||||
? trackStatistics.totalDistance().toKM_Miles(unitSystem)
|
||||
: trackStatistics.totalDuration().toMillis(),
|
||||
trackPoint.hasAltitude()
|
||||
? Distance.of(trackPoint.getAltitude().toM()).toM_FT(unitSystem)
|
||||
trackPoint.position().hasAltitude()
|
||||
? Distance.of(trackPoint.position().altitude().toM()).toM_FT(unitSystem)
|
||||
: null,
|
||||
smoothedSpeed != null
|
||||
? smoothedSpeed.to(unitSystem)
|
||||
@@ -36,14 +36,14 @@ public record ChartPoint(
|
||||
smoothedSpeed != null
|
||||
? smoothedSpeed.toPace(unitSystem).toSeconds() / 60d
|
||||
: null,
|
||||
trackPoint.hasHeartRate()
|
||||
? (double) trackPoint.getHeartRate().getBPM()
|
||||
trackPoint.heartRate() != null
|
||||
? (double) trackPoint.heartRate().getBPM()
|
||||
: null,
|
||||
trackPoint.hasCadence()
|
||||
? (double) trackPoint.getCadence().getRPM()
|
||||
trackPoint.cadence() != null
|
||||
? (double) trackPoint.cadence().getRPM()
|
||||
: null,
|
||||
trackPoint.hasPower()
|
||||
? (double) trackPoint.getPower().getW()
|
||||
trackPoint.power() != null
|
||||
? (double) trackPoint.power().getW()
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Statistics;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
|
||||
import de.dennisguse.opentracks.data.tables.MarkerColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
|
||||
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
|
||||
import de.dennisguse.opentracks.util.FileUtils;
|
||||
|
||||
@@ -544,7 +544,12 @@ public class ContentProviderUtils {
|
||||
int altitudeGainIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_GAIN);
|
||||
int altitudeLossIndex = cursor.getColumnIndexOrThrow(TrackPointsColumns.ALTITUDE_LOSS);
|
||||
|
||||
TrackPoint trackPoint = new TrackPoint(
|
||||
AltitudeGainLoss altitudeGainLoss = null;
|
||||
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
|
||||
altitudeGainLoss = new AltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
|
||||
}
|
||||
|
||||
return new TrackPoint(
|
||||
new TrackPoint.Id(cursor.getInt(idIndex)),
|
||||
TrackPoint.Type.getById(cursor.getInt(typeIndex)),
|
||||
new Position(
|
||||
@@ -556,26 +561,13 @@ public class ContentProviderUtils {
|
||||
!cursor.isNull(accuracyVerticalIndex) ? Distance.of(cursor.getFloat(accuracyVerticalIndex)) : null,
|
||||
!cursor.isNull(bearingIndex) ? cursor.getFloat(bearingIndex) : null,
|
||||
!cursor.isNull(speedIndex) ? Speed.of(cursor.getFloat(speedIndex)) : null
|
||||
));
|
||||
|
||||
if (!cursor.isNull(sensorHeartRateIndex)) {
|
||||
trackPoint.setHeartRate(cursor.getFloat(sensorHeartRateIndex));
|
||||
}
|
||||
if (!cursor.isNull(sensorCadenceIndex)) {
|
||||
trackPoint.setCadence(cursor.getFloat(sensorCadenceIndex));
|
||||
}
|
||||
if (!cursor.isNull(sensorDistanceIndex)) {
|
||||
trackPoint.setSensorDistance(Distance.of(cursor.getFloat(sensorDistanceIndex)));
|
||||
}
|
||||
if (!cursor.isNull(sensorPowerIndex)) {
|
||||
trackPoint.setPower(cursor.getFloat(sensorPowerIndex));
|
||||
}
|
||||
|
||||
if (!cursor.isNull(altitudeGainIndex) && !cursor.isNull(altitudeLossIndex)) {
|
||||
trackPoint.setAltitudeGainLoss(cursor.getFloat(altitudeGainIndex), cursor.getFloat(altitudeLossIndex));
|
||||
}
|
||||
|
||||
return trackPoint;
|
||||
),
|
||||
!cursor.isNull(sensorDistanceIndex) ? Distance.of(cursor.getFloat(sensorDistanceIndex)) : null,
|
||||
!cursor.isNull(sensorHeartRateIndex) ? HeartRate.of(cursor.getFloat(sensorHeartRateIndex)) : null,
|
||||
!cursor.isNull(sensorCadenceIndex) ? Cadence.of(cursor.getFloat(sensorCadenceIndex)) : null,
|
||||
!cursor.isNull(sensorPowerIndex) ? Power.of(cursor.getFloat(sensorPowerIndex)) : null,
|
||||
altitudeGainLoss
|
||||
);
|
||||
}
|
||||
|
||||
//TODO Only used for file import; might be better to replace it.
|
||||
@@ -637,42 +629,42 @@ public class ContentProviderUtils {
|
||||
private ContentValues createContentValues(TrackPoint trackPoint, Track.Id trackId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(TrackPointsColumns.TRACKID, trackId.id());
|
||||
values.put(TrackPointsColumns.TYPE, trackPoint.getType().type_db);
|
||||
values.put(TrackPointsColumns.TYPE, trackPoint.type().type_db);
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.getPosition().latitude() * 1E6));
|
||||
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.getPosition().longitude() * 1E6));
|
||||
if (trackPoint.position().hasLocation()) {
|
||||
values.put(TrackPointsColumns.LATITUDE, (int) (trackPoint.position().latitude() * 1E6));
|
||||
values.put(TrackPointsColumns.LONGITUDE, (int) (trackPoint.position().longitude() * 1E6));
|
||||
}
|
||||
values.put(TrackPointsColumns.TIME, trackPoint.getTime().toEpochMilli());
|
||||
if (trackPoint.hasAltitude()) {
|
||||
values.put(TrackPointsColumns.ALTITUDE, trackPoint.getAltitude().toM());
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
values.put(TrackPointsColumns.ALTITUDE, trackPoint.position().altitude().toM());
|
||||
}
|
||||
if (trackPoint.hasHorizontalAccuracy()) {
|
||||
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.getHorizontalAccuracy().toM());
|
||||
if (trackPoint.position().hasHorizontalAccuracy()) {
|
||||
values.put(TrackPointsColumns.HORIZONTAL_ACCURACY, trackPoint.position().horizontalAccuracy().toM());
|
||||
}
|
||||
if (trackPoint.hasSpeed()) {
|
||||
values.put(TrackPointsColumns.SPEED, trackPoint.getSpeed().toMPS());
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
values.put(TrackPointsColumns.SPEED, trackPoint.position().speed().toMPS());
|
||||
}
|
||||
if (trackPoint.hasBearing()) {
|
||||
values.put(TrackPointsColumns.BEARING, trackPoint.getBearing());
|
||||
if (trackPoint.position().hasBearing()) {
|
||||
values.put(TrackPointsColumns.BEARING, trackPoint.position().bearing());
|
||||
}
|
||||
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.getHeartRate().getBPM());
|
||||
if (trackPoint.heartRate() != null) {
|
||||
values.put(TrackPointsColumns.SENSOR_HEARTRATE, trackPoint.heartRate().getBPM());
|
||||
}
|
||||
if (trackPoint.hasCadence()) {
|
||||
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.getCadence().getRPM());
|
||||
if (trackPoint.cadence() != null) {
|
||||
values.put(TrackPointsColumns.SENSOR_CADENCE, trackPoint.cadence().getRPM());
|
||||
}
|
||||
if (trackPoint.hasSensorDistance()) {
|
||||
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.getSensorDistance().toM());
|
||||
if (trackPoint.sensorDistance() != null) {
|
||||
values.put(TrackPointsColumns.SENSOR_DISTANCE, trackPoint.sensorDistance().toM());
|
||||
}
|
||||
if (trackPoint.hasPower()) {
|
||||
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.getPower().getW());
|
||||
if (trackPoint.power() != null) {
|
||||
values.put(TrackPointsColumns.SENSOR_POWER, trackPoint.power().getW());
|
||||
}
|
||||
|
||||
if (trackPoint.hasAltitudeGainLoss()) {
|
||||
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.getAltitudeGainLoss().gain_m());
|
||||
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.getAltitudeGainLoss().loss_m());
|
||||
if (trackPoint.altitudeGainLoss() != null) {
|
||||
values.put(TrackPointsColumns.ALTITUDE_GAIN, trackPoint.altitudeGainLoss().gain_m());
|
||||
values.put(TrackPointsColumns.ALTITUDE_LOSS, trackPoint.altitudeGainLoss().loss_m());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ import java.util.Set;
|
||||
import de.dennisguse.opentracks.data.models.Statistics;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.data.tables.TrackPointsColumns;
|
||||
import de.dennisguse.opentracks.data.tables.TracksColumns;
|
||||
import de.dennisguse.opentracks.services.RecordingStatus;
|
||||
import de.dennisguse.opentracks.services.TrackRecordingService;
|
||||
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
|
||||
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
|
||||
|
||||
/**
|
||||
* Track data hub.
|
||||
@@ -288,14 +288,14 @@ public class TrackDataHub {
|
||||
}
|
||||
|
||||
trackPoint = trackPointIterator.next();
|
||||
TrackPoint.Id trackPointId = trackPoint.getId();
|
||||
TrackPoint.Id trackPointId = trackPoint.id();
|
||||
|
||||
// Stop if past the last wanted point
|
||||
if (maxPointId != null && trackPointId.id() > maxPointId.id()) {
|
||||
break;
|
||||
}
|
||||
|
||||
egm2008Correction.correctAltitude(context, trackPoint);
|
||||
trackPoint = egm2008Correction.correctAltitude(context, trackPoint);
|
||||
|
||||
if (localFirstSeenTrackPointId == null) {
|
||||
localFirstSeenTrackPointId = trackPointId;
|
||||
@@ -319,7 +319,7 @@ public class TrackDataHub {
|
||||
}
|
||||
|
||||
if (trackPoint != null) {
|
||||
localLastSeenTrackPointIdId = trackPoint.getId();
|
||||
localLastSeenTrackPointIdId = trackPoint.id();
|
||||
}
|
||||
|
||||
if (updateSamplingState) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.dennisguse.opentracks.data.models;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public record HeartRate(float value) {
|
||||
public static HeartRate INVALID = HeartRate.of(0.0f);
|
||||
|
||||
@@ -7,6 +9,12 @@ public record HeartRate(float value) {
|
||||
return new HeartRate(value);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static HeartRate ofOrNull(Float value) {
|
||||
if (value == null) return null;
|
||||
return new HeartRate(value);
|
||||
}
|
||||
|
||||
public float getBPM() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -51,10 +51,10 @@ public final class MarkerBuilder {
|
||||
this.typeLocalized = "";
|
||||
this.photoUrl = null;
|
||||
|
||||
if (!trackPoint.hasLocation())
|
||||
if (!trackPoint.position().hasLocation())
|
||||
throw new RuntimeException("Marker requires a trackpoint with a location.");
|
||||
|
||||
this.position = trackPoint.getPosition();
|
||||
this.position = trackPoint.position();
|
||||
}
|
||||
|
||||
public MarkerBuilder(@Nullable Track.Id trackId, @NonNull TrackPoint trackPoint, String name, String description, String typeLocalized, Uri photoUrl) {
|
||||
|
||||
@@ -17,8 +17,9 @@ public record Position(
|
||||
@Nullable Float bearing,
|
||||
@Nullable Speed speed
|
||||
) {
|
||||
@Deprecated
|
||||
public static Position empty() {
|
||||
return of((Instant) null);
|
||||
return of(null);
|
||||
}
|
||||
|
||||
public static Position of(@NonNull Instant time) {
|
||||
@@ -34,10 +35,6 @@ public record Position(
|
||||
);
|
||||
}
|
||||
|
||||
public static Position of(@NonNull Location location) {
|
||||
return of(location, Instant.ofEpochMilli(location.getTime()));
|
||||
}
|
||||
|
||||
public static Position of(@NonNull Location location, @NonNull Instant time) {
|
||||
return new Position(
|
||||
time,
|
||||
@@ -132,9 +129,8 @@ public record Position(
|
||||
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
|
||||
}
|
||||
|
||||
//TODO Use double
|
||||
public Position withCoordinates(Double latitude, Double longitude) {
|
||||
return new Position(time, latitude, longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
|
||||
public Position withCoordinates(Position position) {
|
||||
return new Position(time, position.latitude, position.longitude, horizontalAccuracy, altitude, verticalAccuracy, bearing, speed);
|
||||
}
|
||||
|
||||
//TODO Use float
|
||||
|
||||
@@ -22,23 +22,168 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Sensor and/or location information for a specific point in time.
|
||||
* <p>
|
||||
* Time is created using the {@link de.dennisguse.opentracks.services.handlers.MonotonicClock}, because system time jump backwards.
|
||||
* GPS time is ignored as for non-GPS events, we could not create GPS-based timestamps.
|
||||
* GPS time is ignored as for non-GPS events, we cannot create GPS-based timestamps.
|
||||
*/
|
||||
//TODO Should be a record (with final properties)
|
||||
public class TrackPoint {
|
||||
public record TrackPoint(
|
||||
|
||||
@Nullable
|
||||
private final TrackPoint.Id id;
|
||||
@Nullable
|
||||
TrackPoint.Id id,
|
||||
|
||||
@NonNull
|
||||
TrackPoint.Type type,
|
||||
|
||||
//Requires: position.time must be non-null
|
||||
@NonNull
|
||||
Position position,
|
||||
|
||||
Distance sensorDistance,
|
||||
HeartRate heartRate,
|
||||
Cadence cadence,
|
||||
Power power,
|
||||
AltitudeGainLoss altitudeGainLoss
|
||||
) {
|
||||
|
||||
public TrackPoint(Type type, Position position) {
|
||||
this(
|
||||
null,
|
||||
type,
|
||||
position,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@Deprecated //TOOD Private?
|
||||
public TrackPoint(Type type, Instant now) {
|
||||
this(
|
||||
null,
|
||||
type,
|
||||
Position.of(now),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
|
||||
return new TrackPoint(TrackPoint.Type.SEGMENT_START_MANUAL, time);
|
||||
}
|
||||
|
||||
public static TrackPoint with(TrackPoint trackPoint, Altitude.EGM2008 correctAltitude) {
|
||||
return new TrackPoint(
|
||||
trackPoint.id,
|
||||
trackPoint.type,
|
||||
new Position(
|
||||
trackPoint.position.time(),
|
||||
trackPoint.position.latitude(),
|
||||
trackPoint.position.longitude(),
|
||||
trackPoint.position.horizontalAccuracy(),
|
||||
correctAltitude,
|
||||
trackPoint.position.verticalAccuracy(),
|
||||
trackPoint.position.bearing(),
|
||||
trackPoint.position.speed()
|
||||
),
|
||||
trackPoint.sensorDistance,
|
||||
trackPoint.heartRate,
|
||||
trackPoint.cadence,
|
||||
trackPoint.power,
|
||||
trackPoint.altitudeGainLoss
|
||||
);
|
||||
}
|
||||
|
||||
public TrackPoint with(Type newType) {
|
||||
return new TrackPoint(
|
||||
id,
|
||||
newType,
|
||||
position,
|
||||
sensorDistance,
|
||||
heartRate,
|
||||
cadence,
|
||||
power,
|
||||
altitudeGainLoss
|
||||
);
|
||||
}
|
||||
|
||||
public TrackPoint with(Position newPosition) {
|
||||
return new TrackPoint(
|
||||
id,
|
||||
type,
|
||||
newPosition,
|
||||
sensorDistance,
|
||||
heartRate,
|
||||
cadence,
|
||||
power,
|
||||
altitudeGainLoss
|
||||
);
|
||||
}
|
||||
|
||||
//Requires: position.time must be non-null
|
||||
@NonNull
|
||||
private Position position;
|
||||
public Instant getTime() {
|
||||
return position.time();
|
||||
}
|
||||
|
||||
|
||||
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
|
||||
Distance newSensorDistance = sensorDistance;
|
||||
if (sensorDistance != null && lastTrackPoint.sensorDistance != null) {
|
||||
newSensorDistance = sensorDistance.minus(lastTrackPoint.sensorDistance);
|
||||
}
|
||||
AltitudeGainLoss newAltitudeGainLoss = altitudeGainLoss;
|
||||
if (altitudeGainLoss != null && lastTrackPoint.altitudeGainLoss() != null) {
|
||||
newAltitudeGainLoss = new AltitudeGainLoss(
|
||||
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
|
||||
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
|
||||
}
|
||||
return new TrackPoint(
|
||||
id,
|
||||
type,
|
||||
position,
|
||||
newSensorDistance,
|
||||
heartRate,
|
||||
cadence,
|
||||
power,
|
||||
newAltitudeGainLoss
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
if (sensorDistance() != null) {
|
||||
return sensorDistance();
|
||||
}
|
||||
return distanceToPreviousFromLocation(previous);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Distance distanceToPreviousFromLocation(@NonNull TrackPoint previous) {
|
||||
if (!position.hasLocation() || !previous.position().hasLocation()) {
|
||||
throw new RuntimeException("Cannot compute distance.");
|
||||
}
|
||||
|
||||
return Distance.of(position().toLocation().distanceTo(previous.position().toLocation()));
|
||||
}
|
||||
|
||||
public boolean isIdleTriggered() {
|
||||
return type == TrackPoint.Type.IDLE;
|
||||
}
|
||||
|
||||
public boolean isSegmentManualStart() {
|
||||
return type == TrackPoint.Type.SEGMENT_START_MANUAL;
|
||||
}
|
||||
|
||||
public boolean isSegmentManualEnd() {
|
||||
return type == TrackPoint.Type.SEGMENT_END_MANUAL;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
|
||||
@@ -75,298 +220,6 @@ public class TrackPoint {
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Type type;
|
||||
|
||||
private Distance sensorDistance;
|
||||
private HeartRate heartRate = null;
|
||||
private Cadence cadence = null;
|
||||
private Power power = null;
|
||||
private AltitudeGainLoss altitudeGainLoss = null;
|
||||
|
||||
public TrackPoint(@Nullable TrackPoint.Id id, @NonNull Type type, @NonNull Position position) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, @NonNull Instant time) {
|
||||
this(null, type, Position.of(time));
|
||||
}
|
||||
|
||||
public TrackPoint(@NonNull Type type, @NonNull Position position) {
|
||||
this(null, type, position);
|
||||
}
|
||||
|
||||
public static TrackPoint createSegmentStartManualWithTime(Instant time) {
|
||||
return new TrackPoint(Type.SEGMENT_START_MANUAL, time);
|
||||
}
|
||||
|
||||
public static TrackPoint createSegmentEndWithTime(Instant time) {
|
||||
return new TrackPoint(Type.SEGMENT_END_MANUAL, time);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Deprecated //Should not be needed.
|
||||
public TrackPoint setType(@NonNull Type type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isIdleTriggered() {
|
||||
return type == Type.IDLE;
|
||||
}
|
||||
|
||||
public boolean isSegmentManualStart() {
|
||||
return type == Type.SEGMENT_START_MANUAL;
|
||||
}
|
||||
|
||||
public boolean isSegmentManualEnd() {
|
||||
return type == Type.SEGMENT_END_MANUAL;
|
||||
}
|
||||
|
||||
//TODO Check if naming is correct.
|
||||
public boolean wasCreatedManually() {
|
||||
return hasLocation() || hasSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* May be null if the track was not loaded from the database.
|
||||
*/
|
||||
@Nullable
|
||||
public TrackPoint.Id getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean hasLocation() {
|
||||
return position.hasLocation();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Position getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public TrackPoint setPosition(Position position) {
|
||||
this.position = position.with(this.position.time());
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasAltitudeGainLoss() {
|
||||
return altitudeGainLoss != null;
|
||||
}
|
||||
|
||||
public AltitudeGainLoss getAltitudeGainLoss() {
|
||||
return altitudeGainLoss;
|
||||
}
|
||||
|
||||
public TrackPoint setAltitudeGainLoss(AltitudeGainLoss altitudeGainLoss) {
|
||||
this.altitudeGainLoss = altitudeGainLoss;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackPoint setAltitudeGainLoss(float altitudeGain_m, float altitudeLoss_m) {
|
||||
setAltitudeGainLoss(new AltitudeGainLoss(altitudeGain_m, altitudeLoss_m));
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Instant getTime() {
|
||||
return position.time();
|
||||
}
|
||||
|
||||
public boolean hasAltitude() {
|
||||
return position.hasAltitude();
|
||||
}
|
||||
|
||||
public Altitude getAltitude() {
|
||||
return position.altitude();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public TrackPoint setAltitude(Altitude altitude) {
|
||||
position = position.with(altitude);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasSpeed() {
|
||||
return position.hasSpeed();
|
||||
}
|
||||
|
||||
public Speed getSpeed() {
|
||||
return position.speed();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public TrackPoint setSpeed(Speed speed) {
|
||||
this.position = position.with(speed);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasBearing() {
|
||||
return position.hasBearing();
|
||||
}
|
||||
|
||||
public float getBearing() {
|
||||
return position.bearing();
|
||||
}
|
||||
|
||||
public TrackPoint setBearing(Float bearing) {
|
||||
this.position = this.position.withBearing(bearing);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasHorizontalAccuracy() {
|
||||
return position.hasHorizontalAccuracy();
|
||||
}
|
||||
|
||||
public Distance getHorizontalAccuracy() {
|
||||
return position.horizontalAccuracy();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public TrackPoint setHorizontalAccuracy(Distance horizontalAccuracy) {
|
||||
this.position = this.position.withHorizontalAccuracy(horizontalAccuracy);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasVerticalAccuracy() {
|
||||
return position.hasVerticalAccuracy();
|
||||
}
|
||||
|
||||
public Distance getVerticalAccuracy() {
|
||||
return position.verticalAccuracy();
|
||||
}
|
||||
|
||||
public TrackPoint setVerticalAccuracy(Distance verticalAccuracy) {
|
||||
this.position = this.position.withVerticalAccuracy(verticalAccuracy);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
|
||||
if (hasSensorDistance()) {
|
||||
return getSensorDistance();
|
||||
}
|
||||
return distanceToPreviousFromLocation(previous);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Distance distanceToPreviousFromLocation(@NonNull TrackPoint previous) {
|
||||
if (!hasLocation() || hasLocation() != previous.hasLocation()) {
|
||||
throw new RuntimeException("Cannot compute distance.");
|
||||
}
|
||||
|
||||
return Distance.of(getPosition().toLocation().distanceTo(previous.getPosition().toLocation()));
|
||||
}
|
||||
|
||||
public boolean fulfillsAccuracy(Distance thresholdHorizontalAccuracy) {
|
||||
return position.fulfillsAccuracy(thresholdHorizontalAccuracy);
|
||||
}
|
||||
|
||||
public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
|
||||
if (!dest.hasLocation() || !hasLocation()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(getPosition().toLocation().bearingTo(dest.getPosition().toLocation()));
|
||||
}
|
||||
|
||||
// Sensor data
|
||||
public boolean hasSensorDistance() {
|
||||
return sensorDistance != null;
|
||||
}
|
||||
|
||||
public Distance getSensorDistance() {
|
||||
return sensorDistance;
|
||||
}
|
||||
|
||||
public TrackPoint setSensorDistance(Distance distance_m) {
|
||||
this.sensorDistance = distance_m;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackPoint minusCumulativeSensorData(@NonNull TrackPoint lastTrackPoint) {
|
||||
if (hasSensorDistance() && lastTrackPoint.hasSensorDistance()) {
|
||||
sensorDistance = sensorDistance.minus(lastTrackPoint.getSensorDistance());
|
||||
}
|
||||
if (hasAltitudeGainLoss() && lastTrackPoint.hasAltitudeGainLoss()) {
|
||||
altitudeGainLoss = new AltitudeGainLoss(
|
||||
altitudeGainLoss.gain_m() - lastTrackPoint.altitudeGainLoss.gain_m(),
|
||||
altitudeGainLoss.loss_m() - lastTrackPoint.altitudeGainLoss.loss_m());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean hasHeartRate() {
|
||||
return heartRate != null;
|
||||
}
|
||||
|
||||
public HeartRate getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public TrackPoint setHeartRate(HeartRate heartRate) {
|
||||
this.heartRate = heartRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackPoint setHeartRate(float heartRate) {
|
||||
return setHeartRate(HeartRate.of(heartRate));
|
||||
}
|
||||
|
||||
public boolean hasCadence() {
|
||||
return cadence != null;
|
||||
}
|
||||
|
||||
public Cadence getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public TrackPoint setCadence(Cadence cadence) {
|
||||
this.cadence = cadence;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackPoint setCadence(float cadence) {
|
||||
return setCadence(Cadence.of(cadence));
|
||||
}
|
||||
|
||||
public boolean hasPower() {
|
||||
return power != null;
|
||||
}
|
||||
|
||||
public Power getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public TrackPoint setPower(Power power) {
|
||||
this.power = power;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackPoint setPower(float power) {
|
||||
return setPower(Power.of(power));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TrackPoint{" +
|
||||
"id=" + id +
|
||||
", type=" + type +
|
||||
", position=" + position +
|
||||
", sensorDistance=" + sensorDistance +
|
||||
", heartRate=" + heartRate +
|
||||
", cadence=" + cadence +
|
||||
", power=" + power +
|
||||
", altitudeGainLoss=" + altitudeGainLoss +
|
||||
'}';
|
||||
}
|
||||
|
||||
public record Id(long id) implements Parcelable {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -154,7 +154,7 @@ public class SegmentStatisticUpdater {
|
||||
Duration movingDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
|
||||
if (movingDuration.isNegative()) {
|
||||
throw new RuntimeException("Moving time cannot be negative");
|
||||
throw new RuntimeException("Moving time cannot be negative: " + lastTrackPoint.getTime() + " is after " + trackPoint.getTime());
|
||||
}
|
||||
this.movingDuration = this.movingDuration.plus(movingDuration);
|
||||
}
|
||||
|
||||
+17
-17
@@ -111,32 +111,32 @@ public class TrackStatisticsUpdater {
|
||||
currentSegment.updateTotalTime(trackPoint.getTime());
|
||||
|
||||
// Process sensor data: barometer
|
||||
if (trackPoint.hasAltitudeGainLoss()) {
|
||||
currentSegment.addTotalAltitudeGainLoss(trackPoint.getAltitudeGainLoss());
|
||||
if (trackPoint.altitudeGainLoss() != null) {
|
||||
currentSegment.addTotalAltitudeGainLoss(trackPoint.altitudeGainLoss());
|
||||
}
|
||||
|
||||
//Update absolute (GPS-based) altitude
|
||||
if (trackPoint.hasAltitude()) {
|
||||
currentSegment.updateAltitudeExtremities(trackPoint.getAltitude());
|
||||
// Update absolute (GPS-based) altitude
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
currentSegment.updateAltitudeExtremities(trackPoint.position().altitude());
|
||||
}
|
||||
|
||||
// Update heart rate
|
||||
if (trackPoint.hasHeartRate() && lastTrackPoint != null) {
|
||||
if (trackPoint.heartRate() != null && lastTrackPoint != null) {
|
||||
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
Duration newTotalDuration = totalHeartRateDuration.plus(trackPointDuration);
|
||||
|
||||
averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.getHeartRate().getBPM()) / newTotalDuration.toMillis();
|
||||
averageHeartRateBPM = (totalHeartRateDuration.toMillis() * averageHeartRateBPM + trackPointDuration.toMillis() * trackPoint.heartRate().getBPM()) / newTotalDuration.toMillis();
|
||||
totalHeartRateDuration = newTotalDuration;
|
||||
|
||||
currentSegment.setAverageHeartRate(HeartRate.of(averageHeartRateBPM));
|
||||
}
|
||||
|
||||
// Update power
|
||||
if (trackPoint.hasPower() && lastTrackPoint != null) {
|
||||
if (trackPoint.power() != null && lastTrackPoint != null) {
|
||||
Duration trackPointDuration = Duration.between(lastTrackPoint.getTime(), trackPoint.getTime());
|
||||
Duration newTotalDuration = totalPowerDuration.plus(trackPointDuration);
|
||||
|
||||
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.getPower().getW()) / newTotalDuration.toMillis();
|
||||
averagePowerW = (totalPowerDuration.toMillis() * averagePowerW + trackPointDuration.toMillis() * trackPoint.power().getW()) / newTotalDuration.toMillis();
|
||||
totalPowerDuration = newTotalDuration;
|
||||
|
||||
currentSegment.setAveragePower(Power.of(averagePowerW));
|
||||
@@ -145,11 +145,11 @@ public class TrackStatisticsUpdater {
|
||||
{
|
||||
// Update total distance
|
||||
Distance movingDistance = null;
|
||||
if (trackPoint.hasSensorDistance()) {
|
||||
movingDistance = trackPoint.getSensorDistance();
|
||||
if (trackPoint.sensorDistance() != null) {
|
||||
movingDistance = trackPoint.sensorDistance();
|
||||
} else if (lastTrackPoint != null
|
||||
&& lastTrackPoint.hasLocation()
|
||||
&& trackPoint.hasLocation()) {
|
||||
&& lastTrackPoint.position().hasLocation()
|
||||
&& trackPoint.position().hasLocation()) {
|
||||
// GPS-based distance/speed
|
||||
movingDistance = trackPoint.distanceToPrevious(lastTrackPoint);
|
||||
}
|
||||
@@ -173,7 +173,7 @@ public class TrackStatisticsUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
if (trackPoint.hasSpeed()) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
updateSpeed(trackPoint);
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ public class TrackStatisticsUpdater {
|
||||
* Updates a speed reading while assuming the user is moving.
|
||||
*/
|
||||
private void updateSpeed(@NonNull TrackPoint trackPoint) {
|
||||
Speed currentSpeed = trackPoint.getSpeed();
|
||||
Speed currentSpeed = trackPoint.position().speed();
|
||||
if (currentSpeed.greaterThan(currentSegment.getMaxSpeed())) {
|
||||
currentSegment.setMaxSpeed(currentSpeed);
|
||||
}
|
||||
@@ -215,8 +215,8 @@ public class TrackStatisticsUpdater {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TrackStatisticsUpdater{" +
|
||||
"segmentStatisticUpdater=" + statisticsWithoutCurrentSegment +
|
||||
", currentSegment=" + currentSegment +
|
||||
"currentSegment=" + currentSegment +
|
||||
", segmentStatisticUpdater=" + statisticsWithoutCurrentSegment +
|
||||
", lastTrackPoint=" + lastTrackPoint +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -90,20 +90,20 @@ public class CSVTrackExporter implements TrackExporter {
|
||||
public boolean writeTrack(@NonNull List<Track> tracks, @NonNull OutputStream outputStream) {
|
||||
List<Column> columns = List.of(
|
||||
new Column("time", null),
|
||||
new Column("trackpoint_type", t -> quote(t.getType().name())),
|
||||
new Column("latitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getPosition().latitude()) : ""),
|
||||
new Column("longitude", t -> t.hasLocation() ? COORDINATE_FORMAT.format(t.getPosition().longitude()) : ""),
|
||||
new Column("altitude", t -> t.hasAltitude() ? ALTITUDE_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("trackpoint_type", t -> quote(t.type().name())),
|
||||
new Column("latitude", t -> t.position().hasLocation() ? COORDINATE_FORMAT.format(t.position().latitude()) : ""),
|
||||
new Column("longitude", t -> t.position().hasLocation() ? COORDINATE_FORMAT.format(t.position().longitude()) : ""),
|
||||
new Column("altitude", t -> t.position().hasAltitude() ? ALTITUDE_FORMAT.format(t.position().altitude().toM()) : ""),
|
||||
new Column("accuracy_horizontal", t -> t.position().hasHorizontalAccuracy() ? DISTANCE_FORMAT.format(t.position().horizontalAccuracy().toM()) : ""),
|
||||
new Column("accuracy_vertical", t -> t.position().hasVerticalAccuracy() ? DISTANCE_FORMAT.format(t.position().verticalAccuracy().toM()) : ""),
|
||||
|
||||
new Column("speed", t -> t.hasSpeed() ? SPEED_FORMAT.format(t.getSpeed().toKMH()) : ""),
|
||||
new Column("altitude_gain", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().gain_m()) : ""),
|
||||
new Column("altitude_loss", t -> t.hasAltitudeGainLoss() ? ALTITUDE_FORMAT.format(t.getAltitudeGainLoss().loss_m()) : ""),
|
||||
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() ? POWER_FORMAT.format(t.getPower().getW()) : ""));
|
||||
new Column("speed", t -> t.position().hasSpeed() ? SPEED_FORMAT.format(t.position().speed().toKMH()) : ""),
|
||||
new Column("altitude_gain", t -> t.altitudeGainLoss() != null ? ALTITUDE_FORMAT.format(t.altitudeGainLoss().gain_m()) : ""),
|
||||
new Column("altitude_loss", t -> t.altitudeGainLoss() != null ? ALTITUDE_FORMAT.format(t.altitudeGainLoss().loss_m()) : ""),
|
||||
new Column("sensor_distance", t -> t.sensorDistance() != null ? DISTANCE_FORMAT.format(t.sensorDistance().toM()) : ""),
|
||||
new Column("heartrate", t -> t.heartRate() != null ? HEARTRATE_FORMAT.format(t.heartRate().getBPM()) : ""),
|
||||
new Column("cadence", t -> t.cadence() != null ? CADENCE_FORMAT.format(t.cadence().getRPM()) : ""),
|
||||
new Column("power", t -> t.power() != null ? POWER_FORMAT.format(t.power().getW()) : ""));
|
||||
|
||||
try {
|
||||
prepare(outputStream);
|
||||
|
||||
@@ -140,7 +140,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
switch (trackPoint.type()) {
|
||||
case SEGMENT_START_MANUAL ->
|
||||
Log.i(TAG, "Exporting " + TrackPoint.Type.SEGMENT_START_MANUAL.name() + " is not supported.");
|
||||
case SEGMENT_END_MANUAL -> {
|
||||
@@ -152,7 +152,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
if (trackPoint.hasLocation()) {
|
||||
if (trackPoint.position().hasLocation()) {
|
||||
trackDistance = trackDistance.plus(writeTrackPoint(track.zoneOffset(), trackPoint, sensorPoints, trackDistance));
|
||||
sensorPoints.clear();
|
||||
} else {
|
||||
@@ -165,7 +165,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
}
|
||||
if (trackPoint.hasLocation()) {
|
||||
if (trackPoint.position().hasLocation()) {
|
||||
trackDistance = trackDistance.plus(writeTrackPoint(track.zoneOffset(), trackPoint, sensorPoints, trackDistance));
|
||||
sensorPoints.clear();
|
||||
} else {
|
||||
@@ -176,7 +176,7 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
// Not supported as IDLE-TrackPoints have no location.
|
||||
}
|
||||
default ->
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.type());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,10 +306,10 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
private Distance writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint, List<TrackPoint> sensorPoints, Distance trackDistance) {
|
||||
Distance cumulativeDistance;
|
||||
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.getPosition()) + ">");
|
||||
printWriter.println("<trkpt " + formatLocation(trackPoint.position()) + ">");
|
||||
|
||||
if (trackPoint.hasAltitude()) {
|
||||
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.getAltitude().toM()) + "</ele>");
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
printWriter.println("<ele>" + ALTITUDE_FORMAT.format(trackPoint.position().altitude().toM()) + "</ele>");
|
||||
}
|
||||
|
||||
printWriter.println("<time>" + StringUtils.formatDateTimeIso8601(trackPoint.getTime(), zoneOffset) + "</time>");
|
||||
@@ -317,41 +317,41 @@ public class GPXTrackExporter implements TrackExporter {
|
||||
{
|
||||
String trackPointExtensionV2Content = "";
|
||||
|
||||
if (trackPoint.hasHeartRate()) {
|
||||
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.getHeartRate().getBPM()) + "</gpxtpx:hr>\n";
|
||||
if (trackPoint.heartRate() != null) {
|
||||
trackPointExtensionV2Content += "<gpxtpx:hr>" + HEARTRATE_FORMAT.format(trackPoint.heartRate().getBPM()) + "</gpxtpx:hr>\n";
|
||||
}
|
||||
|
||||
if (trackPoint.hasCadence()) {
|
||||
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.getCadence().getRPM()) + "</gpxtpx:cad>\n";
|
||||
if (trackPoint.cadence() != null) {
|
||||
trackPointExtensionV2Content += "<gpxtpx:cad>" + CADENCE_FORMAT.format(trackPoint.cadence().getRPM()) + "</gpxtpx:cad>\n";
|
||||
}
|
||||
|
||||
if (trackPoint.hasSpeed()) {
|
||||
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.getSpeed().toMPS()) + "</gpxtpx:speed>\n";
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
trackPointExtensionV2Content += "<gpxtpx:speed>" + SPEED_FORMAT.format(trackPoint.position().speed().toMPS()) + "</gpxtpx:speed>\n";
|
||||
}
|
||||
|
||||
String extensionContent = "";
|
||||
if (trackPoint.hasPower()) {
|
||||
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.getPower().getW()) + "</pwr:PowerInWatts>\n";
|
||||
if (trackPoint.power() != null) {
|
||||
extensionContent += "<pwr:PowerInWatts>" + POWER_FORMAT.format(trackPoint.power().getW()) + "</pwr:PowerInWatts>\n";
|
||||
}
|
||||
|
||||
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().gain_m() : null);
|
||||
Double cumulativeGain = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.altitudeGainLoss() != null ? (double) tp.altitudeGainLoss().gain_m() : null);
|
||||
if (cumulativeGain != null) {
|
||||
extensionContent += ("<opentracks:gain>" + ALTITUDE_FORMAT.format(cumulativeGain) + "</opentracks:gain>\n");
|
||||
}
|
||||
|
||||
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasAltitudeGainLoss() ? (double) tp.getAltitudeGainLoss().loss_m() : null);
|
||||
Double cumulativeLoss = cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.altitudeGainLoss() != null ? (double) tp.altitudeGainLoss().loss_m() : null);
|
||||
if (cumulativeLoss != null) {
|
||||
extensionContent += ("<opentracks:loss>" + ALTITUDE_FORMAT.format(cumulativeLoss) + "</opentracks:loss>\n");
|
||||
}
|
||||
|
||||
if (trackPoint.hasHorizontalAccuracy()) {
|
||||
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.getHorizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
|
||||
if (trackPoint.position().hasHorizontalAccuracy()) {
|
||||
extensionContent += ("<opentracks:accuracy_horizontal>" + DISTANCE_FORMAT.format(trackPoint.position().horizontalAccuracy().toM()) + "</opentracks:accuracy_horizontal>");
|
||||
}
|
||||
if (trackPoint.hasVerticalAccuracy()) {
|
||||
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.getVerticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
|
||||
if (trackPoint.position().hasVerticalAccuracy()) {
|
||||
extensionContent += ("<opentracks:accuracy_vertical>" + DISTANCE_FORMAT.format(trackPoint.position().verticalAccuracy().toM()) + "</opentracks:accuracy_vertical>");
|
||||
}
|
||||
|
||||
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.hasSensorDistance() ? tp.getSensorDistance().toM() : null));
|
||||
cumulativeDistance = Distance.ofOrNull(cumulateSensorData(trackPoint, sensorPoints, (tp) -> tp.sensorDistance() != null ? tp.sensorDistance().toM() : null));
|
||||
if (cumulativeDistance != null) {
|
||||
extensionContent += ("<opentracks:distance>" + DISTANCE_FORMAT.format(cumulativeDistance.toM()) + "</opentracks:distance>\n");
|
||||
extensionContent += ("<cluetrust:distance>" + DISTANCE_FORMAT.format(trackDistance.plus(cumulativeDistance).toM()) + "</cluetrust:distance>\n");
|
||||
|
||||
@@ -95,7 +95,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
private final List<Float> powerList = new ArrayList<>();
|
||||
private final List<Float> cadenceList = new ArrayList<>();
|
||||
private final List<Float> heartRateList = new ArrayList<>();
|
||||
private final List<Float> altitudeGainList = new ArrayList<>();
|
||||
private final List<Float> altitudeGainList = new ArrayList<>(); //TODO Join with altitudeLossList
|
||||
private final List<Float> altitudeLossList = new ArrayList<>();
|
||||
private final List<Float> accuracyHorizontal = new ArrayList<>();
|
||||
private final List<Float> accuracyVertical = new ArrayList<>();
|
||||
@@ -167,7 +167,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
switch (trackPoint.getType()) {
|
||||
switch (trackPoint.type()) {
|
||||
case SEGMENT_START_MANUAL, SEGMENT_START_AUTOMATIC -> {
|
||||
if (wroteSegment) writeCloseSegment();
|
||||
writeOpenSegment();
|
||||
@@ -190,7 +190,7 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
writeTrackPoint(track.zoneOffset(), trackPoint);
|
||||
}
|
||||
default ->
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.getType());
|
||||
throw new RuntimeException("Exporting this TrackPoint type is not implemented: " + trackPoint.type());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,28 +370,28 @@ public class KMLTrackExporter implements TrackExporter {
|
||||
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
|
||||
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getTime()) + "</when>");
|
||||
|
||||
trackpointTypeList.add(trackPoint.getType());
|
||||
trackpointTypeList.add(trackPoint.type());
|
||||
|
||||
if (trackPoint.hasLocation()) {
|
||||
printWriter.println("<coord>" + getCoordinates(trackPoint.getPosition(), " ") + "</coord>");
|
||||
if (trackPoint.position().hasLocation()) {
|
||||
printWriter.println("<coord>" + getCoordinates(trackPoint.position(), " ") + "</coord>");
|
||||
} else {
|
||||
printWriter.println("<coord/>");
|
||||
}
|
||||
speedList.add(trackPoint.hasSpeed() ? (float) trackPoint.getSpeed().toMPS() : null);
|
||||
speedList.add(trackPoint.position().hasSpeed() ? (float) trackPoint.position().speed().toMPS() : null);
|
||||
|
||||
distanceList.add(trackPoint.hasSensorDistance() ? (float) trackPoint.getSensorDistance().toM() : null);
|
||||
heartRateList.add(trackPoint.hasHeartRate() ? trackPoint.getHeartRate().getBPM() : null);
|
||||
cadenceList.add(trackPoint.hasCadence() ? trackPoint.getCadence().getRPM() : null);
|
||||
powerList.add(trackPoint.hasPower() ? trackPoint.getPower().getW() : null);
|
||||
if (trackPoint.hasAltitudeGainLoss()) {
|
||||
altitudeGainList.add(trackPoint.getAltitudeGainLoss().loss_m());
|
||||
altitudeLossList.add(trackPoint.getAltitudeGainLoss().loss_m());
|
||||
distanceList.add(trackPoint.sensorDistance() != null ? (float) trackPoint.sensorDistance().toM() : null);
|
||||
heartRateList.add(trackPoint.heartRate() != null ? trackPoint.heartRate().getBPM() : null);
|
||||
cadenceList.add(trackPoint.cadence() != null ? trackPoint.cadence().getRPM() : null);
|
||||
powerList.add(trackPoint.power() != null ? trackPoint.power().getW() : null);
|
||||
if (trackPoint.altitudeGainLoss() != null) {
|
||||
altitudeGainList.add(trackPoint.altitudeGainLoss().loss_m());
|
||||
altitudeLossList.add(trackPoint.altitudeGainLoss().loss_m());
|
||||
} else {
|
||||
altitudeGainList.add(null);
|
||||
altitudeLossList.add(null);
|
||||
}
|
||||
accuracyHorizontal.add(trackPoint.hasHorizontalAccuracy() ? (float) trackPoint.getHorizontalAccuracy().toM() : null);
|
||||
accuracyVertical.add(trackPoint.hasVerticalAccuracy() ? (float) trackPoint.getVerticalAccuracy().toM() : null);
|
||||
accuracyHorizontal.add(trackPoint.position().hasHorizontalAccuracy() ? (float) trackPoint.position().horizontalAccuracy().toM() : null);
|
||||
accuracyVertical.add(trackPoint.position().hasVerticalAccuracy() ? (float) trackPoint.position().verticalAccuracy().toM() : null);
|
||||
}
|
||||
|
||||
private void writeSimpleArraySensorData(List<Float> list, String name) {
|
||||
|
||||
@@ -32,9 +32,13 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Altitude;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
@@ -265,8 +269,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPoint first = currentSegment.getFirst();
|
||||
first.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
TrackPoint first = currentSegment.removeFirst();
|
||||
currentSegment.addFirst(first.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC));
|
||||
|
||||
trackImporter.addTrackPoints(currentSegment);
|
||||
currentSegment.clear();
|
||||
@@ -332,59 +336,70 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
}
|
||||
}
|
||||
|
||||
TrackPoint trackPoint = new TrackPoint(
|
||||
TrackPoint.Type.TRACKPOINT,
|
||||
new Position(
|
||||
parsedTime.toInstant(),
|
||||
latitudeParsed,
|
||||
longitudeParsed,
|
||||
accuracyHorizontalParsed,
|
||||
altitudeParsed,
|
||||
accuracyVerticalParsed,
|
||||
null,
|
||||
speedParsed
|
||||
));
|
||||
|
||||
if (heartrate != null) {
|
||||
try {
|
||||
trackPoint.setHeartRate(Float.parseFloat(heartrate));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cadence != null) {
|
||||
try {
|
||||
trackPoint.setCadence(Float.parseFloat(cadence));
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (power != null) {
|
||||
try {
|
||||
trackPoint.setPower(Float.parseFloat(power));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (gain != null && loss != null) {
|
||||
try {
|
||||
trackPoint.setAltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
|
||||
}
|
||||
}
|
||||
Distance parsedSensorDistance = null;
|
||||
if (sensorDistance != null) {
|
||||
try {
|
||||
trackPoint.setSensorDistance(Distance.of(Float.parseFloat(sensorDistance)));
|
||||
parsedSensorDistance = Distance.of(Float.parseFloat(sensorDistance));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse distance: %s", sensorDistance)), e);
|
||||
}
|
||||
}
|
||||
|
||||
return trackPoint;
|
||||
HeartRate parsedHeartRate = null;
|
||||
if (heartrate != null) {
|
||||
try {
|
||||
parsedHeartRate = HeartRate.of(Float.parseFloat(heartrate));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse heart rate: %s", heartrate)), e);
|
||||
}
|
||||
}
|
||||
|
||||
Cadence parsedCadence = null;
|
||||
if (cadence != null) {
|
||||
try {
|
||||
parsedCadence = Cadence.of(Float.parseFloat(cadence));
|
||||
} catch (Exception e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse cadence: %s", cadence)), e);
|
||||
}
|
||||
}
|
||||
|
||||
Power parsedPower = null;
|
||||
if (power != null) {
|
||||
try {
|
||||
parsedPower = Power.of(Float.parseFloat(power));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse power: %s", power)), e);
|
||||
}
|
||||
}
|
||||
|
||||
AltitudeGainLoss altitudeGainLoss = null;
|
||||
if (gain != null && loss != null) {
|
||||
try {
|
||||
altitudeGainLoss = new AltitudeGainLoss(Float.parseFloat(gain), Float.parseFloat(loss));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ParsingException(createErrorMessage(String.format(Locale.US, "Unable to parse altitude gain: %s or los: %s", gain, loss)), e);
|
||||
}
|
||||
}
|
||||
|
||||
return new TrackPoint(
|
||||
null,
|
||||
TrackPoint.Type.TRACKPOINT,
|
||||
new Position(
|
||||
parsedTime.toInstant(),
|
||||
latitudeParsed,
|
||||
longitudeParsed,
|
||||
accuracyHorizontalParsed,
|
||||
altitudeParsed,
|
||||
accuracyVerticalParsed,
|
||||
null,
|
||||
speedParsed
|
||||
),
|
||||
parsedSensorDistance,
|
||||
parsedHeartRate,
|
||||
parsedCadence,
|
||||
parsedPower,
|
||||
altitudeGainLoss
|
||||
);
|
||||
}
|
||||
|
||||
private void onTrackPointStart(Attributes attributes) {
|
||||
@@ -425,8 +440,8 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
|
||||
TrackPoint trackPoint = createTrackPoint();
|
||||
|
||||
if (!trackPoint.hasLocation()) {
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.getPosition());
|
||||
if (!trackPoint.position().hasLocation()) {
|
||||
Log.w(TAG, "Marker with invalid coordinates ignored: " + trackPoint.position());
|
||||
return;
|
||||
}
|
||||
markers.add(new Marker(
|
||||
@@ -435,7 +450,7 @@ public class GPXTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
name != null ? name : "",
|
||||
description != null ? description : "",
|
||||
markerType != null ? markerType : "",
|
||||
trackPoint.getPosition(),
|
||||
trackPoint.position(),
|
||||
photoUrl
|
||||
));
|
||||
}
|
||||
|
||||
@@ -33,9 +33,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Marker;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
@@ -95,10 +99,11 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
private ZoneOffset zoneOffset;
|
||||
|
||||
private final ArrayList<Instant> whenList = new ArrayList<>();
|
||||
private final ArrayList<Position> positionList = new ArrayList<>();
|
||||
private final ArrayList<Position> positionList = new ArrayList<>(); //TODO This is only latitude, longitude, and altitude
|
||||
|
||||
private String dataType;
|
||||
|
||||
//TODO For ArrayList use specific data models; and why Float? Double would be easier.
|
||||
private final ArrayList<String> trackpointTypeList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorSpeedList = new ArrayList<>();
|
||||
private final ArrayList<Float> sensorDistanceList = new ArrayList<>();
|
||||
@@ -151,8 +156,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
}
|
||||
onTrackSegmentStart();
|
||||
}
|
||||
case TAG_EXTENDED_DATA, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA ->
|
||||
dataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
case TAG_EXTENDED_DATA, TAG_SIMPLE_ARRAY_DATA, TAG_KML22_SIMPLE_ARRAY_DATA -> dataType = attributes.getValue(ATTRIBUTE_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,62 +321,77 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
for (int i = 0; i < positionList.size(); i++) {
|
||||
Instant time = whenList.get(i);
|
||||
Position position = positionList.get(i);
|
||||
|
||||
TrackPoint trackPoint;
|
||||
if (position == null) {
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(time));
|
||||
position = Position.of(time);
|
||||
} else {
|
||||
trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, position.with(time));
|
||||
position = position.with(time);
|
||||
}
|
||||
|
||||
TrackPoint.Type type = null;
|
||||
Speed speed = null;
|
||||
Distance sensorDistance = null;
|
||||
HeartRate heartRate = null;
|
||||
Cadence cadence = null;
|
||||
Power power = null;
|
||||
AltitudeGainLoss altitudeGainLoss = null;
|
||||
Distance horizontalAccuracy = null;
|
||||
Distance verticalAccuracy = null;
|
||||
|
||||
if (i < trackpointTypeList.size() && trackpointTypeList.get(i) != null) {
|
||||
TrackPoint.Type type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
|
||||
trackPoint.setType(type);
|
||||
type = TrackPoint.Type.valueOf(trackpointTypeList.get(i));
|
||||
}
|
||||
|
||||
if (i < sensorSpeedList.size() && sensorSpeedList.get(i) != null) {
|
||||
trackPoint.setSpeed(Speed.of(sensorSpeedList.get(i)));
|
||||
speed = Speed.of(sensorSpeedList.get(i));
|
||||
}
|
||||
if (i < sensorDistanceList.size() && sensorDistanceList.get(i) != null) {
|
||||
trackPoint.setSensorDistance(Distance.of(sensorDistanceList.get(i)));
|
||||
sensorDistance = Distance.of(sensorDistanceList.get(i));
|
||||
}
|
||||
if (i < sensorHeartRateList.size() && sensorHeartRateList.get(i) != null) {
|
||||
trackPoint.setHeartRate(sensorHeartRateList.get(i));
|
||||
if (i < sensorHeartRateList.size()) {
|
||||
heartRate = HeartRate.ofOrNull(sensorHeartRateList.get(i));
|
||||
}
|
||||
if (i < sensorCadenceList.size() && sensorCadenceList.get(i) != null) {
|
||||
trackPoint.setCadence(sensorCadenceList.get(i));
|
||||
cadence = Cadence.of(sensorCadenceList.get(i));
|
||||
}
|
||||
if (i < sensorPowerList.size() && sensorPowerList.get(i) != null) {
|
||||
trackPoint.setPower(sensorPowerList.get(i));
|
||||
power = Power.of(sensorPowerList.get(i));
|
||||
}
|
||||
if (i < altitudeGainList.size() && i < altitudeLossList.size()) {
|
||||
if (altitudeGainList.get(i) != null && altitudeLossList.get(i) != null) {
|
||||
trackPoint.setAltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
|
||||
}
|
||||
if ((i < altitudeGainList.size() && altitudeGainList.get(i) != null)
|
||||
&& (i < altitudeLossList.size() && altitudeLossList.get(i) != null)) {
|
||||
altitudeGainLoss = new AltitudeGainLoss(altitudeGainList.get(i), altitudeLossList.get(i));
|
||||
}
|
||||
if (i < accuracyHorizontal.size() && accuracyHorizontal.get(i) != null) {
|
||||
trackPoint.setHorizontalAccuracy(Distance.of(accuracyHorizontal.get(i)));
|
||||
horizontalAccuracy = Distance.of(accuracyHorizontal.get(i));
|
||||
}
|
||||
if (i < accuracyVertical.size() && accuracyVertical.get(i) != null) {
|
||||
trackPoint.setVerticalAccuracy(Distance.of(accuracyVertical.get(i)));
|
||||
verticalAccuracy = Distance.of(accuracyVertical.get(i));
|
||||
}
|
||||
|
||||
// Update TrackPoint type for START / STOP.
|
||||
TrackPoint.Type type = trackPoint.getType();
|
||||
if (i == 0) {
|
||||
//first
|
||||
if (!trackPoint.wasCreatedManually()) {
|
||||
type = TrackPoint.Type.SEGMENT_START_MANUAL;
|
||||
} else {
|
||||
if (wasCreatedAutomatically(position)) {
|
||||
type = TrackPoint.Type.SEGMENT_START_AUTOMATIC;
|
||||
} else {
|
||||
type = TrackPoint.Type.SEGMENT_START_MANUAL;
|
||||
}
|
||||
} else if (i == positionList.size() - 1 && !trackPoint.wasCreatedManually()) {
|
||||
} else if (i == positionList.size() - 1 && !wasCreatedAutomatically(position)) {
|
||||
//last
|
||||
type = TrackPoint.Type.SEGMENT_END_MANUAL;
|
||||
}
|
||||
trackPoint.setType(type);
|
||||
|
||||
trackImporter.addTrackPoint(trackPoint);
|
||||
trackImporter.addTrackPoint(new TrackPoint(
|
||||
null,
|
||||
type != null ? type : TrackPoint.Type.TRACKPOINT,
|
||||
position
|
||||
.with(speed)
|
||||
.withHorizontalAccuracy(horizontalAccuracy)
|
||||
.withVerticalAccuracy(verticalAccuracy),
|
||||
sensorDistance,
|
||||
heartRate,
|
||||
cadence,
|
||||
power,
|
||||
altitudeGainLoss
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +411,7 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
}
|
||||
|
||||
private Position createPosition(String latitude, String longitude, String altitude) {
|
||||
//TODO Do not use Location.
|
||||
if (longitude != null && latitude != null) {
|
||||
Location location = new Location("import");
|
||||
try {
|
||||
@@ -439,12 +459,9 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_CADENCE -> sensorCadenceList.add(value);
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_GAIN -> altitudeGainList.add(value);
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ALTITUDE_LOSS -> altitudeLossList.add(value);
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL ->
|
||||
accuracyHorizontal.add(value);
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL ->
|
||||
accuracyVertical.add(value);
|
||||
default ->
|
||||
Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_HORIZONTAL -> accuracyHorizontal.add(value);
|
||||
case KMLTrackExporter.EXTENDED_DATA_TYPE_ACCURACY_VERTICAL -> accuracyVertical.add(value);
|
||||
default -> Log.w(TAG, "Data from extended data " + dataType + " is not (yet) supported.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +474,10 @@ public class KMLTrackImporter extends DefaultHandler implements XMLImporter.Trac
|
||||
trackImporter.finish();
|
||||
}
|
||||
|
||||
public boolean wasCreatedAutomatically(Position position) {
|
||||
return position.hasLocation() || position.hasSpeed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultHandler getHandler() {
|
||||
return this;
|
||||
|
||||
@@ -176,21 +176,19 @@ public class TrackImporter {
|
||||
TrackPoint previous = trackPoints.get(i - 1);
|
||||
TrackPoint current = trackPoints.get(i);
|
||||
|
||||
if (current.hasSensorDistance() || (previous.hasLocation() && current.hasLocation())) {
|
||||
if (current.sensorDistance() != null || (previous.position().hasLocation() && current.position().hasLocation())) {
|
||||
TrackPoint newCurrent = current;
|
||||
|
||||
Distance distanceToPrevious = current.distanceToPrevious(previous);
|
||||
if (!current.hasSpeed()) {
|
||||
if (!current.position().hasSpeed()) {
|
||||
Duration timeDifference = Duration.between(previous.getTime(), current.getTime());
|
||||
current.setSpeed(Speed.of(distanceToPrevious, timeDifference));
|
||||
newCurrent = newCurrent.with(newCurrent.position().with(Speed.of(distanceToPrevious, timeDifference)));
|
||||
}
|
||||
|
||||
if (!current.hasBearing()) {
|
||||
previous.bearingTo(current)
|
||||
.ifPresent(current::setBearing);
|
||||
}
|
||||
|
||||
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
|
||||
current.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
if (current.type().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
|
||||
newCurrent = newCurrent.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
}
|
||||
trackPoints.set(i, newCurrent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,20 +124,20 @@ public class GpsManager implements SensorConnector, LocationListenerCompat, GpsS
|
||||
@Override
|
||||
public void onLocationChanged(@NonNull Location location) {
|
||||
// Send each update to the status; please note that this TrackPoint is not stored.
|
||||
TrackPoint trackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, Position.of(location, trackPointCreator.createNow()));
|
||||
gpsStatusManager.onNewTrackPoint(trackPoint);
|
||||
Position position = Position.of(location, trackPointCreator.createNow());
|
||||
gpsStatusManager.onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, position));
|
||||
|
||||
if (!trackPoint.getPosition().hasValidLocation()) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. location is invalid.");
|
||||
if (!position.hasValidLocation()) {
|
||||
Log.w(TAG, "Ignore newTrackPoint. Location is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackPoint.getPosition().fulfillsAccuracy(thresholdHorizontalAccuracy)) {
|
||||
if (!position.fulfillsAccuracy(thresholdHorizontalAccuracy)) {
|
||||
Log.d(TAG, "Ignore newTrackPoint. Poor accuracy.");
|
||||
return;
|
||||
}
|
||||
|
||||
observer.onChange(new Raw<>(observer.getNow(), Position.of(location)));
|
||||
observer.onChange(new Raw<>(observer.getNow(), position));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@ import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
/**
|
||||
* This class handle GPS status according to received locations and some thresholds.
|
||||
*/
|
||||
//TODO Use Position instead of TrackPoint.
|
||||
class GpsStatusManager {
|
||||
|
||||
private static final String TAG = GpsStatusManager.class.getSimpleName();
|
||||
@@ -100,7 +101,7 @@ class GpsStatusManager {
|
||||
//TODO use MonotonicClock instead of Instant.now()
|
||||
@VisibleForTesting
|
||||
void determineGpsStatusOnTrackpoint(@NonNull TrackPoint lastTrackPoint) {
|
||||
if (lastTrackPoint.fulfillsAccuracy(horizontalAccuracyThreshold)) {
|
||||
if (lastTrackPoint.position().fulfillsAccuracy(horizontalAccuracyThreshold)) {
|
||||
if (gpsStatus != GpsStatusValue.GPS_SIGNAL_FIX) {
|
||||
setGpsStatus(GpsStatusValue.GPS_SIGNAL_FIX);
|
||||
scheduleTimer(); //TODO
|
||||
|
||||
@@ -11,9 +11,9 @@ import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Aggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.Raw;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataAggregator;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.util.SystemUtils;
|
||||
@@ -24,7 +24,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
|
||||
//TODO Should be final and not be visible for testing
|
||||
@VisibleForTesting
|
||||
public SensorDataSet sensorDataSet;
|
||||
public SensorDataAggregator sensorDataAggregator;
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
|
||||
@@ -34,23 +34,23 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
|
||||
@Override
|
||||
public void onConnect(Aggregator<?, ?> aggregator) {
|
||||
sensorDataSet.add(aggregator);
|
||||
sensorDataAggregator.add(aggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(Raw<?> data) {
|
||||
sensorDataSet.update(data);
|
||||
observer.onChange(new SensorDataSet(sensorDataSet));
|
||||
sensorDataAggregator.update(data);
|
||||
observer.onChange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Aggregator<?, ?> aggregator) {
|
||||
sensorDataSet.add(aggregator);
|
||||
sensorDataAggregator.add(aggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemove(Aggregator<?, ?> aggregator) {
|
||||
sensorDataSet.remove(aggregator);
|
||||
sensorDataAggregator.remove(aggregator);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,7 +67,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
|
||||
public SensorManager(TrackPointCreator observer) {
|
||||
this.observer = observer;
|
||||
this.sensorDataSet = new SensorDataSet(observer);
|
||||
this.sensorDataAggregator = new SensorDataAggregator();
|
||||
}
|
||||
|
||||
public void start(Context context, Handler handler) {
|
||||
@@ -100,12 +100,11 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
gpsManager.stop(context);
|
||||
gpsManager = null;
|
||||
|
||||
sensorDataSet.clear();
|
||||
sensorDataAggregator.clear();
|
||||
}
|
||||
|
||||
public SensorDataSet fill(TrackPoint trackPoint) {
|
||||
sensorDataSet.fillTrackPoint(trackPoint);
|
||||
return new SensorDataSet(sensorDataSet);
|
||||
public SensorDataSet getSensorDataSet(Instant now) {
|
||||
return sensorDataAggregator.getSensorData(now);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
@@ -113,7 +112,7 @@ public class SensorManager implements SharedPreferences.OnSharedPreferenceChange
|
||||
Log.d(TAG, "No recording running and no reset necessary.");
|
||||
return;
|
||||
}
|
||||
sensorDataSet.reset();
|
||||
sensorDataAggregator.reset();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
@@ -61,8 +59,8 @@ public abstract class Aggregator<Input, Output> {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Pair<Output, String> getAggregatedValueWithSensorName(Instant now) {
|
||||
return new Pair<>(getAggregatedValue(now), getSensorNameOrAddress());
|
||||
public SensorData<Output> getAggregatedValueWithSensorName(Instant now) {
|
||||
return new SensorData<>(getAggregatedValue(now), getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,8 @@ package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
|
||||
public class AggregatorGPS extends Aggregator<Position, Position> {
|
||||
@@ -36,4 +38,11 @@ public class AggregatorGPS extends Aggregator<Position, Position> {
|
||||
protected Position getNoneValue() {
|
||||
return Position.empty();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Position getAggregatedValue(Instant now) {
|
||||
return super.getAggregatedValue(now)
|
||||
.with(now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
public record SensorData<T>(
|
||||
|
||||
T data,
|
||||
|
||||
String sensorNameOrAddress
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class SensorDataAggregator {
|
||||
|
||||
private static final String TAG = SensorDataAggregator.class.getSimpleName();
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorHeartRate heartRate;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingCadence cyclingCadence;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingPower cyclingPower;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorRunning runningDistanceSpeedCadence;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorBarometer barometer;
|
||||
|
||||
public AggregatorGPS gps;
|
||||
|
||||
@VisibleForTesting
|
||||
public SensorData<HeartRate> getHeartRate(Instant now) {
|
||||
if (heartRate != null) {
|
||||
SensorData<HeartRate> value = heartRate.getAggregatedValueWithSensorName(now);
|
||||
if (value.data().isValid()) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private SensorData<Cadence> getCadence(Instant now) {
|
||||
if (cyclingCadence != null) {
|
||||
return cyclingCadence.getAggregatedValueWithSensorName(now);
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(now).cadence() != null) {
|
||||
return new SensorData<>(runningDistanceSpeedCadence.aggregatedValue.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private SensorData<Distance> getDistance(Instant now) {
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData()) {
|
||||
return new SensorData<>(cyclingDistanceSpeed.getAggregatedValue(now).distanceOverall(), cyclingDistanceSpeed.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData()) {
|
||||
return new SensorData<>(runningDistanceSpeedCadence.getAggregatedValue(now).distance(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//TOOD simplify?
|
||||
private SensorData<Speed> getSpeed(Instant now) {
|
||||
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData() && cyclingDistanceSpeed.getAggregatedValue(now).speed() != null) {
|
||||
return new SensorData<>(cyclingDistanceSpeed.getAggregatedValue(now).speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(now).speed() != null) {
|
||||
return new SensorData<>(runningDistanceSpeedCadence.aggregatedValue.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void add(@NonNull Aggregator<?, ?> data) {
|
||||
set(data, data);
|
||||
}
|
||||
|
||||
public void update(@NonNull Raw<?> data) {
|
||||
Object value = data.value();
|
||||
|
||||
if (value instanceof HeartRate) {
|
||||
this.heartRate.add((Raw<HeartRate>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
|
||||
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData) {
|
||||
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
|
||||
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
|
||||
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
|
||||
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerManagerCyclingPower.Data) {
|
||||
this.cyclingPower.add((Raw<BluetoothHandlerManagerCyclingPower.Data>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof AtmosphericPressure) {
|
||||
this.barometer.add((Raw<AtmosphericPressure>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Position) {
|
||||
this.gps.add((Raw<Position>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
public void remove(@NonNull Aggregator<?, ?> type) {
|
||||
set(type, null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
Log.i(TAG, "Removing all aggregators");
|
||||
this.heartRate = null;
|
||||
this.cyclingCadence = null;
|
||||
this.cyclingDistanceSpeed = null;
|
||||
this.cyclingPower = null;
|
||||
this.runningDistanceSpeedCadence = null;
|
||||
this.barometer = null;
|
||||
this.gps = null;
|
||||
}
|
||||
|
||||
public SensorDataSet getSensorData(Instant now) {
|
||||
return new SensorDataSet(
|
||||
gps != null && gps.hasReceivedData() ? gps.getAggregatedValueWithSensorName(now) : new SensorData<>(Position.of(now), ""),
|
||||
getSpeed(now),
|
||||
getDistance(now),
|
||||
getHeartRate(now),
|
||||
getCadence(now),
|
||||
cyclingPower != null && cyclingPower.hasReceivedData() ? cyclingPower.getAggregatedValueWithSensorName(now) : null,
|
||||
barometer != null && barometer.hasReceivedData() ? barometer.getAggregatedValueWithSensorName(now) : null
|
||||
);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
Log.i(TAG, "Resetting data");
|
||||
|
||||
if (heartRate != null) heartRate.resetAggregated();
|
||||
if (cyclingCadence != null) cyclingCadence.resetAggregated();
|
||||
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.resetAggregated();
|
||||
if (cyclingPower != null) cyclingPower.resetAggregated();
|
||||
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.resetAggregated();
|
||||
if (barometer != null) barometer.resetAggregated();
|
||||
if (gps != null) gps.resetAggregated();
|
||||
}
|
||||
|
||||
//TODO Refactor generic "fun" here: go explicit.
|
||||
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
|
||||
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName() + " to " + sensorData);
|
||||
|
||||
if (type instanceof AggregatorHeartRate) {
|
||||
heartRate = (AggregatorHeartRate) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorCyclingCadence) {
|
||||
cyclingCadence = (AggregatorCyclingCadence) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorCyclingDistanceSpeed) {
|
||||
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorCyclingPower) {
|
||||
cyclingPower = (AggregatorCyclingPower) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorRunning) {
|
||||
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorBarometer) {
|
||||
barometer = (AggregatorBarometer) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorGPS) {
|
||||
gps = (AggregatorGPS) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
|
||||
}
|
||||
}
|
||||
@@ -1,240 +1,30 @@
|
||||
package de.dennisguse.opentracks.sensors.sensorData;
|
||||
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.AtmosphericPressure;
|
||||
import de.dennisguse.opentracks.data.models.AltitudeGainLoss;
|
||||
import de.dennisguse.opentracks.data.models.Cadence;
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.Power;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingDistanceSpeed;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerManagerCyclingPower;
|
||||
import de.dennisguse.opentracks.sensors.BluetoothHandlerRunningSpeedAndCadence;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
|
||||
public class SensorDataSet {
|
||||
|
||||
private static final String TAG = SensorDataSet.class.getSimpleName();
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorHeartRate heartRate;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingCadence cyclingCadence;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingDistanceSpeed cyclingDistanceSpeed;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorCyclingPower cyclingPower;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorRunning runningDistanceSpeedCadence;
|
||||
|
||||
@VisibleForTesting
|
||||
public AggregatorBarometer barometer;
|
||||
|
||||
public AggregatorGPS gps;
|
||||
|
||||
private final TrackPointCreator trackPointCreator;
|
||||
|
||||
public SensorDataSet(TrackPointCreator trackPointCreator) {
|
||||
this.trackPointCreator = trackPointCreator;
|
||||
}
|
||||
|
||||
@Deprecated //TODO This is not a copy constructor anymore, but it should be - aggregators are no value objects; best guess: can be removed
|
||||
public SensorDataSet(SensorDataSet toCopy) {
|
||||
this.trackPointCreator = toCopy.trackPointCreator;
|
||||
this.heartRate = toCopy.heartRate;
|
||||
this.cyclingCadence = toCopy.cyclingCadence;
|
||||
this.cyclingDistanceSpeed = toCopy.cyclingDistanceSpeed;
|
||||
this.cyclingPower = toCopy.cyclingPower;
|
||||
this.runningDistanceSpeedCadence = toCopy.runningDistanceSpeedCadence;
|
||||
this.barometer = toCopy.barometer;
|
||||
this.gps = toCopy.gps;
|
||||
}
|
||||
|
||||
public Pair<HeartRate, String> getHeartRate() {
|
||||
if (heartRate != null) {
|
||||
Pair<HeartRate, String> value = heartRate.getAggregatedValueWithSensorName(trackPointCreator.createNow());
|
||||
if (value.first.isValid()) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Pair<Cadence, String> getCadence() {
|
||||
if (cyclingCadence != null) {
|
||||
return cyclingCadence.getAggregatedValueWithSensorName(trackPointCreator.createNow());
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.aggregatedValue.cadence() != null) {
|
||||
return new Pair<>(runningDistanceSpeedCadence.aggregatedValue.cadence(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Pair<Speed, String> getSpeed() {
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData() && cyclingDistanceSpeed.getAggregatedValue(trackPointCreator.createNow()).speed() != null) {
|
||||
return new Pair<>(cyclingDistanceSpeed.getAggregatedValue(trackPointCreator.createNow()).speed(), cyclingDistanceSpeed.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData() && runningDistanceSpeedCadence.getAggregatedValue(trackPointCreator.createNow()).speed() != null) {
|
||||
return new Pair<>(runningDistanceSpeedCadence.aggregatedValue.speed(), runningDistanceSpeedCadence.getSensorNameOrAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public AggregatorCyclingPower getCyclingPower() {
|
||||
return cyclingPower;
|
||||
}
|
||||
|
||||
public void add(@NonNull Aggregator<?, ?> data) {
|
||||
set(data, data);
|
||||
}
|
||||
|
||||
public void update(@NonNull Raw<?> data) {
|
||||
Object value = data.value();
|
||||
|
||||
if (value instanceof HeartRate) {
|
||||
this.heartRate.add((Raw<HeartRate>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof BluetoothHandlerCyclingCadence.CrankData) {
|
||||
this.cyclingCadence.add((Raw<BluetoothHandlerCyclingCadence.CrankData>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerCyclingDistanceSpeed.WheelData ) {
|
||||
this.cyclingDistanceSpeed.setWheelCircumference(PreferencesUtils.getWheelCircumference()); //TODO Fetch once and then listen for changes.
|
||||
this.cyclingDistanceSpeed.add((Raw<BluetoothHandlerCyclingDistanceSpeed.WheelData>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerRunningSpeedAndCadence.Data) {
|
||||
this.runningDistanceSpeedCadence.add((Raw<BluetoothHandlerRunningSpeedAndCadence.Data>) data);
|
||||
|
||||
return;
|
||||
}
|
||||
if (value instanceof BluetoothHandlerManagerCyclingPower.Data) {
|
||||
this.cyclingPower.add((Raw<BluetoothHandlerManagerCyclingPower.Data>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof AtmosphericPressure) {
|
||||
this.barometer.add((Raw<AtmosphericPressure>) data);
|
||||
return;
|
||||
}
|
||||
if (value instanceof Position) {
|
||||
this.gps.add((Raw<Position>) data);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(data.getClass().getCanonicalName() + " " + data.value().getClass().getCanonicalName());
|
||||
}
|
||||
|
||||
public void remove(@NonNull Aggregator<?, ?> type) {
|
||||
set(type, null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
Log.i(TAG, "Removing all aggregators");
|
||||
this.heartRate = null;
|
||||
this.cyclingCadence = null;
|
||||
this.cyclingDistanceSpeed = null;
|
||||
this.cyclingPower = null;
|
||||
this.runningDistanceSpeedCadence = null;
|
||||
this.barometer = null;
|
||||
this.gps = null;
|
||||
}
|
||||
|
||||
public void fillTrackPoint(TrackPoint trackPoint) {
|
||||
if (gps != null && gps.hasReceivedData()) {
|
||||
trackPoint.setPosition(gps.getAggregatedValue(trackPointCreator.createNow()));
|
||||
}
|
||||
|
||||
Pair<HeartRate, String> heartRate = getHeartRate();
|
||||
if (heartRate != null) {
|
||||
trackPoint.setHeartRate(heartRate.first);
|
||||
}
|
||||
|
||||
if (getCadence() != null) {
|
||||
trackPoint.setCadence(getCadence().first);
|
||||
}
|
||||
|
||||
if (getSpeed() != null) {
|
||||
trackPoint.setSpeed(getSpeed().first);
|
||||
}
|
||||
|
||||
if (cyclingDistanceSpeed != null && cyclingDistanceSpeed.hasReceivedData()) {
|
||||
trackPoint.setSensorDistance(cyclingDistanceSpeed.getAggregatedValue(trackPointCreator.createNow()).distanceOverall());
|
||||
}
|
||||
|
||||
if (cyclingPower != null && cyclingPower.hasReceivedData()) {
|
||||
trackPoint.setPower(cyclingPower.getAggregatedValue(trackPointCreator.createNow()));
|
||||
}
|
||||
|
||||
if (runningDistanceSpeedCadence != null && runningDistanceSpeedCadence.hasReceivedData()) {
|
||||
trackPoint.setSensorDistance(runningDistanceSpeedCadence.getAggregatedValue(trackPointCreator.createNow()).distance());
|
||||
}
|
||||
|
||||
if (barometer != null && barometer.hasReceivedData()) {
|
||||
trackPoint.setAltitudeGainLoss(barometer.getAggregatedValue(trackPointCreator.createNow()).gain_m(), barometer.getAggregatedValue(trackPointCreator.createNow()).loss_m());
|
||||
}
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
Log.i(TAG, "Resetting data");
|
||||
|
||||
if (heartRate != null) heartRate.resetAggregated();
|
||||
if (cyclingCadence != null) cyclingCadence.resetAggregated();
|
||||
if (cyclingDistanceSpeed != null) cyclingDistanceSpeed.resetAggregated();
|
||||
if (cyclingPower != null) cyclingPower.resetAggregated();
|
||||
if (runningDistanceSpeedCadence != null) runningDistanceSpeedCadence.resetAggregated();
|
||||
if (barometer != null) barometer.resetAggregated();
|
||||
if (gps != null) gps.resetAggregated();
|
||||
}
|
||||
|
||||
private void set(@NonNull Aggregator<?, ?> type, @Nullable Aggregator<?, ?> sensorData) {
|
||||
Log.i(TAG, "Setting aggregator " + type.getClass().getCanonicalName() + " to " + sensorData);
|
||||
|
||||
if (type instanceof AggregatorHeartRate) {
|
||||
heartRate = (AggregatorHeartRate) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorCyclingCadence) {
|
||||
cyclingCadence = (AggregatorCyclingCadence) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorCyclingDistanceSpeed) {
|
||||
cyclingDistanceSpeed = (AggregatorCyclingDistanceSpeed) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorCyclingPower) {
|
||||
cyclingPower = (AggregatorCyclingPower) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorRunning) {
|
||||
runningDistanceSpeedCadence = (AggregatorRunning) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorBarometer) {
|
||||
barometer = (AggregatorBarometer) sensorData;
|
||||
return;
|
||||
}
|
||||
if (type instanceof AggregatorGPS) {
|
||||
gps = (AggregatorGPS) sensorData;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException(type.getClass().getCanonicalName());
|
||||
}
|
||||
public record SensorDataSet(
|
||||
@NonNull
|
||||
SensorData<Position> position,
|
||||
@Nullable
|
||||
SensorData<Speed> speed,
|
||||
@Nullable
|
||||
SensorData<Distance> distance,
|
||||
@Nullable
|
||||
SensorData<HeartRate> heartRate,
|
||||
@Nullable
|
||||
SensorData<Cadence> cadence,
|
||||
@Nullable
|
||||
SensorData<Power> power,
|
||||
@Nullable
|
||||
SensorData<AltitudeGainLoss> altitudeGainLoss
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ import de.dennisguse.opentracks.data.models.Statistics;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.models.TrackBuilder;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.services.handlers.AltitudeCorrectionManager;
|
||||
import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.data.statistics.TrackStatisticsUpdater;
|
||||
import de.dennisguse.opentracks.util.TrackNameUtils;
|
||||
|
||||
public class TrackRecordingManager implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@@ -160,14 +160,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
* @return TrackPoint was stored?
|
||||
*/
|
||||
synchronized boolean onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
if (trackPoint.hasSpeed()) {
|
||||
if (trackPoint.position().hasSpeed()) {
|
||||
lastTrackPointUIWithSpeed = trackPoint;
|
||||
}
|
||||
if (trackPoint.hasAltitude()) {
|
||||
if (trackPoint.position().hasAltitude()) {
|
||||
lastTrackPointUIWithAltitude = trackPoint;
|
||||
}
|
||||
|
||||
if (trackPoint.getType() == TrackPoint.Type.IDLE) {
|
||||
if (trackPoint.type() == TrackPoint.Type.IDLE) {
|
||||
insertTrackPoint(trackPoint, true);
|
||||
handler.removeCallbacks(ON_IDLE);
|
||||
return true;
|
||||
@@ -180,14 +180,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trackPoint.hasLocation() && lastStoredTrackPointWithLocation == null) {
|
||||
if (trackPoint.position().hasLocation() && lastStoredTrackPointWithLocation == null) {
|
||||
insertTrackPoint(trackPoint, true);
|
||||
|
||||
scheduleNewIdleTimeout();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!trackPoint.hasLocation() && !trackPoint.hasSensorDistance()) {
|
||||
if (!trackPoint.position().hasLocation() && trackPoint.sensorDistance() == null) {
|
||||
Duration minStorageInterval = Duration.ofSeconds(10); // TODO Should be configurable.
|
||||
boolean shouldStore = lastStoredTrackPoint.getTime().plus(minStorageInterval)
|
||||
.isBefore(trackPoint.getTime());
|
||||
@@ -201,14 +201,14 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
}
|
||||
|
||||
Distance distanceToLastStoredTrackPoint;
|
||||
if (trackPoint.hasLocation() && !lastStoredTrackPoint.hasLocation()) {
|
||||
if (trackPoint.position().hasLocation() && !lastStoredTrackPoint.position().hasLocation()) {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPreviousFromLocation(lastStoredTrackPointWithLocation);
|
||||
} else {
|
||||
distanceToLastStoredTrackPoint = trackPoint.distanceToPrevious(lastStoredTrackPoint);
|
||||
}
|
||||
|
||||
if (distanceToLastStoredTrackPoint.greaterThan(maxRecordingDistance)) {
|
||||
trackPoint.setType(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
trackPoint = trackPoint.with(TrackPoint.Type.SEGMENT_START_AUTOMATIC);
|
||||
insertTrackPoint(trackPoint, true);
|
||||
|
||||
scheduleNewIdleTimeout();
|
||||
@@ -248,8 +248,8 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
Log.w(TAG, "Ignore insertTrackPoint. trackPoint time same as last valid trackId point time.");
|
||||
} else {
|
||||
insertTrackPointHelper(lastTrackPoint);
|
||||
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
|
||||
trackPoint.minusCumulativeSensorData(lastTrackPoint);
|
||||
// Remove the sensorDistance from trackPoint that is already going be stored with lastTrackPoint.
|
||||
trackPoint = trackPoint.minusCumulativeSensorData(lastTrackPoint);
|
||||
}
|
||||
}
|
||||
lastTrackPoint = null;
|
||||
@@ -264,7 +264,7 @@ public class TrackRecordingManager implements SharedPreferences.OnSharedPreferen
|
||||
|
||||
contentProviderUtils.updateTrackStatistics(trackId, trackStatisticsUpdater.getTrackStatistics());
|
||||
lastStoredTrackPoint = trackPoint;
|
||||
if (trackPoint.hasLocation()) {
|
||||
if (trackPoint.position().hasLocation()) {
|
||||
lastStoredTrackPointWithLocation = lastStoredTrackPoint;
|
||||
}
|
||||
} catch (SQLiteException e) {
|
||||
|
||||
@@ -255,7 +255,7 @@ public class TrackRecordingService extends Service implements TrackPointCreator.
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
trackPoint.getPosition(),
|
||||
trackPoint.position(),
|
||||
null
|
||||
);
|
||||
return new ContentProviderUtils(this).insertMarker(marker);
|
||||
|
||||
+4
-4
@@ -109,17 +109,17 @@ class TrackRecordingServiceNotificationManager implements SharedPreferences.OnSh
|
||||
.build(context);
|
||||
|
||||
//TODO The decision if a trackpoint is not accurate enough should happen in the TrackRecordingManager (via RecordingData).
|
||||
if (recordingData.latestTrackPoint().hasHorizontalAccuracy()) {
|
||||
formattedAccuracy = formatter.formatDistance(recordingData.latestTrackPoint().getHorizontalAccuracy());
|
||||
if (recordingData.latestTrackPoint().position().hasHorizontalAccuracy()) {
|
||||
formattedAccuracy = formatter.formatDistance(recordingData.latestTrackPoint().position().horizontalAccuracy());
|
||||
|
||||
boolean currentLocationWasAccurate = recordingData.latestTrackPoint().getHorizontalAccuracy().lessThan(thresholdHorizontalAccuracy);
|
||||
boolean currentLocationWasAccurate = recordingData.latestTrackPoint().position().horizontalAccuracy().lessThan(thresholdHorizontalAccuracy);
|
||||
boolean shouldAlert = !currentLocationWasAccurate && previousLocationWasAccurate;
|
||||
notificationBuilder.setOnlyAlertOnce(!shouldAlert);
|
||||
previousLocationWasAccurate = currentLocationWasAccurate;
|
||||
}
|
||||
|
||||
notificationBuilder.setContentTitle(context.getString(R.string.track_distance_notification, formatter.formatDistance(recordingData.trackStatistics().totalDistance())));
|
||||
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(recordingData.latestTrackPoint().getSpeed());
|
||||
String formattedSpeed = SpeedFormatter.Builder().setUnit(unitSystem).setReportSpeedOrPace(true).build(context).formatSpeed(recordingData.latestTrackPoint().position().speed());
|
||||
notificationBuilder.setContentText(context.getString(R.string.track_speed_notification, formattedSpeed));
|
||||
notificationBuilder.setSubText(context.getString(R.string.track_recording_notification_accuracy, formattedAccuracy));
|
||||
updateNotification();
|
||||
|
||||
+5
-5
@@ -18,7 +18,6 @@ import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.TtsSpan;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -35,9 +34,10 @@ import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.Speed;
|
||||
import de.dennisguse.opentracks.data.models.Statistics;
|
||||
import de.dennisguse.opentracks.data.models.Track;
|
||||
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorData;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
import de.dennisguse.opentracks.settings.UnitSystem;
|
||||
import de.dennisguse.opentracks.data.statistics.SensorStatistics;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
class VoiceAnnouncementUtils {
|
||||
@@ -160,9 +160,9 @@ class VoiceAnnouncementUtils {
|
||||
}
|
||||
}
|
||||
|
||||
Pair<HeartRate, String> heartrate = sensorDataSet.getHeartRate();
|
||||
if (shouldVoiceAnnounceHeartRateCurrent() && heartrate != null && heartrate.first != null && heartrate.first.isValid()) { //TODO Check has an announcable value?
|
||||
int averageHeartRate = Math.round(heartrate.first.getBPM());
|
||||
SensorData<HeartRate> heartrate = sensorDataSet.heartRate();
|
||||
if (shouldVoiceAnnounceHeartRateCurrent() && heartrate != null && heartrate.data().isValid()) { //TODO Check has an announceable value?
|
||||
int averageHeartRate = Math.round(heartrate.data().getBPM());
|
||||
|
||||
builder.append(" ")
|
||||
.append(context.getString(R.string.current_heart_rate));
|
||||
|
||||
+14
-12
@@ -19,22 +19,22 @@ public class AltitudeCorrectionManager {
|
||||
|
||||
private static final String TAG = AltitudeCorrectionManager.class.getSimpleName();
|
||||
|
||||
public void correctAltitude(Context context, TrackPoint trackPoint) {
|
||||
if (!trackPoint.hasLocation() || !trackPoint.hasAltitude()) {
|
||||
return;
|
||||
public TrackPoint correctAltitude(Context context, TrackPoint trackPoint) {
|
||||
if (!trackPoint.position().hasLocation() || !trackPoint.position().hasAltitude()) {
|
||||
return trackPoint;
|
||||
}
|
||||
|
||||
// TODO The following is doing IO and should not be done in main thread.
|
||||
final TrackPoint[] newTrackPoint = {null}; //TODO this is kind of ugly
|
||||
// AltitudeConverterCompat uses internally a RoomDatabase that cannot be access from main thread and thus fails on version <= 34.
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
Location loc = trackPoint.getPosition().toLocation();
|
||||
AltitudeConverterCompat.addMslAltitudeToLocation(context, loc);
|
||||
trackPoint.setAltitude(Altitude.EGM2008.of(LocationCompat.getMslAltitudeMeters(loc)));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Android's AltitudeConverterCompat failed with " + e.getMessage());
|
||||
}
|
||||
});
|
||||
try {
|
||||
Location loc = trackPoint.position().toLocation();
|
||||
AltitudeConverterCompat.addMslAltitudeToLocation(context, loc);
|
||||
newTrackPoint[0] = TrackPoint.with(trackPoint, Altitude.EGM2008.of(LocationCompat.getMslAltitudeMeters(loc)));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Android's AltitudeConverterCompat failed with " + e.getMessage());
|
||||
}
|
||||
});
|
||||
t.start();
|
||||
|
||||
try {
|
||||
@@ -42,5 +42,7 @@ public class AltitudeCorrectionManager {
|
||||
} catch (InterruptedException e) {
|
||||
Log.w(TAG, "Android's AltitudeConverterCompat failed with " + e.getMessage());
|
||||
}
|
||||
|
||||
return newTrackPoint[0];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package de.dennisguse.opentracks.services.handlers;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -15,6 +14,7 @@ import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import de.dennisguse.opentracks.data.models.Distance;
|
||||
import de.dennisguse.opentracks.data.models.Position;
|
||||
import de.dennisguse.opentracks.data.models.TrackPoint;
|
||||
import de.dennisguse.opentracks.sensors.GpsStatusValue;
|
||||
import de.dennisguse.opentracks.sensors.SensorManager;
|
||||
@@ -55,15 +55,6 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
sensorManager.reset();
|
||||
}
|
||||
|
||||
private SensorDataSet addSensorData(TrackPoint trackPoint) {
|
||||
if (!isStarted()) {
|
||||
Log.w(TAG, "Not started, should not be called.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return sensorManager.fill(trackPoint);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
sensorManager.stop(context);
|
||||
this.context = null;
|
||||
@@ -72,13 +63,8 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
/**
|
||||
* Got a new TrackPoint from Bluetooth only; contains no GPS location.
|
||||
*/
|
||||
public synchronized void onChange(@NonNull SensorDataSet unused) {
|
||||
onNewTrackPoint(new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void onNewTrackPoint(@NonNull TrackPoint trackPoint) {
|
||||
addSensorData(trackPoint);
|
||||
public synchronized void onChange() {
|
||||
TrackPoint trackPoint = createTrackPoint(TrackPoint.Type.TRACKPOINT);
|
||||
|
||||
boolean stored = service.newTrackPoint(trackPoint, PreferencesUtils.getThresholdHorizontalAccuracy()); //TODO Cache preference for performance
|
||||
if (stored) {
|
||||
@@ -91,40 +77,73 @@ public class TrackPointCreator implements SharedPreferences.OnSharedPreferenceCh
|
||||
}
|
||||
|
||||
public synchronized TrackPoint createSegmentEnd() {
|
||||
TrackPoint segmentEnd = TrackPoint.createSegmentEndWithTime(createNow());
|
||||
addSensorData(segmentEnd);
|
||||
TrackPoint segmentEnd = createTrackPoint(TrackPoint.Type.SEGMENT_END_MANUAL);
|
||||
reset();
|
||||
return segmentEnd;
|
||||
}
|
||||
|
||||
public synchronized TrackPoint createIdle() {
|
||||
TrackPoint idle = new TrackPoint(TrackPoint.Type.IDLE, createNow());
|
||||
addSensorData(idle);
|
||||
TrackPoint idle = createTrackPoint(TrackPoint.Type.IDLE);
|
||||
reset();
|
||||
return idle;
|
||||
}
|
||||
|
||||
public Pair<TrackPoint, SensorDataSet> createCurrentTrackPoint(@Nullable TrackPoint lastTrackPointUISpeed, @Nullable TrackPoint lastTrackPointUIAltitude, @Nullable TrackPoint lastStoredTrackPointWithLocation) {
|
||||
TrackPoint currentTrackPoint = new TrackPoint(TrackPoint.Type.TRACKPOINT, createNow());
|
||||
Instant now = createNow();
|
||||
SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(now);
|
||||
|
||||
SensorDataSet sensorDataSet = addSensorData(currentTrackPoint);
|
||||
Position position = sensorDataSet.position().data();
|
||||
|
||||
if (!currentTrackPoint.hasLocation() && lastStoredTrackPointWithLocation != null && lastStoredTrackPointWithLocation.hasLocation()) {
|
||||
if (!position.hasLocation()
|
||||
&& lastStoredTrackPointWithLocation != null
|
||||
&& lastStoredTrackPointWithLocation.position().hasLocation()) {
|
||||
//We are taking the coordinates from the last stored TrackPoint, so the distance is monotonously increasing.
|
||||
currentTrackPoint.setPosition(lastStoredTrackPointWithLocation.getPosition());
|
||||
position = position.withCoordinates(lastStoredTrackPointWithLocation.position());
|
||||
}
|
||||
|
||||
if (lastTrackPointUISpeed != null) {
|
||||
currentTrackPoint.setSpeed(lastTrackPointUISpeed.getSpeed());
|
||||
}
|
||||
if (lastTrackPointUISpeed != null)
|
||||
position.with(lastTrackPointUISpeed.position().speed());
|
||||
|
||||
if (lastTrackPointUIAltitude != null) {
|
||||
currentTrackPoint.setAltitude(lastTrackPointUIAltitude.getAltitude());
|
||||
}
|
||||
if (lastTrackPointUIAltitude != null)
|
||||
position.with(lastTrackPointUIAltitude.position().altitude());
|
||||
|
||||
TrackPoint currentTrackPoint = new TrackPoint(
|
||||
null,
|
||||
TrackPoint.Type.TRACKPOINT,
|
||||
position,
|
||||
sensorDataSet.distance() != null ? sensorDataSet.distance().data() : null,
|
||||
sensorDataSet.heartRate() != null ? sensorDataSet.heartRate().data() : null,
|
||||
sensorDataSet.cadence() != null ? sensorDataSet.cadence().data() : null,
|
||||
sensorDataSet.power() != null ? sensorDataSet.power().data() : null,
|
||||
sensorDataSet.altitudeGainLoss() != null ? sensorDataSet.altitudeGainLoss().data() : null
|
||||
);
|
||||
|
||||
return new Pair<>(currentTrackPoint, sensorDataSet);
|
||||
}
|
||||
|
||||
private TrackPoint createTrackPoint(TrackPoint.Type type) {
|
||||
Instant now = createNow();
|
||||
|
||||
SensorDataSet sensorDataSet = sensorManager.getSensorDataSet(now);
|
||||
|
||||
Position position = sensorDataSet.position().data();
|
||||
if (sensorDataSet.speed() != null) {
|
||||
position = position.with(sensorDataSet.speed().data());
|
||||
}
|
||||
|
||||
return
|
||||
new TrackPoint(
|
||||
null,
|
||||
type,
|
||||
position,
|
||||
sensorDataSet.distance() != null ? sensorDataSet.distance().data() : null,
|
||||
sensorDataSet.heartRate() != null ? sensorDataSet.heartRate().data() : null,
|
||||
sensorDataSet.cadence() != null ? sensorDataSet.cadence().data() : null,
|
||||
sensorDataSet.power() != null ? sensorDataSet.power().data() : null,
|
||||
sensorDataSet.altitudeGainLoss() != null ? sensorDataSet.altitudeGainLoss().data() : null
|
||||
);
|
||||
}
|
||||
|
||||
public Instant createNow() {
|
||||
return Instant.now(clock);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public class IntervalStatisticsUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
return trackPoint != null ? trackPoint.getId() : null;
|
||||
return trackPoint != null ? trackPoint.id() : null;
|
||||
}
|
||||
|
||||
public List<Statistics> getIntervalList() {
|
||||
@@ -152,9 +152,9 @@ public class IntervalStatisticsUpdater {
|
||||
if (lastTrackPoint == null) {
|
||||
return;
|
||||
}
|
||||
if (gain_m != null && lastTrackPoint.hasAltitudeGainLoss()) {
|
||||
gain_m = gain_m - lastTrackPoint.getAltitudeGainLoss().gain_m();
|
||||
loss_m = loss_m - lastTrackPoint.getAltitudeGainLoss().loss_m();
|
||||
if (gain_m != null && lastTrackPoint.altitudeGainLoss() != null) {
|
||||
gain_m = gain_m - lastTrackPoint.altitudeGainLoss().gain_m();
|
||||
loss_m = loss_m - lastTrackPoint.altitudeGainLoss().loss_m();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ public class MarkerEditViewModel extends AndroidViewModel {
|
||||
name,
|
||||
"",
|
||||
"",
|
||||
trackPoint.getPosition(),
|
||||
trackPoint.position(),
|
||||
null);
|
||||
|
||||
if (markerData == null) {
|
||||
|
||||
@@ -84,11 +84,11 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
|
||||
SensorDataSet sensorDataSet = data.sensorDataSet();
|
||||
final TrackPoint latestTrackPoint = data.latestTrackPoint();
|
||||
if (sensorDataSet != null && sensorDataSet.getSpeed() != null) {
|
||||
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.getSpeed().first);
|
||||
getBinding().statsDescriptionMain.setText(sensorDataSet.getSpeed().second);
|
||||
if (sensorDataSet != null && sensorDataSet.speed() != null) {
|
||||
valueAndUnit = localSpeedFormatter.getSpeedParts(sensorDataSet.speed().data());
|
||||
getBinding().statsDescriptionMain.setText(sensorDataSet.speed().sensorNameOrAddress());
|
||||
} else {
|
||||
Speed speed = latestTrackPoint != null && latestTrackPoint.hasSpeed() ? latestTrackPoint.getSpeed() : null;
|
||||
Speed speed = latestTrackPoint != null && latestTrackPoint.position().hasSpeed() ? latestTrackPoint.position().speed() : null;
|
||||
valueAndUnit = localSpeedFormatter.getSpeedParts(speed);
|
||||
|
||||
String title = reportSpeed ? getContext().getString(R.string.stats_speed) : getContext().getString(R.string.stats_pace);
|
||||
@@ -249,8 +249,8 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
@Override
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
TrackPoint latestTrackPoint = data.latestTrackPoint();
|
||||
Float altitude = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? (float) latestTrackPoint.getAltitude().toM() : null;
|
||||
String altitudeReference = latestTrackPoint != null && latestTrackPoint.hasAltitude() ? getContext().getString(latestTrackPoint.getAltitude().getLabelId()) : null;
|
||||
Float altitude = latestTrackPoint != null && latestTrackPoint.position().hasAltitude() ? (float) latestTrackPoint.position().altitude().toM() : null;
|
||||
String altitudeReference = latestTrackPoint != null && latestTrackPoint.position().hasAltitude() ? getContext().getString(latestTrackPoint.position().altitude().getLabelId()) : null;
|
||||
Pair<String, String> valueAndUnit = StringUtils.getAltitudeParts(getContext(), altitude, unitSystem);
|
||||
|
||||
getBinding().statsValue.setText(valueAndUnit.first);
|
||||
@@ -294,8 +294,8 @@ public abstract class GenericStatisticsViewHolder extends StatisticViewHolder<St
|
||||
public void onChanged(UnitSystem unitSystem, RecordingData data) {
|
||||
TrackPoint latestTrackPoint = data.latestTrackPoint();
|
||||
String value;
|
||||
if (latestTrackPoint != null && latestTrackPoint.hasLocation()) {
|
||||
value = StringUtils.formatCoordinate(getContext(), latestTrackPoint.getPosition());
|
||||
if (latestTrackPoint != null && latestTrackPoint.position().hasLocation()) {
|
||||
value = StringUtils.formatCoordinate(getContext(), latestTrackPoint.position());
|
||||
} else {
|
||||
value = getContext().getString(R.string.value_unknown);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ package de.dennisguse.opentracks.viewmodels;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.data.models.HeartRate;
|
||||
import de.dennisguse.opentracks.data.models.HeartRateZones;
|
||||
import de.dennisguse.opentracks.databinding.StatsSensorItemBinding;
|
||||
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
|
||||
@@ -37,10 +34,9 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
|
||||
String sensorName = getContext().getString(R.string.value_unknown);
|
||||
|
||||
Pair<String, String> valueAndUnit;
|
||||
Pair<HeartRate, String> heartRate;
|
||||
if (sensorDataSet != null && (heartRate = sensorDataSet.getHeartRate()) != null) {
|
||||
valueAndUnit = StringUtils.getHeartRateParts(getContext(), heartRate.first);
|
||||
sensorName = heartRate.second;
|
||||
if (sensorDataSet != null && sensorDataSet.heartRate() != null) {
|
||||
valueAndUnit = StringUtils.getHeartRateParts(getContext(), sensorDataSet.heartRate().data());
|
||||
sensorName = sensorDataSet.heartRate().sensorNameOrAddress();
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getHeartRateParts(getContext(), null);
|
||||
}
|
||||
@@ -48,8 +44,8 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
|
||||
//TODO Loads preference every time
|
||||
HeartRateZones zones = PreferencesUtils.getHeartRateZones();
|
||||
int textColor;
|
||||
if (sensorDataSet != null && (heartRate = sensorDataSet.getHeartRate()) != null) {
|
||||
textColor = zones.getTextColorForZone(getContext(), heartRate.first);
|
||||
if (sensorDataSet != null && sensorDataSet.heartRate() != null) {
|
||||
textColor = zones.getTextColorForZone(getContext(), sensorDataSet.heartRate().data());
|
||||
} else {
|
||||
textColor = zones.getTextColorForZone(getContext(), null);
|
||||
}
|
||||
@@ -72,9 +68,9 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
|
||||
String sensorName = getContext().getString(R.string.value_unknown);
|
||||
|
||||
Pair<String, String> valueAndUnit;
|
||||
if (sensorDataSet != null && sensorDataSet.getCadence() != null) {
|
||||
valueAndUnit = StringUtils.getCadenceParts(getContext(), sensorDataSet.getCadence().first);
|
||||
sensorName = sensorDataSet.getCadence().second;
|
||||
if (sensorDataSet != null && sensorDataSet.cadence() != null) {
|
||||
valueAndUnit = StringUtils.getCadenceParts(getContext(), sensorDataSet.cadence().data());
|
||||
sensorName = sensorDataSet.cadence().sensorNameOrAddress();
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getCadenceParts(getContext(), null);
|
||||
}
|
||||
@@ -95,9 +91,9 @@ public abstract class SensorStatisticsViewHolder extends StatisticViewHolder<Sta
|
||||
String sensorName = getContext().getString(R.string.value_unknown);
|
||||
|
||||
Pair<String, String> valueAndUnit;
|
||||
if (sensorDataSet != null && sensorDataSet.getCyclingPower() != null) {
|
||||
valueAndUnit = StringUtils.getPowerParts(getContext(), sensorDataSet.getCyclingPower().getAggregatedValue(Instant.now())); //TODO Use MonotonicClock
|
||||
sensorName = sensorDataSet.getCyclingPower().getSensorNameOrAddress();
|
||||
if (sensorDataSet != null && sensorDataSet.power() != null) {
|
||||
valueAndUnit = StringUtils.getPowerParts(getContext(), sensorDataSet.power().data());
|
||||
sensorName = sensorDataSet.power().sensorNameOrAddress();
|
||||
} else {
|
||||
valueAndUnit = StringUtils.getCadenceParts(getContext(), null);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user