/* * Copyright 2010 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package de.dennisguse.opentracks.data.models; import android.location.Location; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import java.time.Duration; import java.time.Instant; import java.util.Optional; /** * Sensor and/or location information for a specific point in time. *
* 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 timestamps.
*/
public class TrackPoint {
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
private TrackPoint.Id id;
@NonNull
private final Instant time;
//TODO We may use Position for these items
private Double latitude;
private Double longitude;
private Distance horizontalAccuracy;
private Distance verticalAccuracy;
private Altitude altitude; //TODO use Altitude.WGS84
private Speed speed;
private Float bearing;
private Distance sensorDistance;
public enum Type {
SEGMENT_START_MANUAL(-2), //Start of a segment due to user interaction (start, resume)
SEGMENT_START_AUTOMATIC(-1), //Start of a segment due to too much distance from previous TrackPoint
TRACKPOINT(0), //Was created due to sensor data (may contain GPS or other BLE data)
// Was used to distinguish the source (i.e., GPS vs BLE sensor), but this was too complicated. Everything is now a TRACKPOINT again.
@Deprecated
SENSORPOINT(2),
IDLE(3), //Device became idle
SEGMENT_END_MANUAL(1); //End of a segment
public final int type_db;
Type(int type_db) {
this.type_db = type_db;
}
@Override
public String toString() {
return name() + "(" + type_db + ")";
}
public static Type getById(int id) {
for (Type e : values()) {
if (e.type_db == id) return e;
}
throw new RuntimeException("unknown id: " + id);
}
}
@NonNull
private Type type;
private HeartRate heartRate = null;
private Cadence cadence = null;
private Power power = null;
private Float altitudeGain_m = null;
private Float altitudeLoss_m = null;
public TrackPoint(@NonNull Type type, @NonNull Instant time) {
this.type = type;
this.time = time;
}
public TrackPoint(@NonNull Location location, @NonNull Instant time) {
this(Type.TRACKPOINT, location, time);
}
public TrackPoint(@NonNull Type type, @NonNull Location location, @NonNull Instant time) {
this(type, time);
setLocation(location);
}
@VisibleForTesting
public TrackPoint(double latitude, double longitude, Altitude altitude, Instant time) {
this(Type.TRACKPOINT, time);
this.latitude = latitude;
this.longitude = longitude;
this.altitude = altitude;
}
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;
}
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;
}
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 void setId(TrackPoint.Id id) {
this.id = id;
}
public boolean hasLocation() {
return latitude != null && longitude != null;
}
public double getLatitude() {
return latitude;
}
public TrackPoint setLatitude(double latitude) {
this.latitude = latitude;
return this;
}
public double getLongitude() {
return longitude;
}
public TrackPoint setLongitude(double longitude) {
this.longitude = longitude;
return this;
}
@NonNull
public Position getPosition() {
return new Position(
time,
latitude,
longitude,
horizontalAccuracy,
altitude,
verticalAccuracy,
bearing,
speed
);
}
public TrackPoint setPosition(Position location) {
this.latitude = location.latitude();
this.longitude = location.longitude();
this.altitude = location.altitude();
this.speed = location.speed();
this.horizontalAccuracy = location.horizontalAccuracy();
this.verticalAccuracy = location.verticalAccuracy();
return this;
}
@Deprecated
@NonNull
public Location getLocation() {
return getPosition().toLocation();
}
@Deprecated
public TrackPoint setLocation(@NonNull Location location) {
setPosition(Position.of(location));
return this;
}
public boolean hasAltitudeGain() {
return altitudeGain_m != null;
}
public float getAltitudeGain() {
return altitudeGain_m;
}
public TrackPoint setAltitudeGain(Float altitudeGain_m) {
this.altitudeGain_m = altitudeGain_m;
return this;
}
public boolean hasAltitudeLoss() {
return altitudeLoss_m != null;
}
public float getAltitudeLoss() {
return altitudeLoss_m;
}
public TrackPoint setAltitudeLoss(Float altitudeLoss_m) {
this.altitudeLoss_m = altitudeLoss_m;
return this;
}
@NonNull
public Instant getTime() {
return time;
}
public boolean isRecent() {
return Instant.now()
.isBefore(time.plus(MAX_LOCATION_AGE));
}
public boolean hasAltitude() {
return altitude != null;
}
public Altitude getAltitude() {
return altitude;
}
@VisibleForTesting
public TrackPoint setAltitude(double altitude_m) {
this.altitude = Altitude.WGS84.of(altitude_m);
return this;
}
public TrackPoint setAltitude(Altitude altitude) {
this.altitude = altitude;
return this;
}
public boolean hasSpeed() {
return speed != null;
}
public Speed getSpeed() {
return speed;
}
public TrackPoint setSpeed(Speed speed) {
this.speed = speed;
return this;
}
public boolean hasBearing() {
return bearing != null;
}
public float getBearing() {
return bearing;
}
public TrackPoint setBearing(Float bearing) {
this.bearing = bearing;
return this;
}
public boolean hasHorizontalAccuracy() {
return horizontalAccuracy != null;
}
public Distance getHorizontalAccuracy() {
return horizontalAccuracy;
}
public TrackPoint setHorizontalAccuracy(Distance horizontalAccuracy) {
this.horizontalAccuracy = horizontalAccuracy;
return this;
}
public boolean hasVerticalAccuracy() {
return verticalAccuracy != null;
}
public Distance getVerticalAccuracy() {
return verticalAccuracy;
}
public TrackPoint setVerticalAccuracy(Distance horizontalAccuracy) {
this.verticalAccuracy = horizontalAccuracy;
return this;
}
@NonNull
public Distance distanceToPrevious(@NonNull TrackPoint previous) {
if (hasSensorDistance()) {
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(getLocation().distanceTo(previous.getLocation()));
}
public boolean fulfillsAccuracy(Distance thresholdHorizontalAccuracy) {
return hasHorizontalAccuracy() && horizontalAccuracy.lessThan(thresholdHorizontalAccuracy);
}
public Optional