Activities

This commit is contained in:
2026-04-19 12:10:40 +02:00
parent 8a57cc4446
commit 47dbec287b
12 changed files with 189 additions and 23 deletions

View File

@@ -33,6 +33,48 @@ public class ActivityStreamDAO {
@SuppressWarnings("NullAway.Init")
DataSource dataSource;
public List<ActivityStreamPoint> getByActivityId(final long activityId) throws SQLException {
try (final Connection conn = dataSource.getConnection();
final PreparedStatement ps = conn.prepareStatement(
"SELECT time, elapsed_s, distance_m, altitude_m, heartrate_bpm, watts, torque_nm, " +
"cadence_rpm, velocity_mps, lat, lng, temp_c, grade_smooth, moving, lr_balance, " +
"hrv_ms, respiration_brpm, fixed_altitude_m, " +
"left_pedal_smoothness_pct, right_pedal_smoothness_pct, " +
"left_torque_effectiveness_pct, right_torque_effectiveness_pct " +
"FROM activity_stream_point WHERE activity_id = ? ORDER BY time ASC")) {
ps.setLong(1, activityId);
try (final ResultSet rs = ps.executeQuery()) {
final List<ActivityStreamPoint> points = new java.util.ArrayList<>();
while (rs.next()) {
final ActivityStreamPoint point = new ActivityStreamPoint(
rs.getTimestamp(1).toInstant(), activityId, rs.getInt(2));
point.setDistanceM(rs.getObject(3) != null ? rs.getDouble(3) : null);
point.setAltitudeM(rs.getObject(4) != null ? rs.getDouble(4) : null);
point.setHeartrateBpm(rs.getObject(5) != null ? rs.getInt(5) : null);
point.setWatts(rs.getObject(6) != null ? rs.getInt(6) : null);
point.setTorqueNm(rs.getObject(7) != null ? rs.getInt(7) : null);
point.setCadenceRpm(rs.getObject(8) != null ? rs.getDouble(8) : null);
point.setVelocityMps(rs.getObject(9) != null ? rs.getDouble(9) : null);
point.setLat(rs.getObject(10) != null ? rs.getDouble(10) : null);
point.setLng(rs.getObject(11) != null ? rs.getDouble(11) : null);
point.setTempC(rs.getObject(12) != null ? rs.getDouble(12) : null);
point.setGradeSmooth(rs.getObject(13) != null ? rs.getDouble(13) : null);
point.setMoving(rs.getObject(14) != null ? rs.getBoolean(14) : null);
point.setLrBalance(rs.getObject(15) != null ? rs.getDouble(15) : null);
point.setHrvMs(rs.getObject(16) != null ? rs.getDouble(16) : null);
point.setRespirationBrpm(rs.getObject(17) != null ? rs.getDouble(17) : null);
point.setFixedAltitudeM(rs.getObject(18) != null ? rs.getInt(18) : null);
point.setLeftPedalSmoothnessPct(rs.getObject(19) != null ? rs.getDouble(19) : null);
point.setRightPedalSmoothnessPct(rs.getObject(20) != null ? rs.getDouble(20) : null);
point.setLeftTorqueEffectivenessPct(rs.getObject(21) != null ? rs.getDouble(21) : null);
point.setRightTorqueEffectivenessPct(rs.getObject(22) != null ? rs.getDouble(22) : null);
points.add(point);
}
return points;
}
}
}
public boolean hasStreams(final long activityId) throws SQLException {
try (final Connection conn = dataSource.getConnection();
final PreparedStatement ps = conn.prepareStatement(

View File

@@ -0,0 +1,17 @@
package com.peakup.activity.model;
import com.peakup.common.model.AbstractTimescaleEntity;
import java.time.Instant;
public class AbstractStream extends AbstractTimescaleEntity {
public AbstractStream(final Instant time) {
super(time);
}
@SuppressWarnings("NullAway.Init")
public AbstractStream() {
// Hibernate
}
}

View File

@@ -1,6 +1,5 @@
package com.peakup.activity.model;
import com.peakup.common.model.AbstractTimescaleEntity;
import com.peakup.common.model.DependentEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -20,7 +19,7 @@ import java.time.Instant;
@IdClass(ActivityMapStreamPoint.Key.class)
@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityMapStreamPoint extends AbstractTimescaleEntity implements DependentEntity {
public class ActivityMapStreamPoint extends AbstractStream implements DependentEntity {
@Id
@Column(nullable = false)

View File

@@ -1,6 +1,5 @@
package com.peakup.activity.model;
import com.peakup.common.model.AbstractTimescaleEntity;
import com.peakup.common.model.DependentEntity;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
@@ -21,7 +20,7 @@ import java.time.Instant;
@IdClass(ActivityStreamPoint.Key.class)
@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityStreamPoint extends AbstractTimescaleEntity implements DependentEntity {
public class ActivityStreamPoint extends AbstractStream implements DependentEntity {
@Id
@Column(nullable = false)

View File

@@ -8,7 +8,20 @@ public enum ActivityTypeEnum implements PersistableEnum<ActivityTypeEnum> {
RUN(2),
SWIM(3),
OPENWATER(4),
;
STRENGTH(5),
WORKOUT(6),
VIRTUAL_SKI(7),
VIRTUAL_RIDE(8),
HIKE(9),
WALK(10),
WEIGHT_TRAINING(11),
PILATES(12),
NORDIC_SKI(13),
VIRTUAL_ROW(14),
ROWING(15),
YOGA(16),
TRAIL_RUN(17),
OTHER(100);
private final int key;

View File

@@ -0,0 +1,25 @@
package com.peakup.activity.service;
import com.peakup.activity.dao.ActivityStreamDAO;
import com.peakup.activity.model.ActivityStreamPoint;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.sql.SQLException;
import java.util.List;
@ApplicationScoped
public class ActivityStreamService {
@Inject
@SuppressWarnings("NullAway.Init")
ActivityStreamDAO activityStreamDAO;
public List<ActivityStreamPoint> getByActivityId(final long activityId) {
try {
return activityStreamDAO.getByActivityId(activityId);
} catch (final SQLException sqlException) {
throw new IllegalStateException("Failed to load stream for activity " + activityId, sqlException);
}
}
}

View File

@@ -1,16 +1,17 @@
package com.peakup.dashboard.model;
import com.peakup.activity.model.ActivityMapStreamPoint;
import com.peakup.activity.model.ActivityStreamPoint;
import com.peakup.activity.model.IntervalsActivity;
import com.peakup.common.model.AbstractDataObject;
import com.peakup.goal.model.Goal;
import com.peakup.goal.model.GoalMetricPeriodTotal;
import jakarta.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
import java.util.Map;
@Data
@AllArgsConstructor
@@ -22,5 +23,6 @@ public class Dashboard extends AbstractDataObject {
private List<IntervalsActivity> activities;
private List<Goal> goals;
private List<GoalMetricPeriodTotal> goalMetricPeriodTotals;
private @Nullable List<ActivityMapStreamPoint> lastActivityMapStream;
private Map<Long, List<ActivityMapStreamPoint>> weekActivityMapStreams;
private Map<Long, List<ActivityStreamPoint>> weekActivityStreams;
}

View File

@@ -1,23 +1,27 @@
package com.peakup.dashboard.service;
import com.peakup.activity.model.ActivityMapStreamPoint;
import com.peakup.activity.model.ActivityStreamPoint;
import com.peakup.activity.model.IntervalsActivity;
import com.peakup.activity.service.ActivityMapStreamService;
import com.peakup.activity.service.ActivityStreamService;
import com.peakup.activity.service.IntervalsActivityService;
import com.peakup.dashboard.model.Dashboard;
import com.peakup.goal.service.GoalMetricPeriodTotalService;
import com.peakup.goal.service.GoalService;
import jakarta.annotation.Nullable;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.WeekFields;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@ApplicationScoped
public class DashboardService {
@@ -30,6 +34,10 @@ public class DashboardService {
@SuppressWarnings("NullAway.Init")
ActivityMapStreamService activityMapStreamService;
@Inject
@SuppressWarnings("NullAway.Init")
ActivityStreamService activityStreamService;
@Inject
@SuppressWarnings("NullAway.Init")
GoalService goalService;
@@ -52,7 +60,10 @@ public class DashboardService {
final String to = rangeTo.format(DateTimeFormatter.ISO_LOCAL_DATE);
final List<IntervalsActivity> activities = intervalsActivityService.getByAthleteIdBetween(athleteId, from, to);
final @Nullable List<ActivityMapStreamPoint> lastActivityMapStream = resolveLastActivityMapStream(activities);
final Map<Long, List<ActivityMapStreamPoint>> weekActivityMapStreams =
resolveWeekActivityMapStreams(activities, selectedWeekMonday);
final Map<Long, List<ActivityStreamPoint>> weekActivityStreams =
resolveWeekActivityStreams(activities, selectedWeekMonday);
return new Dashboard(
calendarWeek,
@@ -60,20 +71,41 @@ public class DashboardService {
activities,
goalService.getByAthleteId(athleteId),
goalMetricPeriodTotalService.getByAthleteAndWeek(athleteId, year, calendarWeek),
lastActivityMapStream
weekActivityMapStreams,
weekActivityStreams
);
}
private @Nullable List<ActivityMapStreamPoint> resolveLastActivityMapStream(final List<IntervalsActivity> activities) {
private List<IntervalsActivity> filterToWeek(final List<IntervalsActivity> activities, final LocalDate weekMonday) {
final LocalDate weekSunday = weekMonday.plusDays(6);
return activities.stream()
.max(Comparator.comparing(IntervalsActivity::getStartTime))
.map(lastActivity -> {
final Long activityId = lastActivity.getId();
if (activityId == null) {
return null;
}
return activityMapStreamService.getByActivityId(activityId);
.filter(activity -> {
final LocalDate activityDate = activity.getStartTime().atZone(ZoneOffset.UTC).toLocalDate();
return !activityDate.isBefore(weekMonday) && !activityDate.isAfter(weekSunday);
})
.orElse(null);
.filter(activity -> activity.getId() != null)
.toList();
}
private Map<Long, List<ActivityMapStreamPoint>> resolveWeekActivityMapStreams(
final List<IntervalsActivity> activities,
final LocalDate weekMonday) {
return filterToWeek(activities, weekMonday).stream()
.collect(Collectors.toMap(
activity -> Objects.requireNonNull(activity.getId()),
activity -> activityMapStreamService.getByActivityId(
Objects.requireNonNull(activity.getId()))
));
}
private Map<Long, List<ActivityStreamPoint>> resolveWeekActivityStreams(
final List<IntervalsActivity> activities,
final LocalDate weekMonday) {
return filterToWeek(activities, weekMonday).stream()
.collect(Collectors.toMap(
activity -> Objects.requireNonNull(activity.getId()),
activity -> activityStreamService.getByActivityId(
Objects.requireNonNull(activity.getId()))
));
}
}

View File

@@ -0,0 +1,7 @@
// GENERATED — do not edit manually.
import type { AbstractTimescaleEntityDo } from './AbstractTimescaleEntityDo';
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface AbstractStreamDo extends AbstractTimescaleEntityDo {
}

View File

@@ -1,8 +1,8 @@
// GENERATED — do not edit manually.
import type { AbstractTimescaleEntityDo } from './AbstractTimescaleEntityDo';
import type { AbstractStreamDo } from './AbstractStreamDo';
export interface ActivityMapStreamPointDo extends AbstractTimescaleEntityDo {
export interface ActivityMapStreamPointDo extends AbstractStreamDo {
activityId: number;
lat: number;
lng: number;

View File

@@ -0,0 +1,28 @@
// GENERATED — do not edit manually.
import type { AbstractStreamDo } from './AbstractStreamDo';
export interface ActivityStreamPointDo extends AbstractStreamDo {
activityId: number;
elapsedS: number;
distanceM: number | null;
altitudeM: number | null;
heartrateBpm: number | null;
watts: number | null;
torqueNm: number | null;
cadenceRpm: number | null;
velocityMps: number | null;
lat: number | null;
lng: number | null;
tempC: number | null;
gradeSmooth: number | null;
moving: boolean | null;
lrBalance: number | null;
hrvMs: number | null;
respirationBrpm: number | null;
fixedAltitudeM: number | null;
leftPedalSmoothnessPct: number | null;
rightPedalSmoothnessPct: number | null;
leftTorqueEffectivenessPct: number | null;
rightTorqueEffectivenessPct: number | null;
}

View File

@@ -5,6 +5,7 @@ import type { IntervalsActivityDo } from './IntervalsActivityDo';
import type { GoalDo } from './GoalDo';
import type { GoalMetricPeriodTotalDo } from './GoalMetricPeriodTotalDo';
import type { ActivityMapStreamPointDo } from './ActivityMapStreamPointDo';
import type { ActivityStreamPointDo } from './ActivityStreamPointDo';
export interface DashboardDo extends AbstractDataObjectDo {
calendarWeek: number;
@@ -12,5 +13,6 @@ export interface DashboardDo extends AbstractDataObjectDo {
activities: IntervalsActivityDo[];
goals: GoalDo[];
goalMetricPeriodTotals: GoalMetricPeriodTotalDo[];
lastActivityMapStream: ActivityMapStreamPointDo[] | null;
weekActivityMapStreams: unknown;
weekActivityStreams: unknown;
}