Suspected location exclusion zone, drawn under real nodes, and update progress bar

This commit is contained in:
stopflock
2025-10-06 22:21:05 -05:00
parent 4a44ab96d6
commit f9351ba272
8 changed files with 206 additions and 21 deletions
+10 -2
View File
@@ -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<TileProvider> 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<void> 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<bool> refreshSuspectedLocations() async {
return await _suspectedLocationState.refreshData();
Future<bool> refreshSuspectedLocations({
void Function(String message, double? progress)? onProgress,
}) async {
return await _suspectedLocationState.refreshData(onProgress: onProgress);
}
void selectSuspectedLocation(SuspectedLocation location) {
@@ -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<void> 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));
},
),
),
),
],
],
);
+14 -3
View File
@@ -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<void> processAndSave(List<Map<String, dynamic>> rawData, DateTime fetchTime) async {
Future<void> processAndSave(
List<Map<String, dynamic>> 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);
+22 -5
View File
@@ -64,8 +64,10 @@ class SuspectedLocationService {
}
/// Manually refresh the data
Future<bool> refreshData() async {
return await _fetchData();
Future<bool> 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<bool> _fetchData() async {
Future<bool> _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;
+16
View File
@@ -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<TileProvider> _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<TileProvider> 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<void> setSuspectedLocationMinDistance(int distance) async {
if (_suspectedLocationMinDistance != distance) {
_suspectedLocationMinDistance = distance;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_suspectedLocationMinDistancePrefsKey, distance);
notifyListeners();
}
}
}
+4 -2
View File
@@ -49,12 +49,14 @@ class SuspectedLocationState extends ChangeNotifier {
}
/// Manually refresh the data
Future<bool> refreshData() async {
Future<bool> 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;
+43 -9
View File
@@ -355,8 +355,6 @@ class MapViewState extends State<MapView> {
// Build suspected location markers
final suspectedLocationMarkers = <Marker>[];
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<MapView> {
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<MapView> {
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<MapView> {
return lines;
}
/// Filter suspected locations that are too close to real nodes
List<SuspectedLocation> _filterSuspectedLocationsByProximity({
required List<SuspectedLocation> suspectedLocations,
required List<OsmNode> realNodes,
required int minDistance, // in meters
}) {
if (minDistance <= 0) return suspectedLocations;
const distance = Distance();
final filteredLocations = <SuspectedLocation>[];
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;
}
}
@@ -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'),
),
],
);
}
}