Improvements

This commit is contained in:
Tim Zöller 2026-04-07 01:03:42 +02:00
parent 139e5c57ff
commit 164e8b5894
8 changed files with 210 additions and 11 deletions

View file

@ -28,6 +28,8 @@ public class DebugController {
private final UserRepository userRepository;
private final net.javahippie.fitpub.service.PeakDetectionService peakDetectionService;
private final net.javahippie.fitpub.service.ActivityFileService activityFileService;
private final net.javahippie.fitpub.repository.ActivityRepository activityRepository;
@GetMapping("/validate-keys")
public Map<String, Object> validateKeys() {
@ -117,4 +119,49 @@ public class DebugController {
peakDetectionService.backfillAllActivities();
return org.springframework.http.ResponseEntity.ok(Map.of("status", "started"));
}
private final java.util.concurrent.atomic.AtomicBoolean elevationReprocessRunning =
new java.util.concurrent.atomic.AtomicBoolean(false);
@org.springframework.web.bind.annotation.PostMapping("/reprocess-gpx-elevation")
public org.springframework.http.ResponseEntity<Map<String, String>> reprocessGpxElevation() {
if (!elevationReprocessRunning.compareAndSet(false, true)) {
return org.springframework.http.ResponseEntity.ok(Map.of("status", "already running"));
}
new Thread(() -> {
try {
var ids = activityRepository.findAllIds();
log.info("Starting GPX elevation reprocessing for {} activities", ids.size());
int updated = 0, skipped = 0, failed = 0;
for (int i = 0; i < ids.size(); i++) {
try {
var activity = activityRepository.findById(ids.get(i)).orElse(null);
if (activity == null || !"GPX".equals(activity.getSourceFileFormat())) {
skipped++;
continue;
}
if (activityFileService.reprocessGpxElevation(activity)) {
updated++;
} else {
skipped++;
}
} catch (Exception e) {
failed++;
log.warn("Failed to reprocess elevation for {}: {}", ids.get(i), e.getMessage());
}
if ((i + 1) % 100 == 0) {
log.info("GPX elevation reprocess progress: {} / {} (updated={}, skipped={}, failed={})",
i + 1, ids.size(), updated, skipped, failed);
}
}
log.info("GPX elevation reprocessing complete: updated={}, skipped={}, failed={}", updated, skipped, failed);
} finally {
elevationReprocessRunning.set(false);
}
}, "gpx-elevation-reprocess").start();
return org.springframework.http.ResponseEntity.ok(Map.of("status", "started"));
}
}

View file

@ -29,6 +29,7 @@ public interface ActivityPeakRepository extends JpaRepository<ActivityPeak, Inte
JOIN peaks p ON p.id = ap.peak_id
JOIN activities a ON a.id = ap.activity_id
WHERE a.user_id = :userId
AND a.visibility = 'PUBLIC'
GROUP BY p.id, p.name, p.wikipedia
ORDER BY p.name
""",

View file

@ -13,12 +13,15 @@ public interface PeakRepository extends JpaRepository<Peak, Integer> {
/**
* Find all peaks within a given distance (meters) of an activity's simplified track.
* Uses ST_DWithin on geography type for accurate meter-based distance.
* Uses a two-stage approach:
* 1. ST_Expand + && for fast GIST index lookup on geometry
* 2. ST_DWithin on geography for precise meter-based filtering
*/
@Query(value = """
SELECT p.* FROM peaks p
JOIN activities a ON a.id = :activityId
WHERE a.simplified_track IS NOT NULL
AND p.geom && ST_Expand(a.simplified_track, 0.002)
AND ST_DWithin(CAST(p.geom AS geography), CAST(a.simplified_track AS geography), :distanceMeters)
ORDER BY p.name
""",

View file

@ -196,6 +196,42 @@ public class ActivityFileService {
}
}
/**
* Reprocess elevation for a GPX activity from its stored raw file.
* Only works for GPX files FIT files use their session summary.
*
* @param activity the activity to reprocess
* @return true if elevation was updated
*/
@Transactional
public boolean reprocessGpxElevation(Activity activity) {
if (!"GPX".equals(activity.getSourceFileFormat())) {
return false;
}
byte[] rawFile = activity.getRawActivityFile();
if (rawFile == null || rawFile.length == 0) {
log.debug("No raw file stored for activity {}, skipping", activity.getId());
return false;
}
try {
ParsedActivityData parsedData = gpxParser.parse(rawFile);
BigDecimal oldGain = activity.getElevationGain();
activity.setElevationGain(parsedData.getElevationGain());
activity.setElevationLoss(parsedData.getElevationLoss());
activityRepository.save(activity);
log.info("Reprocessed GPX elevation for activity {}: {}m -> {}m",
activity.getId(), oldGain, parsedData.getElevationGain());
return true;
} catch (Exception e) {
log.warn("Failed to reprocess elevation for activity {}: {}", activity.getId(), e.getMessage());
return false;
}
}
/**
* Detects file format from content and filename.
* Priority: magic bytes > XML header > file extension

View file

@ -43,6 +43,7 @@ public class BatchImportService {
private final BatchImportJobRepository batchImportJobRepository;
private final BatchImportFileResultRepository batchImportFileResultRepository;
private final ActivityFileService activityFileService;
private final PeakDetectionService peakDetectionService;
private final ActivityRepository activityRepository;
private final UserRepository userRepository;
private final PersonalRecordService personalRecordService;
@ -63,6 +64,7 @@ public class BatchImportService {
HeatmapGridService heatmapGridService,
TrainingLoadService trainingLoadService,
ActivitySummaryService activitySummaryService,
PeakDetectionService peakDetectionService,
@org.springframework.context.annotation.Lazy BatchImportService self) {
this.batchImportJobRepository = batchImportJobRepository;
this.batchImportFileResultRepository = batchImportFileResultRepository;
@ -74,6 +76,7 @@ public class BatchImportService {
this.heatmapGridService = heatmapGridService;
this.trainingLoadService = trainingLoadService;
this.activitySummaryService = activitySummaryService;
this.peakDetectionService = peakDetectionService;
this.self = self;
}
@ -258,6 +261,13 @@ public class BatchImportService {
ActivityFileService.ProcessingOptions.batchImportMode()
);
// Detect peaks (now fast thanks to GIST index hit)
try {
peakDetectionService.detectPeaksForActivity(activity);
} catch (Exception e) {
log.warn("Peak detection failed for activity {}: {}", activity.getId(), e.getMessage());
}
// Mark file as success
fileResult.setStatus(BatchImportFileResult.FileStatus.SUCCESS);
fileResult.setActivityId(activity.getId());

View file

@ -38,7 +38,7 @@ public class GpxParser {
private static final String GPX_NS = "http://www.topografix.com/GPX/1/1";
private static final String GPXTPX_NS = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1";
private static final double ELEVATION_NOISE_THRESHOLD = 2.0; // Ignore elevation changes < 2m
private static final double ELEVATION_NOISE_THRESHOLD = 0.0; // Sum all elevation changes (barometric data is smooth enough)
private static final double STOPPED_SPEED_THRESHOLD = 0.5; // km/h - below this is considered stopped
private static final long STOPPED_TIME_THRESHOLD = 30; // seconds - must be stopped this long to count