fix: render cornering events along route
This commit is contained in:
parent
354e230838
commit
920564505f
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue