import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart' hide Path; import '../../domain/waypoint/flat_waypoint_list.dart'; /// Vollbild-Chart zum Anpassen von Hoehe/Geschwindigkeit per Drag auf den /// Punkten (HTML-Demonstrator: buildFullChart()/attachChartDrag()). Jeder /// Punkt entspricht einem Wegpunkt, X-Position proportional zur /// zurueckgelegten Distanz entlang der Route. /// /// Terrain-Overlay und Wind-Diamanten aus dem Demonstrator sind nicht /// portiert, da Terrain- (Doku 3.9) und Wind-System (Doku 3.8) in der /// Flutter-App noch nicht existieren. class FullValueChart extends StatefulWidget { const FullValueChart({ super.key, required this.waypoints, required this.values, required this.legBad, required this.vertexBad, required this.activeIndex, required this.min, required this.max, required this.step, required this.pxPerUnit, required this.unit, required this.onChanged, }); final List waypoints; final List values; final List legBad; final List vertexBad; final int activeIndex; final double min; final double max; final double step; final double pxPerUnit; final String unit; final void Function(int index, double value) onChanged; static const _distance = Distance(roundResult: false); static List cumulativeDistances(List waypoints) { final cum = [0]; for (var i = 1; i < waypoints.length; i++) { final a = LatLng(waypoints[i - 1].lat, waypoints[i - 1].lon); final b = LatLng(waypoints[i].lat, waypoints[i].lon); cum.add(cum[i - 1] + _distance(a, b)); } return cum; } @override State createState() => _FullValueChartState(); } class _FullValueChartState extends State { int? _dragIndex; double _dragStartY = 0; double _dragStartValue = 0; static const _padX = 50.0; static const _hitToleranceX = 28.0; ({double lo, double hi}) _range() { if (widget.values.isEmpty) return (lo: widget.min, hi: widget.max); final dataMin = widget.values.reduce(math.min); final dataMax = widget.values.reduce(math.max); final span = dataMax - dataMin; final pad = math.max(span * 0.35, widget.step * 2); var lo = dataMin - pad; var hi = dataMax + pad; final minSpan = widget.step * 6; if (hi - lo < minSpan) { final mid = (lo + hi) / 2; lo = mid - minSpan / 2; hi = mid + minSpan / 2; } if (lo < widget.min) lo = widget.min; if (hi > widget.max) hi = widget.max; if (hi <= lo) { lo = widget.min; hi = widget.max; } return (lo: lo, hi: hi); } double _xAt(int idx, List cumDist, double boxW) { final n = widget.waypoints.length; if (n <= 1) return boxW / 2; final total = cumDist.last == 0 ? 1.0 : cumDist.last; return _padX + (cumDist[idx] / total) * (boxW - 2 * _padX); } int? _nearestIndex(Offset local, List cumDist, double boxW) { var best = -1; var bestDist = double.infinity; for (var i = 0; i < widget.waypoints.length; i++) { final dx = (_xAt(i, cumDist, boxW) - local.dx).abs(); if (dx < bestDist) { bestDist = dx; best = i; } } return bestDist <= _hitToleranceX ? best : null; } @override Widget build(BuildContext context) { if (widget.waypoints.isEmpty) { return const Center( child: Text('No waypoints yet', style: TextStyle(color: Colors.white60)), ); } final cumDist = FullValueChart.cumulativeDistances(widget.waypoints); final range = _range(); return LayoutBuilder( builder: (context, constraints) { final size = Size(constraints.maxWidth, constraints.maxHeight); return GestureDetector( onPanStart: (details) { final idx = _nearestIndex(details.localPosition, cumDist, size.width); if (idx == null) return; _dragIndex = idx; _dragStartY = details.localPosition.dy; _dragStartValue = widget.values[idx]; }, onPanUpdate: (details) { final idx = _dragIndex; if (idx == null) return; final deltaY = _dragStartY - details.localPosition.dy; final deltaVal = deltaY / widget.pxPerUnit; var val = _dragStartValue + deltaVal; val = (val / widget.step).round() * widget.step; val = val.clamp(widget.min, widget.max); widget.onChanged(idx, val); }, onPanEnd: (_) => _dragIndex = null, onPanCancel: () => _dragIndex = null, child: CustomPaint( size: size, painter: _ChartPainter( waypoints: widget.waypoints, values: widget.values, cumDist: cumDist, legBad: widget.legBad, vertexBad: widget.vertexBad, activeIndex: widget.activeIndex, lo: range.lo, hi: range.hi, unit: widget.unit, ), ), ); }, ); } } class _ChartPainter extends CustomPainter { const _ChartPainter({ required this.waypoints, required this.values, required this.cumDist, required this.legBad, required this.vertexBad, required this.activeIndex, required this.lo, required this.hi, required this.unit, }); final List waypoints; final List values; final List cumDist; final List legBad; final List vertexBad; final int activeIndex; final double lo; final double hi; final String unit; static const _padX = 50.0; static const _padY = 20.0; double _xAt(int idx, double boxW) { final n = waypoints.length; if (n <= 1) return boxW / 2; final total = cumDist.last == 0 ? 1.0 : cumDist.last; return _padX + (cumDist[idx] / total) * (boxW - 2 * _padX); } double _yAt(double val, double boxH) => boxH - _padY - ((val - lo) / (hi - lo)) * (boxH - 2 * _padY); double _niceStep(double range, int targetTicks) { if (range <= 0) return 1; final rough = range / targetTicks; final mag = math.pow(10, (math.log(rough) / math.ln10).floor()).toDouble(); final norm = rough / mag; if (norm < 1.5) return mag; if (norm < 3) return 2 * mag; if (norm < 7) return 5 * mag; return 10 * mag; } @override void paint(Canvas canvas, Size size) { final boxW = size.width; final boxH = size.height; final n = waypoints.length; // Gridlines mit gerundeten Werten. final step = _niceStep(hi - lo, 5); final gridPaint = Paint() ..color = Colors.white.withValues(alpha: 0.14) ..strokeWidth = 1; final axisPaint = Paint() ..color = Colors.white.withValues(alpha: 0.35) ..strokeWidth = 1.5; canvas.drawLine(Offset(_padX, 0), Offset(_padX, boxH), axisPaint); final labelStyle = TextStyle(color: Colors.white.withValues(alpha: 0.6), fontSize: 10); var v = (lo / step).ceil() * step; while (v <= hi + 1e-6) { final y = _yAt(v, boxH); canvas.drawLine(Offset(_padX, y), Offset(boxW - _padX, y), gridPaint); final tp = TextPainter( text: TextSpan(text: '${v.round()}$unit', style: labelStyle), textDirection: TextDirection.ltr, )..layout(); tp.paint(canvas, Offset(_padX - 8 - tp.width, y - tp.height / 2)); v += step; } // Verbindungslinien (rot bei nicht fliegbarer Steig-/Sinkrate). for (var i = 0; i < n - 1; i++) { final bad = i < legBad.length && legBad[i]; canvas.drawLine( Offset(_xAt(i, boxW), _yAt(values[i], boxH)), Offset(_xAt(i + 1, boxW), _yAt(values[i + 1], boxH)), Paint() ..color = bad ? const Color(0xFFE2574A) : Colors.white ..strokeWidth = 2.5, ); } // Punkte (blau=aktiver Zielpunkt, rot=nicht fliegbar, sonst weiss). for (var i = 0; i < n; i++) { final active = i == activeIndex; final bad = i < vertexBad.length && vertexBad[i]; final color = active ? const Color(0xFF4D9DFF) : (bad ? const Color(0xFFE2574A) : Colors.white); final center = Offset(_xAt(i, boxW), _yAt(values[i], boxH)); canvas.drawCircle(center, active ? 9 : 7, Paint()..color = color); canvas.drawCircle( center, active ? 9 : 7, Paint() ..color = const Color(0xFF2F6FE0) ..style = PaintingStyle.stroke ..strokeWidth = 2, ); } } @override bool shouldRepaint(covariant _ChartPainter oldDelegate) => oldDelegate.values != values || oldDelegate.activeIndex != activeIndex || oldDelegate.lo != lo || oldDelegate.hi != hi; }