cid_app/lib/carro_attr.dart

304 lines
11 KiB
Dart
Raw Permalink Normal View History

2026-02-27 23:26:13 +01:00
import 'dart:convert';
import 'dart:async'; // Necessario per il Timeout
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class CarroAttrezziScreen extends StatefulWidget {
const CarroAttrezziScreen({super.key});
@override
State<CarroAttrezziScreen> createState() => _CarroAttrezziScreenState();
}
class _CarroAttrezziScreenState extends State<CarroAttrezziScreen> {
List<dynamic> _officine = [];
bool _isLoading = true;
String _statusMessage = "Attivazione GPS...";
@override
void initState() {
super.initState();
_avviaRicercaSicura();
}
Future<void> _avviaRicercaSicura() async {
// Piccolo ritardo iniziale per dare tempo alla UI di disegnarsi
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
_cercaSoccorsiVicini();
}
Future<void> _cercaSoccorsiVicini() async {
try {
// 1. GESTIONE PERMESSI ROBUSTA
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!mounted) return;
setState(() {
_statusMessage = "Attiva il GPS per trovare i soccorsi.";
_isLoading = false;
});
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (!mounted) return;
setState(() {
_statusMessage = "Permesso GPS negato.";
_isLoading = false;
});
return;
}
}
if (permission == LocationPermission.deniedForever) {
if (!mounted) return;
setState(() {
_statusMessage = "Permessi GPS bloccati permanentemente.";
_isLoading = false;
});
return;
}
// 2. CICLO WHILE PER OTTENERE LA POSIZIONE (WAIT FOR GPS)
if (mounted) setState(() => _statusMessage = "Ricerca posizione in corso...");
Position? position;
int tentativi = 0;
const int maxTentativi = 3; // Prova 3 volte prima di arrendersi
// FINCHÉ non ho la posizione E non ho superato i tentativi...
while (position == null && tentativi < maxTentativi) {
try {
if (tentativi > 0) {
if (mounted) setState(() => _statusMessage = "Aggancio satelliti (Tentativo ${tentativi + 1}/$maxTentativi)...");
}
// Prova a prendere la posizione con un timeout di 6 secondi per tentativo
position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
timeLimit: const Duration(seconds: 6),
);
} catch (e) {
// Se fallisce, aspetta 1 secondo e riprova
tentativi++;
await Future.delayed(const Duration(seconds: 1));
}
}
// Se dopo il ciclo while è ancora null, proviamo l'ultima posizione nota
if (position == null) {
if (mounted) setState(() => _statusMessage = "Segnale debole, uso ultima posizione...");
position = await Geolocator.getLastKnownPosition();
}
// Se è ancora null, alziamo bandiera bianca
if (position == null) {
throw "Impossibile ottenere la posizione GPS dopo vari tentativi.";
}
// 3. RICERCA SU OVERPASS API (OSM)
if (mounted) setState(() => _statusMessage = "Ricerca soccorsi nei paraggi...");
String query = """
[out:json][timeout:25];
(
node["craft"="car_repair"](around:15000,${position.latitude},${position.longitude});
node["shop"="car_repair"](around:15000,${position.latitude},${position.longitude});
node["service"="vehicle_recovery"](around:15000,${position.latitude},${position.longitude});
);
out body;
""";
final url = Uri.parse('https://overpass-api.de/api/interpreter?data=${Uri.encodeComponent(query)}');
final response = await http.get(
url,
headers: {'User-Agent': 'CidApp_Flutter/1.0'},
).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
final data = json.decode(response.body);
List<dynamic> elements = data['elements'];
List<Map<String, dynamic>> listaElaborata = [];
for (var element in elements) {
if (element['tags'] != null && element['tags']['name'] != null) {
double distanzaMetri = Geolocator.distanceBetween(
position!.latitude, position.longitude, element['lat'], element['lon']
);
listaElaborata.add({
'name': element['tags']['name'],
'phone': element['tags']['phone'] ?? element['tags']['contact:phone'] ?? element['tags']['mobile'],
'street': element['tags']['addr:street'] ?? "",
'city': element['tags']['addr:city'] ?? "",
'distance': distanzaMetri,
'lat': element['lat'],
'lon': element['lon'],
});
}
}
listaElaborata.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double));
if (mounted) {
setState(() {
_officine = listaElaborata;
_isLoading = false;
});
}
} else {
throw "Errore server OSM: ${response.statusCode}";
}
} catch (e) {
if (mounted) {
setState(() {
_statusMessage = "Nessun soccorso trovato o errore GPS.\nRiprova tra poco.";
_isLoading = false;
});
}
}
}
Future<void> _chiamaNumero(String? numero) async {
if (numero == null) return;
// Pulisce il numero da spazi o caratteri strani
final pulito = numero.replaceAll(RegExp(r'[^0-9+]'), '');
final Uri url = Uri.parse("tel:$pulito");
if (await canLaunchUrl(url)) await launchUrl(url);
}
Future<void> _apriMappa(double lat, double lon) async {
// Link universale per aprire la navigazione
final Uri url = Uri.parse("https://www.google.com/maps/search/?api=1&query=$lat,$lon");
try {
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
throw 'Impossibile aprire la mappa';
}
} catch (e) {
debugPrint("Errore apertura mappa: $e");
}
}
Future<void> _cercaSuGoogle(String nome) async {
final Uri url = Uri.parse("https://www.google.com/search?q=soccorso stradale $nome telefono");
if (await canLaunchUrl(url)) await launchUrl(url, mode: LaunchMode.externalApplication);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Soccorso Stradale"),
backgroundColor: Colors.red.shade800,
foregroundColor: Colors.white,
),
body: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: Colors.red),
const SizedBox(height: 20),
Text(_statusMessage, style: const TextStyle(color: Colors.grey)),
],
),
)
: _officine.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.car_crash, size: 60, color: Colors.grey),
const SizedBox(height: 10),
const Text("Nessun soccorso trovato nei paraggi (15km).", textAlign: TextAlign.center),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _cercaSuGoogle("vicino a me"),
child: const Text("Cerca su Google"),
)
],
),
),
)
: ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: _officine.length,
itemBuilder: (context, index) {
final officina = _officine[index];
double km = (officina['distance'] as double) / 1000;
String indirizzo = officina['street'];
if (officina['city'] != "") indirizzo += (indirizzo.isNotEmpty ? ", " : "") + officina['city'];
if (indirizzo.isEmpty) indirizzo = "Posizione GPS";
return Card(
elevation: 3,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
// KM a sinistra
leading: Container(
width: 55,
height: 55,
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade100)
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, color: Colors.red.shade700, size: 24),
Text("${km.toStringAsFixed(1)} km", style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), maxLines: 1),
],
),
),
title: Text(officina['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
subtitle: Text(indirizzo, style: const TextStyle(fontSize: 13), maxLines: 2, overflow: TextOverflow.ellipsis),
// DUE TASTI A DESTRA: MAPPA e CHIAMA
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Tasto MAPPA
IconButton(
icon: const Icon(Icons.directions, color: Colors.blue, size: 32),
onPressed: () => _apriMappa(officina['lat'], officina['lon']),
tooltip: "Naviga",
),
// Tasto CHIAMA
officina['phone'] != null
? IconButton(
icon: const Icon(Icons.phone_in_talk, color: Colors.green, size: 32),
onPressed: () => _chiamaNumero(officina['phone']),
)
: IconButton(
icon: const Icon(Icons.search, color: Colors.orange, size: 32),
onPressed: () => _cercaSuGoogle(officina['name']),
tooltip: "Cerca Web",
),
],
),
),
),
);
},
),
);
}
}