Files
OpenTracks/src/main/java/de/dennisguse/opentracks/data/models/TrackPoint.java
T
2025-06-27 15:51:51 +02:00

428 lines
11 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.Parcel;
import android.os.Parcelable;
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.
*/
//TODO Should be a record (with final properties)
public class TrackPoint {
@Nullable
private TrackPoint.Id id;
//Requires: position.time must be non-null
@NonNull
private Position position;
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;
}
@NonNull
@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 Distance sensorDistance;
private HeartRate heartRate = null;
private Cadence cadence = null;
private Power power = null;
private Float altitudeGain_m = null;
private Float altitudeLoss_m = 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;
}
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;
}
@Deprecated
@NonNull
public Location getLocation() {
return getPosition().toLocation();
}
public boolean hasAltitudeGain() {
return altitudeGain_m != null;
}
public float getAltitudeGain() {
return altitudeGain_m;
}
@Deprecated
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;
}
@Deprecated
public TrackPoint setAltitudeLoss(Float altitudeLoss_m) {
this.altitudeLoss_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(getLocation().distanceTo(previous.getLocation()));
}
public boolean fulfillsAccuracy(Distance thresholdHorizontalAccuracy) {
return position.fulfillsAccuracy(thresholdHorizontalAccuracy);
}
public Optional<Float> bearingTo(@NonNull Position dest) {
if (!dest.hasLocation() || !hasLocation()) {
return Optional.empty();
}
return bearingTo(dest.toLocation());
}
public Optional<Float> bearingTo(@NonNull TrackPoint dest) {
if (!dest.hasLocation() || !hasLocation()) {
return Optional.empty();
}
return bearingTo(dest.getLocation());
}
//TODO Bearing requires a location; what do we do if we don't have any?
@Deprecated
public Optional<Float> bearingTo(@NonNull Location dest) {
if (!hasLocation()) {
return Optional.empty();
}
return Optional.of(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 +
", position=" + position +
", sensorDistance=" + sensorDistance +
", type=" + type +
", heartRate=" + heartRate +
", cadence=" + cadence +
", power=" + power +
", altitudeGain_m=" + altitudeGain_m +
", altitudeLoss_m=" + altitudeLoss_m +
'}';
}
public record Id(long id) implements Parcelable {
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(id);
}
public static final Creator<Id> CREATOR = new Creator<>() {
public Id createFromParcel(Parcel in) {
return new Id(in.readLong());
}
public Id[] newArray(int size) {
return new Id[size];
}
};
}
}