navigation_safety 0.9.5
navigation_safety: ^0.9.5 copied to clipboard
Flutter safety-alert overlay + navigation session state machine with configurable severity thresholds. Display-only, never controlling.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:latlong2/latlong.dart';
import 'package:navigation_safety/navigation_safety.dart';
import 'package:routing_engine/routing_engine.dart';
/// Local adapter mirroring lib/adapters/navigation_route_adapter.dart.
/// Examples sit at the routing→navigation boundary; the production
/// adapter is internal to the main app and not exported from any
/// package, so examples carry their own identical copy.
extension _RouteResultToNavigation on RouteResult {
NavigationRoute toNavigationRoute() => NavigationRoute(
shape: shape,
maneuvers: maneuvers
.map(
(m) => NavigationManeuver(
index: m.index,
instruction: m.instruction,
type: m.type,
lengthKm: m.lengthKm,
timeSeconds: m.timeSeconds,
position: m.position,
),
)
.toList(),
totalDistanceKm: totalDistanceKm,
totalTimeSeconds: totalTimeSeconds,
summary: summary,
);
}
final _exampleRoute = RouteResult(
shape: const [LatLng(35.1709, 136.9066), LatLng(34.9551, 137.1771)],
maneuvers: const [
RouteManeuver(
index: 0,
instruction: 'Depart Sakae Station',
type: 'depart',
lengthKm: 12,
timeSeconds: 900,
position: LatLng(35.1709, 136.9066),
),
RouteManeuver(
index: 1,
instruction: 'Arrive Higashiokazaki Station',
type: 'arrive',
lengthKm: 0,
timeSeconds: 0,
position: LatLng(34.9551, 137.1771),
),
],
totalDistanceKm: 40.0,
totalTimeSeconds: 2400,
summary: '40 km, 40 min',
engineInfo: const EngineInfo(name: 'mock'),
);
void main() {
runApp(const NavigationSafetyExampleApp());
}
class NavigationSafetyExampleApp extends StatelessWidget {
const NavigationSafetyExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => NavigationBloc()
..add(NavigationStarted(route: _exampleRoute.toNavigationRoute())),
child: const _ExampleScreen(),
),
);
}
}
class _ExampleScreen extends StatelessWidget {
const _ExampleScreen();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('navigation_safety example')),
body: Stack(
children: [
Positioned.fill(
child: ColoredBox(
color: Colors.blueGrey.shade50,
child: const Center(child: Text('Map layer placeholder')),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 12,
runSpacing: 12,
alignment: WrapAlignment.center,
children: [
FilledButton(
onPressed: () => context.read<NavigationBloc>().add(
const SafetyAlertReceived(
message: 'Snow expected in 30 minutes',
severity: AlertSeverity.info,
),
),
child: const Text('Info'),
),
FilledButton(
onPressed: () => context.read<NavigationBloc>().add(
const SafetyAlertReceived(
message: 'Icy road conditions ahead',
severity: AlertSeverity.warning,
),
),
child: const Text('Warning'),
),
FilledButton(
onPressed: () => context.read<NavigationBloc>().add(
const SafetyAlertReceived(
message: 'Visibility zero - pull over immediately',
severity: AlertSeverity.critical,
dismissible: false,
),
),
child: const Text('Critical'),
),
OutlinedButton(
onPressed: () => context.read<NavigationBloc>().add(
const SafetyAlertDismissed(),
),
child: const Text('Dismiss'),
),
],
),
),
),
const SafetyOverlay(),
// Hazard-owner clear path — deliberately ABOVE SafetyOverlay.
//
// Everything below the overlay (the Wrap of demo buttons
// included) is unreachable while an alert is active: the
// overlay's modal barrier swallows those taps by design. A
// non-dismissible alert therefore needs a clear path that does
// not go through the driver-facing UI under the barrier.
//
// In a real app this is NOT a button: the condition pipeline
// that raised the alert adds
// SafetyAlertDismissed(conditionCleared: true) when its data
// says the hazard has passed. This demo stands a button in
// for that pipeline so the flow can be exercised by hand.
// Do not wire conditionCleared: true to a driver-facing
// dismiss control — that would defeat the non-dismissible
// contract (plain SafetyAlertDismissed() is the driver's
// verb, and it stays refused for non-dismissible alerts).
BlocBuilder<NavigationBloc, NavigationState>(
builder: (context, state) {
if (!state.hasSafetyAlert) return const SizedBox.shrink();
return Align(
alignment: Alignment.topCenter,
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: FilledButton.tonalIcon(
onPressed: () => context.read<NavigationBloc>().add(
const SafetyAlertDismissed(conditionCleared: true),
),
icon: const Icon(Icons.cloud_done_outlined),
label: const Text('Condition passed (hazard owner)'),
),
),
),
);
},
),
],
),
);
}
}