fix: render cornering events along route

This commit is contained in:
bponders12-cell 2026-09-01 12:03:58 -04:00
parent 354e230838
commit 920564505f
1 changed files with 108 additions and 35 deletions

View File

@ -35,6 +35,8 @@ class _MapScreenState extends ConsumerState<MapScreen> {
static const double _hardAccelThresholdG = 0.35; static const double _hardAccelThresholdG = 0.35;
static const double _hardBrakingThresholdG = -0.35; static const double _hardBrakingThresholdG = -0.35;
static const double _corneringLeanThresholdDeg = 20.0;
static const double _corneringLateralGThreshold = 0.35;
static const Duration _eventMergeWindow = Duration(milliseconds: 1500); static const Duration _eventMergeWindow = Duration(milliseconds: 1500);
@override @override
@ -134,36 +136,39 @@ class _MapScreenState extends ConsumerState<MapScreen> {
List<AppMapMarker> _buildTelemetryEventMarkers(List<TelemetrySample> samples) { List<AppMapMarker> _buildTelemetryEventMarkers(List<TelemetrySample> samples) {
final gps = samples.where((s) => final gps = samples.where((s) =>
s.latitude != null && s.longitude != null && s.latitude != null && s.longitude != null &&
(s.longitudinalG != null || s.accelX != null)).toList(); (s.longitudinalG != null || s.accelX != null || s.lateralG != null || s.leanAngle != null)).toList();
if (gps.isEmpty) return []; if (gps.isEmpty) return [];
final markers = <AppMapMarker>[]; final markers = <AppMapMarker>[];
DateTime? lastAccel; DateTime? lastAccel;
DateTime? lastBrake; DateTime? lastBrake;
DateTime? lastCorner;
for (final sample in gps) { for (final sample in gps) {
final g = sample.longitudinalG ?? sample.accelX; final longitudinalG = sample.longitudinalG ?? sample.accelX;
if (g == null || !g.isFinite) continue; final lateralG = sample.lateralG;
final leanAngle = sample.leanAngle;
if (g >= _hardAccelThresholdG && if (longitudinalG != null && longitudinalG.isFinite) {
if (longitudinalG >= _hardAccelThresholdG &&
(lastAccel == null || sample.timestamp.difference(lastAccel).abs() > _eventMergeWindow)) { (lastAccel == null || sample.timestamp.difference(lastAccel).abs() > _eventMergeWindow)) {
markers.add(_eventMarker( markers.add(_eventMarker(
id: 'telemetry_accel_${sample.id}', id: 'telemetry_accel_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!), position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Hard Acceleration', label: 'Hard Acceleration',
value: g, value: longitudinalG,
timestamp: sample.timestamp, timestamp: sample.timestamp,
markerColor: const Color(0xFF76FF03), markerColor: const Color(0xFF76FF03),
markerIcon: Icons.arrow_upward, markerIcon: Icons.arrow_upward,
)); ));
lastAccel = sample.timestamp; lastAccel = sample.timestamp;
} else if (g <= _hardBrakingThresholdG && } else if (longitudinalG <= _hardBrakingThresholdG &&
(lastBrake == null || sample.timestamp.difference(lastBrake).abs() > _eventMergeWindow)) { (lastBrake == null || sample.timestamp.difference(lastBrake).abs() > _eventMergeWindow)) {
markers.add(_eventMarker( markers.add(_eventMarker(
id: 'telemetry_braking_${sample.id}', id: 'telemetry_braking_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!), position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Hard Braking', label: 'Hard Braking',
value: g, value: longitudinalG,
timestamp: sample.timestamp, timestamp: sample.timestamp,
markerColor: const Color(0xFFFF5252), markerColor: const Color(0xFFFF5252),
markerIcon: Icons.arrow_downward, markerIcon: Icons.arrow_downward,
@ -171,6 +176,26 @@ class _MapScreenState extends ConsumerState<MapScreen> {
lastBrake = sample.timestamp; lastBrake = sample.timestamp;
} }
} }
final isCornering = (leanAngle != null && leanAngle.isFinite && leanAngle.abs() >= _corneringLeanThresholdDeg) ||
(lateralG != null && lateralG.isFinite && lateralG.abs() >= _corneringLateralGThreshold);
if (isCornering &&
(lastCorner == null || sample.timestamp.difference(lastCorner).abs() > _eventMergeWindow)) {
final cornerValue = leanAngle != null && leanAngle.isFinite
? leanAngle
: lateralG!.abs();
markers.add(_eventMarker(
id: 'telemetry_corner_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Cornering',
value: cornerValue,
timestamp: sample.timestamp,
markerColor: const Color(0xFF00E5FF),
markerIcon: Icons.turn_right,
));
lastCorner = sample.timestamp;
}
}
return markers; return markers;
} }
@ -219,7 +244,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDialogRow('Timestamp:', timeStr), _buildDialogRow('Timestamp:', timeStr),
_buildDialogRow('G Value:', value.toStringAsFixed(2)), _buildDialogRow('Value:', value.toStringAsFixed(2)),
_buildDialogRow('GPS Location:', '${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}'), _buildDialogRow('GPS Location:', '${position.latitude.toStringAsFixed(5)}, ${position.longitude.toStringAsFixed(5)}'),
], ],
), ),
@ -238,7 +263,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(label, style: const TextStyle(fontSize: 11, color: Colors.white54)), Text(label, style: const TextStyle(fontSize: 11, color: Colors.white54)),
Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.white)), Flexible(child: Text(value, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: Colors.white), textAlign: TextAlign.right)),
], ],
), ),
); );
@ -299,6 +324,9 @@ class _MapScreenState extends ConsumerState<MapScreen> {
color: const Color(0xFFFF5252), color: const Color(0xFFFF5252),
)); ));
} }
if (_showCornering) {
polylines.addAll(_buildCorneringEventPolylines(validSamples));
}
return polylines; return polylines;
} }
@ -311,10 +339,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
}) { }) {
if (samples.isEmpty) return []; if (samples.isEmpty) return [];
// A threshold crossing is often only one telemetry sample wide. Requiring
// two consecutive qualifying samples made isolated acceleration/braking
// events disappear from the map. Build event runs, then include one GPS
// sample on each side so every event renders as a visible route segment.
final polylines = <AppPolyline>[]; final polylines = <AppPolyline>[];
final eventIndices = <int>[]; final eventIndices = <int>[];
@ -338,9 +362,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
final sample = samples[i]; final sample = samples[i];
points.add(AppLatLng(sample.latitude!, sample.longitude!)); points.add(AppLatLng(sample.latitude!, sample.longitude!));
} }
// The surrounding point guarantees two points for an isolated event
// while preserving a single continuous segment for adjacent events.
if (points.length >= 2) { if (points.length >= 2) {
polylines.add(AppPolyline( polylines.add(AppPolyline(
id: '${idPrefix}_$segmentIndex', id: '${idPrefix}_$segmentIndex',
@ -354,9 +375,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
for (var i = 1; i < eventIndices.length; i++) { for (var i = 1; i < eventIndices.length; i++) {
final index = eventIndices[i]; final index = eventIndices[i];
// Keep nearby threshold samples in one event run. A gap of more than
// two samples starts a new visual event while allowing brief sensor
// dropouts inside a hard maneuver.
if (index - runEnd <= 2) { if (index - runEnd <= 2) {
runEnd = index; runEnd = index;
} else { } else {
@ -365,7 +383,62 @@ class _MapScreenState extends ConsumerState<MapScreen> {
} }
} }
emitRun(runStart, runEnd); emitRun(runStart, runEnd);
return polylines;
}
List<AppPolyline> _buildCorneringEventPolylines(List<TelemetrySample> samples) {
if (samples.isEmpty) return [];
final eventIndices = <int>[];
for (var i = 0; i < samples.length; i++) {
final sample = samples[i];
final lean = sample.leanAngle;
final lateral = sample.lateralG;
final isCornering = (lean != null && lean.isFinite && lean.abs() >= _corneringLeanThresholdDeg) ||
(lateral != null && lateral.isFinite && lateral.abs() >= _corneringLateralGThreshold);
if (isCornering) eventIndices.add(i);
}
if (eventIndices.isEmpty) return [];
final polylines = <AppPolyline>[];
var runStart = eventIndices.first;
var runEnd = eventIndices.first;
var segmentIndex = 0;
void emitRun(int start, int end) {
// Include a GPS breadcrumb before and after the detected cornering
// samples. This makes a short threshold crossing visible as a route
// segment rather than a point hidden beneath a marker.
final expandedStart = start > 0 ? start - 1 : start;
final expandedEnd = end < samples.length - 1 ? end + 1 : end;
final points = <AppLatLng>[];
for (var i = expandedStart; i <= expandedEnd; i++) {
final sample = samples[i];
if (sample.latitude != null && sample.longitude != null) {
points.add(AppLatLng(sample.latitude!, sample.longitude!));
}
}
if (points.length >= 2) {
polylines.add(AppPolyline(
id: 'cornering_layer_$segmentIndex',
points: points,
color: const Color(0xFF00E5FF),
width: 7.0,
));
segmentIndex++;
}
}
for (var i = 1; i < eventIndices.length; i++) {
final index = eventIndices[i];
if (index - runEnd <= 2) {
runEnd = index;
} else {
emitRun(runStart, runEnd);
runStart = runEnd = index;
}
}
emitRun(runStart, runEnd);
return polylines; return polylines;
} }