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 _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,41 +136,64 @@ 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) {
|
||||||
(lastAccel == null || sample.timestamp.difference(lastAccel).abs() > _eventMergeWindow)) {
|
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(
|
markers.add(_eventMarker(
|
||||||
id: 'telemetry_accel_${sample.id}',
|
id: 'telemetry_corner_${sample.id}',
|
||||||
position: AppLatLng(sample.latitude!, sample.longitude!),
|
position: AppLatLng(sample.latitude!, sample.longitude!),
|
||||||
label: 'Hard Acceleration',
|
label: 'Cornering',
|
||||||
value: g,
|
value: cornerValue,
|
||||||
timestamp: sample.timestamp,
|
timestamp: sample.timestamp,
|
||||||
markerColor: const Color(0xFF76FF03),
|
markerColor: const Color(0xFF00E5FF),
|
||||||
markerIcon: Icons.arrow_upward,
|
markerIcon: Icons.turn_right,
|
||||||
));
|
));
|
||||||
lastAccel = sample.timestamp;
|
lastCorner = 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue