/* * 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. *

* 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 final 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 bearingTo(@NonNull Position dest) { if (!dest.hasLocation() || !hasLocation()) { return Optional.empty(); } return bearingTo(dest.toLocation()); } public Optional 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 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 CREATOR = new Creator<>() { public Id createFromParcel(Parcel in) { return new Id(in.readLong()); } public Id[] newArray(int size) { return new Id[size]; } }; } }