Files
deflock-app/lib/widgets/map/map_data_manager.dart

144 lines
5.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../models/osm_node.dart';
import '../../app_state.dart';
import '../node_provider_with_cache.dart';
import '../../dev_config.dart';
/// Manages data fetching, filtering, and node limit logic for the map.
/// Handles profile changes, zoom level restrictions, and node rendering limits.
class MapDataManager {
// Track node limit state for parent notification
bool _lastNodeLimitState = false;
/// Get minimum zoom level for node fetching based on upload mode
int getMinZoomForNodes(UploadMode uploadMode) {
// OSM API (sandbox mode) needs higher zoom level due to bbox size limits
if (uploadMode == UploadMode.sandbox) {
return kOsmApiMinZoomLevel;
} else {
return kNodeMinZoomLevel;
}
}
/// Expand bounds by the given multiplier, maintaining center point.
/// Used to expand rendering bounds to prevent nodes blinking at screen edges.
LatLngBounds _expandBounds(LatLngBounds bounds, double multiplier) {
final centerLat = (bounds.north + bounds.south) / 2;
final centerLng = (bounds.east + bounds.west) / 2;
final latSpan = (bounds.north - bounds.south) * multiplier / 2;
final lngSpan = (bounds.east - bounds.west) * multiplier / 2;
return LatLngBounds(
LatLng(centerLat - latSpan, centerLng - lngSpan),
LatLng(centerLat + latSpan, centerLng + lngSpan),
);
}
/// Get nodes to render based on current map state
/// Returns a MapDataResult containing all relevant node data and limit state
MapDataResult getNodesForRendering({
required double currentZoom,
required LatLngBounds? mapBounds,
required UploadMode uploadMode,
required int maxNodes,
void Function(bool isLimited)? onNodeLimitChanged,
}) {
final minZoom = getMinZoomForNodes(uploadMode);
List<OsmNode> allNodes;
List<OsmNode> nodesToRender;
bool isLimitActive = false;
if (currentZoom >= minZoom) {
// Above minimum zoom - get cached nodes with expanded bounds to prevent edge blinking
if (mapBounds != null) {
final expandedBounds = _expandBounds(mapBounds, kNodeRenderingBoundsExpansion);
allNodes = NodeProviderWithCache.instance.getCachedNodesForBounds(expandedBounds);
} else {
allNodes = <OsmNode>[];
}
// Filter out invalid coordinates before applying limit
final validNodes = allNodes.where((node) {
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
node.coord.latitude.abs() <= 90 &&
node.coord.longitude.abs() <= 180;
}).toList();
// Apply rendering limit to prevent UI lag
if (validNodes.length > maxNodes) {
nodesToRender = validNodes.take(maxNodes).toList();
isLimitActive = true;
debugPrint('[MapDataManager] Node limit active: rendering ${nodesToRender.length} of ${validNodes.length} devices');
} else {
nodesToRender = validNodes;
isLimitActive = false;
}
} else {
// Below minimum zoom - don't render any nodes
allNodes = <OsmNode>[];
nodesToRender = <OsmNode>[];
isLimitActive = false;
}
// Notify parent if limit state changed (for button disabling)
if (isLimitActive != _lastNodeLimitState) {
_lastNodeLimitState = isLimitActive;
// Schedule callback after build completes to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
onNodeLimitChanged?.call(isLimitActive);
});
}
return MapDataResult(
allNodes: allNodes,
nodesToRender: nodesToRender,
isLimitActive: isLimitActive,
validNodesCount: isLimitActive ? allNodes.where((node) {
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
node.coord.latitude.abs() <= 90 &&
node.coord.longitude.abs() <= 180;
}).length : 0,
);
}
/// Show zoom warning if user is below minimum zoom level
void showZoomWarningIfNeeded(BuildContext context, double currentZoom, UploadMode uploadMode) {
final minZoom = getMinZoomForNodes(uploadMode);
// Only show warning once per zoom level to avoid spam
if (currentZoom.floor() == (minZoom - 1)) {
final message = uploadMode == UploadMode.sandbox
? 'Zoom to level $minZoom or higher to see nodes in sandbox mode (OSM API bbox limit)'
: 'Zoom to level $minZoom or higher to see surveillance nodes';
// Show a brief snackbar
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 4),
behavior: SnackBarBehavior.floating,
),
);
}
}
}
/// Result object containing node data and rendering state
class MapDataResult {
final List<OsmNode> allNodes;
final List<OsmNode> nodesToRender;
final bool isLimitActive;
final int validNodesCount;
const MapDataResult({
required this.allNodes,
required this.nodesToRender,
required this.isLimitActive,
required this.validNodesCount,
});
}