forked from upstream-mirrors/OpenTracks
1aeb9ca332
Part of #1118.
498 lines
13 KiB
Java
498 lines
13 KiB
Java
/*
|
|
* 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.Build;
|
|
import android.os.Parcel;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import java.time.Duration;
|
|
import java.time.Instant;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* 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 timestamps.
|
|
*/
|
|
public class TrackPoint {
|
|
|
|
private static final Duration MAX_LOCATION_AGE = Duration.ofMinutes(1);
|
|
|
|
private TrackPoint.Id id;
|
|
|
|
@NonNull
|
|
private final Instant time;
|
|
|
|
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), //Just GPS data and may contain BLE sensor data
|
|
SENSORPOINT(2), //Just BLE sensor data; may have speed and sensorDistance
|
|
|
|
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 isSegmentStart() {
|
|
return type == Type.SEGMENT_START_AUTOMATIC || type == Type.SEGMENT_START_MANUAL;
|
|
}
|
|
|
|
public boolean isSegmentEnd() {
|
|
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;
|
|
}
|
|
|
|
//TODO Better return null, if no location is present aka latitude == null etc.
|
|
@NonNull
|
|
public Location getLocation() {
|
|
Location location = new Location("");
|
|
location.setTime(time.toEpochMilli());
|
|
if (hasLocation()) {
|
|
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;
|
|
}
|
|
|
|
public TrackPoint setLocation(@NonNull Location location) {
|
|
this.latitude = location.getLatitude();
|
|
this.longitude = location.getLongitude();
|
|
this.altitude = location.hasAltitude() ? Altitude.WGS84.of(location.getAltitude()) : null;
|
|
this.speed = location.hasSpeed() ? Speed.of(location.getSpeed()) : null;
|
|
this.horizontalAccuracy = location.hasAccuracy() ? Distance.of(location.getAccuracy()) : null;
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
this.verticalAccuracy = location.hasVerticalAccuracy() ? Distance.of(location.getVerticalAccuracyMeters()) : null;
|
|
}
|
|
|
|
//TODO Should we copy the bearing?
|
|
return this;
|
|
}
|
|
|
|
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 isMoving() {
|
|
return hasSpeed() && getSpeed().isMoving();
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
//TODO Bearing requires a location; what do we do if we don't have any?
|
|
public float bearingTo(@NonNull TrackPoint dest) {
|
|
return getLocation().bearingTo(dest.getLocation());
|
|
}
|
|
|
|
//TODO Bearing requires a location; what do we do if we don't have any?
|
|
public float bearingTo(@NonNull Location dest) {
|
|
return getLocation().bearingTo(dest);
|
|
}
|
|
|
|
// 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 (hasAltitudeGain() && lastTrackPoint.hasAltitudeGain()) {
|
|
altitudeGain_m -= lastTrackPoint.altitudeGain_m;
|
|
}
|
|
if (hasAltitudeLoss() && lastTrackPoint.hasAltitudeLoss()) {
|
|
altitudeLoss_m -= lastTrackPoint.altitudeLoss_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 +
|
|
", time=" + time +
|
|
", latitude=" + latitude +
|
|
", longitude=" + longitude +
|
|
", horizontalAccuracy=" + horizontalAccuracy +
|
|
", altitude=" + altitude +
|
|
", speed=" + speed +
|
|
", bearing=" + bearing +
|
|
", sensorDistance=" + sensorDistance +
|
|
", type=" + type +
|
|
", heartRate_bpm=" + heartRate +
|
|
", cadence_rpm=" + cadence +
|
|
", power=" + power +
|
|
", altitudeGain_m=" + altitudeGain_m +
|
|
", altitudeLoss_m=" + altitudeLoss_m +
|
|
'}';
|
|
}
|
|
|
|
public static class Id {
|
|
|
|
private final long id;
|
|
|
|
public Id(long id) {
|
|
this.id = id;
|
|
}
|
|
|
|
protected Id(Parcel in) {
|
|
id = in.readLong();
|
|
}
|
|
|
|
//TOOD Limit visibility to TrackRecordingService / ContentProvider
|
|
public long getId() {
|
|
return id;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
TrackPoint.Id id1 = (TrackPoint.Id) o;
|
|
return id == id1.id;
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(id);
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public String toString() {
|
|
return String.valueOf(id);
|
|
}
|
|
}
|
|
}
|