mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-03-21 02:13:39 +00:00
Sort nodes by squared distance from viewport center before applying the render limit, so visible nodes always make the cut instead of arbitrary selection causing gaps that shift as you pan. Also: inject node provider for testability, deduplicate validity filter, and reduce debug log spam to state transitions only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
162 lines
6.0 KiB
Dart
162 lines
6.0 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 {
|
|
final List<OsmNode> Function(LatLngBounds bounds) _getNodesForBounds;
|
|
|
|
MapDataManager({
|
|
List<OsmNode> Function(LatLngBounds bounds)? getNodesForBounds,
|
|
}) : _getNodesForBounds = getNodesForBounds ??
|
|
((bounds) => NodeProviderWithCache.instance.getCachedNodesForBounds(bounds));
|
|
|
|
// 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;
|
|
int validNodesCount = 0;
|
|
|
|
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 = _getNodesForBounds(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();
|
|
validNodesCount = validNodes.length;
|
|
|
|
// Apply rendering limit to prevent UI lag.
|
|
// Sort by distance from viewport center so the most visible nodes
|
|
// always make the cut, preventing gaps that shift as you pan.
|
|
if (validNodesCount > maxNodes) {
|
|
final centerLat = (mapBounds!.north + mapBounds.south) / 2;
|
|
final centerLng = (mapBounds.east + mapBounds.west) / 2;
|
|
validNodes.sort((a, b) {
|
|
final distA = (a.coord.latitude - centerLat) * (a.coord.latitude - centerLat) +
|
|
(a.coord.longitude - centerLng) * (a.coord.longitude - centerLng);
|
|
final distB = (b.coord.latitude - centerLat) * (b.coord.latitude - centerLat) +
|
|
(b.coord.longitude - centerLng) * (b.coord.longitude - centerLng);
|
|
return distA.compareTo(distB);
|
|
});
|
|
nodesToRender = validNodes.take(maxNodes).toList();
|
|
isLimitActive = true;
|
|
} 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;
|
|
if (isLimitActive) {
|
|
debugPrint('[MapDataManager] Node limit active: rendering ${nodesToRender.length} of ${allNodes.length} devices');
|
|
}
|
|
// 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 ? validNodesCount : 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,
|
|
});
|
|
} |