Nicer sharepics

This commit is contained in:
Tim Zöller 2025-12-04 22:08:56 +01:00
parent 74137a3395
commit a32b9e99e0
2 changed files with 161 additions and 56 deletions

View file

@ -60,7 +60,7 @@ public class ActivityImageService {
// Calculate bounds once for both map tiles and track rendering
TrackBounds trackBounds = null;
// Render background - either OSM tiles or dark background
// Render background - either OSM tiles or gradient background
if (activity.getTrackPointsJson() != null && !activity.getTrackPointsJson().isEmpty()) {
trackBounds = calculateTrackBounds(activity);
}
@ -75,20 +75,33 @@ public class ActivityImageService {
trackWidth, height);
g2d.drawImage(mapTiles, 0, 0, null);
// Dark background for metadata area (right 40%)
g2d.setColor(new Color(30, 30, 30));
g2d.fillRect(trackWidth, 0, width - trackWidth, height);
// 80s Aerobic style gradient background for metadata area (right 40%)
int metadataX = trackWidth;
GradientPaint gradient = new GradientPaint(
metadataX, 0, new Color(26, 0, 51), // Dark purple
width, height, new Color(45, 0, 82) // Lighter purple
);
g2d.setPaint(gradient);
g2d.fillRect(metadataX, 0, width - metadataX, height);
log.debug("Rendered OSM tiles for activity {}", activity.getId());
} catch (Exception e) {
log.warn("Failed to render OSM tiles, using dark background: {}", e.getMessage());
// Fallback to dark background
g2d.setColor(new Color(30, 30, 30));
log.warn("Failed to render OSM tiles, using gradient background: {}", e.getMessage());
// Fallback to gradient background
GradientPaint gradient = new GradientPaint(
0, 0, new Color(26, 0, 51),
width, height, new Color(45, 0, 82)
);
g2d.setPaint(gradient);
g2d.fillRect(0, 0, width, height);
}
} else {
// OSM tiles disabled or no track data - use dark background
g2d.setColor(new Color(30, 30, 30));
// OSM tiles disabled or no track data - use gradient background
GradientPaint gradient = new GradientPaint(
0, 0, new Color(26, 0, 51),
width, height, new Color(45, 0, 82)
);
g2d.setPaint(gradient);
g2d.fillRect(0, 0, width, height);
}
@ -176,12 +189,74 @@ public class ActivityImageService {
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));
// Draw track segments with privacy fade - 80s neon glow style
// First pass: draw glow effect (thicker, semi-transparent)
g2d.setStroke(new BasicStroke(8.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
final double HIDDEN_DISTANCE = 100.0; // First/last 100m completely hidden
final double FADE_DISTANCE = 200.0; // Fade zone from 100m to 200m
// Draw glow pass
for (int i = 0; i < trackPoints.size() - 1; i++) {
Map<String, Object> point1 = trackPoints.get(i);
Map<String, Object> point2 = trackPoints.get(i + 1);
Double lat1 = getDouble(point1, "latitude");
Double lon1 = getDouble(point1, "longitude");
Double lat2 = getDouble(point2, "latitude");
Double lon2 = getDouble(point2, "longitude");
if (lat1 != null && lon1 != null && lat2 != null && lon2 != null) {
// Convert lat/lon to Web Mercator coordinates (same projection as OSM tiles)
double mercatorX1 = longitudeToWebMercatorX(lon1);
double mercatorY1 = latitudeToWebMercatorY(lat1);
double mercatorX2 = longitudeToWebMercatorX(lon2);
double mercatorY2 = latitudeToWebMercatorY(lat2);
// 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];
double distanceFromEnd = totalDistance - cumulativeDistances[i];
// Calculate fade opacity (0.0 to 1.0)
float opacity = 1.0f;
// Hide first 100m completely, fade in from 100m to 200m
if (distanceFromStart < HIDDEN_DISTANCE) {
opacity = 0.0f;
} else if (distanceFromStart < FADE_DISTANCE) {
opacity = Math.min(opacity, (float) ((distanceFromStart - HIDDEN_DISTANCE) / (FADE_DISTANCE - HIDDEN_DISTANCE)));
}
// Hide last 100m completely, fade out from 200m to 100m before end
if (distanceFromEnd < HIDDEN_DISTANCE) {
opacity = 0.0f;
} else if (distanceFromEnd < FADE_DISTANCE) {
opacity = Math.min(opacity, (float) ((distanceFromEnd - HIDDEN_DISTANCE) / (FADE_DISTANCE - HIDDEN_DISTANCE)));
}
// Skip completely transparent segments
if (opacity <= 0.0f) {
continue;
}
// Apply opacity to glow color (semi-transparent cyan)
int alpha = Math.max(0, Math.min(128, (int) (opacity * 128))); // Max 50% alpha for glow
g2d.setColor(new Color(0, 255, 255, alpha));
// Draw glow segment
g2d.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
}
}
// Second pass: draw main track line (thinner, full opacity)
g2d.setStroke(new BasicStroke(4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
for (int i = 0; i < trackPoints.size() - 1; i++) {
Map<String, Object> point1 = trackPoints.get(i);
Map<String, Object> point2 = trackPoints.get(i + 1);
@ -232,9 +307,9 @@ public class ActivityImageService {
continue;
}
// Apply opacity to track color
// Apply opacity to track color - neon cyan for 80s style
int alpha = Math.max(0, Math.min(255, (int) (opacity * 255)));
g2d.setColor(new Color(0, 180, 216, alpha)); // Bright blue with alpha
g2d.setColor(new Color(0, 255, 255, alpha)); // Neon cyan with alpha
// Draw segment
g2d.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
@ -300,50 +375,69 @@ public class ActivityImageService {
}
/**
* Draw metadata overlay on the right side of the image.
* Draw metadata overlay on the right side of the image in 80s Aerobic style.
*/
private void drawMetadata(Graphics2D g2d, Activity activity, int width, int height) {
int metadataX = (int) (width * 0.62); // Start at 62% to leave some margin
int y = 60;
int y = 80;
int lineHeight = 50;
// Title
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("Arial", Font.BOLD, 32));
String title = activity.getTitle() != null ? activity.getTitle() : "Activity";
if (title.length() > 20) {
title = title.substring(0, 20) + "...";
// Neon colors
Color neonPink = new Color(255, 20, 147);
Color neonCyan = new Color(0, 255, 255);
Color neonOrange = new Color(255, 102, 0);
Color neonGreen = new Color(57, 255, 20);
// Title with neon pink
g2d.setColor(neonPink);
g2d.setFont(new Font("Arial Black", Font.BOLD, 36));
String title = activity.getTitle() != null ? activity.getTitle() : "ACTIVITY";
title = title.toUpperCase();
if (title.length() > 18) {
title = title.substring(0, 18) + "...";
}
g2d.drawString(title, metadataX, y);
y += lineHeight + 20;
// Activity type badge with border
g2d.setFont(new Font("Arial Black", Font.BOLD, 20));
String formattedType = ActivityFormatter.formatActivityType(activity.getActivityType()).toUpperCase();
// Draw border box around type
int typeWidth = g2d.getFontMetrics().stringWidth(formattedType);
int boxPadding = 12;
g2d.setColor(neonCyan);
g2d.setStroke(new BasicStroke(3.0f));
g2d.drawRect(metadataX - boxPadding, y - 25, typeWidth + boxPadding * 2, 35);
g2d.drawString(formattedType, metadataX, y);
y += lineHeight + 10;
// Activity type
g2d.setFont(new Font("Arial", Font.PLAIN, 24));
g2d.setColor(new Color(200, 200, 200));
String formattedType = ActivityFormatter.formatActivityType(activity.getActivityType());
g2d.drawString(formattedType, metadataX, y);
y += lineHeight;
// Distance
// Distance with neon orange value
if (activity.getTotalDistance() != null) {
g2d.setFont(new Font("Arial", Font.BOLD, 28));
g2d.setColor(Color.WHITE);
String distance = String.format("%.2f km", activity.getTotalDistance().doubleValue() / 1000.0);
g2d.setFont(new Font("Arial Black", Font.BOLD, 40));
g2d.setColor(neonOrange);
String distance = String.format("%.2f", activity.getTotalDistance().doubleValue() / 1000.0);
g2d.drawString(distance, metadataX, y);
g2d.setFont(new Font("Arial", Font.PLAIN, 18));
g2d.setColor(new Color(150, 150, 150));
g2d.drawString("Distance", metadataX, y + 25);
y += lineHeight + 30;
g2d.setFont(new Font("Arial Black", Font.BOLD, 22));
g2d.setColor(Color.WHITE);
int distanceWidth = g2d.getFontMetrics(new Font("Arial Black", Font.BOLD, 40)).stringWidth(distance);
g2d.drawString("KM", metadataX + distanceWidth + 10, y);
g2d.setFont(new Font("Arial Black", Font.PLAIN, 16));
g2d.setColor(new Color(180, 180, 180));
g2d.drawString("DISTANCE", metadataX, y + 22);
y += lineHeight + 35;
}
// Duration
// Duration with neon cyan value
if (activity.getTotalDurationSeconds() != null) {
long hours = activity.getTotalDurationSeconds() / 3600;
long minutes = (activity.getTotalDurationSeconds() % 3600) / 60;
long seconds = activity.getTotalDurationSeconds() % 60;
g2d.setFont(new Font("Arial", Font.BOLD, 28));
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("Arial Black", Font.BOLD, 40));
g2d.setColor(neonCyan);
String duration;
if (hours > 0) {
duration = String.format("%d:%02d:%02d", hours, minutes, seconds);
@ -351,28 +445,39 @@ public class ActivityImageService {
duration = String.format("%d:%02d", minutes, seconds);
}
g2d.drawString(duration, metadataX, y);
g2d.setFont(new Font("Arial", Font.PLAIN, 18));
g2d.setColor(new Color(150, 150, 150));
g2d.drawString("Duration", metadataX, y + 25);
y += lineHeight + 30;
g2d.setFont(new Font("Arial Black", Font.PLAIN, 16));
g2d.setColor(new Color(180, 180, 180));
g2d.drawString("DURATION", metadataX, y + 22);
y += lineHeight + 35;
}
// Elevation gain
// Elevation gain with neon green value
if (activity.getElevationGain() != null) {
g2d.setFont(new Font("Arial", Font.BOLD, 28));
g2d.setColor(Color.WHITE);
String elevation = String.format("%.0f m", activity.getElevationGain());
g2d.setFont(new Font("Arial Black", Font.BOLD, 40));
g2d.setColor(neonGreen);
String elevation = String.format("%.0f", activity.getElevationGain());
g2d.drawString(elevation, metadataX, y);
g2d.setFont(new Font("Arial", Font.PLAIN, 18));
g2d.setColor(new Color(150, 150, 150));
g2d.drawString("Elevation Gain", metadataX, y + 25);
y += lineHeight + 30;
g2d.setFont(new Font("Arial Black", Font.BOLD, 22));
g2d.setColor(Color.WHITE);
int elevationWidth = g2d.getFontMetrics(new Font("Arial Black", Font.BOLD, 40)).stringWidth(elevation);
g2d.drawString("M", metadataX + elevationWidth + 10, y);
g2d.setFont(new Font("Arial Black", Font.PLAIN, 16));
g2d.setColor(new Color(180, 180, 180));
g2d.drawString("ELEVATION", metadataX, y + 22);
y += lineHeight + 35;
}
// Branding
g2d.setFont(new Font("Arial", Font.PLAIN, 20));
g2d.setColor(new Color(100, 100, 100));
g2d.drawString("FitPub", metadataX, height - 40);
// Branding with neon pink gradient effect
g2d.setFont(new Font("Arial Black", Font.BOLD, 28));
g2d.setColor(neonPink);
g2d.drawString("FITPUB", metadataX, height - 50);
// Add decorative line above branding
g2d.setColor(neonCyan);
g2d.setStroke(new BasicStroke(3.0f));
g2d.drawLine(metadataX, height - 75, metadataX + 150, height - 75);
}
/**

View file

@ -52,7 +52,7 @@ class ActivityImageServiceTest {
* mvn test -Dtest=ActivityImageServiceTest#testGenerateActivityImage_Manual
*/
@Test
@Disabled("Manual test - run explicitly when needed")
//@Disabled("Manual test - run explicitly when needed")
@DisplayName("Generate activity image from test FIT file")
void testGenerateActivityImage_Manual() throws Exception {
// Load test FIT file