Aves_mio0.88/lib/widgets/activity/preview/activity_track_preview.dart
2026-10-03 12:51:15 +02:00

146 lines
4.2 KiB
Dart

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<List<LatLng>> 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;
}
}