mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-29 11:21:35 +02:00
Suspected location exclusion zone, drawn under real nodes, and update progress bar
This commit is contained in:
+10
-2
@@ -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));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user