382 lines
12 KiB
Dart
382 lines
12 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
|
import '../theme/app_theme.dart';
|
|
import '../models/models.dart';
|
|
import '../widgets/widgets.dart' as w;
|
|
|
|
class MapScreen extends StatefulWidget {
|
|
const MapScreen({super.key});
|
|
|
|
@override
|
|
State<MapScreen> createState() => _MapScreenState();
|
|
}
|
|
|
|
class _MapScreenState extends State<MapScreen> {
|
|
final Completer<GoogleMapController> _mapController = Completer();
|
|
|
|
// Coordenadas de ejemplo — Celaya, Gto.
|
|
static const LatLng _casaPos = LatLng(20.5226, -100.8191);
|
|
static const LatLng _camionPos = LatLng(20.5255, -100.8220);
|
|
static const CameraPosition _camaraInicial = CameraPosition(
|
|
target: LatLng(20.5240, -100.8205),
|
|
zoom: 15.5,
|
|
);
|
|
|
|
// Datos de ejemplo del camión
|
|
final TruckLocation _camion = TruckLocation(
|
|
id: 'truck-01',
|
|
ruta: 'Ruta Norte',
|
|
latitud: _camionPos.latitude,
|
|
longitud: _camionPos.longitude,
|
|
ultimaActualizacion: DateTime.now().subtract(const Duration(seconds: 28)),
|
|
enServicio: true,
|
|
);
|
|
|
|
final HouseModel _casa = HouseModel(
|
|
id: 'casa-01',
|
|
calle: 'Av. Insurgentes 245',
|
|
colonia: 'Centro',
|
|
codigoPostal: '38000',
|
|
latitud: _casaPos.latitude,
|
|
longitud: _casaPos.longitude,
|
|
radioAlertaMetros: 200,
|
|
);
|
|
|
|
Set<Marker> _markers = {};
|
|
Set<Circle> _circles = {};
|
|
Timer? _refreshTimer;
|
|
|
|
// Distancia simulada (metros)
|
|
double get _distanciaMetros => 380;
|
|
int get _minutosEstimados => 8;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_buildMapElements();
|
|
// Simular actualización de posición cada 30s
|
|
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
|
if (mounted) setState(() {});
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_refreshTimer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
void _buildMapElements() {
|
|
_markers = {
|
|
Marker(
|
|
markerId: const MarkerId('camion'),
|
|
position: LatLng(_camion.latitud, _camion.longitud),
|
|
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
|
infoWindow: InfoWindow(
|
|
title: 'Camión · ${_camion.ruta}',
|
|
snippet: _camion.tiempoActualizacion,
|
|
),
|
|
),
|
|
Marker(
|
|
markerId: const MarkerId('casa'),
|
|
position: LatLng(_casa.latitud, _casa.longitud),
|
|
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),
|
|
infoWindow: InfoWindow(title: _casa.alias, snippet: _casa.calle),
|
|
),
|
|
};
|
|
|
|
_circles = {
|
|
Circle(
|
|
circleId: const CircleId('radio-alerta'),
|
|
center: LatLng(_casa.latitud, _casa.longitud),
|
|
radius: _casa.radioAlertaMetros.toDouble(),
|
|
fillColor: AppTheme.blue.withValues(alpha: 0.08),
|
|
strokeColor: AppTheme.blue.withValues(alpha: 0.4),
|
|
strokeWidth: 1,
|
|
),
|
|
};
|
|
}
|
|
|
|
Future<void> _centrarMapa() async {
|
|
final controller = await _mapController.future;
|
|
await controller.animateCamera(
|
|
CameraUpdate.newCameraPosition(_camaraInicial),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
backgroundColor: AppTheme.background,
|
|
appBar: AppBar(
|
|
title: const Text('Rastreo en vivo'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.my_location),
|
|
onPressed: _centrarMapa,
|
|
tooltip: 'Centrar mapa',
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
// ── Mapa ─────────────────────────────────────────────────────
|
|
Expanded(
|
|
flex: 5,
|
|
child: Stack(
|
|
children: [
|
|
GoogleMap(
|
|
initialCameraPosition: _camaraInicial,
|
|
markers: _markers,
|
|
circles: _circles,
|
|
myLocationButtonEnabled: false,
|
|
zoomControlsEnabled: false,
|
|
mapType: MapType.normal,
|
|
onMapCreated: (c) {
|
|
_mapController.complete(c);
|
|
},
|
|
),
|
|
|
|
// Indicador "En vivo"
|
|
Positioned(
|
|
top: 14,
|
|
right: 14,
|
|
child: _LiveBadge(activo: _camion.enServicio),
|
|
),
|
|
|
|
// Actualización
|
|
Positioned(
|
|
top: 14,
|
|
left: 14,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: AppTheme.softShadow,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(Icons.refresh,
|
|
size: 14, color: AppTheme.textSecondary),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
_camion.tiempoActualizacion,
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: AppTheme.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ── Panel inferior ────────────────────────────────────────────
|
|
Expanded(
|
|
flex: 3,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.background,
|
|
borderRadius: const BorderRadius.vertical(
|
|
top: Radius.circular(AppTheme.radiusXl)),
|
|
boxShadow: AppTheme.cardShadow,
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Handle
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(vertical: 10),
|
|
width: 36,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.border,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(
|
|
children: [
|
|
// Camión
|
|
w.InfoRow(
|
|
icon: Icons.delete_outline_rounded,
|
|
label: '${_camion.ruta} · ${_camion.tiempoActualizacion}',
|
|
value: 'Camión a ${_distanciaMetros.toStringAsFixed(0)} m',
|
|
trailing: w.StatusBadge.amber('~$_minutosEstimados min'),
|
|
),
|
|
const SizedBox(height: 10),
|
|
// Casa
|
|
w.InfoRow(
|
|
icon: Icons.home_outlined,
|
|
label: _casa.direccionCompleta,
|
|
value: _casa.alias,
|
|
trailing: w.StatusBadge.green('Activa'),
|
|
),
|
|
const SizedBox(height: 12),
|
|
// Barra de progreso de llegada
|
|
_ArrivalBar(
|
|
distanciaActual: _distanciaMetros,
|
|
distanciaTotal: 1000,
|
|
minutos: _minutosEstimados,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Badge "En vivo" ───────────────────────────────────────────────────────────
|
|
class _LiveBadge extends StatefulWidget {
|
|
final bool activo;
|
|
const _LiveBadge({required this.activo});
|
|
|
|
@override
|
|
State<_LiveBadge> createState() => _LiveBadgeState();
|
|
}
|
|
|
|
class _LiveBadgeState extends State<_LiveBadge>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _anim;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_anim = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 900),
|
|
)..repeat(reverse: true);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_anim.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: AppTheme.softShadow,
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: _anim,
|
|
builder: (_, __) => Container(
|
|
width: 8,
|
|
height: 8,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: widget.activo
|
|
? AppTheme.primary.withValues(alpha: 0.5 + _anim.value * 0.5)
|
|
: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 5),
|
|
Text(
|
|
widget.activo ? 'En vivo' : 'Sin servicio',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: widget.activo ? AppTheme.primary : AppTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Barra de llegada estimada ─────────────────────────────────────────────────
|
|
class _ArrivalBar extends StatelessWidget {
|
|
final double distanciaActual;
|
|
final double distanciaTotal;
|
|
final int minutos;
|
|
|
|
const _ArrivalBar({
|
|
required this.distanciaActual,
|
|
required this.distanciaTotal,
|
|
required this.minutos,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final progreso =
|
|
((distanciaTotal - distanciaActual) / distanciaTotal).clamp(0.0, 1.0);
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: AppTheme.primaryLight,
|
|
borderRadius: BorderRadius.circular(AppTheme.radiusMd),
|
|
border: Border.all(color: AppTheme.primaryMid, width: 0.5),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Text('Llegada estimada',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: AppTheme.primaryDark)),
|
|
const Spacer(),
|
|
Text('~$minutos min',
|
|
style: const TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppTheme.primary)),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4),
|
|
child: LinearProgressIndicator(
|
|
value: progreso,
|
|
backgroundColor: AppTheme.primaryMid.withValues(alpha: 0.4),
|
|
valueColor:
|
|
const AlwaysStoppedAnimation<Color>(AppTheme.primary),
|
|
minHeight: 6,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: const [
|
|
Text('Ahora',
|
|
style: TextStyle(
|
|
fontSize: 10, color: AppTheme.primaryDark)),
|
|
Spacer(),
|
|
Text('Tu casa',
|
|
style: TextStyle(
|
|
fontSize: 10, color: AppTheme.primaryDark)),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|