miler map
This commit is contained in:
@@ -149,7 +149,7 @@ class _MilerAttribution extends StatelessWidget {
|
||||
'© OpenStreetMap',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ColorConstants.secondaryText,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
@@ -345,7 +345,7 @@ class MilerNumberPin extends StatelessWidget {
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
fontFamily: FontConstants.fontFamily,
|
||||
),
|
||||
@@ -512,14 +512,102 @@ class _MilerLegMapState extends State<MilerLegMap> {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many times to ask for the road before settling for the bearing.
|
||||
///
|
||||
/// ── Why one attempt was never enough ──
|
||||
///
|
||||
/// `route()` answers an empty list for every failure — a 429, a cold DNS
|
||||
/// lookup that outran the timeout, a tunnel — and this asked exactly once,
|
||||
/// from `initState`. The only other trigger is `didUpdateWidget`, and it
|
||||
/// fires on a change of *endpoint*: a rider standing at a door changes
|
||||
/// neither. So a single unlucky moment at the instant the sheet opened left
|
||||
/// `_road` empty for as long as the sheet stayed open, and the leg drew as a
|
||||
/// dashed straight line across a route the router would have returned a
|
||||
/// second later. That is the "it shows a straight line, not the real route"
|
||||
/// report, and it is a retry bug rather than a routing one — the public OSRM
|
||||
/// server answers this leg in under a second when asked again.
|
||||
///
|
||||
/// Three tries with a widening gap. Cheap, bounded, and it costs nothing at
|
||||
/// all in the normal case where the first answer is the road.
|
||||
static const int _routeAttempts = 3;
|
||||
static const Duration _retryBackoff = Duration(milliseconds: 700);
|
||||
|
||||
/// Distinguishes a stale in-flight request from the current one, so a leg
|
||||
/// that changed while the network was slow cannot have the previous leg's
|
||||
/// road painted onto it when the old request finally lands.
|
||||
int _fetchGeneration = 0;
|
||||
|
||||
/// ── The backoff has to be cancellable, not just ignored ──
|
||||
///
|
||||
/// A bare `Future.delayed` checks `mounted` on the far side of the wait, which
|
||||
/// is too late for the one observer that counts: the timer is still pending
|
||||
/// when the tree goes, and Flutter's test binding asserts on exactly that —
|
||||
/// *"A Timer is still pending even after the widget tree was disposed"*. It
|
||||
/// failed 15 tests that open this sheet and dispose it inside the retry gap.
|
||||
///
|
||||
/// Off a test bench it is the same leak in slower motion: a rider flicking
|
||||
/// through six stops leaves six timers running against six dead widgets.
|
||||
///
|
||||
/// So the wait is a [Timer] this state owns and [dispose] cancels — and
|
||||
/// disposing also completes the pending waiter, so the retry loop wakes,
|
||||
/// sees `!mounted`, and unwinds instead of parking forever on a future
|
||||
/// nothing will ever finish.
|
||||
Timer? _retryTimer;
|
||||
Completer<void>? _retryWait;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_retryTimer?.cancel();
|
||||
_retryTimer = null;
|
||||
final waiter = _retryWait;
|
||||
_retryWait = null;
|
||||
if (waiter != null && !waiter.isCompleted) waiter.complete();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// A cancellable [Future.delayed]. See [_retryTimer].
|
||||
Future<void> _wait(Duration d) {
|
||||
final completer = Completer<void>();
|
||||
_retryWait = completer;
|
||||
_retryTimer = Timer(d, () {
|
||||
_retryTimer = null;
|
||||
if (identical(_retryWait, completer)) _retryWait = null;
|
||||
if (!completer.isCompleted) completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> _fetchRoad() async {
|
||||
final from = widget.from;
|
||||
if (from == null) return;
|
||||
final road = await MilerRouter.route(from, widget.to);
|
||||
if (!mounted || road.isEmpty) return;
|
||||
setState(() => _road = road);
|
||||
// The road can bulge outside the straight-line bounds; refit around it.
|
||||
_fit();
|
||||
final to = widget.to;
|
||||
final generation = ++_fetchGeneration;
|
||||
|
||||
for (var attempt = 1; attempt <= _routeAttempts; attempt++) {
|
||||
final road = await MilerRouter.route(from, to);
|
||||
// Superseded, or gone from the tree, while the request was in flight.
|
||||
if (!mounted || generation != _fetchGeneration) return;
|
||||
|
||||
if (road.isNotEmpty) {
|
||||
setState(() => _road = road);
|
||||
// The road can bulge outside the straight-line bounds; refit around it.
|
||||
_fit();
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt < _routeAttempts) {
|
||||
await _wait(_retryBackoff * attempt);
|
||||
if (!mounted || generation != _fetchGeneration) return;
|
||||
}
|
||||
}
|
||||
|
||||
// Says which leg gave up, so a rider reporting a dashed line can be matched
|
||||
// against the `[MAP] OSRM …` line that explains why.
|
||||
debugPrint(
|
||||
'[MAP] no road after $_routeAttempts attempts for '
|
||||
'${from.latitude},${from.longitude} -> ${to.latitude},${to.longitude} — '
|
||||
'drawing the dashed bearing instead.',
|
||||
);
|
||||
}
|
||||
|
||||
void _fit() {
|
||||
|
||||
Reference in New Issue
Block a user