Add streams
This commit is contained in:
2
.idea/dataSources.xml
generated
2
.idea/dataSources.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="peakup@localhost" uuid="fe9ac26c-536e-42d2-a0d0-b6ae02f4e756">
|
||||
<data-source source="LOCAL" name="peakup@localhost" uuid="146146a8-fc52-45aa-b5e3-48b7a37a3a5f">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
|
||||
@@ -6,8 +6,6 @@ import jakarta.enterprise.context.ApplicationScoped;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
@@ -22,6 +20,10 @@ public class IntervalsActivityConnector implements ActivityConnector<IntervalsCl
|
||||
return intervalsClient.getActivities(intervalsAthleteId, oldest, newest);
|
||||
}
|
||||
|
||||
public IntervalsClient.StreamsData getStreams(String activityId) {
|
||||
return intervalsClient.getActivityStreams(activityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IntervalsClient.IntervalsActivity> fetchRawActivities(
|
||||
@Nullable Instant after, @Nullable Instant before) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.peakup.activity.dao;
|
||||
|
||||
import com.peakup.activity.model.ActivityAchievement;
|
||||
import com.peakup.activity.model.ActivityAchievement_;
|
||||
import com.peakup.dao.AbstractDAO;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
public class ActivityAchievementDAO extends AbstractDAO<ActivityAchievement> {
|
||||
|
||||
@Override
|
||||
protected Class<ActivityAchievement> getEntityClass() {
|
||||
return ActivityAchievement.class;
|
||||
}
|
||||
|
||||
public List<ActivityAchievement> findByActivityId(long activityId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<ActivityAchievement> cq = cb.createQuery(ActivityAchievement.class);
|
||||
Root<ActivityAchievement> root = cq.from(ActivityAchievement.class);
|
||||
cq.where(cb.equal(root.get(ActivityAchievement_.activityId), activityId));
|
||||
return em.createQuery(cq).getResultList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.peakup.activity.dao;
|
||||
|
||||
import com.peakup.activity.model.ActivityStreamPoint;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.*;
|
||||
import java.util.List;
|
||||
|
||||
@lombok.extern.slf4j.Slf4j
|
||||
@ApplicationScoped
|
||||
public class ActivityStreamDAO {
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
DataSource dataSource;
|
||||
|
||||
private static final String INSERT_SQL = """
|
||||
INSERT INTO activity_stream_point
|
||||
(time, activity_id, elapsed_s, distance_m, altitude_m, heartrate_bpm, watts,
|
||||
cadence_rpm, velocity_mps, lat, lng, temp_c, grade_smooth, moving, lr_balance, extra)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb)
|
||||
ON CONFLICT (activity_id, time) DO NOTHING
|
||||
""";
|
||||
|
||||
public void insertBatch(List<ActivityStreamPoint> points) throws SQLException {
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement ps = conn.prepareStatement(INSERT_SQL)) {
|
||||
for (ActivityStreamPoint p : points) {
|
||||
ps.setTimestamp(1, Timestamp.from(p.getTime()));
|
||||
ps.setLong (2, p.getActivityId());
|
||||
ps.setInt (3, p.getElapsedS());
|
||||
setNullable(ps, 4, p.getDistanceM(), Types.DOUBLE);
|
||||
setNullable(ps, 5, p.getAltitudeM(), Types.DOUBLE);
|
||||
setNullable(ps, 6, p.getHeartrateBpm(), Types.INTEGER);
|
||||
setNullable(ps, 7, p.getWatts(), Types.INTEGER);
|
||||
setNullable(ps, 8, p.getCadenceRpm(), Types.DOUBLE);
|
||||
setNullable(ps, 9, p.getVelocityMps(), Types.DOUBLE);
|
||||
setNullable(ps, 10, p.getLat(), Types.DOUBLE);
|
||||
setNullable(ps, 11, p.getLng(), Types.DOUBLE);
|
||||
setNullable(ps, 12, p.getTempC(), Types.DOUBLE);
|
||||
setNullable(ps, 13, p.getGradeSmooth(), Types.DOUBLE);
|
||||
setNullable(ps, 14, p.getMoving(), Types.BOOLEAN);
|
||||
setNullable(ps, 15, p.getLrBalance(), Types.DOUBLE);
|
||||
setNullable(ps, 16, p.getExtra(), Types.VARCHAR);
|
||||
ps.addBatch();
|
||||
}
|
||||
ps.executeBatch();
|
||||
log.debug("Inserted {} stream points for activity {}",
|
||||
points.size(), points.isEmpty() ? "?" : points.get(0).getActivityId());
|
||||
}
|
||||
}
|
||||
|
||||
private void setNullable(PreparedStatement ps, int idx, @Nullable Object val, int sqlType) throws SQLException {
|
||||
if (val == null) {
|
||||
ps.setNull(idx, sqlType);
|
||||
} else if (val instanceof Double d) {
|
||||
ps.setDouble(idx, d);
|
||||
} else if (val instanceof Integer i) {
|
||||
ps.setInt(idx, i);
|
||||
} else if (val instanceof Boolean b) {
|
||||
ps.setBoolean(idx, b);
|
||||
} else {
|
||||
ps.setString(idx, val.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,27 @@ package com.peakup.activity.importer;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.peakup.activity.connector.IntervalsActivityConnector;
|
||||
import com.peakup.activity.dao.ActivityAchievementDAO;
|
||||
import com.peakup.activity.dao.ActivityStreamDAO;
|
||||
import com.peakup.activity.dao.IntervalsActivityDAO;
|
||||
import com.peakup.activity.model.ActivityAchievement;
|
||||
import com.peakup.activity.model.ActivityAttachment;
|
||||
import com.peakup.activity.model.ActivityStreamPoint;
|
||||
import com.peakup.activity.model.ActivityZone;
|
||||
import com.peakup.activity.model.IgnorePart;
|
||||
import com.peakup.activity.model.IntervalsActivity;
|
||||
import com.peakup.connector.intervals.IntervalsClient;
|
||||
import com.peakup.dao.AbstractDAO;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.peakup.common.NullableUtils.defaultValue;
|
||||
@@ -27,6 +37,14 @@ public class IntervalsActivityImporter
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
IntervalsActivityDAO intervalsActivityDAO;
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
ActivityAchievementDAO activityAchievementDAO;
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
ActivityStreamDAO activityStreamDAO;
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
IntervalsActivityConnector intervalsActivityConnector;
|
||||
@@ -35,6 +53,10 @@ public class IntervalsActivityImporter
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
IntervalsActivityImporter self; // CDI proxy — enables @Transactional on persistSingle()
|
||||
|
||||
@Override
|
||||
protected AbstractDAO<IntervalsActivity> getDAO() {
|
||||
return intervalsActivityDAO;
|
||||
@@ -62,7 +84,14 @@ public class IntervalsActivityImporter
|
||||
a.trainer = src.trainer;
|
||||
a.commute = src.commute;
|
||||
a.race = src.race;
|
||||
a.gearId = src.gear != null ? defaultValue(src.gear.id) : "";
|
||||
if (src.gear != null) {
|
||||
a.gearId = defaultValue(src.gear.id);
|
||||
a.gearName = src.gear.name;
|
||||
a.gearDistanceM = src.gear.distance;
|
||||
a.gearPrimary = src.gear.primary;
|
||||
} else {
|
||||
a.gearId = "";
|
||||
}
|
||||
a.distanceM = defaultValue(src.distance);
|
||||
a.movingTimeS = src.moving_time != null ? src.moving_time : 0L;
|
||||
a.elapsedTimeS = src.elapsed_time != null ? src.elapsed_time : 0L;
|
||||
@@ -94,21 +123,287 @@ public class IntervalsActivityImporter
|
||||
a.hrrEndTimeSec = src.icu_hrr.end_time;
|
||||
a.hrrAverageWatts = src.icu_hrr.average_watts;
|
||||
}
|
||||
if (src.icu_zone_times != null) {
|
||||
try {
|
||||
a.icuZoneTimes = objectMapper.writeValueAsString(src.icu_zone_times);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Failed to serialize icu_zone_times for activity {}", src.id, e);
|
||||
}
|
||||
}
|
||||
a.variabilityIndex = defaultValue(src.icu_variability_index);
|
||||
a.feel = src.feel;
|
||||
a.perceivedExertion = defaultValue(src.perceived_exertion);
|
||||
a.averageStrideLengthM = defaultValue(src.average_stride);
|
||||
a.averageTempC = defaultValue(src.average_temp);
|
||||
a.minTempC = src.min_temp;
|
||||
a.maxTempC = src.max_temp;
|
||||
a.avgLrBalance = src.avg_lr_balance;
|
||||
a.gapMps = src.gap;
|
||||
a.gapModel = src.gap_model;
|
||||
a.deviceName = src.device_name;
|
||||
a.deviceWatts = src.device_watts;
|
||||
a.powerMeter = src.power_meter;
|
||||
a.powerMeterSerial = src.power_meter_serial;
|
||||
a.powerMeterBattery = src.power_meter_battery;
|
||||
a.crankLengthMm = src.crank_length;
|
||||
a.powerField = src.power_field;
|
||||
a.useElevationCorrection = src.use_elevation_correction;
|
||||
a.carbsUsedG = src.carbs_used;
|
||||
a.icuDistanceM = src.icu_distance;
|
||||
a.icuIgnoreTime = src.icu_ignore_time;
|
||||
a.icuRecordingTimeS = src.icu_recording_time;
|
||||
a.coastingTimeS = src.coasting_time;
|
||||
a.icuWarmupTimeS = src.icu_warmup_time;
|
||||
a.icuCooldownTimeS = src.icu_cooldown_time;
|
||||
a.subType = src.sub_type;
|
||||
a.pairedEventId = src.paired_event_id;
|
||||
a.icuWeightKg = src.icu_weight;
|
||||
a.lthr = src.lthr;
|
||||
a.icuRestingHr = src.icu_resting_hr;
|
||||
a.thresholdPaceMps = src.threshold_pace;
|
||||
a.icuPowerSpikeThreshold = src.icu_power_spike_threshold;
|
||||
a.icuSweetSpotMin = src.icu_sweet_spot_min;
|
||||
a.icuSweetSpotMax = src.icu_sweet_spot_max;
|
||||
a.trimp = src.trimp;
|
||||
a.icuWPrimeJ = src.icu_w_prime;
|
||||
a.pMaxWatt = src.p_max;
|
||||
a.icuPmCp = src.icu_pm_cp;
|
||||
a.icuPmWPrimeJ = src.icu_pm_w_prime;
|
||||
a.icuPmPMaxWatt = src.icu_pm_p_max;
|
||||
a.icuPmFtp = src.icu_pm_ftp;
|
||||
a.icuPmFtpSecs = src.icu_pm_ftp_secs;
|
||||
a.icuPmFtpWatts = src.icu_pm_ftp_watts;
|
||||
a.icuRollingCp = src.icu_rolling_cp;
|
||||
a.icuRollingWPrimeJ = src.icu_rolling_w_prime;
|
||||
a.icuRollingPMaxWatt = src.icu_rolling_p_max;
|
||||
a.icuRollingFtp = src.icu_rolling_ftp;
|
||||
a.icuRollingFtpDelta = src.icu_rolling_ftp_delta;
|
||||
a.ssCp = src.ss_cp;
|
||||
a.ssWPrimeJ = src.ss_w_prime;
|
||||
a.ssPMaxWatt = src.ss_p_max;
|
||||
a.icuIgnorePower = src.icu_ignore_power;
|
||||
a.icuIgnoreHr = src.icu_ignore_hr;
|
||||
a.ignoreVelocity = src.ignore_velocity;
|
||||
a.ignorePace = src.ignore_pace;
|
||||
a.hasHeartrate = src.has_heartrate;
|
||||
a.analyzed = src.analyzed;
|
||||
a.hasWeather = src.has_weather;
|
||||
a.hasSegments = src.has_segments;
|
||||
a.fileType = src.file_type;
|
||||
a.fileSportIndex = src.file_sport_index;
|
||||
a.icuAthleteId = src.icu_athlete_id;
|
||||
a.icuCreated = src.created;
|
||||
a.icuSyncDate = src.icu_sync_date;
|
||||
a.icuChatId = src.icu_chat_id;
|
||||
a.startDateLocal = src.start_date_local;
|
||||
a.icuHrZones = toJson(src.icu_hr_zones);
|
||||
a.paceZones = toJson(src.pace_zones);
|
||||
a.icuPowerZones = toJson(src.icu_power_zones);
|
||||
a.icuTrainingLoadData = toJson(src.icu_training_load_data);
|
||||
a.intervalSummary = toJson(src.interval_summary);
|
||||
a.skylineChartBytes = src.skyline_chart_bytes;
|
||||
if (src.stream_types != null) {
|
||||
a.streamTypes = src.stream_types.toArray(new String[0]);
|
||||
}
|
||||
a.icuZoneTimes = toJson(src.icu_zone_times);
|
||||
a.icuHrZoneTimes = toJson(src.icu_hr_zone_times);
|
||||
a.paceZoneTimes = toJson(src.pace_zone_times);
|
||||
a.gapZoneTimes = toJson(src.gap_zone_times);
|
||||
a.useGapZoneTimes = src.use_gap_zone_times;
|
||||
a.tizOrder = src.tiz_order;
|
||||
a.polarizationIndex = src.polarization_index;
|
||||
a.decoupling = src.decoupling;
|
||||
a.icuLapCount = src.icu_lap_count;
|
||||
a.icuJoulesAboveFtp = src.icu_joules_above_ftp;
|
||||
a.icuMaxWbalDepletion = src.icu_max_wbal_depletion;
|
||||
a.icuPowerHrZ2 = src.icu_power_hr_z2;
|
||||
a.icuPowerHrZ2Mins = src.icu_power_hr_z2_mins;
|
||||
a.icuCadenceZ2 = src.icu_cadence_z2;
|
||||
a.icuRpe = src.icu_rpe;
|
||||
a.icuPowerHr = src.icu_power_hr;
|
||||
a.icuMedianTimeDelta = src.icu_median_time_delta;
|
||||
a.p30sExponent = src.p30s_exponent;
|
||||
a.icuIntervalsEdited = src.icu_intervals_edited;
|
||||
a.lockIntervals = src.lock_intervals;
|
||||
a.workoutShiftSecs = src.workout_shift_secs;
|
||||
a.poolLengthM = src.pool_length;
|
||||
a.lengths = toJson(src.lengths);
|
||||
a.powerLoad = src.power_load;
|
||||
a.hrLoad = src.hr_load;
|
||||
a.paceLoad = src.pace_load;
|
||||
a.hrLoadType = src.hr_load_type;
|
||||
a.paceLoadType = src.pace_load_type;
|
||||
a.sessionRpe = src.session_rpe;
|
||||
a.strainScore = src.strain_score;
|
||||
a.kgLifted = src.kg_lifted;
|
||||
a.averageAltitudeM = src.average_altitude;
|
||||
a.averageWeatherTempC = src.average_weather_temp;
|
||||
a.minWeatherTempC = src.min_weather_temp;
|
||||
a.maxWeatherTempC = src.max_weather_temp;
|
||||
a.averageFeelsLikeC = src.average_feels_like;
|
||||
a.minFeelsLikeC = src.min_feels_like;
|
||||
a.maxFeelsLikeC = src.max_feels_like;
|
||||
a.averageWindSpeedMps = src.average_wind_speed;
|
||||
a.averageWindGustMps = src.average_wind_gust;
|
||||
a.prevailingWindDeg = src.prevailing_wind_deg;
|
||||
a.headwindPercent = src.headwind_percent;
|
||||
a.tailwindPercent = src.tailwind_percent;
|
||||
a.averageClouds = src.average_clouds;
|
||||
a.maxRainMm = src.max_rain;
|
||||
a.maxSnowMm = src.max_snow;
|
||||
a.carbsIngestedG = src.carbs_ingested;
|
||||
a.paceMps = src.pace;
|
||||
a.athleteMaxHr = src.athlete_max_hr;
|
||||
a.compliance = src.compliance;
|
||||
a.coachTick = src.coach_tick;
|
||||
a.activityGroup = src.group;
|
||||
a.icuSyncError = src.icu_sync_error;
|
||||
a.icuColor = src.icu_color;
|
||||
a.source = src.source;
|
||||
a.oauthClientId = src.oauth_client_id;
|
||||
a.oauthClientName = src.oauth_client_name;
|
||||
a.stravaId = src.strava_id;
|
||||
a.routeId = src.route_id;
|
||||
a.powerFieldNames = toJson(src.power_field_names);
|
||||
|
||||
// recording_stops
|
||||
if (src.recording_stops != null) {
|
||||
a.recordingStops = src.recording_stops.stream().mapToInt(Integer::intValue).toArray();
|
||||
}
|
||||
|
||||
// ignore_parts
|
||||
if (src.ignore_parts != null) {
|
||||
for (IntervalsClient.IntervalsActivity.Ignore ig : src.ignore_parts) {
|
||||
IgnorePart part = new IgnorePart();
|
||||
part.startIndex = ig.start_index != null ? ig.start_index : 0;
|
||||
part.endIndex = ig.end_index != null ? ig.end_index : 0;
|
||||
part.power = ig.power;
|
||||
part.pace = ig.pace;
|
||||
part.hr = ig.hr;
|
||||
a.ignoreParts.add(part);
|
||||
}
|
||||
}
|
||||
|
||||
// tags
|
||||
if (src.tags != null) {
|
||||
a.tags.addAll(src.tags);
|
||||
}
|
||||
|
||||
// attachments
|
||||
if (src.attachments != null) {
|
||||
for (IntervalsClient.IntervalsActivity.Attachment att : src.attachments) {
|
||||
ActivityAttachment attachment = new ActivityAttachment();
|
||||
attachment.attachmentId = att.id;
|
||||
attachment.filename = att.filename;
|
||||
attachment.mimetype = att.mimetype;
|
||||
attachment.url = att.url;
|
||||
a.attachments.add(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
// custom_zones
|
||||
if (src.custom_zones != null) {
|
||||
for (IntervalsClient.IntervalsActivity.ZoneSet zoneSet : src.custom_zones) {
|
||||
if (zoneSet.zones == null) continue;
|
||||
for (IntervalsClient.IntervalsActivity.ZoneInfo zi : zoneSet.zones) {
|
||||
ActivityZone zone = new ActivityZone();
|
||||
zone.zoneSetCode = defaultValue(zoneSet.code);
|
||||
zone.zoneId = defaultValue(zi.id);
|
||||
zone.rangeStart = defaultValue(zi.start);
|
||||
zone.rangeEnd = defaultValue(zi.end);
|
||||
zone.valueStart = defaultValue(zi.start_value);
|
||||
zone.valueEnd = defaultValue(zi.end_value);
|
||||
zone.secs = zi.secs != null ? zi.secs : 0;
|
||||
a.zones.add(zone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int persist(Long athleteId, List<IntervalsClient.IntervalsActivity> raws) {
|
||||
int count = 0;
|
||||
for (IntervalsClient.IntervalsActivity raw : raws) {
|
||||
if (self.persistSingle(raw, athleteId)) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public boolean persistSingle(IntervalsClient.IntervalsActivity raw, Long athleteId) {
|
||||
if (findExisting(raw) != null) {
|
||||
log.warn("Already imported: id={}, name={}", raw.id, raw.name);
|
||||
return false;
|
||||
}
|
||||
IntervalsActivity activity = map(raw, athleteId);
|
||||
intervalsActivityDAO.insert(activity);
|
||||
persistAchievements(raw, activity);
|
||||
try {
|
||||
importStreams(raw, activity.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("Stream import failed for activity {}: {}", raw.id, e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void persistAchievements(IntervalsClient.IntervalsActivity raw, IntervalsActivity activity) {
|
||||
if (raw.icu_achievements == null || activity.getId() == null) return;
|
||||
for (IntervalsClient.IntervalsActivity.IcuAchievement src : raw.icu_achievements) {
|
||||
ActivityAchievement ach = new ActivityAchievement();
|
||||
ach.activityId = activity.getId();
|
||||
ach.achievementId = src.id;
|
||||
ach.type = src.type;
|
||||
ach.message = src.message;
|
||||
ach.watts = src.watts;
|
||||
ach.secs = src.secs;
|
||||
ach.value = src.value;
|
||||
ach.distance = src.distance;
|
||||
ach.pace = src.pace;
|
||||
activityAchievementDAO.insert(ach);
|
||||
}
|
||||
}
|
||||
|
||||
private void importStreams(IntervalsClient.IntervalsActivity raw, @Nullable Long activityId)
|
||||
throws SQLException {
|
||||
if (raw.id == null || activityId == null || raw.start_date == null) return;
|
||||
IntervalsClient.StreamsData streams = intervalsActivityConnector.getStreams(raw.id);
|
||||
if (streams.size() == 0) return;
|
||||
List<ActivityStreamPoint> points = mapStreams(streams, Instant.parse(raw.start_date), activityId);
|
||||
activityStreamDAO.insertBatch(points);
|
||||
}
|
||||
|
||||
// package-private for testing
|
||||
List<ActivityStreamPoint> mapStreams(IntervalsClient.StreamsData streams, Instant startTime, long activityId) {
|
||||
int n = streams.size();
|
||||
List<ActivityStreamPoint> points = new ArrayList<>(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
List<Integer> timeList = streams.time;
|
||||
if (timeList == null) break;
|
||||
int elapsed = timeList.get(i);
|
||||
ActivityStreamPoint p = new ActivityStreamPoint(startTime.plusSeconds(elapsed), activityId, elapsed);
|
||||
|
||||
if (streams.distance != null) p.setDistanceM(streams.distance.get(i));
|
||||
if (streams.altitude != null) p.setAltitudeM(streams.altitude.get(i));
|
||||
if (streams.heartrate != null) p.setHeartrateBpm(streams.heartrate.get(i));
|
||||
if (streams.watts != null) p.setWatts(streams.watts.get(i));
|
||||
if (streams.cadence != null) p.setCadenceRpm(streams.cadence.get(i));
|
||||
if (streams.velocity_smooth != null) p.setVelocityMps(streams.velocity_smooth.get(i));
|
||||
if (streams.latlng != null) p.setLat(streams.latlng.get(i));
|
||||
if (streams.temp != null) p.setTempC(streams.temp.get(i));
|
||||
if (streams.grade_smooth != null) p.setGradeSmooth(streams.grade_smooth.get(i));
|
||||
if (streams.moving != null) p.setMoving(streams.moving.get(i));
|
||||
if (streams.left_right_balance != null) p.setLrBalance(streams.left_right_balance.get(i));
|
||||
if (!streams.unknownStreams.isEmpty()) p.setExtra(toJson(streams.unknownStreams));
|
||||
|
||||
points.add(p);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private @Nullable String toJson(@Nullable Object obj) {
|
||||
if (obj == null) return null;
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("Failed to serialize JSON field", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void importActivities(Long athleteId, String intervalsAthleteId) {
|
||||
IntervalsActivity latest = intervalsActivityDAO.getLatestActivity(athleteId);
|
||||
String oldest = latest != null
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import com.peakup.model.AbstractDependentEntity;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Entity
|
||||
@Table(name = "activity_achievement")
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ActivityAchievement extends AbstractDependentEntity {
|
||||
|
||||
@Column(name = "activity_id")
|
||||
public long activityId;
|
||||
|
||||
@Column(name = "achievement_id")
|
||||
public @Nullable String achievementId;
|
||||
|
||||
@Column
|
||||
public @Nullable String type;
|
||||
|
||||
@Column
|
||||
public @Nullable String message;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer watts;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer secs;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer value;
|
||||
|
||||
@Column
|
||||
public @Nullable Double distance;
|
||||
|
||||
@Column
|
||||
public @Nullable Double pace;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public ActivityAchievement() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFK() {
|
||||
return getActivityId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import lombok.Data;
|
||||
|
||||
@Embeddable
|
||||
@Data
|
||||
public class ActivityAttachment {
|
||||
|
||||
@Column(name = "attachment_id")
|
||||
public @Nullable String attachmentId;
|
||||
|
||||
@Column
|
||||
public @Nullable String filename;
|
||||
|
||||
@Column
|
||||
public @Nullable String mimetype;
|
||||
|
||||
@Column
|
||||
public @Nullable String url;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public ActivityAttachment() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import com.peakup.model.AbstractTimescaleEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "activity_stream_point")
|
||||
@IdClass(ActivityStreamPoint.Key.class)
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ActivityStreamPoint extends AbstractTimescaleEntity {
|
||||
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
private long activityId;
|
||||
|
||||
@Column(name = "elapsed_s", nullable = false)
|
||||
private int elapsedS;
|
||||
|
||||
@Column(name = "distance_m")
|
||||
private @Nullable Double distanceM;
|
||||
|
||||
@Column(name = "altitude_m")
|
||||
private @Nullable Double altitudeM;
|
||||
|
||||
@Column(name = "heartrate_bpm")
|
||||
private @Nullable Integer heartrateBpm;
|
||||
|
||||
@Column
|
||||
private @Nullable Integer watts;
|
||||
|
||||
@Column(name = "cadence_rpm")
|
||||
private @Nullable Double cadenceRpm;
|
||||
|
||||
@Column(name = "velocity_mps")
|
||||
private @Nullable Double velocityMps;
|
||||
|
||||
@Column
|
||||
private @Nullable Double lat;
|
||||
|
||||
@Column
|
||||
private @Nullable Double lng;
|
||||
|
||||
@Column(name = "temp_c")
|
||||
private @Nullable Double tempC;
|
||||
|
||||
@Column(name = "grade_smooth")
|
||||
private @Nullable Double gradeSmooth;
|
||||
|
||||
@Column
|
||||
private @Nullable Boolean moving;
|
||||
|
||||
@Column(name = "lr_balance")
|
||||
private @Nullable Double lrBalance;
|
||||
|
||||
@Column(columnDefinition = "JSONB")
|
||||
private @Nullable String extra;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public ActivityStreamPoint() {
|
||||
// Hibernate
|
||||
}
|
||||
|
||||
public ActivityStreamPoint(Instant time, long activityId, int elapsedS) {
|
||||
super(time);
|
||||
this.activityId = activityId;
|
||||
this.elapsedS = elapsedS;
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public static class Key implements Serializable {
|
||||
private long activityId;
|
||||
private Instant time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import lombok.Data;
|
||||
|
||||
@Embeddable
|
||||
@Data
|
||||
public class ActivityZone {
|
||||
|
||||
@Column(name = "zone_set_code")
|
||||
public String zoneSetCode;
|
||||
|
||||
@Column(name = "zone_id")
|
||||
public String zoneId;
|
||||
|
||||
@Column(name = "range_start")
|
||||
public double rangeStart;
|
||||
|
||||
@Column(name = "range_end")
|
||||
public double rangeEnd;
|
||||
|
||||
@Column(name = "value_start")
|
||||
public double valueStart;
|
||||
|
||||
@Column(name = "value_end")
|
||||
public double valueEnd;
|
||||
|
||||
@Column
|
||||
public int secs;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public ActivityZone() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import lombok.Data;
|
||||
|
||||
@Embeddable
|
||||
@Data
|
||||
public class IgnorePart {
|
||||
|
||||
@Column(name = "start_index")
|
||||
public int startIndex;
|
||||
|
||||
@Column(name = "end_index")
|
||||
public int endIndex;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean power;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean pace;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean hr;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public IgnorePart() {
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
package com.peakup.activity.model;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "intervals_activity")
|
||||
@Data
|
||||
@@ -33,6 +34,15 @@ public class IntervalsActivity extends AbstractActivity {
|
||||
@Column
|
||||
public String gearId;
|
||||
|
||||
@Column
|
||||
public @Nullable String gearName;
|
||||
|
||||
@Column(name = "gear_distance_m")
|
||||
public @Nullable Double gearDistanceM;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean gearPrimary;
|
||||
|
||||
@Column(name = "total_ascent_m")
|
||||
public Double totalAscentM;
|
||||
|
||||
@@ -81,10 +91,6 @@ public class IntervalsActivity extends AbstractActivity {
|
||||
@Column
|
||||
public @Nullable Integer hrrAverageWatts;
|
||||
|
||||
@Column(columnDefinition = "jsonb")
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
public @Nullable String icuZoneTimes;
|
||||
|
||||
@Column
|
||||
public Double variabilityIndex;
|
||||
|
||||
@@ -100,6 +106,446 @@ public class IntervalsActivity extends AbstractActivity {
|
||||
@Column(name = "average_temp_c")
|
||||
public Double averageTempC;
|
||||
|
||||
@Column(columnDefinition = "integer[]")
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
public @Nullable int[] recordingStops;
|
||||
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_zone", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
public List<ActivityZone> zones = new ArrayList<>();
|
||||
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_ignore_part", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
@OrderColumn(name = "sort_index")
|
||||
public List<IgnorePart> ignoreParts = new ArrayList<>();
|
||||
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_tag", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
@Column(name = "tag")
|
||||
public List<String> tags = new ArrayList<>();
|
||||
|
||||
@ElementCollection(fetch = FetchType.LAZY)
|
||||
@CollectionTable(name = "activity_attachment", joinColumns = @JoinColumn(name = "activity_id"))
|
||||
@OrderColumn(name = "sort_index")
|
||||
public List<ActivityAttachment> attachments = new ArrayList<>();
|
||||
|
||||
// --- Environment ---
|
||||
@Column(name = "min_temp_c")
|
||||
public @Nullable Double minTempC;
|
||||
|
||||
@Column(name = "max_temp_c")
|
||||
public @Nullable Double maxTempC;
|
||||
|
||||
@Column
|
||||
public @Nullable Double avgLrBalance;
|
||||
|
||||
// --- Grade Adjusted Pace ---
|
||||
@Column(name = "gap_mps")
|
||||
public @Nullable Double gapMps;
|
||||
|
||||
@Column
|
||||
public @Nullable String gapModel;
|
||||
|
||||
// --- Device / equipment ---
|
||||
@Column
|
||||
public @Nullable String deviceName;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean deviceWatts;
|
||||
|
||||
@Column
|
||||
public @Nullable String powerMeter;
|
||||
|
||||
@Column
|
||||
public @Nullable String powerMeterSerial;
|
||||
|
||||
@Column
|
||||
public @Nullable String powerMeterBattery;
|
||||
|
||||
@Column(name = "crank_length_mm")
|
||||
public @Nullable Double crankLengthMm;
|
||||
|
||||
@Column
|
||||
public @Nullable String powerField;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean useElevationCorrection;
|
||||
|
||||
// --- Nutrition ---
|
||||
@Column(name = "carbs_used_g")
|
||||
public @Nullable Double carbsUsedG;
|
||||
|
||||
// --- Intervals.icu internal distance ---
|
||||
@Column(name = "icu_distance_m")
|
||||
public @Nullable Double icuDistanceM;
|
||||
|
||||
// --- Time accounting ---
|
||||
@Column
|
||||
public @Nullable Boolean icuIgnoreTime;
|
||||
|
||||
@Column(name = "icu_recording_time_s")
|
||||
public @Nullable Integer icuRecordingTimeS;
|
||||
|
||||
@Column(name = "coasting_time_s")
|
||||
public @Nullable Integer coastingTimeS;
|
||||
|
||||
@Column(name = "icu_warmup_time_s")
|
||||
public @Nullable Integer icuWarmupTimeS;
|
||||
|
||||
@Column(name = "icu_cooldown_time_s")
|
||||
public @Nullable Integer icuCooldownTimeS;
|
||||
|
||||
// --- Activity sub-type and pairing ---
|
||||
@Column
|
||||
public @Nullable String subType;
|
||||
|
||||
@Column
|
||||
public @Nullable String pairedEventId;
|
||||
|
||||
// --- Athlete state at activity time ---
|
||||
@Column(name = "icu_weight_kg")
|
||||
public @Nullable Double icuWeightKg;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer lthr;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuRestingHr;
|
||||
|
||||
@Column(name = "threshold_pace_mps")
|
||||
public @Nullable Double thresholdPaceMps;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuPowerSpikeThreshold;
|
||||
|
||||
@Column
|
||||
public @Nullable Double icuSweetSpotMin;
|
||||
|
||||
@Column
|
||||
public @Nullable Double icuSweetSpotMax;
|
||||
|
||||
@Column
|
||||
public @Nullable Double trimp;
|
||||
|
||||
// --- W' and P-max ---
|
||||
@Column(name = "icu_w_prime_j")
|
||||
public @Nullable Integer icuWPrimeJ;
|
||||
|
||||
@Column(name = "p_max_watt")
|
||||
public @Nullable Integer pMaxWatt;
|
||||
|
||||
// --- Performance model (PM) ---
|
||||
@Column
|
||||
public @Nullable Integer icuPmCp;
|
||||
|
||||
@Column(name = "icu_pm_w_prime_j")
|
||||
public @Nullable Integer icuPmWPrimeJ;
|
||||
|
||||
@Column(name = "icu_pm_p_max_watt")
|
||||
public @Nullable Integer icuPmPMaxWatt;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuPmFtp;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuPmFtpSecs;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuPmFtpWatts;
|
||||
|
||||
// --- Rolling performance ---
|
||||
@Column
|
||||
public @Nullable Integer icuRollingCp;
|
||||
|
||||
@Column(name = "icu_rolling_w_prime_j")
|
||||
public @Nullable Integer icuRollingWPrimeJ;
|
||||
|
||||
@Column(name = "icu_rolling_p_max_watt")
|
||||
public @Nullable Integer icuRollingPMaxWatt;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuRollingFtp;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuRollingFtpDelta;
|
||||
|
||||
// --- Sweet-spot CP model ---
|
||||
@Column
|
||||
public @Nullable Integer ssCp;
|
||||
|
||||
@Column(name = "ss_w_prime_j")
|
||||
public @Nullable Integer ssWPrimeJ;
|
||||
|
||||
@Column(name = "ss_p_max_watt")
|
||||
public @Nullable Integer ssPMaxWatt;
|
||||
|
||||
// --- Ignore flags ---
|
||||
@Column
|
||||
public @Nullable Boolean icuIgnorePower;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean icuIgnoreHr;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean ignoreVelocity;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean ignorePace;
|
||||
|
||||
// --- Activity flags ---
|
||||
@Column
|
||||
public @Nullable Boolean hasHeartrate;
|
||||
|
||||
@Column
|
||||
public @Nullable String analyzed;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean hasWeather;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean hasSegments;
|
||||
|
||||
// --- File / sync metadata ---
|
||||
@Column
|
||||
public @Nullable String fileType;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer fileSportIndex;
|
||||
|
||||
@Column
|
||||
public @Nullable String icuAthleteId;
|
||||
|
||||
@Column
|
||||
public @Nullable String icuCreated;
|
||||
|
||||
@Column
|
||||
public @Nullable String icuSyncDate;
|
||||
|
||||
@Column
|
||||
public @Nullable Long icuChatId;
|
||||
|
||||
// --- Local-timezone start ---
|
||||
@Column
|
||||
public @Nullable String startDateLocal;
|
||||
|
||||
// --- Zone definitions (JSON) ---
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String icuHrZones;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String paceZones;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String icuPowerZones;
|
||||
|
||||
// --- Complex nested data (JSON) ---
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String icuTrainingLoadData;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String intervalSummary;
|
||||
|
||||
// --- Available stream names ---
|
||||
@Column(columnDefinition = "text[]")
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
public @Nullable String[] streamTypes;
|
||||
|
||||
// --- Skyline chart ---
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String skylineChartBytes;
|
||||
|
||||
// --- Zone times (JSON) ---
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String icuZoneTimes;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String icuHrZoneTimes;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String paceZoneTimes;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String gapZoneTimes;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean useGapZoneTimes;
|
||||
|
||||
@Column
|
||||
public @Nullable String tizOrder;
|
||||
|
||||
// --- Performance / load metrics ---
|
||||
@Column
|
||||
public @Nullable Double polarizationIndex;
|
||||
|
||||
@Column
|
||||
public @Nullable Double decoupling;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuLapCount;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuJoulesAboveFtp;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuMaxWbalDepletion;
|
||||
|
||||
@Column
|
||||
public @Nullable Double icuPowerHrZ2;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuPowerHrZ2Mins;
|
||||
|
||||
@Column
|
||||
public @Nullable Double icuCadenceZ2;
|
||||
|
||||
@Column
|
||||
public @Nullable Double icuRpe;
|
||||
|
||||
@Column
|
||||
public @Nullable Double icuPowerHr;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer icuMedianTimeDelta;
|
||||
|
||||
@Column
|
||||
public @Nullable Double p30sExponent;
|
||||
|
||||
// --- Intervals editing ---
|
||||
@Column
|
||||
public @Nullable Boolean icuIntervalsEdited;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean lockIntervals;
|
||||
|
||||
@Column
|
||||
public @Nullable Integer workoutShiftSecs;
|
||||
|
||||
// --- Swimming ---
|
||||
@Column(name = "pool_length_m")
|
||||
public @Nullable Integer poolLengthM;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String lengths;
|
||||
|
||||
// --- Load scores ---
|
||||
@Column
|
||||
public @Nullable Double powerLoad;
|
||||
|
||||
@Column
|
||||
public @Nullable Double hrLoad;
|
||||
|
||||
@Column
|
||||
public @Nullable Double paceLoad;
|
||||
|
||||
@Column
|
||||
public @Nullable String hrLoadType;
|
||||
|
||||
@Column
|
||||
public @Nullable String paceLoadType;
|
||||
|
||||
@Column
|
||||
public @Nullable Double sessionRpe;
|
||||
|
||||
@Column
|
||||
public @Nullable Double strainScore;
|
||||
|
||||
@Column
|
||||
public @Nullable Double kgLifted;
|
||||
|
||||
// --- Weather ---
|
||||
@Column(name = "average_altitude_m")
|
||||
public @Nullable Double averageAltitudeM;
|
||||
|
||||
@Column(name = "average_weather_temp_c")
|
||||
public @Nullable Double averageWeatherTempC;
|
||||
|
||||
@Column(name = "min_weather_temp_c")
|
||||
public @Nullable Double minWeatherTempC;
|
||||
|
||||
@Column(name = "max_weather_temp_c")
|
||||
public @Nullable Double maxWeatherTempC;
|
||||
|
||||
@Column(name = "average_feels_like_c")
|
||||
public @Nullable Double averageFeelsLikeC;
|
||||
|
||||
@Column(name = "min_feels_like_c")
|
||||
public @Nullable Double minFeelsLikeC;
|
||||
|
||||
@Column(name = "max_feels_like_c")
|
||||
public @Nullable Double maxFeelsLikeC;
|
||||
|
||||
@Column(name = "average_wind_speed_mps")
|
||||
public @Nullable Double averageWindSpeedMps;
|
||||
|
||||
@Column(name = "average_wind_gust_mps")
|
||||
public @Nullable Double averageWindGustMps;
|
||||
|
||||
@Column
|
||||
public @Nullable Double prevailingWindDeg;
|
||||
|
||||
@Column
|
||||
public @Nullable Double headwindPercent;
|
||||
|
||||
@Column
|
||||
public @Nullable Double tailwindPercent;
|
||||
|
||||
@Column
|
||||
public @Nullable Double averageClouds;
|
||||
|
||||
@Column(name = "max_rain_mm")
|
||||
public @Nullable Double maxRainMm;
|
||||
|
||||
@Column(name = "max_snow_mm")
|
||||
public @Nullable Double maxSnowMm;
|
||||
|
||||
// --- Nutrition ---
|
||||
@Column(name = "carbs_ingested_g")
|
||||
public @Nullable Double carbsIngestedG;
|
||||
|
||||
// --- Running pace ---
|
||||
@Column(name = "pace_mps")
|
||||
public @Nullable Double paceMps;
|
||||
|
||||
// --- Athlete / HR ---
|
||||
@Column
|
||||
public @Nullable Integer athleteMaxHr;
|
||||
|
||||
// --- Coach / compliance ---
|
||||
@Column
|
||||
public @Nullable String compliance;
|
||||
|
||||
@Column
|
||||
public @Nullable Boolean coachTick;
|
||||
|
||||
@Column(name = "activity_group")
|
||||
public @Nullable String activityGroup;
|
||||
|
||||
// --- Source / sync metadata ---
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String icuSyncError;
|
||||
|
||||
@Column
|
||||
public @Nullable String icuColor;
|
||||
|
||||
@Column
|
||||
public @Nullable String source;
|
||||
|
||||
@Column
|
||||
public @Nullable String oauthClientId;
|
||||
|
||||
@Column
|
||||
public @Nullable String oauthClientName;
|
||||
|
||||
@Column
|
||||
public @Nullable Long stravaId;
|
||||
|
||||
@Column
|
||||
public @Nullable Long routeId;
|
||||
|
||||
// --- Power field names ---
|
||||
@Column(columnDefinition = "TEXT")
|
||||
public @Nullable String powerFieldNames;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public IntervalsActivity() {
|
||||
// hibernate
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package com.peakup.connector.intervals;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.ws.rs.*;
|
||||
@@ -7,7 +15,10 @@ import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@ApplicationScoped
|
||||
@Path("/api/v1")
|
||||
@@ -29,6 +40,87 @@ public interface IntervalsClient {
|
||||
@Path("/activity/{id}")
|
||||
IntervalsActivity getActivity(@PathParam("id") String id);
|
||||
|
||||
@GET
|
||||
@Path("/activity/{id}/streams")
|
||||
StreamsData getActivityStreams(@PathParam("id") String id);
|
||||
|
||||
/**
|
||||
* Flat view of all activity stream arrays.
|
||||
* The API returns {@code [{"type":"time","data":[...]}, ...]} — the inner
|
||||
* {@link Deserializer} converts that into this flat structure.
|
||||
*/
|
||||
@JsonDeserialize(using = StreamsData.Deserializer.class)
|
||||
class StreamsData {
|
||||
|
||||
public @Nullable List<Integer> time;
|
||||
public @Nullable List<Double> distance;
|
||||
public @Nullable List<Double> altitude;
|
||||
public @Nullable List<Integer> heartrate;
|
||||
public @Nullable List<Integer> watts;
|
||||
public @Nullable List<Double> cadence;
|
||||
public @Nullable List<Double> velocity_smooth;
|
||||
/** Latitude per time point (intervals.icu does not provide a separate longitude stream). */
|
||||
public @Nullable List<Double> latlng;
|
||||
public @Nullable List<Double> temp;
|
||||
public @Nullable List<Double> grade_smooth;
|
||||
public @Nullable List<Boolean> moving;
|
||||
public @Nullable List<Double> left_right_balance;
|
||||
|
||||
public Map<String, Object> unknownStreams = new LinkedHashMap<>();
|
||||
|
||||
/** Returns the number of data points (length of the time array). */
|
||||
public int size() {
|
||||
return time != null ? time.size() : 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private StreamsData() {}
|
||||
|
||||
// --- inner deserializer ---
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static class StreamEntry {
|
||||
public @Nullable String type;
|
||||
public @Nullable List<Object> data;
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
StreamEntry() {}
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullAway")
|
||||
static class Deserializer extends JsonDeserializer<StreamsData> {
|
||||
private static final TypeReference<List<Integer>> INT_LIST = new TypeReference<>() {};
|
||||
private static final TypeReference<List<Double>> DBL_LIST = new TypeReference<>() {};
|
||||
private static final TypeReference<List<Boolean>> BOOL_LIST = new TypeReference<>() {};
|
||||
|
||||
@Override
|
||||
public StreamsData deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
ObjectMapper mapper = (ObjectMapper) p.getCodec();
|
||||
JsonNode root = mapper.readTree(p);
|
||||
StreamsData result = new StreamsData();
|
||||
for (JsonNode entry : root) {
|
||||
String type = entry.path("type").asText("");
|
||||
JsonNode data = entry.path("data");
|
||||
switch (type) {
|
||||
case "time" -> result.time = mapper.convertValue(data, INT_LIST);
|
||||
case "distance" -> result.distance = mapper.convertValue(data, DBL_LIST);
|
||||
case "altitude" -> result.altitude = mapper.convertValue(data, DBL_LIST);
|
||||
case "heartrate" -> result.heartrate = mapper.convertValue(data, INT_LIST);
|
||||
case "watts" -> result.watts = mapper.convertValue(data, INT_LIST);
|
||||
case "cadence" -> result.cadence = mapper.convertValue(data, DBL_LIST);
|
||||
case "velocity_smooth" -> result.velocity_smooth = mapper.convertValue(data, DBL_LIST);
|
||||
case "latlng" -> result.latlng = mapper.convertValue(data, DBL_LIST);
|
||||
case "temp" -> result.temp = mapper.convertValue(data, DBL_LIST);
|
||||
case "grade_smooth" -> result.grade_smooth = mapper.convertValue(data, DBL_LIST);
|
||||
case "moving" -> result.moving = mapper.convertValue(data, BOOL_LIST);
|
||||
case "left_right_balance" -> result.left_right_balance = mapper.convertValue(data, DBL_LIST);
|
||||
default -> result.unknownStreams.put(type, mapper.convertValue(data, Object.class));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class IntervalsActivity {
|
||||
|
||||
public @Nullable String id; // intervals.icu activity id, e.g. "a12345678"
|
||||
@@ -81,14 +173,218 @@ public interface IntervalsClient {
|
||||
public @Nullable Integer feel; // 1–5
|
||||
public @Nullable Double perceived_exertion; // RPE 1–10
|
||||
|
||||
// Power zone time-in-zone
|
||||
public @Nullable java.util.List<IcuZoneTime> icu_zone_times;
|
||||
// Zone times and definitions (custom_zones supersedes icu_zone_times)
|
||||
public @Nullable List<ZoneSet> custom_zones;
|
||||
|
||||
// Nested collections
|
||||
public @Nullable List<Integer> recording_stops;
|
||||
public @Nullable List<Ignore> ignore_parts;
|
||||
public @Nullable List<IcuAchievement> icu_achievements;
|
||||
public @Nullable List<String> tags;
|
||||
public @Nullable List<Attachment> attachments;
|
||||
|
||||
// Running dynamics
|
||||
public @Nullable Double average_stride; // meters
|
||||
|
||||
// Environment
|
||||
public @Nullable Double average_temp; // °C
|
||||
public @Nullable Double min_temp;
|
||||
public @Nullable Double max_temp;
|
||||
public @Nullable Double avg_lr_balance; // L/R power balance %
|
||||
|
||||
// Grade Adjusted Pace
|
||||
public @Nullable Double gap; // m/s
|
||||
public @Nullable String gap_model;
|
||||
|
||||
// Device / equipment
|
||||
public @Nullable String device_name;
|
||||
public @Nullable Boolean device_watts;
|
||||
public @Nullable String power_meter;
|
||||
public @Nullable String power_meter_serial;
|
||||
public @Nullable String power_meter_battery; // e.g. "GOOD", "LOW"
|
||||
public @Nullable Double crank_length; // mm
|
||||
public @Nullable String power_field;
|
||||
public @Nullable Boolean use_elevation_correction;
|
||||
|
||||
// Nutrition
|
||||
public @Nullable Double carbs_used; // grams
|
||||
|
||||
// Intervals.icu internal distance (may differ from Strava)
|
||||
public @Nullable Double icu_distance; // meters
|
||||
|
||||
// Time accounting
|
||||
public @Nullable Boolean icu_ignore_time;
|
||||
public @Nullable Integer icu_recording_time; // seconds
|
||||
public @Nullable Integer coasting_time; // seconds
|
||||
public @Nullable Integer icu_warmup_time; // seconds
|
||||
public @Nullable Integer icu_cooldown_time; // seconds
|
||||
|
||||
// Activity sub-type and pairing
|
||||
public @Nullable String sub_type;
|
||||
public @Nullable String paired_event_id;
|
||||
|
||||
// Athlete state at activity time
|
||||
public @Nullable Double icu_weight; // kg
|
||||
public @Nullable Integer lthr; // lactate threshold HR bpm
|
||||
public @Nullable Integer icu_resting_hr; // bpm
|
||||
public @Nullable Double threshold_pace; // m/s
|
||||
public @Nullable Integer icu_power_spike_threshold;
|
||||
public @Nullable Double icu_sweet_spot_min; // % FTP
|
||||
public @Nullable Double icu_sweet_spot_max; // % FTP
|
||||
public @Nullable Double trimp;
|
||||
|
||||
// W' (W-prime) and P-max
|
||||
public @Nullable Integer icu_w_prime; // joules
|
||||
public @Nullable Integer p_max; // watts
|
||||
|
||||
// Performance model (PM) — intervals.icu modelled values
|
||||
public @Nullable Integer icu_pm_cp;
|
||||
public @Nullable Integer icu_pm_w_prime; // joules
|
||||
public @Nullable Integer icu_pm_p_max; // watts
|
||||
public @Nullable Integer icu_pm_ftp;
|
||||
public @Nullable Integer icu_pm_ftp_secs;
|
||||
public @Nullable Integer icu_pm_ftp_watts;
|
||||
|
||||
// Rolling (recent-history) performance
|
||||
public @Nullable Integer icu_rolling_cp;
|
||||
public @Nullable Integer icu_rolling_w_prime; // joules
|
||||
public @Nullable Integer icu_rolling_p_max; // watts
|
||||
public @Nullable Integer icu_rolling_ftp;
|
||||
public @Nullable Integer icu_rolling_ftp_delta;
|
||||
|
||||
// Sweet-spot CP model
|
||||
public @Nullable Integer ss_cp;
|
||||
public @Nullable Integer ss_w_prime; // joules
|
||||
public @Nullable Integer ss_p_max; // watts
|
||||
|
||||
// Ignore flags
|
||||
public @Nullable Boolean icu_ignore_power;
|
||||
public @Nullable Boolean icu_ignore_hr;
|
||||
public @Nullable Boolean ignore_velocity;
|
||||
public @Nullable Boolean ignore_pace;
|
||||
|
||||
// Flags
|
||||
public @Nullable Boolean has_heartrate;
|
||||
public @Nullable String analyzed; // ISO datetime of analysis
|
||||
public @Nullable Boolean has_weather;
|
||||
public @Nullable Boolean has_segments;
|
||||
|
||||
// File / sync metadata
|
||||
public @Nullable String file_type;
|
||||
public @Nullable Integer file_sport_index;
|
||||
public @Nullable String icu_athlete_id;
|
||||
public @Nullable String created;
|
||||
public @Nullable String icu_sync_date;
|
||||
public @Nullable Long icu_chat_id;
|
||||
|
||||
// Local-timezone start (display only, not UTC)
|
||||
public @Nullable String start_date_local;
|
||||
|
||||
// Zone definitions (HR / pace / power — parallel to custom_zones)
|
||||
public @Nullable Object icu_hr_zones;
|
||||
public @Nullable Object pace_zones;
|
||||
public @Nullable Object icu_power_zones;
|
||||
|
||||
// Complex nested data — stored as raw JSON
|
||||
public @Nullable Object icu_training_load_data;
|
||||
public @Nullable Object interval_summary;
|
||||
|
||||
// Available data-stream names
|
||||
public @Nullable List<String> stream_types;
|
||||
|
||||
// Skyline chart (base64-encoded)
|
||||
public @Nullable String skyline_chart_bytes;
|
||||
|
||||
// Zone times (simplified parallel to custom_zones)
|
||||
public @Nullable Object icu_zone_times;
|
||||
public @Nullable Object icu_hr_zone_times;
|
||||
public @Nullable Object pace_zone_times;
|
||||
public @Nullable Object gap_zone_times;
|
||||
public @Nullable Boolean use_gap_zone_times;
|
||||
public @Nullable String tiz_order;
|
||||
|
||||
// Performance / load metrics
|
||||
public @Nullable Double polarization_index;
|
||||
public @Nullable Double decoupling; // aerobic decoupling %
|
||||
public @Nullable Integer icu_lap_count;
|
||||
public @Nullable Integer icu_joules_above_ftp;
|
||||
public @Nullable Integer icu_max_wbal_depletion;
|
||||
public @Nullable Double icu_power_hr_z2;
|
||||
public @Nullable Integer icu_power_hr_z2_mins;
|
||||
public @Nullable Double icu_cadence_z2;
|
||||
public @Nullable Double icu_rpe;
|
||||
public @Nullable Double icu_power_hr;
|
||||
public @Nullable Integer icu_median_time_delta; // ms
|
||||
public @Nullable Double p30s_exponent;
|
||||
|
||||
// Intervals editing
|
||||
public @Nullable Boolean icu_intervals_edited;
|
||||
public @Nullable Boolean lock_intervals;
|
||||
public @Nullable Integer workout_shift_secs;
|
||||
|
||||
// Swimming
|
||||
public @Nullable Integer pool_length; // meters
|
||||
public @Nullable Object lengths; // lap objects
|
||||
|
||||
// Load scores
|
||||
public @Nullable Double power_load;
|
||||
public @Nullable Double hr_load;
|
||||
public @Nullable Double pace_load;
|
||||
public @Nullable String hr_load_type;
|
||||
public @Nullable String pace_load_type;
|
||||
public @Nullable Double session_rpe;
|
||||
public @Nullable Double strain_score;
|
||||
public @Nullable Double kg_lifted;
|
||||
|
||||
// Weather
|
||||
public @Nullable Double average_altitude; // meters
|
||||
public @Nullable Double average_weather_temp; // °C
|
||||
public @Nullable Double min_weather_temp;
|
||||
public @Nullable Double max_weather_temp;
|
||||
public @Nullable Double average_feels_like; // °C
|
||||
public @Nullable Double min_feels_like;
|
||||
public @Nullable Double max_feels_like;
|
||||
public @Nullable Double average_wind_speed; // m/s
|
||||
public @Nullable Double average_wind_gust; // m/s
|
||||
public @Nullable Double prevailing_wind_deg;
|
||||
public @Nullable Double headwind_percent;
|
||||
public @Nullable Double tailwind_percent;
|
||||
public @Nullable Double average_clouds; // %
|
||||
public @Nullable Double max_rain; // mm
|
||||
public @Nullable Double max_snow; // mm
|
||||
|
||||
// Nutrition
|
||||
public @Nullable Double carbs_ingested; // grams
|
||||
|
||||
// Running pace
|
||||
public @Nullable Double pace; // m/s
|
||||
|
||||
// Athlete / HR
|
||||
public @Nullable Integer athlete_max_hr;
|
||||
|
||||
// Coach / compliance
|
||||
public @Nullable String compliance; // e.g. "GOOD", "MODERATE"
|
||||
public @Nullable Boolean coach_tick;
|
||||
public @Nullable String group;
|
||||
|
||||
// Source / sync metadata
|
||||
public @Nullable String icu_sync_error;
|
||||
public @Nullable String icu_color;
|
||||
public @Nullable String source;
|
||||
public @Nullable String oauth_client_id;
|
||||
public @Nullable String oauth_client_name;
|
||||
public @Nullable Long strava_id;
|
||||
public @Nullable Long route_id;
|
||||
|
||||
// Power field names and misc
|
||||
public @Nullable Object power_field_names;
|
||||
|
||||
public Map<String, Object> unknownFields = new LinkedHashMap<>();
|
||||
|
||||
@com.fasterxml.jackson.annotation.JsonAnySetter
|
||||
public void setUnknownField(String name, Object value) {
|
||||
unknownFields.put(name, value);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private IntervalsActivity() {
|
||||
@@ -97,6 +393,9 @@ public interface IntervalsClient {
|
||||
|
||||
public static class StravaGear {
|
||||
public @Nullable String id;
|
||||
public @Nullable String name;
|
||||
public @Nullable Double distance;
|
||||
public @Nullable Boolean primary;
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private StravaGear() {}
|
||||
@@ -114,12 +413,59 @@ public interface IntervalsClient {
|
||||
private IcuHrr() {}
|
||||
}
|
||||
|
||||
public static class IcuZoneTime {
|
||||
public @Nullable String id; // e.g. "Z1", "Z2", "SS"
|
||||
public @Nullable Integer secs; // seconds spent in zone
|
||||
public static class ZoneSet {
|
||||
public @Nullable String code; // e.g. "POWER", "HR", "PACE", "GAP"
|
||||
public @Nullable List<ZoneInfo> zones;
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private IcuZoneTime() {}
|
||||
private ZoneSet() {}
|
||||
}
|
||||
|
||||
public static class ZoneInfo {
|
||||
public @Nullable String id; // e.g. "Z1", "Z2", "SS"
|
||||
public @Nullable Double start; // % of FTP/LTHR
|
||||
public @Nullable Double end;
|
||||
public @Nullable Double start_value; // absolute watts/bpm/pace
|
||||
public @Nullable Double end_value;
|
||||
public @Nullable Integer secs;
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private ZoneInfo() {}
|
||||
}
|
||||
|
||||
public static class IcuAchievement {
|
||||
public @Nullable String id;
|
||||
public @Nullable String type; // BEST_POWER, FTP_UP, LTHR_UP, BEST_PACE
|
||||
public @Nullable String message;
|
||||
public @Nullable Integer watts;
|
||||
public @Nullable Integer secs;
|
||||
public @Nullable Integer value;
|
||||
public @Nullable Double distance;
|
||||
public @Nullable Double pace;
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private IcuAchievement() {}
|
||||
}
|
||||
|
||||
public static class Attachment {
|
||||
public @Nullable String id;
|
||||
public @Nullable String filename;
|
||||
public @Nullable String mimetype;
|
||||
public @Nullable String url;
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private Attachment() {}
|
||||
}
|
||||
|
||||
public static class Ignore {
|
||||
public @Nullable Integer start_index;
|
||||
public @Nullable Integer end_index;
|
||||
public @Nullable Boolean power;
|
||||
public @Nullable Boolean pace;
|
||||
public @Nullable Boolean hr;
|
||||
|
||||
@SuppressWarnings({"NullAway.Init", "unused"})
|
||||
private Ignore() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.peakup.model;
|
||||
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
|
||||
@MappedSuperclass
|
||||
public abstract class AbstractDependentEntity extends AbstractEntity {
|
||||
|
||||
public abstract long getFK();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.peakup.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@MappedSuperclass
|
||||
@Data
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
public abstract class AbstractTimescaleEntity {
|
||||
|
||||
@Id
|
||||
@Column(nullable = false)
|
||||
private Instant time;
|
||||
|
||||
protected AbstractTimescaleEntity() {
|
||||
// Hibernate
|
||||
}
|
||||
|
||||
protected AbstractTimescaleEntity(Instant time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import com.peakup.connector.strava.service.StravaConnectorService;
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@lombok.extern.slf4j.Slf4j
|
||||
@@ -38,7 +39,7 @@ public class ActivityImportScheduler {
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@Scheduled(every = "5m")
|
||||
@Scheduled(every = "20m")
|
||||
void importActivities() {
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
log.warn("Import already running, skipping this trigger.");
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
-- V10: Replace icu_zone_times JSONB with relational model; add nested activity tables
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
DROP COLUMN icu_zone_times,
|
||||
ADD COLUMN gear_name VARCHAR(255),
|
||||
ADD COLUMN gear_distance_m DOUBLE PRECISION,
|
||||
ADD COLUMN gear_primary BOOLEAN,
|
||||
ADD COLUMN recording_stops INTEGER[];
|
||||
|
||||
CREATE TABLE activity_zone (
|
||||
activity_id BIGINT NOT NULL REFERENCES intervals_activity(id) ON DELETE CASCADE,
|
||||
zone_set_code VARCHAR(50) NOT NULL,
|
||||
zone_id VARCHAR(20) NOT NULL,
|
||||
range_start DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
range_end DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
value_start DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
value_end DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
secs INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE activity_achievement (
|
||||
id BIGINT PRIMARY KEY,
|
||||
activity_id BIGINT NOT NULL REFERENCES intervals_activity(id) ON DELETE CASCADE,
|
||||
achievement_id VARCHAR(255),
|
||||
type VARCHAR(50),
|
||||
message VARCHAR(500),
|
||||
watts INTEGER,
|
||||
secs INTEGER,
|
||||
value INTEGER,
|
||||
distance DOUBLE PRECISION,
|
||||
pace DOUBLE PRECISION
|
||||
);
|
||||
|
||||
CREATE TABLE activity_ignore_part (
|
||||
activity_id BIGINT NOT NULL REFERENCES intervals_activity(id) ON DELETE CASCADE,
|
||||
sort_index INTEGER NOT NULL,
|
||||
start_index INTEGER NOT NULL,
|
||||
end_index INTEGER NOT NULL,
|
||||
power BOOLEAN,
|
||||
pace BOOLEAN,
|
||||
hr BOOLEAN
|
||||
);
|
||||
|
||||
CREATE TABLE activity_tag (
|
||||
activity_id BIGINT NOT NULL REFERENCES intervals_activity(id) ON DELETE CASCADE,
|
||||
tag VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE activity_attachment (
|
||||
activity_id BIGINT NOT NULL REFERENCES intervals_activity(id) ON DELETE CASCADE,
|
||||
sort_index INTEGER NOT NULL,
|
||||
attachment_id VARCHAR(255),
|
||||
filename VARCHAR(255),
|
||||
mimetype VARCHAR(100),
|
||||
url VARCHAR(1000)
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
-- V11: Add all intervals.icu API fields not yet stored on intervals_activity
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
-- Environment
|
||||
ADD COLUMN min_temp_c DOUBLE PRECISION,
|
||||
ADD COLUMN max_temp_c DOUBLE PRECISION,
|
||||
ADD COLUMN avg_lr_balance DOUBLE PRECISION,
|
||||
-- Grade Adjusted Pace
|
||||
ADD COLUMN gap_mps DOUBLE PRECISION,
|
||||
ADD COLUMN gap_model VARCHAR(100),
|
||||
-- Device / equipment
|
||||
ADD COLUMN device_name VARCHAR(255),
|
||||
ADD COLUMN device_watts BOOLEAN,
|
||||
ADD COLUMN power_meter VARCHAR(255),
|
||||
ADD COLUMN power_meter_serial VARCHAR(255),
|
||||
ADD COLUMN power_meter_battery DOUBLE PRECISION,
|
||||
ADD COLUMN crank_length_mm DOUBLE PRECISION,
|
||||
ADD COLUMN power_field VARCHAR(100),
|
||||
ADD COLUMN use_elevation_correction BOOLEAN,
|
||||
-- Nutrition
|
||||
ADD COLUMN carbs_used_g DOUBLE PRECISION,
|
||||
-- Intervals.icu internal distance
|
||||
ADD COLUMN icu_distance_m DOUBLE PRECISION,
|
||||
-- Time accounting
|
||||
ADD COLUMN icu_ignore_time_s INTEGER,
|
||||
ADD COLUMN icu_recording_time_s INTEGER,
|
||||
ADD COLUMN coasting_time_s INTEGER,
|
||||
ADD COLUMN icu_warmup_time_s INTEGER,
|
||||
ADD COLUMN icu_cooldown_time_s INTEGER,
|
||||
-- Activity sub-type and pairing
|
||||
ADD COLUMN sub_type VARCHAR(100),
|
||||
ADD COLUMN paired_event_id VARCHAR(255),
|
||||
-- Athlete state at activity time
|
||||
ADD COLUMN icu_weight_kg DOUBLE PRECISION,
|
||||
ADD COLUMN lthr INTEGER,
|
||||
ADD COLUMN icu_resting_hr INTEGER,
|
||||
ADD COLUMN threshold_pace_mps DOUBLE PRECISION,
|
||||
ADD COLUMN icu_power_spike_threshold INTEGER,
|
||||
ADD COLUMN icu_sweet_spot_min DOUBLE PRECISION,
|
||||
ADD COLUMN icu_sweet_spot_max DOUBLE PRECISION,
|
||||
ADD COLUMN trimp DOUBLE PRECISION,
|
||||
-- W' and P-max
|
||||
ADD COLUMN icu_w_prime_j INTEGER,
|
||||
ADD COLUMN p_max_watt INTEGER,
|
||||
-- Performance model (PM)
|
||||
ADD COLUMN icu_pm_cp INTEGER,
|
||||
ADD COLUMN icu_pm_w_prime_j INTEGER,
|
||||
ADD COLUMN icu_pm_p_max_watt INTEGER,
|
||||
ADD COLUMN icu_pm_ftp INTEGER,
|
||||
ADD COLUMN icu_pm_ftp_secs INTEGER,
|
||||
ADD COLUMN icu_pm_ftp_watts INTEGER,
|
||||
-- Rolling performance
|
||||
ADD COLUMN icu_rolling_cp INTEGER,
|
||||
ADD COLUMN icu_rolling_w_prime_j INTEGER,
|
||||
ADD COLUMN icu_rolling_p_max_watt INTEGER,
|
||||
ADD COLUMN icu_rolling_ftp INTEGER,
|
||||
ADD COLUMN icu_rolling_ftp_delta INTEGER,
|
||||
-- Sweet-spot CP model
|
||||
ADD COLUMN ss_cp INTEGER,
|
||||
ADD COLUMN ss_w_prime_j INTEGER,
|
||||
ADD COLUMN ss_p_max_watt INTEGER,
|
||||
-- Ignore flags
|
||||
ADD COLUMN icu_ignore_power BOOLEAN,
|
||||
ADD COLUMN icu_ignore_hr BOOLEAN,
|
||||
ADD COLUMN ignore_velocity BOOLEAN,
|
||||
ADD COLUMN ignore_pace BOOLEAN,
|
||||
-- Activity flags
|
||||
ADD COLUMN has_heartrate BOOLEAN,
|
||||
ADD COLUMN analyzed BOOLEAN,
|
||||
ADD COLUMN has_weather BOOLEAN,
|
||||
ADD COLUMN has_segments BOOLEAN,
|
||||
-- File / sync metadata
|
||||
ADD COLUMN file_type VARCHAR(50),
|
||||
ADD COLUMN file_sport_index INTEGER,
|
||||
ADD COLUMN icu_athlete_id VARCHAR(50),
|
||||
ADD COLUMN icu_created VARCHAR(50),
|
||||
ADD COLUMN icu_sync_date VARCHAR(50),
|
||||
ADD COLUMN icu_chat_id BIGINT,
|
||||
-- Local-timezone start
|
||||
ADD COLUMN start_date_local VARCHAR(50),
|
||||
-- Zone definitions (JSON text)
|
||||
ADD COLUMN icu_hr_zones TEXT,
|
||||
ADD COLUMN pace_zones TEXT,
|
||||
ADD COLUMN icu_power_zones TEXT,
|
||||
-- Complex nested data (JSON text)
|
||||
ADD COLUMN icu_training_load_data TEXT,
|
||||
ADD COLUMN interval_summary TEXT,
|
||||
-- Available stream names
|
||||
ADD COLUMN stream_types TEXT[],
|
||||
-- Skyline chart (base64)
|
||||
ADD COLUMN skyline_chart_bytes TEXT;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V12: icu_ignore_time is a boolean flag, not an integer duration
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
DROP COLUMN icu_ignore_time_s,
|
||||
ADD COLUMN icu_ignore_time BOOLEAN;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V13: analyzed is an ISO timestamp string, not a boolean
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
DROP COLUMN analyzed,
|
||||
ADD COLUMN analyzed VARCHAR(50);
|
||||
@@ -0,0 +1,75 @@
|
||||
-- V14: Add second batch of intervals.icu API fields
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
-- Zone times (JSON)
|
||||
ADD COLUMN icu_zone_times TEXT,
|
||||
ADD COLUMN icu_hr_zone_times TEXT,
|
||||
ADD COLUMN pace_zone_times TEXT,
|
||||
ADD COLUMN gap_zone_times TEXT,
|
||||
ADD COLUMN use_gap_zone_times BOOLEAN,
|
||||
ADD COLUMN tiz_order VARCHAR(50),
|
||||
-- Performance / load metrics
|
||||
ADD COLUMN polarization_index DOUBLE PRECISION,
|
||||
ADD COLUMN decoupling DOUBLE PRECISION,
|
||||
ADD COLUMN icu_lap_count INTEGER,
|
||||
ADD COLUMN icu_joules_above_ftp INTEGER,
|
||||
ADD COLUMN icu_max_wbal_depletion INTEGER,
|
||||
ADD COLUMN icu_power_hr_z2 DOUBLE PRECISION,
|
||||
ADD COLUMN icu_power_hr_z2_mins INTEGER,
|
||||
ADD COLUMN icu_cadence_z2 DOUBLE PRECISION,
|
||||
ADD COLUMN icu_rpe DOUBLE PRECISION,
|
||||
ADD COLUMN icu_power_hr DOUBLE PRECISION,
|
||||
ADD COLUMN icu_median_time_delta INTEGER,
|
||||
ADD COLUMN p30s_exponent DOUBLE PRECISION,
|
||||
-- Intervals editing
|
||||
ADD COLUMN icu_intervals_edited BOOLEAN,
|
||||
ADD COLUMN lock_intervals BOOLEAN,
|
||||
ADD COLUMN workout_shift_secs INTEGER,
|
||||
-- Swimming
|
||||
ADD COLUMN pool_length_m INTEGER,
|
||||
ADD COLUMN lengths TEXT,
|
||||
-- Load scores
|
||||
ADD COLUMN power_load DOUBLE PRECISION,
|
||||
ADD COLUMN hr_load DOUBLE PRECISION,
|
||||
ADD COLUMN pace_load DOUBLE PRECISION,
|
||||
ADD COLUMN hr_load_type VARCHAR(50),
|
||||
ADD COLUMN pace_load_type VARCHAR(50),
|
||||
ADD COLUMN session_rpe DOUBLE PRECISION,
|
||||
ADD COLUMN strain_score DOUBLE PRECISION,
|
||||
ADD COLUMN kg_lifted DOUBLE PRECISION,
|
||||
-- Weather
|
||||
ADD COLUMN average_altitude_m DOUBLE PRECISION,
|
||||
ADD COLUMN average_weather_temp_c DOUBLE PRECISION,
|
||||
ADD COLUMN min_weather_temp_c DOUBLE PRECISION,
|
||||
ADD COLUMN max_weather_temp_c DOUBLE PRECISION,
|
||||
ADD COLUMN average_feels_like_c DOUBLE PRECISION,
|
||||
ADD COLUMN min_feels_like_c DOUBLE PRECISION,
|
||||
ADD COLUMN max_feels_like_c DOUBLE PRECISION,
|
||||
ADD COLUMN average_wind_speed_mps DOUBLE PRECISION,
|
||||
ADD COLUMN average_wind_gust_mps DOUBLE PRECISION,
|
||||
ADD COLUMN prevailing_wind_deg DOUBLE PRECISION,
|
||||
ADD COLUMN headwind_percent DOUBLE PRECISION,
|
||||
ADD COLUMN tailwind_percent DOUBLE PRECISION,
|
||||
ADD COLUMN average_clouds DOUBLE PRECISION,
|
||||
ADD COLUMN max_rain_mm DOUBLE PRECISION,
|
||||
ADD COLUMN max_snow_mm DOUBLE PRECISION,
|
||||
-- Nutrition
|
||||
ADD COLUMN carbs_ingested_g DOUBLE PRECISION,
|
||||
-- Running pace
|
||||
ADD COLUMN pace_mps DOUBLE PRECISION,
|
||||
-- Athlete / HR
|
||||
ADD COLUMN athlete_max_hr INTEGER,
|
||||
-- Coach / compliance
|
||||
ADD COLUMN compliance DOUBLE PRECISION,
|
||||
ADD COLUMN coach_tick BOOLEAN,
|
||||
ADD COLUMN activity_group VARCHAR(255),
|
||||
-- Source / sync metadata
|
||||
ADD COLUMN icu_sync_error TEXT,
|
||||
ADD COLUMN icu_color VARCHAR(20),
|
||||
ADD COLUMN source VARCHAR(100),
|
||||
ADD COLUMN oauth_client_id VARCHAR(255),
|
||||
ADD COLUMN oauth_client_name VARCHAR(255),
|
||||
ADD COLUMN strava_id BIGINT,
|
||||
ADD COLUMN route_id BIGINT,
|
||||
-- Power field names
|
||||
ADD COLUMN power_field_names TEXT;
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V15: compliance is a string value (e.g. "GOOD"), not a number
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
DROP COLUMN compliance,
|
||||
ADD COLUMN compliance VARCHAR(50);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- V16: power_meter_battery is a status string (e.g. "GOOD"), not a number
|
||||
|
||||
ALTER TABLE intervals_activity
|
||||
DROP COLUMN power_meter_battery,
|
||||
ADD COLUMN power_meter_battery VARCHAR(20);
|
||||
@@ -0,0 +1,29 @@
|
||||
-- V17: Activity stream points stored in a TimescaleDB hypertable.
|
||||
-- The natural key (activity_id, time) is the primary key.
|
||||
-- TimescaleDB requires the partition column (time) to be part of the primary key.
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
|
||||
CREATE TABLE activity_stream_point (
|
||||
time TIMESTAMPTZ NOT NULL, -- actual UTC timestamp of the point
|
||||
activity_id BIGINT NOT NULL,
|
||||
elapsed_s INTEGER NOT NULL, -- elapsed seconds from activity start
|
||||
distance_m DOUBLE PRECISION,
|
||||
altitude_m DOUBLE PRECISION,
|
||||
heartrate_bpm INTEGER,
|
||||
watts INTEGER,
|
||||
cadence_rpm DOUBLE PRECISION,
|
||||
velocity_mps DOUBLE PRECISION,
|
||||
lat DOUBLE PRECISION,
|
||||
lng DOUBLE PRECISION,
|
||||
temp_c DOUBLE PRECISION,
|
||||
grade_smooth DOUBLE PRECISION,
|
||||
moving BOOLEAN,
|
||||
lr_balance DOUBLE PRECISION,
|
||||
extra JSONB, -- any stream types not covered above
|
||||
PRIMARY KEY (activity_id, time)
|
||||
);
|
||||
|
||||
SELECT create_hypertable('activity_stream_point', by_range('time'));
|
||||
|
||||
CREATE INDEX ON activity_stream_point (activity_id, time DESC);
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.peakup.activity.importer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.peakup.activity.model.ActivityAttachment;
|
||||
import com.peakup.activity.model.ActivityStreamPoint;
|
||||
import com.peakup.activity.model.ActivityZone;
|
||||
import com.peakup.activity.model.IgnorePart;
|
||||
import com.peakup.activity.model.IntervalsActivity;
|
||||
import com.peakup.connector.intervals.IntervalsClient;
|
||||
import com.peakup.connector.intervals.IntervalsContext;
|
||||
@@ -13,7 +15,10 @@ import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.peakup.common.NullableUtils.defaultValue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@@ -21,6 +26,106 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
@QuarkusTest
|
||||
class IntervalsActivityMappingTest {
|
||||
|
||||
// StreamsData field → ActivityStreamPoint field(s)
|
||||
// "time" covers both: elapsedS (the raw elapsed value) and the inherited `time` (computed Instant)
|
||||
private static final Map<String, List<String>> STREAMS_DTO_TO_ENTITY;
|
||||
static {
|
||||
Map<String, List<String>> m = new LinkedHashMap<>();
|
||||
m.put("time", List.of("elapsedS", "time"));
|
||||
m.put("distance", List.of("distanceM"));
|
||||
m.put("altitude", List.of("altitudeM"));
|
||||
m.put("heartrate", List.of("heartrateBpm"));
|
||||
m.put("watts", List.of("watts"));
|
||||
m.put("cadence", List.of("cadenceRpm"));
|
||||
m.put("velocity_smooth", List.of("velocityMps"));
|
||||
m.put("latlng", List.of("lat"));
|
||||
m.put("temp", List.of("tempC"));
|
||||
m.put("grade_smooth", List.of("gradeSmooth"));
|
||||
m.put("moving", List.of("moving"));
|
||||
m.put("left_right_balance", List.of("lrBalance"));
|
||||
m.put("unknownStreams", List.of("extra"));
|
||||
STREAMS_DTO_TO_ENTITY = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
private static final Map<String, List<String>> DTO_TO_ENTITY;
|
||||
static {
|
||||
Map<String, List<String>> m = new LinkedHashMap<>();
|
||||
m.put("id", List.of("originId"));
|
||||
m.put("external_id", List.of("stravaOriginId"));
|
||||
m.put("start_date", List.of("startTime"));
|
||||
m.put("distance", List.of("distanceM"));
|
||||
m.put("moving_time", List.of("movingTimeS"));
|
||||
m.put("elapsed_time", List.of("elapsedTimeS"));
|
||||
m.put("average_speed", List.of("averageSpeedMps"));
|
||||
m.put("max_speed", List.of("maxSpeedMps"));
|
||||
m.put("average_heartrate", List.of("averageHeartrateBpm"));
|
||||
m.put("max_heartrate", List.of("maxHeartrateBPM"));
|
||||
m.put("average_cadence", List.of("averageCadenceRPM"));
|
||||
m.put("icu_average_watts", List.of("averagePowerWatt"));
|
||||
m.put("icu_weighted_avg_watts", List.of("normalizedPowerWatt"));
|
||||
m.put("icu_joules", List.of("totalWorkKJ"));
|
||||
m.put("total_elevation_gain", List.of("totalElevationGainM", "totalAscentM"));
|
||||
m.put("total_elevation_loss", List.of("totalDescentM"));
|
||||
m.put("min_altitude", List.of("minAltitudeM"));
|
||||
m.put("max_altitude", List.of("maxAltitudeM"));
|
||||
m.put("icu_training_load", List.of("trainingLoad"));
|
||||
m.put("icu_intensity", List.of("intensityFactor"));
|
||||
m.put("icu_efficiency_factor", List.of("efficiencyFactor"));
|
||||
m.put("icu_ftp", List.of("ftp"));
|
||||
m.put("icu_atl", List.of("atl"));
|
||||
m.put("icu_ctl", List.of("ctl"));
|
||||
m.put("icu_variability_index", List.of("variabilityIndex"));
|
||||
m.put("average_stride", List.of("averageStrideLengthM"));
|
||||
m.put("average_temp", List.of("averageTempC"));
|
||||
m.put("custom_zones", List.of("zones"));
|
||||
m.put("icu_hrr", List.of("hrrBpmDrop", "hrrStartBpm", "hrrEndBpm",
|
||||
"hrrStartTimeSec", "hrrEndTimeSec", "hrrAverageWatts"));
|
||||
m.put("gear", List.of("gearId", "gearName", "gearDistanceM", "gearPrimary"));
|
||||
m.put("icu_achievements", List.of()); // stored in ActivityAchievement, not on entity
|
||||
// New fields — unit-suffix renames
|
||||
// icu_ignore_time → icuIgnoreTime (Boolean flag, direct camelCase match)
|
||||
m.put("icu_recording_time", List.of("icuRecordingTimeS"));
|
||||
m.put("coasting_time", List.of("coastingTimeS"));
|
||||
m.put("icu_warmup_time", List.of("icuWarmupTimeS"));
|
||||
m.put("icu_cooldown_time", List.of("icuCooldownTimeS"));
|
||||
m.put("icu_distance", List.of("icuDistanceM"));
|
||||
m.put("min_temp", List.of("minTempC"));
|
||||
m.put("max_temp", List.of("maxTempC"));
|
||||
m.put("gap", List.of("gapMps"));
|
||||
m.put("crank_length", List.of("crankLengthMm"));
|
||||
m.put("threshold_pace", List.of("thresholdPaceMps"));
|
||||
m.put("icu_weight", List.of("icuWeightKg"));
|
||||
m.put("carbs_used", List.of("carbsUsedG"));
|
||||
m.put("icu_pm_w_prime", List.of("icuPmWPrimeJ"));
|
||||
m.put("icu_pm_p_max", List.of("icuPmPMaxWatt"));
|
||||
m.put("icu_rolling_w_prime", List.of("icuRollingWPrimeJ"));
|
||||
m.put("icu_rolling_p_max", List.of("icuRollingPMaxWatt"));
|
||||
m.put("ss_w_prime", List.of("ssWPrimeJ"));
|
||||
m.put("ss_p_max", List.of("ssPMaxWatt"));
|
||||
m.put("icu_w_prime", List.of("icuWPrimeJ"));
|
||||
m.put("p_max", List.of("pMaxWatt"));
|
||||
// created → icuCreated (to avoid clash with AbstractTimestampedEntity.createdAt)
|
||||
m.put("created", List.of("icuCreated"));
|
||||
// Second batch — unit-suffix renames
|
||||
m.put("pool_length", List.of("poolLengthM"));
|
||||
m.put("average_altitude", List.of("averageAltitudeM"));
|
||||
m.put("average_weather_temp", List.of("averageWeatherTempC"));
|
||||
m.put("min_weather_temp", List.of("minWeatherTempC"));
|
||||
m.put("max_weather_temp", List.of("maxWeatherTempC"));
|
||||
m.put("average_feels_like", List.of("averageFeelsLikeC"));
|
||||
m.put("min_feels_like", List.of("minFeelsLikeC"));
|
||||
m.put("max_feels_like", List.of("maxFeelsLikeC"));
|
||||
m.put("average_wind_speed", List.of("averageWindSpeedMps"));
|
||||
m.put("average_wind_gust", List.of("averageWindGustMps"));
|
||||
m.put("max_rain", List.of("maxRainMm"));
|
||||
m.put("max_snow", List.of("maxSnowMm"));
|
||||
m.put("carbs_ingested", List.of("carbsIngestedG"));
|
||||
m.put("pace", List.of("paceMps"));
|
||||
// group → activityGroup (SQL reserved word avoidance)
|
||||
m.put("group", List.of("activityGroup"));
|
||||
DTO_TO_ENTITY = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
private static final String INTERVALS_ATHLETE_ID = "i157824";
|
||||
private static final List<String> ORIGIN_IDS = List.of(
|
||||
"i132106734", "i130231052", "i112771483", "i120252407", "i94213198"
|
||||
@@ -43,10 +148,6 @@ class IntervalsActivityMappingTest {
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
IntervalsClient intervalsClient;
|
||||
|
||||
@Inject
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
ObjectMapper objectMapper;
|
||||
|
||||
private long athleteId;
|
||||
|
||||
@BeforeEach
|
||||
@@ -64,10 +165,169 @@ class IntervalsActivityMappingTest {
|
||||
for (String originId : ORIGIN_IDS) {
|
||||
IntervalsClient.IntervalsActivity raw = intervalsClient.getActivity(originId);
|
||||
IntervalsActivity mapped = importer.map(raw, athleteId);
|
||||
checkCoverage(raw, "[" + originId + "] ");
|
||||
assertMapping(raw, mapped, "[" + originId + "] ");
|
||||
}
|
||||
}
|
||||
|
||||
private void checkCoverage(IntervalsClient.IntervalsActivity raw, String ctx) {
|
||||
assertTrue(raw.unknownFields.isEmpty(),
|
||||
ctx + "API returned JSON fields not declared in DTO: " + raw.unknownFields.keySet());
|
||||
|
||||
for (java.lang.reflect.Field dtoField : IntervalsClient.IntervalsActivity.class.getDeclaredFields()) {
|
||||
if (!java.lang.reflect.Modifier.isPublic(dtoField.getModifiers())) continue;
|
||||
String dtoName = dtoField.getName();
|
||||
if (dtoName.equals("unknownFields")) continue;
|
||||
|
||||
List<String> entityNames = DTO_TO_ENTITY.getOrDefault(dtoName, List.of(snakeToCamel(dtoName)));
|
||||
|
||||
for (String entityName : entityNames) {
|
||||
assertNotNull(findEntityField(entityName),
|
||||
ctx + "DTO field '" + dtoName + "' → no entity field '" + entityName + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private @jakarta.annotation.Nullable java.lang.reflect.Field findEntityField(String name) {
|
||||
Class<?> cls = IntervalsActivity.class;
|
||||
while (cls != null && cls != Object.class) {
|
||||
try { return cls.getDeclaredField(name); }
|
||||
catch (NoSuchFieldException ignored) { cls = cls.getSuperclass(); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private @jakarta.annotation.Nullable java.lang.reflect.Field findStreamEntityField(String name) {
|
||||
Class<?> cls = ActivityStreamPoint.class;
|
||||
while (cls != null && cls != Object.class) {
|
||||
try { return cls.getDeclaredField(name); }
|
||||
catch (NoSuchFieldException ignored) { cls = cls.getSuperclass(); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStreamsMappedCorrectly() {
|
||||
for (String originId : ORIGIN_IDS) {
|
||||
IntervalsClient.IntervalsActivity raw = intervalsClient.getActivity(originId);
|
||||
if (raw.start_date == null) continue;
|
||||
IntervalsClient.StreamsData streams = intervalsClient.getActivityStreams(originId);
|
||||
String ctx = "[" + originId + "] ";
|
||||
checkStreamsCoverage(ctx);
|
||||
if (streams.size() == 0) continue;
|
||||
List<ActivityStreamPoint> points = importer.mapStreams(streams, Instant.parse(raw.start_date), 999L);
|
||||
assertStreamsMapping(streams, points, Instant.parse(raw.start_date), ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reflection check: every StreamsData field has a corresponding ActivityStreamPoint field. */
|
||||
private void checkStreamsCoverage(String ctx) {
|
||||
for (java.lang.reflect.Field dtoField : IntervalsClient.StreamsData.class.getDeclaredFields()) {
|
||||
if (!java.lang.reflect.Modifier.isPublic(dtoField.getModifiers())) continue;
|
||||
String dtoName = dtoField.getName();
|
||||
List<String> entityNames = STREAMS_DTO_TO_ENTITY.getOrDefault(dtoName, List.of(snakeToCamel(dtoName)));
|
||||
for (String entityName : entityNames) {
|
||||
assertNotNull(findStreamEntityField(entityName),
|
||||
ctx + "StreamsData field '" + dtoName + "' → no ActivityStreamPoint field '" + entityName + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Value assertions for sampled points (first, middle, last). */
|
||||
private void assertStreamsMapping(IntervalsClient.StreamsData streams,
|
||||
List<ActivityStreamPoint> points,
|
||||
Instant startTime,
|
||||
String ctx) {
|
||||
assertEquals(streams.size(), points.size(), ctx + "point count");
|
||||
|
||||
int n = points.size();
|
||||
int[] indices = {0, n / 2, n - 1};
|
||||
for (int i : indices) {
|
||||
ActivityStreamPoint p = points.get(i);
|
||||
String pfx = ctx + "[" + i + "] ";
|
||||
|
||||
List<Integer> timeList = streams.time;
|
||||
assertNotNull(timeList, ctx + "time list");
|
||||
int elapsed = timeList.get(i);
|
||||
assertEquals(elapsed, p.getElapsedS(), pfx + "elapsedS");
|
||||
assertEquals(startTime.plusSeconds(elapsed), p.getTime(), pfx + "time");
|
||||
|
||||
if (streams.distance != null)
|
||||
assertEquals(streams.distance.get(i), p.getDistanceM(), pfx + "distanceM");
|
||||
else
|
||||
assertNull(p.getDistanceM(), pfx + "distanceM null");
|
||||
|
||||
if (streams.altitude != null)
|
||||
assertEquals(streams.altitude.get(i), p.getAltitudeM(), pfx + "altitudeM");
|
||||
else
|
||||
assertNull(p.getAltitudeM(), pfx + "altitudeM null");
|
||||
|
||||
if (streams.heartrate != null)
|
||||
assertEquals(streams.heartrate.get(i), p.getHeartrateBpm(), pfx + "heartrateBpm");
|
||||
else
|
||||
assertNull(p.getHeartrateBpm(), pfx + "heartrateBpm null");
|
||||
|
||||
if (streams.watts != null)
|
||||
assertEquals(streams.watts.get(i), p.getWatts(), pfx + "watts");
|
||||
else
|
||||
assertNull(p.getWatts(), pfx + "watts null");
|
||||
|
||||
if (streams.cadence != null)
|
||||
assertEquals(streams.cadence.get(i), p.getCadenceRpm(), pfx + "cadenceRpm");
|
||||
else
|
||||
assertNull(p.getCadenceRpm(), pfx + "cadenceRpm null");
|
||||
|
||||
if (streams.velocity_smooth != null)
|
||||
assertEquals(streams.velocity_smooth.get(i), p.getVelocityMps(), pfx + "velocityMps");
|
||||
else
|
||||
assertNull(p.getVelocityMps(), pfx + "velocityMps null");
|
||||
|
||||
if (streams.latlng != null)
|
||||
assertEquals(streams.latlng.get(i), p.getLat(), pfx + "lat");
|
||||
else
|
||||
assertNull(p.getLat(), pfx + "lat null");
|
||||
// lng not provided by intervals.icu streams
|
||||
assertNull(p.getLng(), pfx + "lng always null");
|
||||
|
||||
if (streams.temp != null)
|
||||
assertEquals(streams.temp.get(i), p.getTempC(), pfx + "tempC");
|
||||
else
|
||||
assertNull(p.getTempC(), pfx + "tempC null");
|
||||
|
||||
if (streams.grade_smooth != null)
|
||||
assertEquals(streams.grade_smooth.get(i), p.getGradeSmooth(), pfx + "gradeSmooth");
|
||||
else
|
||||
assertNull(p.getGradeSmooth(), pfx + "gradeSmooth null");
|
||||
|
||||
if (streams.moving != null)
|
||||
assertEquals(streams.moving.get(i), p.getMoving(), pfx + "moving");
|
||||
else
|
||||
assertNull(p.getMoving(), pfx + "moving null");
|
||||
|
||||
if (streams.left_right_balance != null)
|
||||
assertEquals(streams.left_right_balance.get(i), p.getLrBalance(), pfx + "lrBalance");
|
||||
else
|
||||
assertNull(p.getLrBalance(), pfx + "lrBalance null");
|
||||
|
||||
if (streams.unknownStreams.isEmpty())
|
||||
assertNull(p.getExtra(), pfx + "extra null");
|
||||
else
|
||||
assertNotNull(p.getExtra(), pfx + "extra non-null");
|
||||
}
|
||||
}
|
||||
|
||||
private String snakeToCamel(String snake) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean upper = false;
|
||||
for (int i = 0; i < snake.length(); i++) {
|
||||
char c = snake.charAt(i);
|
||||
if (c == '_') { upper = true; }
|
||||
else if (upper) { sb.append(Character.toUpperCase(c)); upper = false; }
|
||||
else { sb.append(c); }
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void assertMapping(IntervalsClient.IntervalsActivity raw, IntervalsActivity entity, String ctx) {
|
||||
// --- AbstractActivity fields ---
|
||||
assertEquals(defaultValue(raw.id), entity.originId, ctx + "originId");
|
||||
@@ -93,12 +353,19 @@ class IntervalsActivityMappingTest {
|
||||
assertEquals(raw.icu_joules != null ? raw.icu_joules / 1000.0 : 0.0, entity.totalWorkKJ, 0.001, ctx + "totalWorkKJ");
|
||||
assertEquals(defaultValue(raw.total_elevation_gain), entity.totalElevationGainM, 0.001, ctx + "totalElevationGainM");
|
||||
|
||||
// --- IntervalsActivity-specific fields ---
|
||||
// --- IntervalsActivity-specific scalar fields ---
|
||||
assertEquals(raw.description, entity.description, ctx + "description");
|
||||
assertEquals(raw.trainer, entity.trainer, ctx + "trainer");
|
||||
assertEquals(raw.commute, entity.commute, ctx + "commute");
|
||||
assertEquals(raw.race, entity.race, ctx + "race");
|
||||
assertEquals(raw.gear != null ? defaultValue(raw.gear.id) : "", entity.gearId, ctx + "gearId");
|
||||
if (raw.gear != null) {
|
||||
assertEquals(defaultValue(raw.gear.id), entity.gearId, ctx + "gearId");
|
||||
assertEquals(raw.gear.name, entity.gearName, ctx + "gearName");
|
||||
assertEquals(raw.gear.distance, entity.gearDistanceM, ctx + "gearDistanceM");
|
||||
assertEquals(raw.gear.primary, entity.gearPrimary, ctx + "gearPrimary");
|
||||
} else {
|
||||
assertEquals("", entity.gearId, ctx + "gearId (no gear)");
|
||||
}
|
||||
assertEquals(defaultValue(raw.total_elevation_gain), entity.totalAscentM, 0.001, ctx + "totalAscentM");
|
||||
assertEquals(defaultValue(raw.total_elevation_loss), entity.totalDescentM, 0.001, ctx + "totalDescentM");
|
||||
assertEquals(defaultValue(raw.min_altitude), entity.minAltitudeM, 0.001, ctx + "minAltitudeM");
|
||||
@@ -125,18 +392,213 @@ class IntervalsActivityMappingTest {
|
||||
assertEquals(raw.icu_hrr.average_watts, entity.hrrAverageWatts, ctx + "hrrAverageWatts");
|
||||
}
|
||||
|
||||
// --- Zone times (JSON) ---
|
||||
if (raw.icu_zone_times != null) {
|
||||
assertNotNull(entity.icuZoneTimes, ctx + "icuZoneTimes should not be null");
|
||||
try {
|
||||
assertEquals(
|
||||
objectMapper.readTree(objectMapper.writeValueAsString(raw.icu_zone_times)),
|
||||
objectMapper.readTree(entity.icuZoneTimes),
|
||||
ctx + "icuZoneTimes JSON content"
|
||||
);
|
||||
} catch (JsonProcessingException e) {
|
||||
fail(ctx + "icuZoneTimes JSON parse failed: " + e.getMessage());
|
||||
// --- recording_stops ---
|
||||
if (raw.recording_stops != null) {
|
||||
assertNotNull(entity.recordingStops, ctx + "recordingStops");
|
||||
assertEquals(raw.recording_stops.size(), entity.recordingStops.length, ctx + "recordingStops.length");
|
||||
for (int i = 0; i < raw.recording_stops.size(); i++) {
|
||||
assertEquals(raw.recording_stops.get(i).intValue(), entity.recordingStops[i], ctx + "recordingStops[" + i + "]");
|
||||
}
|
||||
}
|
||||
|
||||
// --- ignore_parts ---
|
||||
int expectedIgnoreParts = raw.ignore_parts != null ? raw.ignore_parts.size() : 0;
|
||||
assertEquals(expectedIgnoreParts, entity.ignoreParts.size(), ctx + "ignoreParts.size");
|
||||
if (raw.ignore_parts != null) {
|
||||
for (int i = 0; i < raw.ignore_parts.size(); i++) {
|
||||
IntervalsClient.IntervalsActivity.Ignore rawPart = raw.ignore_parts.get(i);
|
||||
IgnorePart part = entity.ignoreParts.get(i);
|
||||
assertEquals(rawPart.start_index != null ? rawPart.start_index : 0, part.startIndex, ctx + "ignoreParts[" + i + "].startIndex");
|
||||
assertEquals(rawPart.end_index != null ? rawPart.end_index : 0, part.endIndex, ctx + "ignoreParts[" + i + "].endIndex");
|
||||
assertEquals(rawPart.power, part.power, ctx + "ignoreParts[" + i + "].power");
|
||||
assertEquals(rawPart.pace, part.pace, ctx + "ignoreParts[" + i + "].pace");
|
||||
assertEquals(rawPart.hr, part.hr, ctx + "ignoreParts[" + i + "].hr");
|
||||
}
|
||||
}
|
||||
|
||||
// --- tags ---
|
||||
int expectedTags = raw.tags != null ? raw.tags.size() : 0;
|
||||
assertEquals(expectedTags, entity.tags.size(), ctx + "tags.size");
|
||||
if (raw.tags != null) {
|
||||
assertEquals(raw.tags, entity.tags, ctx + "tags");
|
||||
}
|
||||
|
||||
// --- zones ---
|
||||
if (raw.custom_zones != null) {
|
||||
int expectedZones = raw.custom_zones.stream()
|
||||
.mapToInt(zs -> zs.zones != null ? zs.zones.size() : 0)
|
||||
.sum();
|
||||
assertEquals(expectedZones, entity.zones.size(), ctx + "zones.size");
|
||||
int idx = 0;
|
||||
for (IntervalsClient.IntervalsActivity.ZoneSet zoneSet : raw.custom_zones) {
|
||||
if (zoneSet.zones == null) continue;
|
||||
for (IntervalsClient.IntervalsActivity.ZoneInfo zi : zoneSet.zones) {
|
||||
ActivityZone zone = entity.zones.get(idx++);
|
||||
assertEquals(defaultValue(zoneSet.code), zone.zoneSetCode, ctx + "zones[" + idx + "].zoneSetCode");
|
||||
assertEquals(defaultValue(zi.id), zone.zoneId, ctx + "zones[" + idx + "].zoneId");
|
||||
assertEquals(defaultValue(zi.start), zone.rangeStart, 0.001, ctx + "zones[" + idx + "].rangeStart");
|
||||
assertEquals(defaultValue(zi.end), zone.rangeEnd, 0.001, ctx + "zones[" + idx + "].rangeEnd");
|
||||
assertEquals(defaultValue(zi.start_value), zone.valueStart, 0.001, ctx + "zones[" + idx + "].valueStart");
|
||||
assertEquals(defaultValue(zi.end_value), zone.valueEnd, 0.001, ctx + "zones[" + idx + "].valueEnd");
|
||||
assertEquals(zi.secs != null ? zi.secs : 0, zone.secs, ctx + "zones[" + idx + "].secs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- attachments ---
|
||||
int expectedAttachments = raw.attachments != null ? raw.attachments.size() : 0;
|
||||
assertEquals(expectedAttachments, entity.attachments.size(), ctx + "attachments.size");
|
||||
if (raw.attachments != null) {
|
||||
for (int i = 0; i < raw.attachments.size(); i++) {
|
||||
IntervalsClient.IntervalsActivity.Attachment rawAtt = raw.attachments.get(i);
|
||||
ActivityAttachment att = entity.attachments.get(i);
|
||||
assertEquals(rawAtt.id, att.attachmentId, ctx + "attachments[" + i + "].attachmentId");
|
||||
assertEquals(rawAtt.filename, att.filename, ctx + "attachments[" + i + "].filename");
|
||||
assertEquals(rawAtt.mimetype, att.mimetype, ctx + "attachments[" + i + "].mimetype");
|
||||
assertEquals(rawAtt.url, att.url, ctx + "attachments[" + i + "].url");
|
||||
}
|
||||
}
|
||||
|
||||
// --- new extended fields ---
|
||||
assertEquals(raw.min_temp, entity.minTempC, ctx + "minTempC");
|
||||
assertEquals(raw.max_temp, entity.maxTempC, ctx + "maxTempC");
|
||||
assertEquals(raw.avg_lr_balance, entity.avgLrBalance, ctx + "avgLrBalance");
|
||||
assertEquals(raw.gap, entity.gapMps, ctx + "gapMps");
|
||||
assertEquals(raw.gap_model, entity.gapModel, ctx + "gapModel");
|
||||
assertEquals(raw.device_name, entity.deviceName, ctx + "deviceName");
|
||||
assertEquals(raw.device_watts, entity.deviceWatts, ctx + "deviceWatts");
|
||||
assertEquals(raw.power_meter, entity.powerMeter, ctx + "powerMeter");
|
||||
assertEquals(raw.power_meter_serial, entity.powerMeterSerial, ctx + "powerMeterSerial");
|
||||
assertEquals(raw.power_meter_battery, entity.powerMeterBattery, ctx + "powerMeterBattery");
|
||||
assertEquals(raw.crank_length, entity.crankLengthMm, ctx + "crankLengthMm");
|
||||
assertEquals(raw.power_field, entity.powerField, ctx + "powerField");
|
||||
assertEquals(raw.use_elevation_correction, entity.useElevationCorrection, ctx + "useElevationCorrection");
|
||||
assertEquals(raw.carbs_used, entity.carbsUsedG, ctx + "carbsUsedG");
|
||||
assertEquals(raw.icu_distance, entity.icuDistanceM, ctx + "icuDistanceM");
|
||||
assertEquals(raw.icu_ignore_time, entity.icuIgnoreTime, ctx + "icuIgnoreTime");
|
||||
assertEquals(raw.icu_recording_time, entity.icuRecordingTimeS, ctx + "icuRecordingTimeS");
|
||||
assertEquals(raw.coasting_time, entity.coastingTimeS, ctx + "coastingTimeS");
|
||||
assertEquals(raw.icu_warmup_time, entity.icuWarmupTimeS, ctx + "icuWarmupTimeS");
|
||||
assertEquals(raw.icu_cooldown_time, entity.icuCooldownTimeS, ctx + "icuCooldownTimeS");
|
||||
assertEquals(raw.sub_type, entity.subType, ctx + "subType");
|
||||
assertEquals(raw.paired_event_id, entity.pairedEventId, ctx + "pairedEventId");
|
||||
assertEquals(raw.icu_weight, entity.icuWeightKg, ctx + "icuWeightKg");
|
||||
assertEquals(raw.lthr, entity.lthr, ctx + "lthr");
|
||||
assertEquals(raw.icu_resting_hr, entity.icuRestingHr, ctx + "icuRestingHr");
|
||||
assertEquals(raw.threshold_pace, entity.thresholdPaceMps, ctx + "thresholdPaceMps");
|
||||
assertEquals(raw.icu_power_spike_threshold, entity.icuPowerSpikeThreshold, ctx + "icuPowerSpikeThreshold");
|
||||
assertEquals(raw.icu_sweet_spot_min, entity.icuSweetSpotMin, ctx + "icuSweetSpotMin");
|
||||
assertEquals(raw.icu_sweet_spot_max, entity.icuSweetSpotMax, ctx + "icuSweetSpotMax");
|
||||
assertEquals(raw.trimp, entity.trimp, ctx + "trimp");
|
||||
assertEquals(raw.icu_w_prime, entity.icuWPrimeJ, ctx + "icuWPrimeJ");
|
||||
assertEquals(raw.p_max, entity.pMaxWatt, ctx + "pMaxWatt");
|
||||
assertEquals(raw.icu_pm_cp, entity.icuPmCp, ctx + "icuPmCp");
|
||||
assertEquals(raw.icu_pm_w_prime, entity.icuPmWPrimeJ, ctx + "icuPmWPrimeJ");
|
||||
assertEquals(raw.icu_pm_p_max, entity.icuPmPMaxWatt, ctx + "icuPmPMaxWatt");
|
||||
assertEquals(raw.icu_pm_ftp, entity.icuPmFtp, ctx + "icuPmFtp");
|
||||
assertEquals(raw.icu_pm_ftp_secs, entity.icuPmFtpSecs, ctx + "icuPmFtpSecs");
|
||||
assertEquals(raw.icu_pm_ftp_watts, entity.icuPmFtpWatts, ctx + "icuPmFtpWatts");
|
||||
assertEquals(raw.icu_rolling_cp, entity.icuRollingCp, ctx + "icuRollingCp");
|
||||
assertEquals(raw.icu_rolling_w_prime, entity.icuRollingWPrimeJ, ctx + "icuRollingWPrimeJ");
|
||||
assertEquals(raw.icu_rolling_p_max, entity.icuRollingPMaxWatt, ctx + "icuRollingPMaxWatt");
|
||||
assertEquals(raw.icu_rolling_ftp, entity.icuRollingFtp, ctx + "icuRollingFtp");
|
||||
assertEquals(raw.icu_rolling_ftp_delta, entity.icuRollingFtpDelta, ctx + "icuRollingFtpDelta");
|
||||
assertEquals(raw.ss_cp, entity.ssCp, ctx + "ssCp");
|
||||
assertEquals(raw.ss_w_prime, entity.ssWPrimeJ, ctx + "ssWPrimeJ");
|
||||
assertEquals(raw.ss_p_max, entity.ssPMaxWatt, ctx + "ssPMaxWatt");
|
||||
assertEquals(raw.icu_ignore_power, entity.icuIgnorePower, ctx + "icuIgnorePower");
|
||||
assertEquals(raw.icu_ignore_hr, entity.icuIgnoreHr, ctx + "icuIgnoreHr");
|
||||
assertEquals(raw.ignore_velocity, entity.ignoreVelocity, ctx + "ignoreVelocity");
|
||||
assertEquals(raw.ignore_pace, entity.ignorePace, ctx + "ignorePace");
|
||||
assertEquals(raw.has_heartrate, entity.hasHeartrate, ctx + "hasHeartrate");
|
||||
assertEquals(raw.analyzed, entity.analyzed, ctx + "analyzed");
|
||||
assertEquals(raw.has_weather, entity.hasWeather, ctx + "hasWeather");
|
||||
assertEquals(raw.has_segments, entity.hasSegments, ctx + "hasSegments");
|
||||
assertEquals(raw.file_type, entity.fileType, ctx + "fileType");
|
||||
assertEquals(raw.file_sport_index, entity.fileSportIndex, ctx + "fileSportIndex");
|
||||
assertEquals(raw.icu_athlete_id, entity.icuAthleteId, ctx + "icuAthleteId");
|
||||
assertEquals(raw.created, entity.icuCreated, ctx + "icuCreated");
|
||||
assertEquals(raw.icu_sync_date, entity.icuSyncDate, ctx + "icuSyncDate");
|
||||
assertEquals(raw.icu_chat_id, entity.icuChatId, ctx + "icuChatId");
|
||||
assertEquals(raw.start_date_local, entity.startDateLocal, ctx + "startDateLocal");
|
||||
assertEquals(raw.skyline_chart_bytes, entity.skylineChartBytes, ctx + "skylineChartBytes");
|
||||
// complex JSON fields: null ↔ null, non-null ↔ non-null
|
||||
assertEquals(raw.icu_hr_zones == null, entity.icuHrZones == null, ctx + "icuHrZones nullness");
|
||||
assertEquals(raw.pace_zones == null, entity.paceZones == null, ctx + "paceZones nullness");
|
||||
assertEquals(raw.icu_power_zones == null, entity.icuPowerZones == null, ctx + "icuPowerZones nullness");
|
||||
assertEquals(raw.icu_training_load_data == null, entity.icuTrainingLoadData == null, ctx + "icuTrainingLoadData nullness");
|
||||
assertEquals(raw.interval_summary == null, entity.intervalSummary == null, ctx + "intervalSummary nullness");
|
||||
// stream_types
|
||||
if (raw.stream_types != null) {
|
||||
assertNotNull(entity.streamTypes, ctx + "streamTypes");
|
||||
assertEquals(raw.stream_types.size(), entity.streamTypes.length, ctx + "streamTypes.length");
|
||||
for (int i = 0; i < raw.stream_types.size(); i++) {
|
||||
assertEquals(raw.stream_types.get(i), entity.streamTypes[i], ctx + "streamTypes[" + i + "]");
|
||||
}
|
||||
} else {
|
||||
assertNull(entity.streamTypes, ctx + "streamTypes null");
|
||||
}
|
||||
|
||||
// --- second batch ---
|
||||
assertEquals(raw.icu_zone_times == null, entity.icuZoneTimes == null, ctx + "icuZoneTimes nullness");
|
||||
assertEquals(raw.icu_hr_zone_times == null, entity.icuHrZoneTimes == null, ctx + "icuHrZoneTimes nullness");
|
||||
assertEquals(raw.pace_zone_times == null, entity.paceZoneTimes == null, ctx + "paceZoneTimes nullness");
|
||||
assertEquals(raw.gap_zone_times == null, entity.gapZoneTimes == null, ctx + "gapZoneTimes nullness");
|
||||
assertEquals(raw.use_gap_zone_times, entity.useGapZoneTimes, ctx + "useGapZoneTimes");
|
||||
assertEquals(raw.tiz_order, entity.tizOrder, ctx + "tizOrder");
|
||||
assertEquals(raw.polarization_index, entity.polarizationIndex, ctx + "polarizationIndex");
|
||||
assertEquals(raw.decoupling, entity.decoupling, ctx + "decoupling");
|
||||
assertEquals(raw.icu_lap_count, entity.icuLapCount, ctx + "icuLapCount");
|
||||
assertEquals(raw.icu_joules_above_ftp, entity.icuJoulesAboveFtp, ctx + "icuJoulesAboveFtp");
|
||||
assertEquals(raw.icu_max_wbal_depletion, entity.icuMaxWbalDepletion, ctx + "icuMaxWbalDepletion");
|
||||
assertEquals(raw.icu_power_hr_z2, entity.icuPowerHrZ2, ctx + "icuPowerHrZ2");
|
||||
assertEquals(raw.icu_power_hr_z2_mins, entity.icuPowerHrZ2Mins, ctx + "icuPowerHrZ2Mins");
|
||||
assertEquals(raw.icu_cadence_z2, entity.icuCadenceZ2, ctx + "icuCadenceZ2");
|
||||
assertEquals(raw.icu_rpe, entity.icuRpe, ctx + "icuRpe");
|
||||
assertEquals(raw.icu_power_hr, entity.icuPowerHr, ctx + "icuPowerHr");
|
||||
assertEquals(raw.icu_median_time_delta, entity.icuMedianTimeDelta, ctx + "icuMedianTimeDelta");
|
||||
assertEquals(raw.p30s_exponent, entity.p30sExponent, ctx + "p30sExponent");
|
||||
assertEquals(raw.icu_intervals_edited, entity.icuIntervalsEdited, ctx + "icuIntervalsEdited");
|
||||
assertEquals(raw.lock_intervals, entity.lockIntervals, ctx + "lockIntervals");
|
||||
assertEquals(raw.workout_shift_secs, entity.workoutShiftSecs, ctx + "workoutShiftSecs");
|
||||
assertEquals(raw.pool_length, entity.poolLengthM, ctx + "poolLengthM");
|
||||
assertEquals(raw.lengths == null, entity.lengths == null, ctx + "lengths nullness");
|
||||
assertEquals(raw.power_load, entity.powerLoad, ctx + "powerLoad");
|
||||
assertEquals(raw.hr_load, entity.hrLoad, ctx + "hrLoad");
|
||||
assertEquals(raw.pace_load, entity.paceLoad, ctx + "paceLoad");
|
||||
assertEquals(raw.hr_load_type, entity.hrLoadType, ctx + "hrLoadType");
|
||||
assertEquals(raw.pace_load_type, entity.paceLoadType, ctx + "paceLoadType");
|
||||
assertEquals(raw.session_rpe, entity.sessionRpe, ctx + "sessionRpe");
|
||||
assertEquals(raw.strain_score, entity.strainScore, ctx + "strainScore");
|
||||
assertEquals(raw.kg_lifted, entity.kgLifted, ctx + "kgLifted");
|
||||
assertEquals(raw.average_altitude, entity.averageAltitudeM, ctx + "averageAltitudeM");
|
||||
assertEquals(raw.average_weather_temp, entity.averageWeatherTempC, ctx + "averageWeatherTempC");
|
||||
assertEquals(raw.min_weather_temp, entity.minWeatherTempC, ctx + "minWeatherTempC");
|
||||
assertEquals(raw.max_weather_temp, entity.maxWeatherTempC, ctx + "maxWeatherTempC");
|
||||
assertEquals(raw.average_feels_like, entity.averageFeelsLikeC, ctx + "averageFeelsLikeC");
|
||||
assertEquals(raw.min_feels_like, entity.minFeelsLikeC, ctx + "minFeelsLikeC");
|
||||
assertEquals(raw.max_feels_like, entity.maxFeelsLikeC, ctx + "maxFeelsLikeC");
|
||||
assertEquals(raw.average_wind_speed, entity.averageWindSpeedMps, ctx + "averageWindSpeedMps");
|
||||
assertEquals(raw.average_wind_gust, entity.averageWindGustMps, ctx + "averageWindGustMps");
|
||||
assertEquals(raw.prevailing_wind_deg, entity.prevailingWindDeg, ctx + "prevailingWindDeg");
|
||||
assertEquals(raw.headwind_percent, entity.headwindPercent, ctx + "headwindPercent");
|
||||
assertEquals(raw.tailwind_percent, entity.tailwindPercent, ctx + "tailwindPercent");
|
||||
assertEquals(raw.average_clouds, entity.averageClouds, ctx + "averageClouds");
|
||||
assertEquals(raw.max_rain, entity.maxRainMm, ctx + "maxRainMm");
|
||||
assertEquals(raw.max_snow, entity.maxSnowMm, ctx + "maxSnowMm");
|
||||
assertEquals(raw.carbs_ingested, entity.carbsIngestedG, ctx + "carbsIngestedG");
|
||||
assertEquals(raw.pace, entity.paceMps, ctx + "paceMps");
|
||||
assertEquals(raw.athlete_max_hr, entity.athleteMaxHr, ctx + "athleteMaxHr");
|
||||
assertEquals(raw.compliance, entity.compliance, ctx + "compliance");
|
||||
assertEquals(raw.coach_tick, entity.coachTick, ctx + "coachTick");
|
||||
assertEquals(raw.group, entity.activityGroup, ctx + "activityGroup");
|
||||
assertEquals(raw.icu_sync_error, entity.icuSyncError, ctx + "icuSyncError");
|
||||
assertEquals(raw.icu_color, entity.icuColor, ctx + "icuColor");
|
||||
assertEquals(raw.source, entity.source, ctx + "source");
|
||||
assertEquals(raw.oauth_client_id, entity.oauthClientId, ctx + "oauthClientId");
|
||||
assertEquals(raw.oauth_client_name, entity.oauthClientName, ctx + "oauthClientName");
|
||||
assertEquals(raw.strava_id, entity.stravaId, ctx + "stravaId");
|
||||
assertEquals(raw.route_id, entity.routeId, ctx + "routeId");
|
||||
assertEquals(raw.power_field_names == null, entity.powerFieldNames == null, ctx + "powerFieldNames nullness");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user