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 _hardBrakingThresholdG = -0.35;
static const double _corneringLeanThresholdDeg = 20.0;
static const double _corneringLateralGThreshold = 0.35;
static const Duration _eventMergeWindow = Duration(milliseconds: 1500);
@override
@ -134,41 +136,64 @@ class _MapScreenState extends ConsumerState<MapScreen> {
List<AppMapMarker> _buildTelemetryEventMarkers(List<TelemetrySample> samples) {
final gps = samples.where((s) =>
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 [];
final markers = <AppMapMarker>[];
DateTime? lastAccel;
DateTime? lastBrake;
DateTime? lastCorner;
for (final sample in gps) {
final g = sample.longitudinalG ?? sample.accelX;
if (g == null || !g.isFinite) continue;
final longitudinalG = sample.longitudinalG ?? sample.accelX;
final lateralG = sample.lateralG;
final leanAngle = sample.leanAngle;
if (g >= _hardAccelThresholdG &&
(lastAccel == null || sample.timestamp.difference(lastAccel).abs() > _eventMergeWindow)) {
if (longitudinalG != null && longitudinalG.isFinite) {
if (longitudinalG >= _hardAccelThresholdG &&
(lastAccel == null || sample.timestamp.difference(lastAccel).abs() > _eventMergeWindow)) {
markers.add(_eventMarker(
id: 'telemetry_accel_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Hard Acceleration',
value: longitudinalG,
timestamp: sample.timestamp,
markerColor: const Color(0xFF76FF03),
markerIcon: Icons.arrow_upward,
));
lastAccel = sample.timestamp;
} else if (longitudinalG <= _hardBrakingThresholdG &&
(lastBrake == null || sample.timestamp.difference(lastBrake).abs() > _eventMergeWindow)) {
markers.add(_eventMarker(
id: 'telemetry_braking_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Hard Braking',
value: longitudinalG,
timestamp: sample.timestamp,
markerColor: const Color(0xFFFF5252),
markerIcon: Icons.arrow_downward,
));
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_accel_${sample.id}',
id: 'telemetry_corner_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Hard Acceleration',
value: g,
label: 'Cornering',
value: cornerValue,
timestamp: sample.timestamp,
markerColor: const Color(0xFF76FF03),
markerIcon: Icons.arrow_upward,
markerColor: const Color(0xFF00E5FF),
markerIcon: Icons.turn_right,
));
lastAccel = sample.timestamp;
} else if (g <= _hardBrakingThresholdG &&
(lastBrake == null || sample.timestamp.difference(lastBrake).abs() > _eventMergeWindow)) {
markers.add(_eventMarker(
id: 'telemetry_braking_${sample.id}',
position: AppLatLng(sample.latitude!, sample.longitude!),
label: 'Hard Braking',
value: g,
timestamp: sample.timestamp,
markerColor: const Color(0xFFFF5252),
markerIcon: Icons.arrow_downward,
));
lastBrake = sample.timestamp;
lastCorner = sample.timestamp;
}
}
return markers;
@ -219,7 +244,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_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)}'),
],
),
@ -238,7 +263,7 @@ class _MapScreenState extends ConsumerState<MapScreen> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
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),
));
}
if (_showCornering) {
polylines.addAll(_buildCorneringEventPolylines(validSamples));
}
return polylines;
}
@ -311,10 +339,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
}) {
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 eventIndices = <int>[];
@ -338,9 +362,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
final sample = samples[i];
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) {
polylines.add(AppPolyline(
id: '${idPrefix}_$segmentIndex',
@ -354,9 +375,6 @@ class _MapScreenState extends ConsumerState<MapScreen> {
for (var i = 1; i < eventIndices.length; 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) {
runEnd = index;
} else {
@ -365,7 +383,62 @@ class _MapScreenState extends ConsumerState<MapScreen> {
}
}
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;
}