fleet_hazard
Turn scattered vehicle reports into map-ready hazard zones. Clusters individual snow/ice reports by geography and severity — pure Dart, no Flutter dependency.
Use fleet_hazard when you need to aggregate telemetry from multiple vehicles
into hazard zones that can be rendered on any map widget.
Features
FleetReportmodel with road condition, timestamp, confidence, and position — the input atom, carrying avehicleId.HazardZonecluster model with severity, vehicle count, and average confidence. The zone retains anonymizedZoneObservations — it drops thevehicleId, so the aggregate is genuinely anonymized and not a re-identifiable per-vehicle trail.HazardAggregatorpure-Dart clustering algorithm for snowy and icy reports.FleetProviderabstract interface so apps can swap simulated, local, or remote telemetry backends.- Pure Dart package with no Flutter dependency.
Install
dependencies:
fleet_hazard: ^0.5.0
Quick Start
import 'package:fleet_hazard/fleet_hazard.dart';
import 'package:latlong2/latlong.dart';
final reports = [
FleetReport(
vehicleId: 'V-001',
position: const LatLng(35.050, 137.250),
timestamp: DateTime.now(),
condition: RoadCondition.snowy,
),
FleetReport(
vehicleId: 'V-002',
position: const LatLng(35.052, 137.252),
timestamp: DateTime.now(),
condition: RoadCondition.icy,
),
];
final zones = HazardAggregator.aggregate(reports);
for (final zone in zones) {
print('${zone.severity.name}: ${zone.vehicleCount} vehicles');
}
Integration Pattern
fleet_hazard usually lives between a telemetry stream and a map overlay.
Collect reports from your provider, aggregate them into zones, then render the
zone summaries in whatever map widget or overlay layer your app uses.
import 'dart:async';
import 'package:fleet_hazard/fleet_hazard.dart';
import 'package:flutter/material.dart';
class HazardSummaryList extends StatefulWidget {
const HazardSummaryList({
super.key,
required this.provider,
});
final FleetProvider provider;
@override
State<HazardSummaryList> createState() => _HazardSummaryListState();
}
class _HazardSummaryListState extends State<HazardSummaryList> {
final reports = <FleetReport>[];
StreamSubscription<FleetReport>? subscription;
@override
void initState() {
super.initState();
subscription = widget.provider.reports.listen((report) {
setState(() {
reports.add(report);
});
});
widget.provider.startListening();
}
@override
void dispose() {
subscription?.cancel();
widget.provider.stopListening();
widget.provider.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final zones = HazardAggregator.aggregate(reports);
return ListView(
shrinkWrap: true,
children: [
for (final zone in zones)
ListTile(
title: Text('${zone.severity.name} zone'),
subtitle: Text(
'${zone.vehicleCount} vehicles • '
'${zone.radiusMeters.toStringAsFixed(0)}m radius',
),
),
],
);
}
}
In a full navigation UI, this same zones list is what you would pass to a map
overlay layer so the driver sees clustered snowy and icy segments, not raw per-
vehicle noise.
An empty result is not an all-clear
HazardAggregator.aggregate() returns an empty list in three different
situations, and it cannot tell you which one you are in:
- nobody has reported this road;
- the fleet drove it and found it clear;
- the fleet drove it and every sensor returned
RoadCondition.unknown.
If you render an empty list as a clear road, you will show a driver a clear road
on stretches where nothing is known at all. Use aggregateWithProvenance() when
a person is going to act on the answer:
// oracle:placeholders reports
final result = HazardAggregator.aggregateWithProvenance(
reports,
maxAge: const Duration(minutes: 30),
);
if (result.isSilent) {
// No fresh reports at all. Say so — do not render a clear road.
} else if (result.isAllUnknown) {
// Vehicles drove it; none could read it. Also not a clear road.
} else if (result.isMeasuredClear) {
// Vehicles drove it and found it fine. This you may render as clear.
} else {
for (final zone in result.zones) {
// Hazard zones, same clustering as aggregate().
}
}
maxAge is what makes a claim like "12 vehicles reported in the last 30
minutes" true: reports older than it are excluded from clustering and counted
in result.staleCount. Omit maxAge and no age filter is applied at all —
a report from last week counts the same as one from a minute ago.
result.freshestReportAt is null when nothing was reported. null means NOT
KNOWN — do not coalesce it into a time or render it as freshness.
Implement a provider
import 'dart:async';
class MyFleetProvider implements FleetProvider {
final _controller = StreamController<FleetReport>.broadcast();
@override
Stream<FleetReport> get reports => _controller.stream;
@override
Future<void> startListening() async {
// Connect to your telemetry source, and add each report:
// _controller.add(report);
}
@override
Future<void> stopListening() async {
// Stop updates
}
@override
void dispose() {
_controller.close();
}
}
API Overview
| Type | Purpose |
|---|---|
FleetReport |
Individual vehicle report (the input atom) carrying vehicleId, position, road condition, confidence, and timestamp. |
ZoneObservation |
Anonymized observation retained inside a zone — position, condition, timestamp, confidence; no vehicleId. |
HazardZone |
Clustered geographic hazard summary with severity, a precomputed vehicleCount, and confidence rollups over anonymized observations. |
HazardAggregator |
Pure Dart clustering algorithm that converts reports into hazard zones. aggregate() returns a bare List<HazardZone>; aggregateWithProvenance() returns a FleetAggregate and enforces maxAge. |
FleetAggregate |
Aggregation result that keeps absence distinct from a measured all-clear: zones, reportCount, unknownCount, staleCount, freshestReportAt, plus isSilent / isMeasuredClear / isAllUnknown. |
FleetProvider |
Stream-based interface for simulated, local, or remote fleet telemetry sources. |
RoadCondition |
Canonical hazard labels such as dry, snowy, and icy. |
Works With
| Package | How |
|---|---|
| driving_weather | Weather conditions correlate with fleet-reported hazards |
| map_viewport_bloc | Hazard zones render at Z3 in the viewport layer stack |
| driving_consent | Fleet data sharing requires explicit consent |
See Also
- driving_conditions — Road surface and safety scoring
- navigation_safety — Safety alert overlay
- kalman_dr — Dead reckoning through GPS loss
Part of SNGNav — 11 packages for offline-first navigation on Flutter.
License
BSD-3-Clause — see LICENSE.
Libraries
- fleet_hazard
- Fleet telemetry hazard model and clustering.