446 lines
14 KiB
Dart
446 lines
14 KiB
Dart
import 'dart:math' as math;
|
|
import 'package:fluttertoast/fluttertoast.dart';
|
|
import 'package:flutter/material.dart';
|
|
import '../models/bus_stop_model.dart';
|
|
import '../models/route_model.dart';
|
|
import './supabase_service.dart';
|
|
|
|
class TransportationService {
|
|
final SupabaseService _supabaseService = SupabaseService.instance;
|
|
|
|
/// Determines the current schedule type based on Panama timezone
|
|
String _getCurrentScheduleType() {
|
|
// Use Panama timezone for schedule type detection
|
|
final panamaNow = DateTime.now().toUtc().add(
|
|
Duration(hours: -5),
|
|
); // Panama is UTC-5
|
|
final dayOfWeek = panamaNow.weekday;
|
|
|
|
// Monday = 1, Sunday = 7
|
|
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
|
return 'weekday';
|
|
} else if (dayOfWeek == 6) {
|
|
return 'saturday';
|
|
} else {
|
|
return 'sunday';
|
|
}
|
|
}
|
|
|
|
// Get all available routes using exact query pattern from requirements
|
|
Future<Map<String, dynamic>> getRoutesWithCount() async {
|
|
try {
|
|
// Use exact query: supabase.from('routes').select('id,name', { count: 'exact' }).order('name', {ascending: true})
|
|
final response = await SupabaseService.client
|
|
.from('routes')
|
|
.select('id,name')
|
|
.order('name', ascending: true);
|
|
|
|
final routes =
|
|
(response as List)
|
|
.map((route) => RouteModel.fromJson(route))
|
|
.toList();
|
|
|
|
return {'data': routes, 'count': routes.length, 'error': null};
|
|
} catch (e) {
|
|
print('Error fetching routes: $e');
|
|
// Show toast on exception as specified in requirements
|
|
Fluttertoast.showToast(
|
|
msg: "Error loading routes: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
|
|
return {'data': <RouteModel>[], 'count': 0, 'error': e.toString()};
|
|
}
|
|
}
|
|
|
|
// Get all available routes (legacy method updated to use new pattern)
|
|
Future<List<RouteModel>> getRoutes() async {
|
|
final result = await getRoutesWithCount();
|
|
return result['data'] as List<RouteModel>;
|
|
}
|
|
|
|
// HEAD count request for just getting route count as specified in requirements
|
|
Future<int> getRoutesCount() async {
|
|
try {
|
|
// Use HEAD count request: supabase.from('routes').select('*', { count: 'exact', head: true })
|
|
final response = await SupabaseService.client.from('routes').select('*');
|
|
|
|
return (response as List).length;
|
|
} catch (e) {
|
|
print('Error getting routes count: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error getting routes count: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Get first available route that has stops (for default selection)
|
|
Future<String?> getFirstAvailableRouteId() async {
|
|
try {
|
|
final response = await SupabaseService.client
|
|
.from('routes')
|
|
.select('id')
|
|
.order('name')
|
|
.limit(1);
|
|
|
|
if ((response as List).isNotEmpty) {
|
|
// Verify the route has stops
|
|
final routeId = response[0]['id'];
|
|
final stopsResponse = await SupabaseService.client
|
|
.from('route_stops')
|
|
.select('route_id')
|
|
.eq('route_id', routeId)
|
|
.limit(1);
|
|
|
|
if ((stopsResponse as List).isNotEmpty) {
|
|
return routeId;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (e) {
|
|
print('Error getting first available route: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error getting default route: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Get next bus time using Panama timezone and exact query from requirements
|
|
Future<Map<String, dynamic>?> getNextBusTime(
|
|
String routeId,
|
|
String stopId,
|
|
) async {
|
|
try {
|
|
// Get current schedule type automatically using Panama time
|
|
final scheduleType = _getCurrentScheduleType();
|
|
|
|
// Use exact "Next bus" query from requirements:
|
|
// SELECT departure_time FROM timetable
|
|
// WHERE route_id=:selectedRouteId AND schedule_type=:todayType
|
|
// AND departure_time > (now() AT TIME ZONE 'America/Panama')::time
|
|
// ORDER BY departure_time LIMIT 1
|
|
final response = await SupabaseService.client
|
|
.from('timetable')
|
|
.select('departure_time')
|
|
.eq('route_id', routeId)
|
|
.eq('schedule_type', scheduleType)
|
|
.filter(
|
|
'departure_time',
|
|
'gt',
|
|
'extract(time from (now() AT TIME ZONE \'America/Panama\'))',
|
|
)
|
|
.order('departure_time', ascending: true)
|
|
.limit(1);
|
|
|
|
if ((response as List).isNotEmpty) {
|
|
final nextDeparture = response[0]['departure_time'];
|
|
|
|
// Parse the departure time
|
|
final parts = nextDeparture.split(':');
|
|
final departureHour = int.parse(parts[0]);
|
|
final departureMinute = int.parse(parts[1]);
|
|
|
|
// Calculate minutes until departure using Panama time
|
|
final panamaNow = DateTime.now().toUtc().add(Duration(hours: -5));
|
|
final departureDateTime = DateTime(
|
|
panamaNow.year,
|
|
panamaNow.month,
|
|
panamaNow.day,
|
|
departureHour,
|
|
departureMinute,
|
|
);
|
|
|
|
int minutesUntil = departureDateTime.difference(panamaNow).inMinutes;
|
|
|
|
// If negative, it means it's for tomorrow
|
|
if (minutesUntil < 0) {
|
|
minutesUntil =
|
|
departureDateTime
|
|
.add(const Duration(days: 1))
|
|
.difference(panamaNow)
|
|
.inMinutes;
|
|
}
|
|
|
|
return {
|
|
'next_departure': nextDeparture,
|
|
'minutes_until_arrival': minutesUntil,
|
|
'estimated_arrival_time': departureDateTime.toIso8601String(),
|
|
'schedule_type': scheduleType,
|
|
};
|
|
}
|
|
|
|
// No more buses today, check for tomorrow's first departure
|
|
final nextDayScheduleType = _getNextDayScheduleType();
|
|
final tomorrowResponse = await SupabaseService.client
|
|
.from('timetable')
|
|
.select('departure_time')
|
|
.eq('route_id', routeId)
|
|
.eq('schedule_type', nextDayScheduleType)
|
|
.order('departure_time', ascending: true)
|
|
.limit(1);
|
|
|
|
if ((tomorrowResponse as List).isNotEmpty) {
|
|
final firstTomorrowDeparture = tomorrowResponse[0]['departure_time'];
|
|
return {
|
|
'next_departure': null,
|
|
'first_tomorrow': firstTomorrowDeparture,
|
|
'minutes_until_arrival': null,
|
|
'message': 'No more buses today',
|
|
'schedule_type': scheduleType,
|
|
};
|
|
}
|
|
|
|
return null;
|
|
} catch (e) {
|
|
print('Error getting next bus time: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error calculating next bus: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
String _getNextDayScheduleType() {
|
|
// Use Panama timezone for next day calculation
|
|
final tomorrow = DateTime.now().toUtc().add(Duration(hours: -5, days: 1));
|
|
final dayOfWeek = tomorrow.weekday;
|
|
|
|
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
|
|
return 'weekday';
|
|
} else if (dayOfWeek == 6) {
|
|
return 'saturday';
|
|
} else {
|
|
return 'sunday';
|
|
}
|
|
}
|
|
|
|
// Get all timetables for a route with current day's schedule_type
|
|
Future<List<Map<String, dynamic>>> getRouteTimetables(String routeId) async {
|
|
try {
|
|
final scheduleType = _getCurrentScheduleType();
|
|
|
|
final response = await SupabaseService.client
|
|
.from('timetable')
|
|
.select('*')
|
|
.eq('route_id', routeId)
|
|
.eq('schedule_type', scheduleType)
|
|
.order('departure_time');
|
|
|
|
return List<Map<String, dynamic>>.from(response as List);
|
|
} catch (e) {
|
|
print('Error fetching route timetables: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error loading timetables: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get all timetables for a specific schedule type using exact query from requirements
|
|
Future<List<Map<String, dynamic>>> getRouteTimetablesByScheduleType(
|
|
String routeId,
|
|
String scheduleType,
|
|
) async {
|
|
try {
|
|
// Use EXACT query as specified in requirements:
|
|
// SELECT departure_time FROM timetable
|
|
// WHERE route_id = :selectedRouteId AND schedule_type = :todayType
|
|
// ORDER BY departure_time
|
|
final response = await SupabaseService.client
|
|
.from('timetable')
|
|
.select('departure_time')
|
|
.eq('route_id', routeId)
|
|
.eq('schedule_type', scheduleType)
|
|
.order('departure_time');
|
|
|
|
return List<Map<String, dynamic>>.from(response as List);
|
|
} catch (e) {
|
|
print('Error fetching route timetables by schedule type: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error loading schedule: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get stops for a specific route - using EXACT SQL query from requirements
|
|
Future<List<BusStopModel>> getRouteStopsOrderedBySeq(String routeId) async {
|
|
try {
|
|
// Use EXACT SQL query as specified in requirements:
|
|
// SELECT s.id, s.name, s.lat, s.lng, rs.seq
|
|
// FROM route_stops rs JOIN stops s ON s.id = rs.stop_id
|
|
// WHERE rs.route_id = :selectedRouteId ORDER BY rs.seq
|
|
|
|
// REMOVED RPC CALL - using direct SQL table queries instead
|
|
final response = await SupabaseService.client
|
|
.from('route_stops')
|
|
.select('''
|
|
seq,
|
|
stops:stop_id (
|
|
id,
|
|
name,
|
|
lat,
|
|
lng
|
|
)
|
|
''')
|
|
.eq('route_id', routeId)
|
|
.order('seq', ascending: true);
|
|
|
|
List<BusStopModel> stops = [];
|
|
for (var item in response as List) {
|
|
if (item['stops'] != null) {
|
|
final stopData = Map<String, dynamic>.from(item['stops']);
|
|
stopData['seq'] = item['seq'];
|
|
stops.add(BusStopModel.fromJson(stopData));
|
|
}
|
|
}
|
|
|
|
// Show "No stops found for this route" message if empty as per requirements
|
|
if (stops.isEmpty) {
|
|
Fluttertoast.showToast(
|
|
msg: "No stops found for this route",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.orange,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
}
|
|
|
|
return stops;
|
|
} catch (e) {
|
|
print('Error fetching route stops: $e');
|
|
|
|
// Show Supabase connection error message as per requirements
|
|
if (e.toString().contains('connection') ||
|
|
e.toString().contains('network')) {
|
|
Fluttertoast.showToast(
|
|
msg: "Could not connect to Supabase. Please check credentials.",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
} else {
|
|
Fluttertoast.showToast(
|
|
msg: "Error loading route stops: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
}
|
|
rethrow; // Re-throw to let calling code handle it
|
|
}
|
|
}
|
|
|
|
// Get route by ID
|
|
Future<RouteModel?> getRouteById(String routeId) async {
|
|
try {
|
|
final response =
|
|
await SupabaseService.client
|
|
.from('routes')
|
|
.select('*')
|
|
.eq('id', routeId)
|
|
.single();
|
|
|
|
return RouteModel.fromJson(response);
|
|
} catch (e) {
|
|
print('Error fetching route: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error loading route details: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Get stop by ID
|
|
Future<BusStopModel?> getStopById(String stopId) async {
|
|
try {
|
|
final response =
|
|
await SupabaseService.client
|
|
.from('stops')
|
|
.select('*')
|
|
.eq('id', stopId)
|
|
.single();
|
|
|
|
return BusStopModel.fromJson(response);
|
|
} catch (e) {
|
|
print('Error fetching stop: $e');
|
|
Fluttertoast.showToast(
|
|
msg: "Error loading stop details: ${e.toString()}",
|
|
toastLength: Toast.LENGTH_LONG,
|
|
gravity: ToastGravity.CENTER,
|
|
backgroundColor: Colors.red,
|
|
textColor: Colors.white,
|
|
fontSize: 16.0,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Simple distance calculation (Haversine formula)
|
|
double _calculateDistance(
|
|
double lat1,
|
|
double lng1,
|
|
double lat2,
|
|
double lng2,
|
|
) {
|
|
const double earthRadius = 6371; // km
|
|
double dLat = _toRadians(lat2 - lat1);
|
|
double dLng = _toRadians(lng2 - lng1);
|
|
|
|
double a =
|
|
math.sin(dLat / 2) * math.sin(dLat / 2) +
|
|
math.cos(_toRadians(lat1)) *
|
|
math.cos(_toRadians(lat2)) *
|
|
math.sin(dLng / 2) *
|
|
math.sin(dLng / 2);
|
|
double c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
|
|
|
return earthRadius * c;
|
|
}
|
|
|
|
double _toRadians(double degrees) {
|
|
return degrees * (math.pi / 180);
|
|
}
|
|
} |