Aves_mio0.88/lib/widgets/activity/preview/activity_entry_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_preview_point.dart';
import 'package:flutter/material.dart';
class ActivityEntryPreview extends StatelessWidget {
final List<ActivityPreviewPoint> 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<ActivityPreviewPoint> 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 = <int, List<ActivityPreviewPoint>>{};
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;
}
}