Introduced Position.

Internal value object replacing android.location.Location.

Follow-up to #1898.
This commit is contained in:
Dennis Guse
2024-06-22 00:18:20 +02:00
parent 382acadc6a
commit e442558eed
8 changed files with 160 additions and 67 deletions
@@ -9,7 +9,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Looper; import android.os.Looper;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@@ -65,7 +64,6 @@ import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingCadence;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed; import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingDistanceSpeed;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower; import de.dennisguse.opentracks.sensors.sensorData.AggregatorCyclingPower;
import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate; import de.dennisguse.opentracks.sensors.sensorData.AggregatorHeartRate;
import de.dennisguse.opentracks.sensors.sensorData.Raw;
import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet; import de.dennisguse.opentracks.sensors.sensorData.SensorDataSet;
import de.dennisguse.opentracks.services.TrackRecordingService; import de.dennisguse.opentracks.services.TrackRecordingService;
import de.dennisguse.opentracks.services.handlers.TrackPointCreator; import de.dennisguse.opentracks.services.handlers.TrackPointCreator;
@@ -596,15 +594,13 @@ public class ExportImportTest {
location.setLatitude(latitude); location.setLatitude(latitude);
location.setLongitude(longitude); location.setLongitude(longitude);
location.setAccuracy(accuracy); location.setAccuracy(accuracy);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { location.setVerticalAccuracyMeters(verticalAccuracy);
location.setVerticalAccuracyMeters(verticalAccuracy);
}
location.setSpeed(speed); location.setSpeed(speed);
location.setAltitude(altitude); location.setAltitude(altitude);
mockAltitudeChange(trackPointCreator, altitudeGain); mockAltitudeChange(trackPointCreator, altitudeGain);
trackPointCreator.setClock(time); trackPointCreator.setClock(time);
trackPointCreator.getSensorManager().onChanged(new Raw<>(location)); trackPointCreator.getSensorManager().getGpsManager().onLocationChanged(location);
} }
} }
@@ -0,0 +1,88 @@
package de.dennisguse.opentracks.data.models;
import android.location.Location;
import java.time.Instant;
//TODO Use everywhere instead of android.location.Location.
public record Position(
Instant time,
Double latitude,
Double longitude,
Distance horizontalAccuracy,
Altitude altitude,
Distance verticalAccuracy,
Float bearing,
Speed speed
) {
public static Position empty() {
return new Position(
null,
null,
null,
null,
null,
null,
null,
null
);
}
public static Position of(Location location) {
return new Position(
Instant.ofEpochMilli(location.getTime()),
location.getLatitude(),
location.getLongitude(),
location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null,
location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null,
location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null,
location.hasBearing() ? location.getBearing() : null,
location.hasSpeed() ? Speed.of(location.getSpeed()) : null
);
}
public boolean hasLocation() {
return latitude != null && longitude != null;
}
public boolean hasHorizontalAccuracy() {
return horizontalAccuracy != null;
}
public boolean hasAltitude() {
return altitude != null;
}
public boolean hasBearing() {
return bearing != null;
}
public boolean hasSpeed() {
return speed != null;
}
public Location toLocation() {
if (!hasLocation()) {
throw new RuntimeException("Cannot convert to Location.");
}
Location location = new Location("");
location.setTime(time.toEpochMilli());
location.setLatitude(latitude);
location.setLongitude(longitude);
if (hasBearing()) {
location.setBearing(bearing);
}
if (hasHorizontalAccuracy()) {
location.setAccuracy((float) horizontalAccuracy.toM());
}
if (hasAltitude()) {
location.setAltitude(altitude.toM());
}
if (hasSpeed()) {
location.setSpeed((float) speed.toMPS());
}
return location;
}
}
@@ -16,7 +16,6 @@
package de.dennisguse.opentracks.data.models; package de.dennisguse.opentracks.data.models;
import android.location.Location; import android.location.Location;
import android.os.Build;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
@@ -26,6 +25,7 @@ import androidx.annotation.VisibleForTesting;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Optional;
/** /**
* Sensor and/or location information for a specific point in time. * Sensor and/or location information for a specific point in time.
@@ -42,6 +42,7 @@ public class TrackPoint {
@NonNull @NonNull
private final Instant time; private final Instant time;
//TODO We may use Position for these items
private Double latitude; private Double latitude;
private Double longitude; private Double longitude;
private Distance horizontalAccuracy; private Distance horizontalAccuracy;
@@ -185,42 +186,40 @@ public class TrackPoint {
return this; return this;
} }
//TODO Better return null, if no location is present aka latitude == null etc.
@NonNull @NonNull
public Location getLocation() { public Position getPosition() {
Location location = new Location(""); return new Position(
location.setTime(time.toEpochMilli()); time,
if (hasLocation()) { latitude,
location.setLatitude(latitude); longitude,
location.setLongitude(longitude); horizontalAccuracy,
} altitude,
if (hasBearing()) { verticalAccuracy,
location.setBearing(bearing); bearing,
} speed
if (hasHorizontalAccuracy()) { );
location.setAccuracy((float) horizontalAccuracy.toM());
}
if (hasAltitude()) {
location.setAltitude(altitude.toM());
}
if (hasSpeed()) {
location.setSpeed((float) speed.toMPS());
}
return location;
} }
public TrackPoint setLocation(@NonNull Location location) { public TrackPoint setPosition(Position location) {
this.latitude = location.getLatitude(); this.latitude = location.latitude();
this.longitude = location.getLongitude(); this.longitude = location.longitude();
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null; this.altitude = location.altitude();
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null; this.speed = location.speed();
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null; this.horizontalAccuracy = location.horizontalAccuracy();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { this.verticalAccuracy = location.verticalAccuracy();
this.verticalAccuracy = location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null;
}
//TODO Should we copy the bearing? return this;
}
@Deprecated
@NonNull
public Location getLocation() {
return getPosition().toLocation();
}
@Deprecated
public TrackPoint setLocation(@NonNull Location location) {
setPosition(Position.of(location));
return this; return this;
} }
@@ -292,6 +291,7 @@ public class TrackPoint {
this.speed = speed; this.speed = speed;
return this; return this;
} }
public boolean hasBearing() { public boolean hasBearing() {
return bearing != null; return bearing != null;
} }
@@ -352,13 +352,20 @@ public class TrackPoint {
return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy); return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy);
} }
public float bearingTo(@NonNull TrackPoint dest) { public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
if (!dest.hasLocation() || !hasLocation()) {
return Optional.empty();
}
return bearingTo(dest.getLocation()); return bearingTo(dest.getLocation());
} }
//TODO Bearing requires a location; what do we do if we don't have any? //TODO Bearing requires a location; what do we do if we don't have any?
public float bearingTo(@NonNull Location dest) { @Deprecated
return getLocation().bearingTo(dest); public Optional<Float> bearingTo(@NonNull Location dest) {
if (!hasLocation()) {
return Optional.empty();
}
return Optional.of(getLocation().bearingTo(dest));
} }
// Sensor data // Sensor data
@@ -33,6 +33,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.ContentProviderUtils; import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -274,7 +275,7 @@ public class KMLTrackExporter implements TrackExporter {
float heading = getHeading(marker.getTrackId(), marker.getLocation()); float heading = getHeading(marker.getTrackId(), marker.getLocation());
writePhotoOverlay(marker, heading, zoneOffset); writePhotoOverlay(marker, heading, zoneOffset);
} else { } else {
writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), zoneOffset); writePlacemark(marker.getName(), marker.getCategory(), marker.getDescription(), marker.getLocation(), marker.getTime(), zoneOffset);
} }
} }
@@ -368,7 +369,7 @@ public class KMLTrackExporter implements TrackExporter {
@VisibleForTesting @VisibleForTesting
void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) { void writeTrackPoint(ZoneOffset zoneOffset, TrackPoint trackPoint) {
printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getLocation()) + "</when>"); printWriter.println("<when>" + getTime(zoneOffset, trackPoint.getTime()) + "</when>");
trackpointTypeList.add(trackPoint.getType()); trackpointTypeList.add(trackPoint.getType());
@@ -411,12 +412,12 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.println("</SimpleArrayData>"); printWriter.println("</SimpleArrayData>");
} }
private void writePlacemark(String name, String activityType, String description, Location location, ZoneOffset zoneOffset) { private void writePlacemark(String name, String activityType, String description, Location location, Instant time, ZoneOffset zoneOffset) {
if (location != null) { if (location != null) {
printWriter.println("<Placemark>"); printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>"); printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>"); printWriter.println("<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, location) + "</when></TimeStamp>"); printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, time) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>"); printWriter.println("<styleUrl>#" + KMLTrackExporter.MARKER_STYLE + "</styleUrl>");
writeTypeLocalized(activityType); writeTypeLocalized(activityType);
printWriter.println("<Point>"); printWriter.println("<Point>");
@@ -437,7 +438,7 @@ public class KMLTrackExporter implements TrackExporter {
printWriter.print("<heading>" + heading + "</heading>"); printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>"); printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>"); printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getLocation()) + "</when></TimeStamp>"); printWriter.println("<TimeStamp><when>" + getTime(zoneOffset, marker.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>"); printWriter.println("<styleUrl>#" + MARKER_STYLE + "</styleUrl>");
writeTypeLocalized(marker.getCategory()); writeTypeLocalized(marker.getCategory());
@@ -460,14 +461,13 @@ public class KMLTrackExporter implements TrackExporter {
/** /**
* Returns the formatted time of the location; either absolute or relative depending exportTrackDetail. * Returns the formatted time of the location; either absolute or relative depending exportTrackDetail.
*
* @param location the location
*/ */
private String getTime(ZoneOffset zoneOffset, Location location) { private String getTime(ZoneOffset zoneOffset, Instant instant) {
return StringUtils.formatDateTimeIso8601(Instant.ofEpochMilli(location.getTime()), zoneOffset); return StringUtils.formatDateTimeIso8601(instant, zoneOffset);
} }
/** /**
* TODO: check if this is a useful feature (likely not).
* Gets the heading to a location. * Gets the heading to a location.
* *
* @param trackId the track id containing the location * @param trackId the track id containing the location
@@ -480,7 +480,10 @@ public class KMLTrackExporter implements TrackExporter {
} }
TrackPoint viewLocation = contentProviderUtils.getLastValidTrackPoint(trackId); TrackPoint viewLocation = contentProviderUtils.getLastValidTrackPoint(trackId);
if (viewLocation != null) { if (viewLocation != null) {
return viewLocation.bearingTo(location); Optional<Float> bearing = viewLocation.bearingTo(location);
if (bearing.isPresent()) {
return bearing.get();
}
} }
return location.getBearing(); return location.getBearing();
@@ -206,7 +206,8 @@ public class TrackImporter {
} }
if (!current.hasBearing()) { if (!current.hasBearing()) {
current.setBearing(previous.bearingTo(current)); previous.bearingTo(current)
.ifPresent(current::setBearing);
} }
if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) { if (current.getType().equals(TrackPoint.Type.TRACKPOINT) && distanceToPrevious.greaterThan(maxRecordingDistance)) {
@@ -1,12 +1,10 @@
package de.dennisguse.opentracks.sensors.sensorData; package de.dennisguse.opentracks.sensors.sensorData;
import android.location.Location;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.util.Optional; import de.dennisguse.opentracks.data.models.Position;
public class AggregatorGPS extends Aggregator<Location, Optional<Location>> { public class AggregatorGPS extends Aggregator<Position, Position> {
public AggregatorGPS(String sensorAddress) { public AggregatorGPS(String sensorAddress) {
@@ -14,8 +12,8 @@ public class AggregatorGPS extends Aggregator<Location, Optional<Location>> {
} }
@Override @Override
protected void computeValue(Raw<Location> current) { protected void computeValue(Raw<Position> current) {
value = Optional.of(current.value()); value = current.value();
} }
@Override @Override
@@ -25,7 +23,7 @@ public class AggregatorGPS extends Aggregator<Location, Optional<Location>> {
@NonNull @NonNull
@Override @Override
protected Optional<Location> getNoneValue() { protected Position getNoneValue() {
return Optional.empty(); return Position.empty();
} }
} }
@@ -1,6 +1,5 @@
package de.dennisguse.opentracks.sensors.sensorData; package de.dennisguse.opentracks.sensors.sensorData;
import android.location.Location;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
@@ -11,6 +10,7 @@ import androidx.annotation.VisibleForTesting;
import de.dennisguse.opentracks.data.models.AtmosphericPressure; import de.dennisguse.opentracks.data.models.AtmosphericPressure;
import de.dennisguse.opentracks.data.models.Cadence; import de.dennisguse.opentracks.data.models.Cadence;
import de.dennisguse.opentracks.data.models.HeartRate; 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.data.models.Speed;
import de.dennisguse.opentracks.data.models.TrackPoint; import de.dennisguse.opentracks.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence; import de.dennisguse.opentracks.sensors.BluetoothHandlerCyclingCadence;
@@ -127,8 +127,8 @@ public final class SensorDataSet {
this.barometer.add((Raw<AtmosphericPressure>) data); this.barometer.add((Raw<AtmosphericPressure>) data);
return; return;
} }
if (value instanceof Location) { if (value instanceof Position) {
this.gps.add((Raw<Location>) data); this.gps.add((Raw<Position>) data);
return; return;
} }
@@ -152,8 +152,7 @@ public final class SensorDataSet {
public void fillTrackPoint(TrackPoint trackPoint) { public void fillTrackPoint(TrackPoint trackPoint) {
if (gps != null && gps.hasValue()) { if (gps != null && gps.hasValue()) {
gps.getValue() trackPoint.setPosition(gps.getValue());
.ifPresent(trackPoint::setLocation);
} }
if (getHeartRate() != null) { if (getHeartRate() != null) {
@@ -18,6 +18,7 @@ import java.time.Duration;
import de.dennisguse.opentracks.R; import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Distance; 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.data.models.TrackPoint;
import de.dennisguse.opentracks.sensors.SensorConnector; import de.dennisguse.opentracks.sensors.SensorConnector;
import de.dennisguse.opentracks.sensors.SensorManager; import de.dennisguse.opentracks.sensors.SensorManager;
@@ -138,7 +139,7 @@ public class GPSManager implements SensorConnector, LocationListenerCompat, GpsS
return; return;
} }
listener.onChange(new Raw<>(location)); listener.onChange(new Raw<>(Position.of(location)));
} }
@Override @Override