From f9351ba2728e1eb1f28b32ba746dd0194e129c39 Mon Sep 17 00:00:00 2001 From: stopflock Date: Mon, 6 Oct 2025 22:21:05 -0500 Subject: [PATCH] Suspected location exclusion zone, drawn under real nodes, and update progress bar --- lib/app_state.dart | 12 ++++- .../sections/suspected_locations_section.dart | 43 +++++++++++++++ lib/services/suspected_location_cache.dart | 17 ++++-- lib/services/suspected_location_service.dart | 27 ++++++++-- lib/state/settings_state.dart | 16 ++++++ lib/state/suspected_location_state.dart | 6 ++- lib/widgets/map_view.dart | 52 ++++++++++++++---- .../suspected_location_progress_dialog.dart | 54 +++++++++++++++++++ 8 files changed, 206 insertions(+), 21 deletions(-) create mode 100644 lib/widgets/suspected_location_progress_dialog.dart diff --git a/lib/app_state.dart b/lib/app_state.dart index 8565f22..4bff1ed 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -132,6 +132,7 @@ class AppState extends ChangeNotifier { bool get proximityAlertsEnabled => _settingsState.proximityAlertsEnabled; int get proximityAlertDistance => _settingsState.proximityAlertDistance; bool get networkStatusIndicatorEnabled => _settingsState.networkStatusIndicatorEnabled; + int get suspectedLocationMinDistance => _settingsState.suspectedLocationMinDistance; // Tile provider state List get tileProviders => _settingsState.tileProviders; @@ -420,6 +421,11 @@ class AppState extends ChangeNotifier { await _settingsState.setNetworkStatusIndicatorEnabled(enabled); } + /// Set suspected location minimum distance from real nodes + Future setSuspectedLocationMinDistance(int distance) async { + await _settingsState.setSuspectedLocationMinDistance(distance); + } + // ---------- Queue Methods ---------- void clearQueue() { _uploadQueueState.clearQueue(); @@ -439,8 +445,10 @@ class AppState extends ChangeNotifier { await _suspectedLocationState.setEnabled(enabled); } - Future refreshSuspectedLocations() async { - return await _suspectedLocationState.refreshData(); + Future refreshSuspectedLocations({ + void Function(String message, double? progress)? onProgress, + }) async { + return await _suspectedLocationState.refreshData(onProgress: onProgress); } void selectSuspectedLocation(SuspectedLocation location) { diff --git a/lib/screens/settings/sections/suspected_locations_section.dart b/lib/screens/settings/sections/suspected_locations_section.dart index 3d180c2..b6732ef 100644 --- a/lib/screens/settings/sections/suspected_locations_section.dart +++ b/lib/screens/settings/sections/suspected_locations_section.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../../app_state.dart'; import '../../../services/localization_service.dart'; +import '../../../widgets/suspected_location_progress_dialog.dart'; class SuspectedLocationsSection extends StatelessWidget { const SuspectedLocationsSection({super.key}); @@ -36,8 +37,26 @@ class SuspectedLocationsSection extends StatelessWidget { } Future handleRefresh() async { + if (!context.mounted) return; + + // Show simple progress dialog + showDialog( + context: context, + barrierDismissible: false, + builder: (progressContext) => const SuspectedLocationProgressDialog( + title: 'Updating Suspected Locations', + message: 'Downloading and processing data...', + ), + ); + + // Start the refresh final success = await appState.refreshSuspectedLocations(); + + // Close progress dialog if (context.mounted) { + Navigator.of(context).pop(); + + // Show result snackbar ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(success @@ -97,6 +116,30 @@ class SuspectedLocationsSection extends StatelessWidget { title: const Text('Data Source'), subtitle: const Text('Utility permit data indicating potential surveillance infrastructure installation sites'), ), + + // Minimum distance setting + ListTile( + leading: const Icon(Icons.social_distance), + title: const Text('Minimum Distance from Real Nodes'), + subtitle: Text('Hide suspected locations within ${appState.suspectedLocationMinDistance}m of existing surveillance devices'), + trailing: SizedBox( + width: 80, + child: TextFormField( + initialValue: appState.suspectedLocationMinDistance.toString(), + keyboardType: TextInputType.number, + decoration: const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 6, horizontal: 8), + border: OutlineInputBorder(), + suffixText: 'm', + ), + onFieldSubmitted: (value) { + final distance = int.tryParse(value) ?? 100; + appState.setSuspectedLocationMinDistance(distance.clamp(0, 1000)); + }, + ), + ), + ), ], ], ); diff --git a/lib/services/suspected_location_cache.dart b/lib/services/suspected_location_cache.dart index ba28f2c..1657d47 100644 --- a/lib/services/suspected_location_cache.dart +++ b/lib/services/suspected_location_cache.dart @@ -52,7 +52,7 @@ class SuspectedLocationCache extends ChangeNotifier { final boundsKey = '${bounds.north.toStringAsFixed(4)},${bounds.south.toStringAsFixed(4)},${bounds.east.toStringAsFixed(4)},${bounds.west.toStringAsFixed(4)}'; - debugPrint('[SuspectedLocationCache] Getting locations for bounds: $boundsKey, processed entries count: ${_processedEntries.length}'); + // debugPrint('[SuspectedLocationCache] Getting locations for bounds: $boundsKey, processed entries count: ${_processedEntries.length}'); // Check cache first if (_boundsCache.containsKey(boundsKey)) { @@ -83,7 +83,7 @@ class SuspectedLocationCache extends ChangeNotifier { } } - debugPrint('[SuspectedLocationCache] Checked ${_processedEntries.length} entries, $inBoundsCount in bounds, result: ${locations.length} locations'); + // debugPrint('[SuspectedLocationCache] Checked ${_processedEntries.length} entries, $inBoundsCount in bounds, result: ${locations.length} locations'); // Cache the result _boundsCache[boundsKey] = locations; @@ -125,7 +125,11 @@ class SuspectedLocationCache extends ChangeNotifier { } /// Process raw CSV data and save to storage (calculates centroids once) - Future processAndSave(List> rawData, DateTime fetchTime) async { + Future processAndSave( + List> rawData, + DateTime fetchTime, { + void Function(String message, double? progress)? onProgress, + }) async { try { debugPrint('[SuspectedLocationCache] Processing ${rawData.length} raw entries...'); @@ -136,6 +140,13 @@ class SuspectedLocationCache extends ChangeNotifier { for (int i = 0; i < rawData.length; i++) { final rowData = rawData[i]; + + // Report progress every 1000 entries + if (i % 1000 == 0) { + final progress = i / rawData.length; + onProgress?.call('Calculating coordinates: ${i + 1}/${rawData.length}', progress); + } + try { // Create a temporary SuspectedLocation to extract the centroid final tempLocation = SuspectedLocation.fromCsvRow(rowData); diff --git a/lib/services/suspected_location_service.dart b/lib/services/suspected_location_service.dart index 75ecb74..b60b014 100644 --- a/lib/services/suspected_location_service.dart +++ b/lib/services/suspected_location_service.dart @@ -64,8 +64,10 @@ class SuspectedLocationService { } /// Manually refresh the data - Future refreshData() async { - return await _fetchData(); + Future refreshData({ + void Function(String message, double? progress)? onProgress, + }) async { + return await _fetchData(onProgress: onProgress); } /// Check if data should be refreshed @@ -90,11 +92,14 @@ class SuspectedLocationService { } /// Fetch data from the CSV URL - Future _fetchData() async { + Future _fetchData({ + void Function(String message, double? progress)? onProgress, + }) async { if (_isLoading) return false; _isLoading = true; try { + onProgress?.call('Downloading CSV data...', null); debugPrint('[SuspectedLocationService] Fetching CSV data from $_csvUrl'); final response = await http.get( @@ -109,6 +114,8 @@ class SuspectedLocationService { return false; } + onProgress?.call('Parsing CSV data...', 0.2); + // Parse CSV with proper field separator and quote handling final csvData = const CsvToListConverter( fieldDelimiter: ',', @@ -180,9 +187,11 @@ class SuspectedLocationService { validRows++; } - // Log progress every 1000 rows + // Log progress every 1000 rows and report to UI if (rowIndex % 1000 == 0) { debugPrint('[SuspectedLocationService] Processing row $rowIndex...'); + final progress = 0.4 + (rowIndex / dataRows.length) * 0.4; // 40% to 80% of total + onProgress?.call('Processing row $rowIndex...', progress); } } catch (e, stackTrace) { // Skip rows that can't be parsed @@ -191,10 +200,18 @@ class SuspectedLocationService { } } + onProgress?.call('Calculating coordinates...', 0.8); + final fetchTime = DateTime.now(); // Process raw data and save (calculates centroids once) - await _cache.processAndSave(rawDataList, fetchTime); + await _cache.processAndSave(rawDataList, fetchTime, onProgress: (message, progress) { + // Map cache progress to final 20% (0.8 to 1.0) + final finalProgress = 0.8 + (progress ?? 0) * 0.2; + onProgress?.call(message, finalProgress); + }); + + onProgress?.call('Complete!', 1.0); debugPrint('[SuspectedLocationService] Successfully fetched and stored $validRows valid raw entries (${rawDataList.length} total)'); return true; diff --git a/lib/state/settings_state.dart b/lib/state/settings_state.dart index 0834954..0f69f9f 100644 --- a/lib/state/settings_state.dart +++ b/lib/state/settings_state.dart @@ -27,6 +27,7 @@ class SettingsState extends ChangeNotifier { static const String _proximityAlertsEnabledPrefsKey = 'proximity_alerts_enabled'; static const String _proximityAlertDistancePrefsKey = 'proximity_alert_distance'; static const String _networkStatusIndicatorEnabledPrefsKey = 'network_status_indicator_enabled'; + static const String _suspectedLocationMinDistancePrefsKey = 'suspected_location_min_distance'; bool _offlineMode = false; int _maxCameras = 250; @@ -35,6 +36,7 @@ class SettingsState extends ChangeNotifier { bool _proximityAlertsEnabled = false; int _proximityAlertDistance = kProximityAlertDefaultDistance; bool _networkStatusIndicatorEnabled = false; + int _suspectedLocationMinDistance = 100; // meters List _tileProviders = []; String _selectedTileTypeId = ''; @@ -46,6 +48,7 @@ class SettingsState extends ChangeNotifier { bool get proximityAlertsEnabled => _proximityAlertsEnabled; int get proximityAlertDistance => _proximityAlertDistance; bool get networkStatusIndicatorEnabled => _networkStatusIndicatorEnabled; + int get suspectedLocationMinDistance => _suspectedLocationMinDistance; List get tileProviders => List.unmodifiable(_tileProviders); String get selectedTileTypeId => _selectedTileTypeId; @@ -101,6 +104,9 @@ class SettingsState extends ChangeNotifier { // Load network status indicator setting _networkStatusIndicatorEnabled = prefs.getBool(_networkStatusIndicatorEnabledPrefsKey) ?? false; + // Load suspected location minimum distance + _suspectedLocationMinDistance = prefs.getInt(_suspectedLocationMinDistancePrefsKey) ?? 100; + // Load upload mode (including migration from old test_mode bool) if (prefs.containsKey(_uploadModePrefsKey)) { final idx = prefs.getInt(_uploadModePrefsKey) ?? 0; @@ -323,4 +329,14 @@ class SettingsState extends ChangeNotifier { } } + /// Set suspected location minimum distance from real nodes + Future setSuspectedLocationMinDistance(int distance) async { + if (_suspectedLocationMinDistance != distance) { + _suspectedLocationMinDistance = distance; + final prefs = await SharedPreferences.getInstance(); + await prefs.setInt(_suspectedLocationMinDistancePrefsKey, distance); + notifyListeners(); + } + } + } \ No newline at end of file diff --git a/lib/state/suspected_location_state.dart b/lib/state/suspected_location_state.dart index b929480..e2e2a73 100644 --- a/lib/state/suspected_location_state.dart +++ b/lib/state/suspected_location_state.dart @@ -49,12 +49,14 @@ class SuspectedLocationState extends ChangeNotifier { } /// Manually refresh the data - Future refreshData() async { + Future refreshData({ + void Function(String message, double? progress)? onProgress, + }) async { _isLoading = true; notifyListeners(); try { - final success = await _service.refreshData(); + final success = await _service.refreshData(onProgress: onProgress); return success; } finally { _isLoading = false; diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index fa69e93..2fd5fee 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -355,8 +355,6 @@ class MapViewState extends State { // Build suspected location markers final suspectedLocationMarkers = []; if (appState.suspectedLocationsEnabled && mapBounds != null) { - debugPrint('[MapView] Suspected locations enabled, getting bounds: N${mapBounds.north.toStringAsFixed(4)}, S${mapBounds.south.toStringAsFixed(4)}, E${mapBounds.east.toStringAsFixed(4)}, W${mapBounds.west.toStringAsFixed(4)}'); - final suspectedLocations = appState.getSuspectedLocationsInBounds( north: mapBounds.north, south: mapBounds.south, @@ -364,20 +362,21 @@ class MapViewState extends State { west: mapBounds.west, ); - debugPrint('[MapView] Found ${suspectedLocations.length} suspected locations in bounds'); + // Filter out suspected locations that are too close to real nodes + final filteredSuspectedLocations = _filterSuspectedLocationsByProximity( + suspectedLocations: suspectedLocations, + realNodes: cameras, + minDistance: appState.suspectedLocationMinDistance, + ); suspectedLocationMarkers.addAll( SuspectedLocationMarkersBuilder.buildSuspectedLocationMarkers( - locations: suspectedLocations, + locations: filteredSuspectedLocations, mapController: _controller.mapController, selectedLocationId: appState.selectedSuspectedLocation?.ticketNo, onLocationTap: widget.onSuspectedLocationTap, ), ); - - debugPrint('[MapView] Created ${suspectedLocationMarkers.length} suspected location markers'); - } else { - debugPrint('[MapView] Suspected locations not enabled (${appState.suspectedLocationsEnabled}) or no mapBounds ($mapBounds)'); } // Get current zoom level for direction cones @@ -487,7 +486,7 @@ class MapViewState extends State { PolygonLayer(polygons: overlays), if (editLines.isNotEmpty) PolylineLayer(polylines: editLines), if (routeLines.isNotEmpty) PolylineLayer(polylines: routeLines), - MarkerLayer(markers: [...markers, ...suspectedLocationMarkers, ...centerMarkers]), + MarkerLayer(markers: [...suspectedLocationMarkers, ...markers, ...centerMarkers]), ], ); } @@ -633,5 +632,40 @@ class MapViewState extends State { return lines; } + + /// Filter suspected locations that are too close to real nodes + List _filterSuspectedLocationsByProximity({ + required List suspectedLocations, + required List realNodes, + required int minDistance, // in meters + }) { + if (minDistance <= 0) return suspectedLocations; + + const distance = Distance(); + final filteredLocations = []; + + for (final suspected in suspectedLocations) { + bool tooClose = false; + + for (final realNode in realNodes) { + final distanceMeters = distance.as( + LengthUnit.Meter, + suspected.centroid, + realNode.coord, + ); + + if (distanceMeters < minDistance) { + tooClose = true; + break; + } + } + + if (!tooClose) { + filteredLocations.add(suspected); + } + } + + return filteredLocations; + } } diff --git a/lib/widgets/suspected_location_progress_dialog.dart b/lib/widgets/suspected_location_progress_dialog.dart new file mode 100644 index 0000000..2f6dc3f --- /dev/null +++ b/lib/widgets/suspected_location_progress_dialog.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +class SuspectedLocationProgressDialog extends StatelessWidget { + final String title; + final String message; + final double? progress; // 0.0 to 1.0, null for indeterminate + final VoidCallback? onCancel; + + const SuspectedLocationProgressDialog({ + super.key, + required this.title, + required this.message, + this.progress, + this.onCancel, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + const Icon(Icons.help_outline, color: Colors.orange), + const SizedBox(width: 8), + Expanded(child: Text(title)), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(message), + const SizedBox(height: 16), + if (progress != null) + LinearProgressIndicator(value: progress) + else + const LinearProgressIndicator(), + const SizedBox(height: 8), + if (progress != null) + Text( + '${(progress! * 100).toInt()}%', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + actions: [ + if (onCancel != null) + TextButton( + onPressed: onCancel, + child: const Text('Cancel'), + ), + ], + ); + } +} \ No newline at end of file