Suspected location localizations, credit alprwatch

This commit is contained in:
stopflock
2025-10-07 14:03:11 -05:00
parent 5c28057fa1
commit b00db130d7
13 changed files with 299 additions and 74 deletions

View File

@@ -341,5 +341,40 @@
"imperial": "Britisch (mi, ft)",
"meters": "Meter",
"feet": "Fuß"
},
"suspectedLocations": {
"title": "Verdächtige Standorte",
"showSuspectedLocations": "Verdächtige Standorte anzeigen",
"showSuspectedLocationsSubtitle": "Fragezeichen-Marker für vermutete Überwachungsstandorte aus Versorgungsgenehmigungsdaten anzeigen",
"lastUpdated": "Zuletzt aktualisiert",
"refreshNow": "Jetzt aktualisieren",
"dataSource": "Datenquelle",
"dataSourceDescription": "Versorgungsgenehmigungsdaten, die auf potenzielle Installationsstandorte für Überwachungsinfrastruktur hinweisen",
"dataSourceCredit": "Datensammlung und -hosting bereitgestellt von alprwatch.org",
"minimumDistance": "Mindestabstand zu echten Geräten",
"minimumDistanceSubtitle": "Verdächtige Standorte innerhalb von {}m vorhandener Überwachungsgeräte ausblenden",
"updating": "Verdächtige Standorte werden aktualisiert",
"downloadingAndProcessing": "Daten werden heruntergeladen und verarbeitet...",
"updateSuccess": "Verdächtige Standorte erfolgreich aktualisiert",
"updateFailed": "Aktualisierung der verdächtigen Standorte fehlgeschlagen",
"neverFetched": "Nie abgerufen",
"daysAgo": "vor {} Tagen",
"hoursAgo": "vor {} Stunden",
"minutesAgo": "vor {} Minuten",
"justNow": "Gerade eben"
},
"suspectedLocation": {
"title": "Verdächtiger Standort #{}",
"ticketNo": "Ticket-Nr.",
"address": "Adresse",
"street": "Straße",
"city": "Stadt",
"state": "Bundesland",
"intersectingStreet": "Kreuzende Straße",
"workDoneFor": "Arbeit ausgeführt für",
"remarks": "Bemerkungen",
"url": "URL",
"coordinates": "Koordinaten",
"noAddressAvailable": "Keine Adresse verfügbar"
}
}

View File

@@ -341,5 +341,40 @@
"imperial": "Imperial (mi, ft)",
"meters": "meters",
"feet": "feet"
},
"suspectedLocations": {
"title": "Suspected Locations",
"showSuspectedLocations": "Show Suspected Locations",
"showSuspectedLocationsSubtitle": "Show question mark markers for suspected surveillance sites from utility permit data",
"lastUpdated": "Last Updated",
"refreshNow": "Refresh now",
"dataSource": "Data Source",
"dataSourceDescription": "Utility permit data indicating potential surveillance infrastructure installation sites",
"dataSourceCredit": "Data collection and hosting provided by alprwatch.org",
"minimumDistance": "Minimum Distance from Real Nodes",
"minimumDistanceSubtitle": "Hide suspected locations within {}m of existing surveillance devices",
"updating": "Updating Suspected Locations",
"downloadingAndProcessing": "Downloading and processing data...",
"updateSuccess": "Suspected locations updated successfully",
"updateFailed": "Failed to update suspected locations",
"neverFetched": "Never fetched",
"daysAgo": "{} days ago",
"hoursAgo": "{} hours ago",
"minutesAgo": "{} minutes ago",
"justNow": "Just now"
},
"suspectedLocation": {
"title": "Suspected Location #{}",
"ticketNo": "Ticket No",
"address": "Address",
"street": "Street",
"city": "City",
"state": "State",
"intersectingStreet": "Intersecting Street",
"workDoneFor": "Work Done For",
"remarks": "Remarks",
"url": "URL",
"coordinates": "Coordinates",
"noAddressAvailable": "No address available"
}
}

View File

@@ -341,5 +341,40 @@
"imperial": "Imperial (mi, ft)",
"meters": "metros",
"feet": "pies"
},
"suspectedLocations": {
"title": "Ubicaciones Sospechosas",
"showSuspectedLocations": "Mostrar Ubicaciones Sospechosas",
"showSuspectedLocationsSubtitle": "Mostrar marcadores de interrogación para sitios de vigilancia sospechosos de datos de permisos de servicios públicos",
"lastUpdated": "Última Actualización",
"refreshNow": "Actualizar ahora",
"dataSource": "Fuente de Datos",
"dataSourceDescription": "Datos de permisos de servicios públicos que indican posibles sitios de instalación de infraestructura de vigilancia",
"dataSourceCredit": "Recopilación y alojamiento de datos proporcionado por alprwatch.org",
"minimumDistance": "Distancia Mínima de Nodos Reales",
"minimumDistanceSubtitle": "Ocultar ubicaciones sospechosas dentro de {}m de dispositivos de vigilancia existentes",
"updating": "Actualizando Ubicaciones Sospechosas",
"downloadingAndProcessing": "Descargando y procesando datos...",
"updateSuccess": "Ubicaciones sospechosas actualizadas exitosamente",
"updateFailed": "Error al actualizar ubicaciones sospechosas",
"neverFetched": "Nunca obtenido",
"daysAgo": "hace {} días",
"hoursAgo": "hace {} horas",
"minutesAgo": "hace {} minutos",
"justNow": "Ahora mismo"
},
"suspectedLocation": {
"title": "Ubicación Sospechosa #{}",
"ticketNo": "No. de Ticket",
"address": "Dirección",
"street": "Calle",
"city": "Ciudad",
"state": "Estado",
"intersectingStreet": "Calle que Intersecta",
"workDoneFor": "Trabajo Realizado Para",
"remarks": "Observaciones",
"url": "URL",
"coordinates": "Coordenadas",
"noAddressAvailable": "No hay dirección disponible"
}
}

View File

@@ -341,5 +341,40 @@
"imperial": "Impérial (mi, ft)",
"meters": "mètres",
"feet": "pieds"
},
"suspectedLocations": {
"title": "Emplacements Suspects",
"showSuspectedLocations": "Afficher les Emplacements Suspects",
"showSuspectedLocationsSubtitle": "Afficher des marqueurs en point d'interrogation pour les sites de surveillance suspectés à partir des données de permis de services publics",
"lastUpdated": "Dernière Mise à Jour",
"refreshNow": "Actualiser maintenant",
"dataSource": "Source de Données",
"dataSourceDescription": "Données de permis de services publics indiquant des sites d'installation potentiels d'infrastructure de surveillance",
"dataSourceCredit": "Collecte et hébergement des données fournis par alprwatch.org",
"minimumDistance": "Distance Minimale des Nœuds Réels",
"minimumDistanceSubtitle": "Masquer les emplacements suspects dans un rayon de {}m des dispositifs de surveillance existants",
"updating": "Mise à Jour des Emplacements Suspects",
"downloadingAndProcessing": "Téléchargement et traitement des données...",
"updateSuccess": "Emplacements suspects mis à jour avec succès",
"updateFailed": "Échec de la mise à jour des emplacements suspects",
"neverFetched": "Jamais récupéré",
"daysAgo": "il y a {} jours",
"hoursAgo": "il y a {} heures",
"minutesAgo": "il y a {} minutes",
"justNow": "À l'instant"
},
"suspectedLocation": {
"title": "Emplacement Suspect #{}",
"ticketNo": "N° de Ticket",
"address": "Adresse",
"street": "Rue",
"city": "Ville",
"state": "État",
"intersectingStreet": "Rue Transversale",
"workDoneFor": "Travail Effectué Pour",
"remarks": "Remarques",
"url": "URL",
"coordinates": "Coordonnées",
"noAddressAvailable": "Aucune adresse disponible"
}
}

View File

@@ -341,5 +341,40 @@
"imperial": "Imperiale (mi, ft)",
"meters": "metri",
"feet": "piedi"
},
"suspectedLocations": {
"title": "Posizioni Sospette",
"showSuspectedLocations": "Mostra Posizioni Sospette",
"showSuspectedLocationsSubtitle": "Mostra marcatori punto interrogativo per siti di sorveglianza sospetti dai dati dei permessi dei servizi pubblici",
"lastUpdated": "Ultimo Aggiornamento",
"refreshNow": "Aggiorna ora",
"dataSource": "Fonte Dati",
"dataSourceDescription": "Dati dei permessi dei servizi pubblici che indicano potenziali siti di installazione di infrastrutture di sorveglianza",
"dataSourceCredit": "Raccolta e hosting dei dati forniti da alprwatch.org",
"minimumDistance": "Distanza Minima dai Nodi Reali",
"minimumDistanceSubtitle": "Nascondi posizioni sospette entro {}m dai dispositivi di sorveglianza esistenti",
"updating": "Aggiornamento Posizioni Sospette",
"downloadingAndProcessing": "Scaricamento e elaborazione dati...",
"updateSuccess": "Posizioni sospette aggiornate con successo",
"updateFailed": "Aggiornamento posizioni sospette fallito",
"neverFetched": "Mai recuperato",
"daysAgo": "{} giorni fa",
"hoursAgo": "{} ore fa",
"minutesAgo": "{} minuti fa",
"justNow": "Proprio ora"
},
"suspectedLocation": {
"title": "Posizione Sospetta #{}",
"ticketNo": "N. Ticket",
"address": "Indirizzo",
"street": "Via",
"city": "Città",
"state": "Stato",
"intersectingStreet": "Via che Interseca",
"workDoneFor": "Lavoro Svolto Per",
"remarks": "Osservazioni",
"url": "URL",
"coordinates": "Coordinate",
"noAddressAvailable": "Nessun indirizzo disponibile"
}
}

View File

@@ -341,5 +341,40 @@
"imperial": "Imperial (mi, ft)",
"meters": "metros",
"feet": "pés"
},
"suspectedLocations": {
"title": "Localizações Suspeitas",
"showSuspectedLocations": "Mostrar Localizações Suspeitas",
"showSuspectedLocationsSubtitle": "Mostrar marcadores de ponto de interrogação para sites de vigilância suspeitos de dados de licenças de serviços públicos",
"lastUpdated": "Última Atualização",
"refreshNow": "Atualizar agora",
"dataSource": "Fonte de Dados",
"dataSourceDescription": "Dados de licenças de serviços públicos indicando possíveis locais de instalação de infraestrutura de vigilância",
"dataSourceCredit": "Coleta e hospedagem de dados fornecidas por alprwatch.org",
"minimumDistance": "Distância Mínima de Nós Reais",
"minimumDistanceSubtitle": "Ocultar localizações suspeitas dentro de {}m de dispositivos de vigilância existentes",
"updating": "Atualizando Localizações Suspeitas",
"downloadingAndProcessing": "Baixando e processando dados...",
"updateSuccess": "Localizações suspeitas atualizadas com sucesso",
"updateFailed": "Falha ao atualizar localizações suspeitas",
"neverFetched": "Nunca buscado",
"daysAgo": "{} dias atrás",
"hoursAgo": "{} horas atrás",
"minutesAgo": "{} minutos atrás",
"justNow": "Agora mesmo"
},
"suspectedLocation": {
"title": "Localização Suspeita #{}",
"ticketNo": "N° do Ticket",
"address": "Endereço",
"street": "Rua",
"city": "Cidade",
"state": "Estado",
"intersectingStreet": "Rua que Cruza",
"workDoneFor": "Trabalho Feito Para",
"remarks": "Observações",
"url": "URL",
"coordinates": "Coordenadas",
"noAddressAvailable": "Nenhum endereço disponível"
}
}

View File

@@ -341,5 +341,40 @@
"imperial": "英制(英里,英尺)",
"meters": "米",
"feet": "英尺"
},
"suspectedLocations": {
"title": "疑似位置",
"showSuspectedLocations": "显示疑似位置",
"showSuspectedLocationsSubtitle": "根据公用事业许可数据显示疑似监控站点的问号标记",
"lastUpdated": "最后更新",
"refreshNow": "立即刷新",
"dataSource": "数据源",
"dataSourceDescription": "公用事业许可数据,表明潜在的监控基础设施安装站点",
"dataSourceCredit": "数据收集和托管由 alprwatch.org 提供",
"minimumDistance": "与真实节点的最小距离",
"minimumDistanceSubtitle": "隐藏现有监控设备{}米范围内的疑似位置",
"updating": "正在更新疑似位置",
"downloadingAndProcessing": "正在下载和处理数据...",
"updateSuccess": "疑似位置更新成功",
"updateFailed": "疑似位置更新失败",
"neverFetched": "从未获取",
"daysAgo": "{}天前",
"hoursAgo": "{}小时前",
"minutesAgo": "{}分钟前",
"justNow": "刚刚"
},
"suspectedLocation": {
"title": "疑似位置 #{}",
"ticketNo": "工单号",
"address": "地址",
"street": "街道",
"city": "城市",
"state": "州/省",
"intersectingStreet": "交叉街道",
"workDoneFor": "工作完成方",
"remarks": "备注",
"url": "网址",
"coordinates": "坐标",
"noAddressAvailable": "无可用地址"
}
}

View File

@@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:latlong2/latlong.dart';
/// A suspected surveillance location from the CSV data
@@ -42,36 +41,10 @@ class SuspectedLocation {
// Parse GeoJSON if available
if (locationString != null && locationString.isNotEmpty) {
try {
// Only log first few entries to avoid spam
final ticketNo = row['ticket_no']?.toString() ?? 'unknown';
if (ticketNo.endsWith('0') || ticketNo.endsWith('1') || ticketNo.endsWith('2')) {
print('[SuspectedLocation] Raw location string for ticket $ticketNo: ${locationString.substring(0, math.min(100, locationString.length))}...');
}
if (ticketNo.endsWith('0') || ticketNo.endsWith('1') || ticketNo.endsWith('2')) {
if (centroid.latitude != 0 || centroid.longitude != 0) {
print('[SuspectedLocation] Successfully parsed centroid: $centroid');
} else {
print('[SuspectedLocation] Parsed but got zero coordinates');
}
}
geoJson = jsonDecode(locationString) as Map<String, dynamic>;
final coordinates = _extractCoordinatesFromGeoJson(geoJson);
centroid = coordinates.centroid;
bounds = coordinates.bounds;
if (ticketNo.endsWith('0') || ticketNo.endsWith('1') || ticketNo.endsWith('2')) {
if (centroid.latitude != 0 || centroid.longitude != 0) {
print('[SuspectedLocation] Successfully parsed centroid: $centroid');
} else {
print('[SuspectedLocation] Parsed but got zero coordinates');
}
}
if (ticketNo.endsWith('0') || ticketNo.endsWith('1') || ticketNo.endsWith('2')) {
if (centroid.latitude != 0 || centroid.longitude != 0) {
print('[SuspectedLocation] Successfully parsed centroid: $centroid');
} else {
print('[SuspectedLocation] Parsed but got zero coordinates');
}
}
} catch (e) {
// If GeoJSON parsing fails, use default coordinates
print('[SuspectedLocation] Failed to parse GeoJSON for ticket ${row['ticket_no']}: $e');

View File

@@ -20,18 +20,18 @@ class SuspectedLocationsSection extends StatelessWidget {
String getLastFetchText() {
if (lastFetch == null) {
return 'Never fetched';
return locService.t('suspectedLocations.neverFetched');
} else {
final now = DateTime.now();
final diff = now.difference(lastFetch);
if (diff.inDays > 0) {
return '${diff.inDays} days ago';
return locService.t('suspectedLocations.daysAgo', params: [diff.inDays.toString()]);
} else if (diff.inHours > 0) {
return '${diff.inHours} hours ago';
return locService.t('suspectedLocations.hoursAgo', params: [diff.inHours.toString()]);
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes} minutes ago';
return locService.t('suspectedLocations.minutesAgo', params: [diff.inMinutes.toString()]);
} else {
return 'Just now';
return locService.t('suspectedLocations.justNow');
}
}
}
@@ -43,9 +43,9 @@ class SuspectedLocationsSection extends StatelessWidget {
showDialog(
context: context,
barrierDismissible: false,
builder: (progressContext) => const SuspectedLocationProgressDialog(
title: 'Updating Suspected Locations',
message: 'Downloading and processing data...',
builder: (progressContext) => SuspectedLocationProgressDialog(
title: locService.t('suspectedLocations.updating'),
message: locService.t('suspectedLocations.downloadingAndProcessing'),
),
);
@@ -57,13 +57,13 @@ class SuspectedLocationsSection extends StatelessWidget {
Navigator.of(context).pop();
// Show result snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? 'Suspected locations updated successfully'
: 'Failed to update suspected locations'),
),
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(success
? locService.t('suspectedLocations.updateSuccess')
: locService.t('suspectedLocations.updateFailed')),
),
);
}
}
@@ -71,7 +71,7 @@ class SuspectedLocationsSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Suspected Locations',
locService.t('suspectedLocations.title'),
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
@@ -79,8 +79,8 @@ class SuspectedLocationsSection extends StatelessWidget {
// Enable/disable switch
ListTile(
leading: const Icon(Icons.help_outline),
title: const Text('Show Suspected Locations'),
subtitle: const Text('Show question mark markers for suspected surveillance sites from utility permit data'),
title: Text(locService.t('suspectedLocations.showSuspectedLocations')),
subtitle: Text(locService.t('suspectedLocations.showSuspectedLocationsSubtitle')),
trailing: Switch(
value: isEnabled,
onChanged: (enabled) {
@@ -95,7 +95,7 @@ class SuspectedLocationsSection extends StatelessWidget {
// Last update time
ListTile(
leading: const Icon(Icons.schedule),
title: const Text('Last Updated'),
title: Text(locService.t('suspectedLocations.lastUpdated')),
subtitle: Text(getLastFetchText()),
trailing: isLoading
? const SizedBox(
@@ -106,22 +106,35 @@ class SuspectedLocationsSection extends StatelessWidget {
: IconButton(
icon: const Icon(Icons.refresh),
onPressed: handleRefresh,
tooltip: 'Refresh now',
tooltip: locService.t('suspectedLocations.refreshNow'),
),
),
// Data info
// Data info with credit
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('Data Source'),
subtitle: const Text('Utility permit data indicating potential surveillance infrastructure installation sites'),
title: Text(locService.t('suspectedLocations.dataSource')),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(locService.t('suspectedLocations.dataSourceDescription')),
const SizedBox(height: 4),
Text(
locService.t('suspectedLocations.dataSourceCredit'),
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.primary,
),
),
],
),
),
// 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'),
title: Text(locService.t('suspectedLocations.minimumDistance')),
subtitle: Text(locService.t('suspectedLocations.minimumDistanceSubtitle', params: [appState.suspectedLocationMinDistance.toString()])),
trailing: SizedBox(
width: 80,
child: TextFormField(

View File

@@ -160,15 +160,9 @@ class SuspectedLocationCache extends ChangeNotifier {
validCount++;
} else {
zeroCoordCount++;
if (i < 3) { // Log first few zero coord cases
debugPrint('[SuspectedLocationCache] Row $i has zero coordinates: ticket=${rowData['ticket_no']}, location=${rowData['location']?.toString().length} chars');
}
}
} catch (e) {
errorCount++;
if (errorCount <= 5) { // Log first few errors
debugPrint('[SuspectedLocationCache] Row $i error: $e, ticket=${rowData['ticket_no']}');
}
continue;
}
}

View File

@@ -15,7 +15,7 @@ class SuspectedLocationService {
factory SuspectedLocationService() => _instance;
SuspectedLocationService._();
static const String _csvUrl = 'https://alprwatch.org/pub/flock_utilities_mini_2025-10-06.csv';
static const String _csvUrl = 'https://alprwatch.org/pub/flock_utilities_mini_latest.csv';
static const String _prefsKeyEnabled = 'suspected_locations_enabled';
static const Duration _maxAge = Duration(days: 7);
static const Duration _timeout = Duration(seconds: 30);
@@ -187,9 +187,8 @@ class SuspectedLocationService {
validRows++;
}
// Log progress every 1000 rows and report to UI
// Report progress every 1000 rows
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);
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../services/localization_service.dart';
class SuspectedLocationProgressDialog extends StatelessWidget {
final String title;
@@ -46,7 +47,7 @@ class SuspectedLocationProgressDialog extends StatelessWidget {
if (onCancel != null)
TextButton(
onPressed: onCancel,
child: const Text('Cancel'),
child: Text(LocalizationService.instance.cancel),
),
],
);

View File

@@ -36,17 +36,17 @@ class SuspectedLocationSheet extends StatelessWidget {
}
}
// Create display data map
// Create display data map using localized labels
final Map<String, String?> displayData = {
'Ticket No': location.ticketNo,
'Address': location.addr,
'Street': location.street,
'City': location.city,
'State': location.state,
'Intersecting Street': location.digSiteIntersectingStreet,
'Work Done For': location.digWorkDoneFor,
'Remarks': location.digSiteRemarks,
'URL': location.urlFull,
locService.t('suspectedLocation.ticketNo'): location.ticketNo,
locService.t('suspectedLocation.address'): location.addr,
locService.t('suspectedLocation.street'): location.street,
locService.t('suspectedLocation.city'): location.city,
locService.t('suspectedLocation.state'): location.state,
locService.t('suspectedLocation.intersectingStreet'): location.digSiteIntersectingStreet,
locService.t('suspectedLocation.workDoneFor'): location.digWorkDoneFor,
locService.t('suspectedLocation.remarks'): location.digSiteRemarks,
locService.t('suspectedLocation.url'): location.urlFull,
};
return SafeArea(
@@ -58,7 +58,7 @@ class SuspectedLocationSheet extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Suspected Location #${location.ticketNo}',
locService.t('suspectedLocation.title', params: [location.ticketNo]),
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 12),
@@ -113,7 +113,7 @@ class SuspectedLocationSheet extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Coordinates',
locService.t('suspectedLocation.coordinates'),
style: TextStyle(
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurface,