Moving Time ergänzt
This commit is contained in:
parent
1e833d52b6
commit
f34ce5723e
7 changed files with 138 additions and 7 deletions
|
|
@ -82,6 +82,14 @@ public class ActivityDTO {
|
|||
return metrics != null ? metrics.getCalories() : null;
|
||||
}
|
||||
|
||||
public Long getMovingTimeSeconds() {
|
||||
return metrics != null ? metrics.getMovingTimeSeconds() : null;
|
||||
}
|
||||
|
||||
public Long getStoppedTimeSeconds() {
|
||||
return metrics != null ? metrics.getStoppedTimeSeconds() : null;
|
||||
}
|
||||
|
||||
// Alias for frontend compatibility
|
||||
public Long getTotalDuration() {
|
||||
return totalDurationSeconds;
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ public class TimelineActivityDTO {
|
|||
private LocalDateTime endedAt;
|
||||
private Double totalDistance;
|
||||
private Long totalDurationSeconds;
|
||||
private Long movingTimeSeconds;
|
||||
private Long stoppedTimeSeconds;
|
||||
private Double elevationGain;
|
||||
private Double elevationLoss;
|
||||
private String visibility;
|
||||
|
|
@ -72,6 +74,8 @@ public class TimelineActivityDTO {
|
|||
.endedAt(activity.getEndedAt())
|
||||
.totalDistance(activity.getTotalDistance() != null ? activity.getTotalDistance().doubleValue() : null)
|
||||
.totalDurationSeconds(activity.getTotalDurationSeconds())
|
||||
.movingTimeSeconds(activity.getMetrics() != null ? activity.getMetrics().getMovingTimeSeconds() : null)
|
||||
.stoppedTimeSeconds(activity.getMetrics() != null ? activity.getMetrics().getStoppedTimeSeconds() : null)
|
||||
.elevationGain(activity.getElevationGain() != null ? activity.getElevationGain().doubleValue() : null)
|
||||
.elevationLoss(activity.getElevationLoss() != null ? activity.getElevationLoss().doubleValue() : null)
|
||||
.visibility(activity.getVisibility().name())
|
||||
|
|
@ -140,6 +144,8 @@ public class TimelineActivityDTO {
|
|||
private Long averagePaceSeconds;
|
||||
private Integer averagePower;
|
||||
private Integer calories;
|
||||
private Long movingTimeSeconds;
|
||||
private Long stoppedTimeSeconds;
|
||||
|
||||
public static ActivityMetricsSummary fromMetrics(org.operaton.fitpub.model.entity.ActivityMetrics metrics) {
|
||||
return ActivityMetricsSummary.builder()
|
||||
|
|
@ -150,6 +156,8 @@ public class TimelineActivityDTO {
|
|||
.averagePaceSeconds(metrics.getAveragePaceSeconds())
|
||||
.averagePower(metrics.getAveragePower())
|
||||
.calories(metrics.getCalories())
|
||||
.movingTimeSeconds(metrics.getMovingTimeSeconds())
|
||||
.stoppedTimeSeconds(metrics.getStoppedTimeSeconds())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -447,11 +447,17 @@ public class ActivityImageService {
|
|||
y += lineHeight + 35;
|
||||
}
|
||||
|
||||
// Duration with neon cyan value
|
||||
// Duration/Moving Time with neon cyan value
|
||||
if (activity.getTotalDurationSeconds() != null) {
|
||||
long hours = activity.getTotalDurationSeconds() / 3600;
|
||||
long minutes = (activity.getTotalDurationSeconds() % 3600) / 60;
|
||||
long seconds = activity.getTotalDurationSeconds() % 60;
|
||||
// Check if we have moving time that's different from total duration
|
||||
Long movingTime = activity.getMetrics() != null ? activity.getMetrics().getMovingTimeSeconds() : null;
|
||||
Long totalDuration = activity.getTotalDurationSeconds();
|
||||
boolean showMovingTime = movingTime != null && movingTime < totalDuration;
|
||||
|
||||
long timeToDisplay = showMovingTime ? movingTime : totalDuration;
|
||||
long hours = timeToDisplay / 3600;
|
||||
long minutes = (timeToDisplay % 3600) / 60;
|
||||
long seconds = timeToDisplay % 60;
|
||||
|
||||
g2d.setFont(new Font("Arial Black", Font.BOLD, 40));
|
||||
g2d.setColor(neonCyan);
|
||||
|
|
@ -464,7 +470,8 @@ public class ActivityImageService {
|
|||
g2d.drawString(duration, metadataX, y);
|
||||
g2d.setFont(new Font("Arial Black", Font.PLAIN, 16));
|
||||
g2d.setColor(new Color(180, 180, 180));
|
||||
g2d.drawString("DURATION", metadataX, y + 22);
|
||||
String label = showMovingTime ? "MOVING TIME" : "DURATION";
|
||||
g2d.drawString(label, metadataX, y + 22);
|
||||
y += lineHeight + 35;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ public class FitParser {
|
|||
|
||||
private static final double SEMICIRCLES_TO_DEGREES = 180.0 / Math.pow(2, 31);
|
||||
private static final double MPS_TO_KPH = 3.6;
|
||||
private static final double STOPPED_SPEED_THRESHOLD = 0.5; // km/h - below this is considered stopped
|
||||
private static final long STOPPED_TIME_THRESHOLD = 30; // seconds - must be stopped this long to count
|
||||
|
||||
// Lazy-loaded timezone engine (expensive to initialize)
|
||||
private static TimeZoneEngine timezoneEngine = null;
|
||||
|
|
@ -275,6 +277,13 @@ public class FitParser {
|
|||
|
||||
if (session.getTotalMovingTime() != null) {
|
||||
metrics.setMovingTime(Duration.ofSeconds(session.getTotalMovingTime().longValue()));
|
||||
} else {
|
||||
// Fallback: Calculate moving time from track points if native value is not available
|
||||
Duration calculatedMovingTime = calculateMovingTimeFromTrackPoints(parsedData);
|
||||
if (calculatedMovingTime != null) {
|
||||
metrics.setMovingTime(calculatedMovingTime);
|
||||
log.debug("Calculated moving time from track points: {}", calculatedMovingTime);
|
||||
}
|
||||
}
|
||||
|
||||
if (session.getTotalStrides() != null) {
|
||||
|
|
@ -401,4 +410,82 @@ public class FitParser {
|
|||
return Activity.ActivityType.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates moving time from track points when native moving time is not available.
|
||||
* Uses same logic as GPX parser: speed < 0.5 km/h for > 30 seconds = stopped.
|
||||
*/
|
||||
private Duration calculateMovingTimeFromTrackPoints(ParsedActivityData parsedData) {
|
||||
List<TrackPointData> trackPoints = parsedData.getTrackPoints();
|
||||
|
||||
// For indoor activities or activities without track points, use total duration
|
||||
if (trackPoints == null || trackPoints.isEmpty()) {
|
||||
Duration totalDuration = parsedData.getTotalDuration();
|
||||
if (totalDuration != null) {
|
||||
log.debug("No track points available, using total duration as moving time: {}", totalDuration);
|
||||
return totalDuration;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Need at least 2 points to calculate moving time
|
||||
if (trackPoints.size() < 2) {
|
||||
Duration totalDuration = parsedData.getTotalDuration();
|
||||
log.debug("Only 1 track point, using total duration as moving time: {}", totalDuration);
|
||||
return totalDuration;
|
||||
}
|
||||
|
||||
Duration movingTime = Duration.ZERO;
|
||||
Duration stoppedTime = Duration.ZERO;
|
||||
LocalDateTime lastStoppedTime = null;
|
||||
|
||||
for (int i = 1; i < trackPoints.size(); i++) {
|
||||
TrackPointData prev = trackPoints.get(i - 1);
|
||||
TrackPointData curr = trackPoints.get(i);
|
||||
|
||||
if (prev.getTimestamp() == null || curr.getTimestamp() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Duration timeDelta = Duration.between(prev.getTimestamp(), curr.getTimestamp());
|
||||
|
||||
// Skip unrealistic time deltas (> 1 hour between points)
|
||||
if (timeDelta.getSeconds() > 3600) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if we have speed data
|
||||
BigDecimal speed = curr.getSpeed();
|
||||
if (speed != null) {
|
||||
double speedKmh = speed.doubleValue(); // Already in km/h from FIT parser
|
||||
|
||||
// Track moving vs stopped time
|
||||
if (speedKmh < STOPPED_SPEED_THRESHOLD) {
|
||||
if (lastStoppedTime == null) {
|
||||
lastStoppedTime = prev.getTimestamp();
|
||||
}
|
||||
Duration currentStopDuration = Duration.between(lastStoppedTime, curr.getTimestamp());
|
||||
if (currentStopDuration.getSeconds() > STOPPED_TIME_THRESHOLD) {
|
||||
stoppedTime = stoppedTime.plus(timeDelta);
|
||||
}
|
||||
} else {
|
||||
lastStoppedTime = null;
|
||||
movingTime = movingTime.plus(timeDelta);
|
||||
}
|
||||
} else {
|
||||
// No speed data, assume moving
|
||||
movingTime = movingTime.plus(timeDelta);
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't calculate any moving time, use total duration
|
||||
if (movingTime.isZero() && stoppedTime.isZero()) {
|
||||
Duration totalDuration = parsedData.getTotalDuration();
|
||||
log.debug("No speed data in track points, using total duration as moving time: {}", totalDuration);
|
||||
return totalDuration;
|
||||
}
|
||||
|
||||
log.debug("Calculated moving time from track points: moving={}, stopped={}", movingTime, stoppedTime);
|
||||
return movingTime;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,10 +157,12 @@ const FitPubTimeline = {
|
|||
<small class="text-muted">
|
||||
${activity.hasGpsTrack
|
||||
? `<strong>Distance:</strong> ${this.formatDistance(activity.totalDistance)} •
|
||||
<strong>Duration:</strong> ${this.formatDuration(activity.totalDurationSeconds)} •
|
||||
<strong>Duration:</strong> ${this.formatDuration(activity.totalDurationSeconds)}
|
||||
${activity.movingTimeSeconds && activity.movingTimeSeconds < activity.totalDurationSeconds ? ` • <strong>Moving:</strong> ${this.formatDuration(activity.movingTimeSeconds)}` : ''} •
|
||||
<strong>Pace:</strong> ${this.formatPace(activity.totalDurationSeconds, activity.totalDistance)} •
|
||||
<strong>Elevation:</strong> ${activity.elevationGain ? Math.round(activity.elevationGain) + 'm' : 'N/A'}`
|
||||
: `<strong>Duration:</strong> ${this.formatDuration(activity.totalDurationSeconds)}
|
||||
${activity.movingTimeSeconds && activity.movingTimeSeconds < activity.totalDurationSeconds ? ` • <strong>Moving:</strong> ${this.formatDuration(activity.movingTimeSeconds)}` : ''}
|
||||
${activity.metrics?.averageHeartRate ? ` • <strong>Avg HR:</strong> ${activity.metrics.averageHeartRate} bpm` : ''}
|
||||
${activity.metrics?.calories ? ` • <strong>Calories:</strong> ${activity.metrics.calories} kcal` : ''}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Moving Time -->
|
||||
<div class="col-md-3 col-6" id="metricMovingTimeContainer" style="display: none;">
|
||||
<div class="d-flex align-items-center py-2">
|
||||
<i class="bi bi-clock-history text-primary me-2" style="font-size: 1.5rem;"></i>
|
||||
<div>
|
||||
<div class="text-muted small">Moving Time</div>
|
||||
<div class="fw-bold" id="metricMovingTime">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Elevation Gain -->
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="d-flex align-items-center py-2">
|
||||
|
|
@ -512,6 +522,12 @@
|
|||
// Duration is always shown
|
||||
document.getElementById('metricDuration').textContent = formatDuration(activity.totalDuration);
|
||||
|
||||
// Moving Time - only show if available and different from total duration
|
||||
if (activity.movingTimeSeconds != null && activity.movingTimeSeconds < activity.totalDuration) {
|
||||
document.getElementById('metricMovingTimeContainer').style.display = 'block';
|
||||
document.getElementById('metricMovingTime').textContent = formatDuration(activity.movingTimeSeconds);
|
||||
}
|
||||
|
||||
// Additional Metrics (conditional)
|
||||
// Note: averageSpeed is already in km/h from backend (converted in FitParser)
|
||||
if (activity.averageSpeed && hasGpsTrack) {
|
||||
|
|
|
|||
|
|
@ -174,7 +174,10 @@
|
|||
<div class="col-md-4">
|
||||
<small class="text-muted">
|
||||
<strong>Distance:</strong> ${formatDistance(activity.totalDistance)}<br>
|
||||
<strong>Duration:</strong> ${formatDuration(activity.totalDuration)}<br>
|
||||
<strong>Duration:</strong> ${formatDuration(activity.totalDuration)}
|
||||
${activity.movingTimeSeconds && activity.movingTimeSeconds < activity.totalDuration
|
||||
? `<br><strong>Moving:</strong> ${formatDuration(activity.movingTimeSeconds)}`
|
||||
: ''}<br>
|
||||
<strong>Elevation:</strong> ${activity.elevationGain ? Math.round(activity.elevationGain) + 'm' : 'N/A'}
|
||||
</small>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue