import 'dart:math' as math; import 'package:aves/model/activity/activity_preview_point.dart'; import 'package:flutter/material.dart'; class ActivityEntryPreview extends StatelessWidget { final List points; final double size; const ActivityEntryPreview({ super.key, required this.points, 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: _ActivityEntryPreviewPainter( points: points, lineColor: scheme.primary, pointColor: scheme.secondary, ), ), ), ), ); } } class _ActivityEntryPreviewPainter extends CustomPainter { final List points; final Color lineColor; final Color pointColor; const _ActivityEntryPreviewPainter({ required this.points, required this.lineColor, required this.pointColor, }); @override void paint(Canvas canvas, Size size) { final validPoints = points .where( (point) => point.latitude.isFinite && point.longitude.isFinite && point.latitude.abs() <= 90 && point.longitude.abs() <= 180, ) .toList(); if (validPoints.isEmpty) { _drawEmpty(canvas, size); return; } final minLat = validPoints.map((p) => p.latitude).reduce((a, b) => a < b ? a : b); final maxLat = validPoints.map((p) => p.latitude).reduce((a, b) => a > b ? a : b); final minLon = validPoints.map((p) => p.longitude).reduce((a, b) => a < b ? a : b); final maxLon = validPoints.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(ActivityPreviewPoint 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; final grouped = >{}; for (final point in validPoints) { grouped.putIfAbsent(point.segmentIndex, () => []).add(point); } for (final segment in grouped.values) { segment.sort((a, b) => a.pointIndex.compareTo(b.pointIndex)); if (segment.length < 2) continue; final first = project(segment.first); final path = Path()..moveTo(first.dx, first.dy); for (int i = 1; i < segment.length; i++) { final projected = project(segment[i]); path.lineTo(projected.dx, projected.dy); } canvas.drawPath(path, paint); } final start = project(validPoints.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 _ActivityEntryPreviewPainter oldDelegate) { return oldDelegate.points != points || oldDelegate.lineColor != lineColor || oldDelegate.pointColor != pointColor; } }