Rundet das Altitude-Tab um ein reales Gelaendeprofil ab, damit sichtbar wird, ob die geplante Flughoehe ausreichend Bodenabstand haelt: - domain/mission/terrain_math.dart: reine Sampling-/Geometrie-Funktionen (routeKeyForTerrain fuer Caching, distanzbasiertes Sampling alle 30m, lineare Sollhoehen-Interpolation, terrainDangerRanges fuer Abschnitte mit < 10m Bodenabstand) - unabhaengig testbar ohne Netzwerk/UI. - services/terrain/terrain_service.dart: laedt AWS-Terrarium-PNG-Kacheln (elevation-tiles-prod, zoom 13) und dekodiert sie ueber dart:ui (instantiateImageCodec/toByteData), ohne zusaetzliches Bildpaket. Elevation = R*256 + G + B/256 - 32768 pro Pixel; Kacheln werden pro Route nur einmal geladen (nach Kachel gruppierte Sample-Punkte). - ui/providers/terrain_provider.dart: cached das geladene Profil ueber routeKeyForTerrain - reine Werteaenderungen (Hoehe/Speed) loesen keinen erneuten Kachel-Download aus, nur eine tatsaechliche Ortsverschiebung. - ui/widgets/full_value_chart.dart: _paintDangerBands/_paintTerrain zeichnen rote Gefahrenbaender bzw. die braune Gelaende-Flaeche, in der gleichen Reihenfolge wie im HTML-Demonstrator (dangerHtml, grid, terrainHtml, dann Soll-Kurve obenauf). - ui/widgets/waypoint_list_panel.dart: triggert ensureFor() beim Wechsel auf den Altitude-Tab sowie bei Routenaenderungen waehrend dieser aktiv ist (ref.listenManual auf currentMissionProvider). Getestet: 13 neue Unit-Tests fuer die reine Geometrie/Sampling-Logik, 3 Service-Tests mit einer zur Testzeit synthetisch erzeugten PNG (dart:ui-Encoding, keine Testasset-Datei noetig). Alle 80 Tests sowie flutter analyze bestehen. Manuell auf dem Pixel_10a-Emulator verifiziert: Altitude-Tab zeigt das reale Amsterdam-Gelaendeprofil (nahe Meereshoehe) korrekt als Flaeche unter der Sollhoehen-Linie. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
515 lines
17 KiB
Dart
515 lines
17 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:latlong2/latlong.dart' hide Path;
|
|
|
|
import '../../domain/mission/terrain_math.dart';
|
|
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.
|
|
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,
|
|
this.legWind,
|
|
this.groundSpeeds,
|
|
this.terrainPoints,
|
|
this.terrainDangerRanges,
|
|
this.terrainLoading = false,
|
|
});
|
|
|
|
final List<Waypoint> waypoints;
|
|
final List<double> values;
|
|
final List<bool> legBad;
|
|
final List<bool> 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;
|
|
|
|
/// Kopf-/Rueckenwind-Komponente pro Leg (nur Speed-Tab, Doku 3.8) -
|
|
/// Laenge waypoints.length - 1, null wo noch kein Wind geladen ist.
|
|
final List<double?>? legWind;
|
|
|
|
/// Erreichbare Bodengeschwindigkeit pro Leg (nur Speed-Tab), gleiche
|
|
/// Laenge/Null-Semantik wie [legWind].
|
|
final List<double?>? groundSpeeds;
|
|
|
|
/// Gelaendeprofil entlang der Route (nur Altitude-Tab, Doku 3.9), relativ
|
|
/// zum ersten gueltigen Sample - null, solange noch nicht geladen.
|
|
final List<TerrainSamplePoint>? terrainPoints;
|
|
|
|
/// Streckenabschnitte mit zu geringem Bodenabstand (Doku 3.9), als
|
|
/// [start, end]-Distanzpaare.
|
|
final List<(double, double)>? terrainDangerRanges;
|
|
|
|
/// Zeigt einen dezenten Hinweis, solange das Gelaendeprofil noch laedt.
|
|
final bool terrainLoading;
|
|
|
|
static const _distance = Distance(roundResult: false);
|
|
|
|
static List<double> cumulativeDistances(List<Waypoint> waypoints) {
|
|
final cum = <double>[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<FullValueChart> createState() => _FullValueChartState();
|
|
}
|
|
|
|
class _FullValueChartState extends State<FullValueChart> {
|
|
int? _dragIndex;
|
|
double _dragStartY = 0;
|
|
double _dragStartValue = 0;
|
|
|
|
static const _padX = 50.0;
|
|
static const _hitToleranceX = 28.0;
|
|
|
|
({double lo, double hi}) _range() {
|
|
final allVals = <double>[
|
|
...widget.values,
|
|
for (final v in widget.legWind ?? const <double?>[]) ?v,
|
|
for (final v in widget.groundSpeeds ?? const <double?>[]) ?v,
|
|
for (final p in widget.terrainPoints ?? const <TerrainSamplePoint>[])
|
|
?p.elevationRelM,
|
|
];
|
|
if (allVals.isEmpty) return (lo: widget.min, hi: widget.max);
|
|
final dataMin = allVals.reduce(math.min);
|
|
final dataMax = allVals.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;
|
|
}
|
|
// Nicht auf widget.min/max (gueltiger Drohnen-Geschwindigkeitsbereich)
|
|
// klemmen: Windkomponenten/Bodengeschwindigkeit (Doku 3.8) sind keine
|
|
// gueltigen Soll-Geschwindigkeiten und duerfen den sichtbaren Bereich
|
|
// durchaus unter min bzw. ueber max ziehen (HTML-Demonstrator:
|
|
// computeSpeedChartRange() klemmt ebenfalls nicht auf SPD_MIN/SPD_MAX).
|
|
if (hi <= lo) {
|
|
lo = widget.min;
|
|
hi = widget.max;
|
|
}
|
|
return (lo: lo, hi: hi);
|
|
}
|
|
|
|
double _xAt(int idx, List<double> 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<double> 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: Stack(
|
|
children: [
|
|
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,
|
|
legWind: widget.legWind,
|
|
groundSpeeds: widget.groundSpeeds,
|
|
terrainPoints: widget.terrainPoints,
|
|
terrainDangerRanges: widget.terrainDangerRanges,
|
|
),
|
|
),
|
|
if (widget.terrainLoading && widget.terrainPoints == null)
|
|
const Positioned(
|
|
left: 50,
|
|
bottom: 4,
|
|
child: Text(
|
|
'Loading terrain…',
|
|
style: TextStyle(color: Colors.white38, fontSize: 10),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
this.legWind,
|
|
this.groundSpeeds,
|
|
this.terrainPoints,
|
|
this.terrainDangerRanges,
|
|
});
|
|
|
|
final List<Waypoint> waypoints;
|
|
final List<double> values;
|
|
final List<double> cumDist;
|
|
final List<bool> legBad;
|
|
final List<bool> vertexBad;
|
|
final int activeIndex;
|
|
final double lo;
|
|
final double hi;
|
|
final String unit;
|
|
final List<double?>? legWind;
|
|
final List<double?>? groundSpeeds;
|
|
final List<TerrainSamplePoint>? terrainPoints;
|
|
final List<(double, double)>? terrainDangerRanges;
|
|
|
|
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 _xAtDist(double distanceM, double boxW) {
|
|
final total = cumDist.isEmpty || cumDist.last == 0 ? 1.0 : cumDist.last;
|
|
return _padX + (distanceM / 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;
|
|
|
|
// Gefahrenzonen zuunterst, damit Gitter/Gelaende/Kurve gut lesbar
|
|
// darueber liegen (HTML-Demonstrator: dangerHtml vor grid).
|
|
_paintDangerBands(canvas, boxW, boxH);
|
|
|
|
// 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;
|
|
}
|
|
|
|
_paintTerrain(canvas, boxW, boxH);
|
|
_paintWindAndGroundSpeed(canvas, boxW, boxH);
|
|
|
|
// 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,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Rote Bereiche mit zu geringem Bodenabstand (nur Altitude-Tab, Doku
|
|
/// 3.9/3.10) - analog dangerHtml im HTML-Demonstrator, ganz zuunterst
|
|
/// gezeichnet.
|
|
void _paintDangerBands(Canvas canvas, double boxW, double boxH) {
|
|
final ranges = terrainDangerRanges;
|
|
if (ranges == null || ranges.isEmpty) return;
|
|
final paint = Paint()..color = const Color(0xFFE2574A).withValues(alpha: 0.22);
|
|
for (final (start, end) in ranges) {
|
|
final x0 = _xAtDist(start, boxW);
|
|
final x1 = _xAtDist(end, boxW);
|
|
canvas.drawRect(Rect.fromLTWH(x0, 0, math.max(x1 - x0, 2), boxH), paint);
|
|
}
|
|
}
|
|
|
|
/// Braune Gelaende-Flaeche unter dem Hoehenprofil (nur Altitude-Tab, Doku
|
|
/// 3.9) - analog terrainHtml im HTML-Demonstrator, direkt nach dem
|
|
/// Gitter und vor Wind/Soll-Kurve gezeichnet.
|
|
void _paintTerrain(Canvas canvas, double boxW, double boxH) {
|
|
final points = terrainPoints;
|
|
if (points == null) return;
|
|
final valid = points.where((p) => p.elevationRelM != null).toList();
|
|
if (valid.length < 2) return;
|
|
|
|
final path = Path()..moveTo(_xAtDist(valid.first.distanceM, boxW), boxH - _padY);
|
|
for (final p in valid) {
|
|
path.lineTo(_xAtDist(p.distanceM, boxW), _yAt(p.elevationRelM!, boxH));
|
|
}
|
|
path.lineTo(_xAtDist(valid.last.distanceM, boxW), boxH - _padY);
|
|
path.close();
|
|
|
|
canvas.drawPath(path, Paint()..color = const Color(0xFF786450).withValues(alpha: 0.45));
|
|
canvas.drawPath(
|
|
path,
|
|
Paint()
|
|
..color = const Color(0xFFAA9678).withValues(alpha: 0.8)
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1.5,
|
|
);
|
|
}
|
|
|
|
/// Wind-Diamanten je Leg (Kopf-/Rueckenwind-Komponente) und die daraus
|
|
/// erreichbare Bodengeschwindigkeit als gestrichelte Stufenlinie - nur auf
|
|
/// dem Speed-Tab genutzt (Doku 3.8, windHtml/groundSpeedHtml im HTML-
|
|
/// Demonstrator). Vor den eigentlichen Soll-Werte-Punkten gezeichnet,
|
|
/// damit diese immer obenauf liegen.
|
|
void _paintWindAndGroundSpeed(Canvas canvas, double boxW, double boxH) {
|
|
final wind = legWind;
|
|
if (wind != null && wind.any((v) => v != null)) {
|
|
final wZeroY = _yAt(0, boxH);
|
|
_drawDashedLine(
|
|
canvas,
|
|
Offset(_padX, wZeroY),
|
|
Offset(boxW - _padX, wZeroY),
|
|
Paint()
|
|
..color = Colors.white.withValues(alpha: 0.4)
|
|
..strokeWidth = 1.5,
|
|
dashWidth: 3,
|
|
gapWidth: 4,
|
|
);
|
|
final zeroLabel = TextPainter(
|
|
text: TextSpan(
|
|
text: '0 (no wind effect on track)',
|
|
style: TextStyle(color: Colors.white.withValues(alpha: 0.55), fontSize: 10),
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
)..layout();
|
|
zeroLabel.paint(canvas, Offset(_padX, wZeroY - 5 - zeroLabel.height));
|
|
|
|
final windPts = <Offset>[];
|
|
for (var i = 0; i < wind.length; i++) {
|
|
final w = wind[i];
|
|
if (w == null) continue;
|
|
final mx = (_xAt(i, boxW) + _xAt(i + 1, boxW)) / 2;
|
|
final my = _yAt(w, boxH);
|
|
windPts.add(Offset(mx, my));
|
|
|
|
final isTail = w >= 0;
|
|
final color = isTail ? const Color(0xFF3ECF8E) : const Color(0xFFE2574A);
|
|
final diamond = Path()
|
|
..moveTo(mx, my - 7)
|
|
..lineTo(mx + 7, my)
|
|
..lineTo(mx, my + 7)
|
|
..lineTo(mx - 7, my)
|
|
..close();
|
|
canvas.drawPath(diamond, Paint()..color = color);
|
|
canvas.drawPath(
|
|
diamond,
|
|
Paint()
|
|
..color = const Color(0xFF0C0C0C)
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = 1,
|
|
);
|
|
|
|
final valueLabel = TextPainter(
|
|
text: TextSpan(
|
|
text: '${isTail ? '+' : ''}${w.toStringAsFixed(1)}',
|
|
style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.w600),
|
|
),
|
|
textDirection: TextDirection.ltr,
|
|
)..layout();
|
|
valueLabel.paint(
|
|
canvas,
|
|
Offset(mx - valueLabel.width / 2, my - 12 - valueLabel.height),
|
|
);
|
|
}
|
|
|
|
final connectPaint = Paint()
|
|
..color = Colors.white.withValues(alpha: 0.25)
|
|
..strokeWidth = 1;
|
|
for (var i = 0; i < windPts.length - 1; i++) {
|
|
_drawDashedLine(canvas, windPts[i], windPts[i + 1], connectPaint,
|
|
dashWidth: 2, gapWidth: 3);
|
|
}
|
|
}
|
|
|
|
final ground = groundSpeeds;
|
|
if (ground != null) {
|
|
double? prevLevelY;
|
|
final groundPaint = Paint()
|
|
..color = const Color(0xFF94BBE9).withValues(alpha: 0.55)
|
|
..strokeWidth = 1.5;
|
|
for (var i = 0; i < ground.length; i++) {
|
|
final g = ground[i];
|
|
if (g == null) {
|
|
prevLevelY = null;
|
|
continue;
|
|
}
|
|
final gy = _yAt(g, boxH);
|
|
final gx0 = _xAt(i, boxW);
|
|
final gx1 = _xAt(i + 1, boxW);
|
|
_drawDashedLine(canvas, Offset(gx0, gy), Offset(gx1, gy), groundPaint,
|
|
dashWidth: 5, gapWidth: 3);
|
|
if (prevLevelY != null) {
|
|
_drawDashedLine(canvas, Offset(gx0, prevLevelY), Offset(gx0, gy),
|
|
groundPaint, dashWidth: 5, gapWidth: 3);
|
|
}
|
|
prevLevelY = gy;
|
|
}
|
|
}
|
|
}
|
|
|
|
void _drawDashedLine(
|
|
Canvas canvas,
|
|
Offset p1,
|
|
Offset p2,
|
|
Paint paint, {
|
|
double dashWidth = 4,
|
|
double gapWidth = 3,
|
|
}) {
|
|
final total = (p2 - p1).distance;
|
|
if (total == 0) return;
|
|
final direction = (p2 - p1) / total;
|
|
var traveled = 0.0;
|
|
while (traveled < total) {
|
|
final segEnd = math.min(traveled + dashWidth, total);
|
|
canvas.drawLine(
|
|
p1 + direction * traveled,
|
|
p1 + direction * segEnd,
|
|
paint,
|
|
);
|
|
traveled += dashWidth + gapWidth;
|
|
}
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _ChartPainter oldDelegate) =>
|
|
oldDelegate.values != values ||
|
|
oldDelegate.activeIndex != activeIndex ||
|
|
oldDelegate.lo != lo ||
|
|
oldDelegate.hi != hi ||
|
|
oldDelegate.legWind != legWind ||
|
|
oldDelegate.groundSpeeds != groundSpeeds ||
|
|
oldDelegate.terrainPoints != terrainPoints ||
|
|
oldDelegate.terrainDangerRanges != terrainDangerRanges;
|
|
}
|