Stuff
This commit is contained in:
parent
1d7000d592
commit
7d07653d2a
12 changed files with 358 additions and 22 deletions
7
pom.xml
7
pom.xml
|
|
@ -100,6 +100,13 @@
|
|||
<version>21.141.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Timezone lookup from GPS coordinates -->
|
||||
<dependency>
|
||||
<groupId>net.iakovlev</groupId>
|
||||
<artifactId>timeshape</artifactId>
|
||||
<version>2025b.26</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public class ActivityDTO {
|
|||
private String description;
|
||||
private LocalDateTime startedAt;
|
||||
private LocalDateTime endedAt;
|
||||
private String timezone; // IANA timezone ID (e.g., "Europe/Berlin")
|
||||
private String visibility;
|
||||
private BigDecimal totalDistance;
|
||||
private Long totalDurationSeconds;
|
||||
|
|
@ -97,6 +98,7 @@ public class ActivityDTO {
|
|||
.description(activity.getDescription())
|
||||
.startedAt(activity.getStartedAt())
|
||||
.endedAt(activity.getEndedAt())
|
||||
.timezone(activity.getTimezone())
|
||||
.visibility(activity.getVisibility().name())
|
||||
.totalDistance(activity.getTotalDistance())
|
||||
.elevationGain(activity.getElevationGain())
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ public class Activity {
|
|||
@Column(name = "ended_at", nullable = false)
|
||||
private LocalDateTime endedAt;
|
||||
|
||||
/**
|
||||
* Timezone ID where the activity was recorded (e.g., "Europe/Berlin", "America/New_York").
|
||||
* Stored to display activity times in the athlete's local timezone.
|
||||
* Defaults to UTC if timezone cannot be determined from FIT file.
|
||||
*/
|
||||
@Column(name = "timezone", length = 50)
|
||||
private String timezone;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Visibility visibility;
|
||||
|
|
|
|||
|
|
@ -153,6 +153,11 @@ public class ActivityImageService {
|
|||
// Get letterbox transformation from OSM renderer
|
||||
OsmTileRenderer.LetterboxTransform letterbox = osmTileRenderer.getLastLetterboxTransform();
|
||||
|
||||
if (letterbox == null) {
|
||||
log.warn("No letterbox transform available, track overlay may be misaligned");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert bounds to Web Mercator normalized coordinates (0-1)
|
||||
// This matches the projection used by OSM tiles
|
||||
double minX = longitudeToWebMercatorX(bounds.minLon);
|
||||
|
|
@ -160,16 +165,16 @@ public class ActivityImageService {
|
|||
double minY = latitudeToWebMercatorY(bounds.maxLat); // Note: maxLat -> minY (inverted)
|
||||
double maxY = latitudeToWebMercatorY(bounds.minLat); // Note: minLat -> maxY (inverted)
|
||||
|
||||
// Calculate scale to map Web Mercator coordinates to pixels
|
||||
// Apply letterbox scaling if available
|
||||
double baseScaleX = trackWidth / (maxX - minX);
|
||||
double baseScaleY = trackHeight / (maxY - minY);
|
||||
// The letterbox transform gives us the actual rendered area within trackWidth x trackHeight
|
||||
// We need to map our mercator coordinates to fit within that rendered area
|
||||
|
||||
double scaleX = letterbox != null ? baseScaleX * letterbox.scaleFactorX : baseScaleX;
|
||||
double scaleY = letterbox != null ? baseScaleY * letterbox.scaleFactorY : baseScaleY;
|
||||
// Calculate the mercator range that corresponds to the letterboxed (cropped/scaled) map
|
||||
double mercatorWidth = maxX - minX;
|
||||
double mercatorHeight = maxY - minY;
|
||||
|
||||
int offsetX = letterbox != null ? letterbox.offsetX : 0;
|
||||
int offsetY = letterbox != null ? letterbox.offsetY : 0;
|
||||
// The scale factors tell us how the mercator coordinates map to the letterboxed area
|
||||
double pixelsPerMercatorX = letterbox.scaledWidth / mercatorWidth;
|
||||
double pixelsPerMercatorY = letterbox.scaledHeight / mercatorHeight;
|
||||
|
||||
// Draw track segments with privacy fade
|
||||
g2d.setStroke(new BasicStroke(4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
|
||||
|
|
@ -193,12 +198,11 @@ public class ActivityImageService {
|
|||
double mercatorX2 = longitudeToWebMercatorX(lon2);
|
||||
double mercatorY2 = latitudeToWebMercatorY(lat2);
|
||||
|
||||
// Map Web Mercator coordinates to pixel coordinates
|
||||
// Apply letterbox offset
|
||||
double x1 = (mercatorX1 - minX) * scaleX + offsetX;
|
||||
double y1 = (mercatorY1 - minY) * scaleY + offsetY;
|
||||
double x2 = (mercatorX2 - minX) * scaleX + offsetX;
|
||||
double y2 = (mercatorY2 - minY) * scaleY + offsetY;
|
||||
// Map Web Mercator coordinates to pixel coordinates within the letterbox
|
||||
double x1 = (mercatorX1 - minX) * pixelsPerMercatorX + letterbox.offsetX;
|
||||
double y1 = (mercatorY1 - minY) * pixelsPerMercatorY + letterbox.offsetY;
|
||||
double x2 = (mercatorX2 - minX) * pixelsPerMercatorX + letterbox.offsetX;
|
||||
double y2 = (mercatorY2 - minY) * pixelsPerMercatorY + letterbox.offsetY;
|
||||
|
||||
// Calculate opacity based on distance from start/end
|
||||
double distanceFromStart = cumulativeDistances[i];
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ public class FitFileService {
|
|||
.description(description)
|
||||
.startedAt(parsedData.getStartTime())
|
||||
.endedAt(parsedData.getEndTime())
|
||||
.timezone(parsedData.getTimezone())
|
||||
.visibility(activityVisibility)
|
||||
.totalDistance(parsedData.getTotalDistance())
|
||||
.totalDurationSeconds(parsedData.getTotalDuration() != null ? parsedData.getTotalDuration().getSeconds() : null)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.operaton.fitpub.util;
|
|||
|
||||
import com.garmin.fit.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.iakovlev.timeshape.TimeZoneEngine;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.locationtech.jts.geom.Point;
|
||||
|
|
@ -21,6 +22,7 @@ import java.time.LocalDateTime;
|
|||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Parser for Garmin FIT files.
|
||||
|
|
@ -37,6 +39,9 @@ public class FitParser {
|
|||
private static final double SEMICIRCLES_TO_DEGREES = 180.0 / Math.pow(2, 31);
|
||||
private static final double MPS_TO_KPH = 3.6;
|
||||
|
||||
// Lazy-loaded timezone engine (expensive to initialize)
|
||||
private static TimeZoneEngine timezoneEngine = null;
|
||||
|
||||
/**
|
||||
* Parses a FIT file and returns the extracted data.
|
||||
*
|
||||
|
|
@ -96,8 +101,11 @@ public class FitParser {
|
|||
throw new FitFileProcessingException("No GPS track points found in FIT file");
|
||||
}
|
||||
|
||||
log.info("Successfully parsed FIT file: {} track points, activity type: {}",
|
||||
parsedData.getTrackPoints().size(), parsedData.getActivityType());
|
||||
// Determine timezone from first GPS coordinate
|
||||
determineTimezone(parsedData);
|
||||
|
||||
log.info("Successfully parsed FIT file: {} track points, activity type: {}, timezone: {}",
|
||||
parsedData.getTrackPoints().size(), parsedData.getActivityType(), parsedData.getTimezone());
|
||||
|
||||
return parsedData;
|
||||
} catch (FitRuntimeException e) {
|
||||
|
|
@ -280,6 +288,43 @@ public class FitParser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the timezone based on the first GPS coordinate.
|
||||
* Uses TimeZoneEngine library for accurate timezone lookup from coordinates.
|
||||
*/
|
||||
private void determineTimezone(ParsedFitData parsedData) {
|
||||
if (parsedData.getTrackPoints().isEmpty()) {
|
||||
parsedData.setTimezone("UTC");
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPointData firstPoint = parsedData.getTrackPoints().get(0);
|
||||
double latitude = firstPoint.getLatitude();
|
||||
double longitude = firstPoint.getLongitude();
|
||||
|
||||
try {
|
||||
// Lazy-load timezone engine (expensive initialization ~200ms first time)
|
||||
if (timezoneEngine == null) {
|
||||
log.info("Initializing TimeZoneEngine for timezone lookup...");
|
||||
timezoneEngine = TimeZoneEngine.initialize();
|
||||
}
|
||||
|
||||
Optional<ZoneId> zoneId = timezoneEngine.query(latitude, longitude);
|
||||
if (zoneId.isPresent()) {
|
||||
parsedData.setTimezone(zoneId.get().getId());
|
||||
log.debug("Determined timezone: {} from coordinates ({}, {})",
|
||||
zoneId.get().getId(), latitude, longitude);
|
||||
} else {
|
||||
log.warn("Could not determine timezone for coordinates ({}, {}), defaulting to UTC",
|
||||
latitude, longitude);
|
||||
parsedData.setTimezone("UTC");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error determining timezone, defaulting to UTC", e);
|
||||
parsedData.setTimezone("UTC");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts FIT DateTime to LocalDateTime.
|
||||
* FIT timestamps use a special epoch: December 31, 1989, 00:00:00 UTC.
|
||||
|
|
@ -397,6 +442,7 @@ public class FitParser {
|
|||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
private LocalDateTime activityTimestamp;
|
||||
private String timezone; // IANA timezone ID (e.g., "Europe/Berlin")
|
||||
private BigDecimal totalDistance;
|
||||
private Duration totalDuration;
|
||||
private BigDecimal elevationGain;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
-- V12: Add timezone column to activities table
|
||||
-- This allows storing the timezone where the activity was recorded,
|
||||
-- enabling proper display of activity times in the athlete's local timezone
|
||||
|
||||
-- Add timezone column (nullable for backward compatibility with existing activities)
|
||||
ALTER TABLE activities
|
||||
ADD COLUMN timezone VARCHAR(50);
|
||||
|
||||
-- Set default timezone to UTC for existing activities
|
||||
UPDATE activities
|
||||
SET timezone = 'UTC'
|
||||
WHERE timezone IS NULL;
|
||||
|
||||
-- Add comment explaining the column
|
||||
COMMENT ON COLUMN activities.timezone IS 'IANA timezone ID where the activity was recorded (e.g., Europe/Berlin, America/New_York). Used to display activity times in athlete''s local timezone.';
|
||||
|
|
@ -418,6 +418,58 @@ function formatPace(speed) {
|
|||
return `${minutes}:${seconds.toString().padStart(2, '0')} /km`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp with timezone awareness
|
||||
*
|
||||
* @param {string} timestamp - ISO timestamp or LocalDateTime string
|
||||
* @param {string} timezone - IANA timezone ID (e.g., "Europe/Berlin")
|
||||
* @param {object} options - Intl.DateTimeFormat options
|
||||
* @returns {string} Formatted date/time string
|
||||
*/
|
||||
function formatDateTimeWithTimezone(timestamp, timezone, options = {}) {
|
||||
if (!timestamp) return '';
|
||||
|
||||
// Parse the timestamp - backend sends LocalDateTime without 'Z'
|
||||
// We need to interpret it in the specified timezone
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Default options for date/time display
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
timeZone: timezone || 'UTC',
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat('en-US', defaultOptions).format(date);
|
||||
} catch (e) {
|
||||
console.error('Error formatting date with timezone:', e);
|
||||
// Fallback to simple formatting
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date with timezone awareness (date only, no time)
|
||||
*
|
||||
* @param {string} timestamp - ISO timestamp or LocalDateTime string
|
||||
* @param {string} timezone - IANA timezone ID
|
||||
* @returns {string} Formatted date string
|
||||
*/
|
||||
function formatDateWithTimezone(timestamp, timezone) {
|
||||
return formatDateTimeWithTimezone(timestamp, timezone, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: undefined,
|
||||
minute: undefined
|
||||
});
|
||||
}
|
||||
|
||||
// Make functions available globally for inline scripts
|
||||
window.FitPub = {
|
||||
createActivityMap,
|
||||
|
|
@ -425,5 +477,7 @@ window.FitPub = {
|
|||
showAlert,
|
||||
formatDuration,
|
||||
formatDistance,
|
||||
formatPace
|
||||
formatPace,
|
||||
formatDateTimeWithTimezone,
|
||||
formatDateWithTimezone
|
||||
};
|
||||
|
|
|
|||
|
|
@ -391,7 +391,11 @@
|
|||
document.getElementById('activityTitle').textContent = activity.title || 'Untitled Activity';
|
||||
document.getElementById('activityType').textContent = activity.activityType;
|
||||
document.getElementById('activityType').className = `activity-type-badge activity-type-${activity.activityType.toLowerCase()}`;
|
||||
document.getElementById('activityDate').textContent = new Date(activity.startedAt).toLocaleString();
|
||||
// Format date with timezone awareness
|
||||
document.getElementById('activityDate').textContent = FitPub.formatDateTimeWithTimezone(
|
||||
activity.startedAt,
|
||||
activity.timezone || 'UTC'
|
||||
);
|
||||
document.getElementById('activityVisibility').textContent = activity.visibility;
|
||||
|
||||
// Visibility icon
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@
|
|||
</span>
|
||||
<span class="ms-2">
|
||||
<i class="bi bi-calendar"></i>
|
||||
${new Date(activity.startedAt).toLocaleDateString()}
|
||||
${FitPub.formatDateWithTimezone(activity.startedAt, activity.timezone || 'UTC')}
|
||||
</span>
|
||||
<span class="ms-2 visibility-${activity.visibility.toLowerCase()}">
|
||||
<i class="bi bi-${getVisibilityIcon(activity.visibility)}"></i>
|
||||
|
|
|
|||
|
|
@ -303,16 +303,23 @@
|
|||
metadataSection.classList.remove('d-none');
|
||||
|
||||
// Populate summary
|
||||
const formattedDateTime = FitPub.formatDateTimeWithTimezone(
|
||||
response.startedAt,
|
||||
response.timezone || 'UTC'
|
||||
);
|
||||
const formattedDate = FitPub.formatDateWithTimezone(
|
||||
response.startedAt,
|
||||
response.timezone || 'UTC'
|
||||
);
|
||||
document.getElementById('summaryContent').innerHTML = `
|
||||
<p class="mb-1"><strong>Type:</strong> ${response.activityType || 'Unknown'}</p>
|
||||
<p class="mb-1"><strong>Distance:</strong> ${formatDistance(response.totalDistance)}</p>
|
||||
<p class="mb-1"><strong>Duration:</strong> ${formatDuration(response.totalDurationSeconds)}</p>
|
||||
<p class="mb-0"><strong>Date:</strong> ${new Date(response.startedAt).toLocaleString()}</p>
|
||||
<p class="mb-0"><strong>Date:</strong> ${formattedDateTime}</p>
|
||||
`;
|
||||
|
||||
// Pre-fill title with activity type and date
|
||||
const activityDate = new Date(response.startedAt);
|
||||
document.getElementById('title').value = `${response.activityType || 'Activity'} - ${activityDate.toLocaleDateString()}`;
|
||||
document.getElementById('title').value = `${response.activityType || 'Activity'} - ${formattedDate}`;
|
||||
}
|
||||
|
||||
// Reset form state
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
package org.operaton.fitpub.service;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.operaton.fitpub.model.entity.Activity;
|
||||
import org.operaton.fitpub.model.entity.ActivityMetrics;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.ActivityRepository;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.operaton.fitpub.util.FitParser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Manual test for ActivityImageService.
|
||||
* These tests are disabled by default and should only be run manually.
|
||||
*/
|
||||
@SpringBootTest(properties = {
|
||||
"fitpub.image.osm-tiles.enabled=true"
|
||||
})
|
||||
@ActiveProfiles("test")
|
||||
class ActivityImageServiceTest {
|
||||
|
||||
@Autowired
|
||||
private ActivityImageService activityImageService;
|
||||
|
||||
@Autowired
|
||||
private FitParser fitParser;
|
||||
|
||||
@Autowired
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Manual test to generate an activity image from the test FIT file.
|
||||
* The image will be written to target/test-activity-image.png.
|
||||
*
|
||||
* To run this test manually:
|
||||
* mvn test -Dtest=ActivityImageServiceTest#testGenerateActivityImage_Manual
|
||||
*/
|
||||
@Test
|
||||
@Disabled("Manual test - run explicitly when needed")
|
||||
@DisplayName("Generate activity image from test FIT file")
|
||||
void testGenerateActivityImage_Manual() throws Exception {
|
||||
// Load test FIT file
|
||||
Path fitFilePath = Paths.get("src/test/resources/69287079d5e0a4532ba818ee.fit");
|
||||
assertTrue(Files.exists(fitFilePath), "Test FIT file should exist");
|
||||
|
||||
byte[] fitFileData = Files.readAllBytes(fitFilePath);
|
||||
assertNotNull(fitFileData);
|
||||
assertTrue(fitFileData.length > 0, "FIT file should not be empty");
|
||||
|
||||
// Parse FIT file
|
||||
FitParser.ParsedFitData parsedData = fitParser.parse(fitFileData);
|
||||
assertNotNull(parsedData);
|
||||
assertNotNull(parsedData.getStartTime());
|
||||
assertNotNull(parsedData.getEndTime());
|
||||
assertFalse(parsedData.getTrackPoints().isEmpty(), "Should have track points");
|
||||
|
||||
System.out.println("Parsed FIT file:");
|
||||
System.out.println(" Start time: " + parsedData.getStartTime());
|
||||
System.out.println(" End time: " + parsedData.getEndTime());
|
||||
System.out.println(" Timezone: " + parsedData.getTimezone());
|
||||
System.out.println(" Track points: " + parsedData.getTrackPoints().size());
|
||||
System.out.println(" Activity type: " + parsedData.getActivityType());
|
||||
System.out.println(" Total distance: " + parsedData.getTotalDistance() + " m");
|
||||
System.out.println(" Total duration: " + parsedData.getTotalDuration());
|
||||
|
||||
// Create a test user with required fields
|
||||
User testUser = new User();
|
||||
testUser.setUsername("testuser_" + System.currentTimeMillis());
|
||||
testUser.setEmail("test@example.com");
|
||||
testUser.setPasswordHash("hashedpassword");
|
||||
testUser.setDisplayName("Test User");
|
||||
testUser.setEnabled(true);
|
||||
// Dummy RSA keys for ActivityPub (not used in this test)
|
||||
testUser.setPublicKey("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\n-----END PUBLIC KEY-----");
|
||||
testUser.setPrivateKey("-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC\n-----END PRIVATE KEY-----");
|
||||
testUser = userRepository.save(testUser);
|
||||
|
||||
// Create a test activity entity
|
||||
Activity activity = Activity.builder()
|
||||
.id(UUID.randomUUID())
|
||||
.userId(testUser.getId())
|
||||
.activityType(parsedData.getActivityType())
|
||||
.title("Test Activity - Manual Image Rendering")
|
||||
.description("This is a test activity for manual image rendering")
|
||||
.startedAt(parsedData.getStartTime())
|
||||
.endedAt(parsedData.getEndTime())
|
||||
.timezone(parsedData.getTimezone())
|
||||
.visibility(Activity.Visibility.PUBLIC)
|
||||
.totalDistance(parsedData.getTotalDistance())
|
||||
.totalDurationSeconds(parsedData.getTotalDuration() != null ? parsedData.getTotalDuration().getSeconds() : null)
|
||||
.elevationGain(parsedData.getElevationGain())
|
||||
.elevationLoss(parsedData.getElevationLoss())
|
||||
.build();
|
||||
|
||||
// Add metrics if available
|
||||
if (parsedData.getMetrics() != null) {
|
||||
ActivityMetrics metrics = parsedData.getMetrics().toEntity(activity);
|
||||
activity.setMetrics(metrics);
|
||||
}
|
||||
|
||||
// Convert track points to JSON
|
||||
String trackPointsJson = convertTrackPointsToJson(parsedData);
|
||||
activity.setTrackPointsJson(trackPointsJson);
|
||||
|
||||
// Save activity temporarily (needed for image generation)
|
||||
Activity savedActivity = activityRepository.save(activity);
|
||||
|
||||
try {
|
||||
System.out.println("\nGenerating activity image...");
|
||||
|
||||
// Generate the image
|
||||
String imageUrl = activityImageService.generateActivityImage(savedActivity);
|
||||
|
||||
System.out.println("Generated image URL: " + imageUrl);
|
||||
|
||||
if (imageUrl == null) {
|
||||
System.err.println("ERROR: Image generation returned null! Check logs for errors.");
|
||||
fail("Image generation failed - returned null");
|
||||
}
|
||||
|
||||
// Try multiple possible locations for the generated image
|
||||
String[] possiblePaths = {
|
||||
"fitpub-images/" + savedActivity.getId() + ".png",
|
||||
"/tmp/fitpub/images/" + savedActivity.getId() + ".png",
|
||||
System.getProperty("java.io.tmpdir") + "/fitpub/images/" + savedActivity.getId() + ".png"
|
||||
};
|
||||
|
||||
Path foundImagePath = null;
|
||||
for (String pathStr : possiblePaths) {
|
||||
Path testPath = Paths.get(pathStr);
|
||||
System.out.println("Checking: " + testPath.toAbsolutePath());
|
||||
if (Files.exists(testPath)) {
|
||||
foundImagePath = testPath;
|
||||
System.out.println("Found image at: " + testPath.toAbsolutePath());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundImagePath != null) {
|
||||
// Copy the generated image to target directory for easy inspection
|
||||
Path targetPath = Paths.get("target", "test-activity-image.png");
|
||||
Files.copy(foundImagePath, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
System.out.println("\n✓ SUCCESS! Image copied to: " + targetPath.toAbsolutePath());
|
||||
System.out.println("Open this file to inspect the generated image with track overlay.");
|
||||
|
||||
// Verify file size
|
||||
long fileSize = Files.size(targetPath);
|
||||
System.out.println("Image file size: " + fileSize + " bytes");
|
||||
assertTrue(fileSize > 1000, "Image file should be larger than 1KB");
|
||||
} else {
|
||||
System.err.println("ERROR: Generated image not found in any expected location!");
|
||||
fail("Generated image file not found");
|
||||
}
|
||||
|
||||
} finally {
|
||||
// Clean up test activity and user
|
||||
activityRepository.delete(savedActivity);
|
||||
userRepository.delete(testUser);
|
||||
System.out.println("\nTest activity and user cleaned up.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to convert parsed track points to JSON format.
|
||||
*/
|
||||
private String convertTrackPointsToJson(FitParser.ParsedFitData parsedData) throws IOException {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
|
||||
mapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule());
|
||||
|
||||
return mapper.writeValueAsString(parsedData.getTrackPoints());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue