304 lines
11 KiB
Dart
304 lines
11 KiB
Dart
|
|
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",
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|