diff --git a/src/main/java/org/operaton/fitpub/service/ActivityImageService.java b/src/main/java/org/operaton/fitpub/service/ActivityImageService.java index 65ee2e0..2184ba8 100644 --- a/src/main/java/org/operaton/fitpub/service/ActivityImageService.java +++ b/src/main/java/org/operaton/fitpub/service/ActivityImageService.java @@ -90,7 +90,8 @@ public class ActivityImageService { } /** - * Draw the track outline from high-resolution track points. + * Draw the track outline from high-resolution track points with privacy protection. + * Fades in/out the first and last 300 meters to hide start/end locations. */ private void drawTrack(Graphics2D g2d, Activity activity, int width, int height) { List> trackPoints = parseTrackPoints(activity.getTrackPointsJson()); @@ -98,6 +99,10 @@ public class ActivityImageService { return; } + // Calculate cumulative distances along the track + double[] cumulativeDistances = calculateCumulativeDistances(trackPoints); + double totalDistance = cumulativeDistances[cumulativeDistances.length - 1]; + // Find bounds double minLat = Double.MAX_VALUE, maxLat = -Double.MAX_VALUE; double minLon = Double.MAX_VALUE, maxLon = -Double.MAX_VALUE; @@ -129,38 +134,50 @@ public class ActivityImageService { double scaleY = trackHeight / (maxLat - minLat); double scale = Math.min(scaleX, scaleY); - // Create path - Path2D.Double path = new Path2D.Double(); - boolean first = true; - - for (Map point : trackPoints) { - Double lat = getDouble(point, "latitude"); - Double lon = getDouble(point, "longitude"); - if (lat != null && lon != null) { - double x = (lon - minLon) * scale; - double y = trackHeight - (lat - minLat) * scale; // Flip Y axis - - if (first) { - path.moveTo(x, y); - first = false; - } else { - path.lineTo(x, y); - } - } - } - - // Draw track - g2d.setColor(new Color(0, 180, 216)); // Bright blue + // Draw track segments with privacy fade g2d.setStroke(new BasicStroke(4.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - g2d.draw(path); - // Draw start and end markers - if (!trackPoints.isEmpty()) { - Map firstPoint = trackPoints.get(0); - Map lastPoint = trackPoints.get(trackPoints.size() - 1); + final double FADE_DISTANCE = 300.0; // 300 meters fade zone - drawMarker(g2d, firstPoint, minLat, minLon, scale, trackHeight, new Color(0, 255, 0)); // Green start - drawMarker(g2d, lastPoint, minLat, minLon, scale, trackHeight, new Color(255, 0, 0)); // Red end + for (int i = 0; i < trackPoints.size() - 1; i++) { + Map point1 = trackPoints.get(i); + Map 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) { + double x1 = (lon1 - minLon) * scale; + double y1 = trackHeight - (lat1 - minLat) * scale; + double x2 = (lon2 - minLon) * scale; + double y2 = trackHeight - (lat2 - minLat) * scale; + + // 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; + + if (distanceFromStart < FADE_DISTANCE) { + // Fade in from start + opacity = Math.min(opacity, (float) (distanceFromStart / FADE_DISTANCE)); + } + + if (distanceFromEnd < FADE_DISTANCE) { + // Fade out at end + opacity = Math.min(opacity, (float) (distanceFromEnd / FADE_DISTANCE)); + } + + // Apply opacity to track color + int alpha = Math.max(0, Math.min(255, (int) (opacity * 255))); + g2d.setColor(new Color(0, 180, 216, alpha)); // Bright blue with alpha + + // Draw segment + g2d.drawLine((int) x1, (int) y1, (int) x2, (int) y2); + } } } @@ -175,25 +192,50 @@ public class ActivityImageService { } /** - * Draw a circular marker at a track point. + * Calculate cumulative distances along the track using Haversine formula. + * Returns an array where each element is the total distance from the start to that point. */ - private void drawMarker(Graphics2D g2d, Map point, double minLat, double minLon, - double scale, int trackHeight, Color color) { - Double lat = getDouble(point, "latitude"); - Double lon = getDouble(point, "longitude"); - if (lat != null && lon != null) { - double x = (lon - minLon) * scale; - double y = trackHeight - (lat - minLat) * scale; + private double[] calculateCumulativeDistances(List> trackPoints) { + double[] distances = new double[trackPoints.size()]; + distances[0] = 0.0; - g2d.setColor(color); - int markerSize = 12; - g2d.fillOval((int) x - markerSize / 2, (int) y - markerSize / 2, markerSize, markerSize); + for (int i = 1; i < trackPoints.size(); i++) { + Map point1 = trackPoints.get(i - 1); + Map point2 = trackPoints.get(i); - // White outline - g2d.setColor(Color.WHITE); - g2d.setStroke(new BasicStroke(2.0f)); - g2d.drawOval((int) x - markerSize / 2, (int) y - markerSize / 2, markerSize, markerSize); + 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) { + double segmentDistance = haversineDistance(lat1, lon1, lat2, lon2); + distances[i] = distances[i - 1] + segmentDistance; + } else { + distances[i] = distances[i - 1]; + } } + + return distances; + } + + /** + * Calculate distance between two GPS coordinates using Haversine formula. + * Returns distance in meters. + */ + private double haversineDistance(double lat1, double lon1, double lat2, double lon2) { + final double EARTH_RADIUS = 6371000.0; // Earth radius in meters + + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + + double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return EARTH_RADIUS * c; } /** diff --git a/src/main/resources/static/js/fitpub.js b/src/main/resources/static/js/fitpub.js index 4e2c20b..ae878c4 100644 --- a/src/main/resources/static/js/fitpub.js +++ b/src/main/resources/static/js/fitpub.js @@ -270,71 +270,15 @@ function createActivityMap(containerId, geoJsonData, options = {}) { /** * Add start and finish markers to the map + * PRIVACY: This function is deprecated and does nothing. + * Start/finish markers are no longer displayed to protect athlete privacy. * * @param {Object} map - Leaflet map instance * @param {Object} geoJsonData - GeoJSON track data */ function addStartFinishMarkers(map, geoJsonData) { - if (!geoJsonData) { - return; - } - - let coordinates; - - // Handle both LineString and FeatureCollection - if (geoJsonData.type === 'LineString') { - coordinates = geoJsonData.coordinates; - } else if (geoJsonData.type === 'Feature') { - coordinates = geoJsonData.geometry.coordinates; - } else if (geoJsonData.type === 'FeatureCollection' && geoJsonData.features && geoJsonData.features.length > 0) { - coordinates = geoJsonData.features[0].geometry.coordinates; - } - - if (!coordinates || coordinates.length < 2) { - return; - } - - // Start marker (green) - const startCoord = coordinates[0]; - const startMarker = L.marker([startCoord[1], startCoord[0]], { - icon: L.divIcon({ - className: 'start-finish-marker', - html: `
`, - iconSize: [24, 24], - iconAnchor: [12, 12] - }), - title: 'Start' - }).addTo(map); - - startMarker.bindPopup('Start'); - - // Finish marker (red) - const finishCoord = coordinates[coordinates.length - 1]; - const finishMarker = L.marker([finishCoord[1], finishCoord[0]], { - icon: L.divIcon({ - className: 'start-finish-marker', - html: `
`, - iconSize: [24, 24], - iconAnchor: [12, 12] - }), - title: 'Finish' - }).addTo(map); - - finishMarker.bindPopup('Finish'); + // Privacy protection: Do not show start/end markers to hide athlete home locations + return; } /**