diff --git a/img.png b/img.png new file mode 100644 index 0000000..a216405 Binary files /dev/null and b/img.png differ diff --git a/src/main/java/org/operaton/fitpub/service/ActivityImageService.java b/src/main/java/org/operaton/fitpub/service/ActivityImageService.java index bbccb0c..27a3ff4 100644 --- a/src/main/java/org/operaton/fitpub/service/ActivityImageService.java +++ b/src/main/java/org/operaton/fitpub/service/ActivityImageService.java @@ -150,11 +150,16 @@ public class ActivityImageService { int trackWidth = (int) (width * 0.6); int trackHeight = height; - // The OSM tile renderer already scales the image to fit trackWidth x trackHeight - // So we can use simple linear mapping from geographic bounds to pixel coordinates - double scaleX = trackWidth / (bounds.maxLon - bounds.minLon); - double scaleY = trackHeight / (bounds.maxLat - bounds.minLat); - double scale = Math.min(scaleX, scaleY); + // Convert bounds to Web Mercator normalized coordinates (0-1) + // This matches the projection used by OSM tiles + double minX = longitudeToWebMercatorX(bounds.minLon); + double maxX = longitudeToWebMercatorX(bounds.maxLon); + 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 + double scaleX = trackWidth / (maxX - minX); + double scaleY = trackHeight / (maxY - minY); // Draw track segments with privacy fade g2d.setStroke(new BasicStroke(4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); @@ -172,12 +177,17 @@ public class ActivityImageService { Double lon2 = getDouble(point2, "longitude"); if (lat1 != null && lon1 != null && lat2 != null && lon2 != null) { - // Convert geographic coordinates to pixel coordinates - // The OSM renderer already handles the projection, so we use linear mapping - double x1 = (lon1 - bounds.minLon) * scale; - double y1 = trackHeight - (lat1 - bounds.minLat) * scale; - double x2 = (lon2 - bounds.minLon) * scale; - double y2 = trackHeight - (lat2 - bounds.minLat) * scale; + // 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 + double x1 = (mercatorX1 - minX) * scaleX; + double y1 = (mercatorY1 - minY) * scaleY; + double x2 = (mercatorX2 - minX) * scaleX; + double y2 = (mercatorY2 - minY) * scaleY; // Calculate opacity based on distance from start/end double distanceFromStart = cumulativeDistances[i]; @@ -434,6 +444,24 @@ public class ActivityImageService { return new TrackBounds(minLat, maxLat, minLon, maxLon); } + /** + * Convert longitude to Web Mercator X coordinate (normalized 0-1). + * This must match the projection used by OsmTileRenderer. + */ + private double longitudeToWebMercatorX(double lon) { + return (lon + 180.0) / 360.0; + } + + /** + * Convert latitude to Web Mercator Y coordinate (normalized 0-1). + * This must match the projection used by OsmTileRenderer. + * Uses the same logarithmic transformation as OSM tiles. + */ + private double latitudeToWebMercatorY(double lat) { + return (1.0 - Math.log(Math.tan(Math.toRadians(lat)) + + 1.0 / Math.cos(Math.toRadians(lat))) / Math.PI) / 2.0; + } + /** * Helper class to store track geographic bounds. */