Improvements to suspected locations

This commit is contained in:
stopflock
2025-11-13 13:22:46 -06:00
parent 3810dfa8d2
commit 2cf840e74d
8 changed files with 66 additions and 81 deletions

View File

@@ -1,6 +1,6 @@
{
"1.3.1": {
"content": "• UX: Network status indicator is now always enabled by default\n• UX: Direction control buttons now use configurable dimensions\n• INTERNAL: Cleaned up advanced settings - removed redundant network status toggle"
"content": "• UX: Network status indicator is now always enabled by default\n• UX: Direction control buttons now use configurable dimensions\n• UX: Fixed iOS keyboard missing 'Done' button on integer input fields\n• UX: Direction control buttons now always visible but greyed out when not needed\n• UX: Fixed multi-direction nodes showing all directions in upload queue\n• UX: Improved suspected locations loading indicator - removed popup, fixed stuck spinner\n• INTERNAL: Cleaned up advanced settings - removed redundant network status toggle"
},
"1.2.8": {
"content": "• UX: Profile selection is now a required step to prevent accidental submission of default profile.\n• NEW: Note in welcome message about not submitting data you cannot vouch for personally (no street view etc)\n• NEW: Added default operator profiles for the most common private operators nationwide (Lowe's, Home Depot, et al)\n• NEW: Support for cardinal directions in OSM data, multiple directions on a node."

View File

@@ -485,10 +485,8 @@ class AppState extends ChangeNotifier {
await _suspectedLocationState.setEnabled(enabled);
}
Future<bool> refreshSuspectedLocations({
void Function(String message, double? progress)? onProgress,
}) async {
return await _suspectedLocationState.refreshData(onProgress: onProgress);
Future<bool> refreshSuspectedLocations() async {
return await _suspectedLocationState.refreshData();
}
void selectSuspectedLocation(SuspectedLocation location) {

View File

@@ -70,7 +70,7 @@ class _MaxNodesSectionState extends State<MaxNodesSection> {
width: 80,
child: TextFormField(
controller: _controller,
keyboardType: TextInputType.number,
keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true),
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
isDense: true,

View File

@@ -181,7 +181,7 @@ class _ProximityAlertsSectionState extends State<ProximityAlertsSection> {
width: 80,
child: TextField(
controller: _distanceController,
keyboardType: TextInputType.number,
keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true),
textInputAction: TextInputAction.done,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,

View File

@@ -2,7 +2,6 @@ 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});
@@ -39,31 +38,19 @@ class SuspectedLocationsSection extends StatelessWidget {
Future<void> handleRefresh() async {
if (!context.mounted) return;
// Show simple progress dialog
showDialog(
context: context,
barrierDismissible: false,
builder: (progressContext) => SuspectedLocationProgressDialog(
title: locService.t('suspectedLocations.updating'),
message: locService.t('suspectedLocations.downloadingAndProcessing'),
),
);
// Start the refresh
// Use the inline loading indicator by calling refreshSuspectedLocations
// The loading state will be managed by suspected location state
final success = await appState.refreshSuspectedLocations();
// Close progress dialog
// Show result snackbar
if (context.mounted) {
Navigator.of(context).pop();
// Show result snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? locService.t('suspectedLocations.updateSuccess')
: locService.t('suspectedLocations.updateFailed')),
),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? locService.t('suspectedLocations.updateSuccess')
: locService.t('suspectedLocations.updateFailed')),
),
);
}
}
@@ -139,7 +126,7 @@ class SuspectedLocationsSection extends StatelessWidget {
width: 80,
child: TextFormField(
initialValue: appState.suspectedLocationMinDistance.toString(),
keyboardType: TextInputType.number,
keyboardType: const TextInputType.numberWithOptions(signed: true, decimal: true),
textInputAction: TextInputAction.done,
decoration: const InputDecoration(
isDense: true,

View File

@@ -127,9 +127,8 @@ 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, {
void Function(String message, double? progress)? onProgress,
}) async {
DateTime fetchTime,
) async {
try {
debugPrint('[SuspectedLocationCache] Processing ${rawData.length} raw entries...');
@@ -141,10 +140,9 @@ class SuspectedLocationCache extends ChangeNotifier {
for (int i = 0; i < rawData.length; i++) {
final rowData = rawData[i];
// Report progress every 1000 entries
// Log progress every 1000 entries for debugging
if (i % 1000 == 0) {
final progress = i / rawData.length;
onProgress?.call('Calculating coordinates: ${i + 1}/${rawData.length}', progress);
debugPrint('[SuspectedLocationCache] Processed ${i + 1}/${rawData.length} entries...');
}
try {

View File

@@ -22,7 +22,6 @@ class SuspectedLocationService {
final SuspectedLocationCache _cache = SuspectedLocationCache();
bool _isEnabled = false;
bool _isLoading = false;
/// Get last fetch time
DateTime? get lastFetchTime => _cache.lastFetchTime;
@@ -30,9 +29,6 @@ class SuspectedLocationService {
/// Check if suspected locations are enabled
bool get isEnabled => _isEnabled;
/// Check if currently loading
bool get isLoading => _isLoading;
/// Initialize the service - load from storage and check if refresh needed
Future<void> init({bool offlineMode = false}) async {
await _loadFromStorage();
@@ -55,22 +51,31 @@ class SuspectedLocationService {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_prefsKeyEnabled, enabled);
// If enabling for the first time and no data, fetch it in background
if (enabled && !_cache.hasData) {
_fetchData(); // Don't await - let it run in background so UI updates immediately
}
// If disabling, clear the cache
if (!enabled) {
_cache.clear();
}
// Note: If enabling and no data, the state layer will call fetchDataIfNeeded()
}
/// Manually refresh the data
Future<bool> refreshData({
void Function(String message, double? progress)? onProgress,
}) async {
return await _fetchData(onProgress: onProgress);
/// Check if cache has any data
bool get hasData => _cache.hasData;
/// Get last fetch time
DateTime? get lastFetch => _cache.lastFetchTime;
/// Fetch data if needed (for enabling suspected locations when no data exists)
Future<bool> fetchDataIfNeeded() async {
if (!_shouldRefresh()) {
debugPrint('[SuspectedLocationService] Data is fresh, skipping fetch');
return true; // Already have fresh data
}
return await _fetchData();
}
/// Force refresh the data (for manual refresh button)
Future<bool> forceRefresh() async {
return await _fetchData();
}
/// Check if data should be refreshed
@@ -95,14 +100,8 @@ class SuspectedLocationService {
}
/// Fetch data from the CSV URL
Future<bool> _fetchData({
void Function(String message, double? progress)? onProgress,
}) async {
if (_isLoading) return false;
_isLoading = true;
Future<bool> _fetchData() async {
try {
onProgress?.call('Downloading CSV data...', null);
debugPrint('[SuspectedLocationService] Fetching CSV data from $kSuspectedLocationsCsvUrl');
final response = await http.get(
@@ -117,8 +116,6 @@ class SuspectedLocationService {
return false;
}
onProgress?.call('Parsing CSV data...', 0.2);
// Parse CSV with proper field separator and quote handling
final csvData = await compute(_parseCSV, response.body);
debugPrint('[SuspectedLocationService] Parsed ${csvData.length} rows from CSV');
@@ -170,11 +167,6 @@ class SuspectedLocationService {
validRows++;
}
// Report progress every 1000 rows
if (rowIndex % 1000 == 0) {
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
debugPrint('[SuspectedLocationService] Error parsing row $rowIndex: $e');
@@ -182,18 +174,12 @@ class SuspectedLocationService {
}
}
onProgress?.call('Calculating coordinates...', 0.8);
debugPrint('[SuspectedLocationService] Parsed $validRows valid rows from ${dataRows.length} total rows');
final fetchTime = DateTime.now();
// Process raw data and save (calculates centroids once)
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);
await _cache.processAndSave(rawDataList, fetchTime);
debugPrint('[SuspectedLocationService] Successfully fetched and stored $validRows valid raw entries (${rawDataList.length} total)');
return true;
@@ -202,8 +188,6 @@ class SuspectedLocationService {
debugPrint('[SuspectedLocationService] Error fetching data: $e');
debugPrint('[SuspectedLocationService] Stack trace: $stackTrace');
return false;
} finally {
_isLoading = false;
}
}

View File

@@ -31,7 +31,7 @@ class SuspectedLocationState extends ChangeNotifier {
bool get isEnabled => _service.isEnabled;
/// Whether currently loading data
bool get isLoading => _isLoading || _service.isLoading;
bool get isLoading => _isLoading;
/// Last time data was fetched
DateTime? get lastFetchTime => _service.lastFetchTime;
@@ -45,18 +45,36 @@ class SuspectedLocationState extends ChangeNotifier {
/// Enable or disable suspected locations
Future<void> setEnabled(bool enabled) async {
await _service.setEnabled(enabled);
// If enabling and no data exists, fetch it now
if (enabled && !_service.hasData) {
await _fetchData();
}
notifyListeners();
}
/// Manually refresh the data
Future<bool> refreshData({
void Function(String message, double? progress)? onProgress,
}) async {
/// Manually refresh the data (force refresh)
Future<bool> refreshData() async {
_isLoading = true;
notifyListeners();
try {
final success = await _service.refreshData(onProgress: onProgress);
final success = await _service.forceRefresh();
return success;
} finally {
_isLoading = false;
notifyListeners();
}
}
/// Internal method to fetch data if needed with loading state management
Future<bool> _fetchData() async {
_isLoading = true;
notifyListeners();
try {
final success = await _service.fetchDataIfNeeded();
return success;
} finally {
_isLoading = false;