mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-09-09 19:28:56 +02:00
- Increase background refresh threshold: Raised the edge distance check in GpsController from 1000m to 2500m to guarantee new Overpass data is fetched well before the user drives out of the cached bounding box. - Fix bounding box race condition: Added await to the background fetch handler in MapView so tracking bounds only shift forward after the network request successfully finishes. - Make debounced fetches awaitable: Refactored fetchAndUpdate in NodeProviderWithCache to return a Future<void> using a Completer, bridging the 400ms debounce timer. - Prevent hanging futures: Added state checks for _activeCompleter to instantly resolve canceled network requests during rapid map panning, preventing fatal StateError crashes and infinite UI hangs.
126 lines
4.2 KiB
Dart
126 lines
4.2 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
|
|
|
|
import '../services/node_data_manager.dart';
|
|
import '../services/node_spatial_cache.dart';
|
|
import '../models/node_profile.dart';
|
|
import '../models/osm_node.dart';
|
|
import '../app_state.dart';
|
|
|
|
/// Provides surveillance nodes for a map view, using an in-memory cache and optionally
|
|
/// merging in new results from Overpass via MapDataProvider when not offline.
|
|
class NodeProviderWithCache extends ChangeNotifier {
|
|
static final NodeProviderWithCache instance = NodeProviderWithCache._internal();
|
|
factory NodeProviderWithCache() => instance;
|
|
NodeProviderWithCache._internal();
|
|
|
|
final NodeDataManager _nodeDataManager = NodeDataManager();
|
|
Timer? _debounceTimer;
|
|
Completer<void>? _activeCompleter;
|
|
|
|
/// Get cached nodes for the given bounds, filtered by enabled profiles
|
|
List<OsmNode> getCachedNodesForBounds(LatLngBounds bounds) {
|
|
// Use the same cache instance as NodeDataManager
|
|
final allNodes = NodeSpatialCache().getNodesFor(bounds);
|
|
final enabledProfiles = AppState.instance.enabledProfiles;
|
|
|
|
// If no profiles are enabled, show no nodes
|
|
if (enabledProfiles.isEmpty) return [];
|
|
|
|
// Filter nodes to only show those matching enabled profiles
|
|
return allNodes.where((node) {
|
|
return _matchesAnyProfile(node, enabledProfiles);
|
|
}).toList();
|
|
}
|
|
|
|
/// Fetch and update nodes for the given view, with debouncing for rapid map movement
|
|
Future<void> fetchAndUpdate({
|
|
required LatLngBounds bounds,
|
|
required List<NodeProfile> profiles,
|
|
UploadMode uploadMode = UploadMode.production,
|
|
}) {
|
|
// Serve cached immediately
|
|
notifyListeners();
|
|
|
|
// Prevent hanging awaits by completing the previous request if it gets cancelled
|
|
if (_activeCompleter != null && !_activeCompleter!.isCompleted) {
|
|
_activeCompleter!.complete();
|
|
}
|
|
|
|
_activeCompleter = Completer<void>();
|
|
|
|
// Debounce rapid panning/zooming
|
|
_debounceTimer?.cancel();
|
|
_debounceTimer = Timer(const Duration(milliseconds: 400), () async {
|
|
try {
|
|
await _nodeDataManager.getNodesFor(
|
|
bounds: bounds,
|
|
profiles: profiles,
|
|
uploadMode: uploadMode,
|
|
isUserInitiated: true,
|
|
);
|
|
|
|
// Notify UI of new data
|
|
notifyListeners();
|
|
} catch (e) {
|
|
debugPrint('[NodeProviderWithCache] Node fetch failed: $e');
|
|
// Cache already holds whatever is available for the view
|
|
} finally {
|
|
// Ensure we only complete if it hasn't been superseded by another call
|
|
if (_activeCompleter != null && !_activeCompleter!.isCompleted) {
|
|
_activeCompleter!.complete();
|
|
}
|
|
}
|
|
});
|
|
|
|
return _activeCompleter!.future;
|
|
}
|
|
|
|
/// Clear the cache and repopulate with pending nodes from upload queue
|
|
void clearCache() {
|
|
_nodeDataManager.clearCache();
|
|
// Repopulate with pending nodes from upload queue if available
|
|
_repopulatePendingNodesAfterClear();
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Repopulate pending nodes after cache clear
|
|
void _repopulatePendingNodesAfterClear() {
|
|
Future.microtask(() {
|
|
_onCacheCleared?.call();
|
|
});
|
|
}
|
|
|
|
VoidCallback? _onCacheCleared;
|
|
|
|
/// Set callback for when cache is cleared (used by app state to repopulate pending nodes)
|
|
void setOnCacheClearedCallback(VoidCallback? callback) {
|
|
_onCacheCleared = callback;
|
|
}
|
|
|
|
/// Force refresh the display (useful when filters change but cache doesn't)
|
|
void refreshDisplay() {
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Check if a node matches any of the provided profiles
|
|
bool _matchesAnyProfile(OsmNode node, List<NodeProfile> profiles) {
|
|
for (final profile in profiles) {
|
|
if (_nodeMatchesProfile(node, profile)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Check if a node matches a specific profile (all non-empty profile tags must match)
|
|
bool _nodeMatchesProfile(OsmNode node, NodeProfile profile) {
|
|
for (final entry in profile.tags.entries) {
|
|
// Skip empty values - they are used for refinement UI, not matching
|
|
if (entry.value.trim().isEmpty) continue;
|
|
|
|
if (node.tags[entry.key] != entry.value) return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|