import 'dart:math' as math; import 'package:aves/model/activity/activity_track.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart' show LatLng; class ActivityTrackPreview extends StatelessWidget { final ActivityTrack activity; final double size; const ActivityTrackPreview({ super.key, required this.activity, this.size = 64, }); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return ClipRRect( borderRadius: BorderRadius.circular(12), child: DecoratedBox( decoration: BoxDecoration( color: scheme.surfaceContainerHighest, border: Border.all( color: scheme.outlineVariant, ), ), child: SizedBox.square( dimension: size, child: CustomPaint( painter: _ActivityTrackPreviewPainter( tracks: activity.tracks, lineColor: scheme.primary, pointColor: scheme.secondary, ), ), ), ), ); } } class _ActivityTrackPreviewPainter extends CustomPainter { final Set> tracks; final Color lineColor; final Color pointColor; const _ActivityTrackPreviewPainter({ required this.tracks, required this.lineColor, required this.pointColor, }); @override void paint(Canvas canvas, Size size) { final allPoints = tracks .expand((segment) => segment) .where( (point) => point.latitude.isFinite && point.longitude.isFinite && point.latitude.abs() <= 90 && point.longitude.abs() <= 180, ) .toList(); if (allPoints.isEmpty) { _drawEmpty(canvas, size); return; } final minLat = allPoints.map((p) => p.latitude).reduce((a, b) => a < b ? a : b); final maxLat = allPoints.map((p) => p.latitude).reduce((a, b) => a > b ? a : b); final minLon = allPoints.map((p) => p.longitude).reduce((a, b) => a < b ? a : b); final maxLon = allPoints.map((p) => p.longitude).reduce((a, b) => a > b ? a : b); final latSpan = math.max(0.000001, maxLat - minLat); final lonSpan = math.max(0.000001, maxLon - minLon); const padding = 8.0; final drawableWidth = math.max(1.0, size.width - padding * 2); final drawableHeight = math.max(1.0, size.height - padding * 2); final scale = math.min(drawableWidth / lonSpan, drawableHeight / latSpan); final usedWidth = lonSpan * scale; final usedHeight = latSpan * scale; final offsetX = padding + (drawableWidth - usedWidth) / 2; final offsetY = padding + (drawableHeight - usedHeight) / 2; Offset project(LatLng point) { final x = offsetX + (point.longitude - minLon) * scale; final y = offsetY + (maxLat - point.latitude) * scale; return Offset(x, y); } final paint = Paint() ..color = lineColor ..strokeWidth = 2.2 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round; for (final segment in tracks) { final validSegment = segment .where( (point) => point.latitude.isFinite && point.longitude.isFinite && point.latitude.abs() <= 90 && point.longitude.abs() <= 180, ) .toList(); if (validSegment.length < 2) continue; final first = project(validSegment.first); final path = Path()..moveTo(first.dx, first.dy); for (int i = 1; i < validSegment.length; i++) { final projected = project(validSegment[i]); path.lineTo(projected.dx, projected.dy); } canvas.drawPath(path, paint); } final start = project(allPoints.first); canvas.drawCircle( start, 3.2, Paint()..color = pointColor, ); } void _drawEmpty(Canvas canvas, Size size) { final paint = Paint() ..color = lineColor.withOpacity(0.4) ..strokeWidth = 1.5 ..style = PaintingStyle.stroke; canvas.drawLine( Offset(size.width * 0.25, size.height * 0.50), Offset(size.width * 0.75, size.height * 0.50), paint, ); } @override bool shouldRepaint(covariant _ActivityTrackPreviewPainter oldDelegate) { return oldDelegate.tracks != tracks || oldDelegate.lineColor != lineColor || oldDelegate.pointColor != pointColor; } }