diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..624711d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI +on: + push: + branches-ignore: + - "tools/*" + pull_request: +jobs: + Compiles: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v6 + - name: Flutter + uses: subosito/flutter-action@v2 + with: + # latest stable version + channel: stable + - name: Write dummy firebase_options.dart for CI build + run: | + cat > lib/firebase_options.dart << 'EOF' + import 'package:firebase_core/firebase_core.dart'; + class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform => const FirebaseOptions( + apiKey: 'fake-key-for-ci', + appId: 'fake-app-id', + messagingSenderId: 'fake-sender-id', + projectId: 'fake-project-id', + ); + } + EOF + - name: Generate Client + run: | + rm -rf lib/backend + dart run build_runner clean + dart run swagger_parser + dart run build_runner build + - name: Analyze + run: | + flutter analyze --no-fatal-infos --no-fatal-warnings + diff --git a/.gitignore b/.gitignore index ff4106c..ea778ba 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ google-services*.json GoogleService-Info.plist # auto generated lib/firebase_options.dart +lib/backend +spec.json # Flutter repo-specific /bin/cache/ diff --git a/lib/bluebus_api.dart b/lib/bluebus_api.dart index 2fe35a9..ee0a402 100644 --- a/lib/bluebus_api.dart +++ b/lib/bluebus_api.dart @@ -1,143 +1,41 @@ import 'dart:convert'; -import 'dart:math' as Math; -import 'package:flutter/cupertino.dart'; +import 'package:dio/dio.dart'; import 'package:http/http.dart' as http; -import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'constants.dart'; -import 'models/bus_stop.dart'; import 'models/bus.dart'; import 'models/bus_route_line.dart'; import 'services/route_color_service.dart'; -import 'utils/geometry.dart'; class BlueBusApi { static const String baseUrl = BACKEND_URL; // Fetch all routes and their polylines/stops static Future> fetchRoutes(Function(String route, String error) onError) async { - final response = await http.get(Uri.parse('$baseUrl/getAllRoutes')); - if (response.statusCode != 200) throw Exception('Failed to load routes'); - final data = jsonDecode(response.body); - final routes = []; - final routeJson = data['routes'] as Map; - - await RouteColorService.initialize(); - - routeJson.forEach((routeId, subroutes) { - for (final subroute in subroutes) { - try { - final points = []; - final stops = <(int, BusStop)>[]; - - // Cast to list to be able to be able to get different elements - final pointList = subroute['pt'] as List; - - for (int i = 0; i < pointList.length; i++) { - final point = pointList[i]; - final isLast = i == pointList.length - 1; // bool to check if last - points.add( - LatLng( - point['lat']?.toDouble() ?? 0, - point['lon']?.toDouble() ?? 0, - ), - ); - if (point['typ'] == 'S') { - // get rotation of stop - double stopRotation; - if (isLast) { - // use the previous 2 points to calculate rotation - stopRotation = pointRotation( - pointList[i - 2]['lat']?.toDouble() ?? 0, - pointList[i - 2]['lon']?.toDouble() ?? 0, - pointList[i - 1]['lat']?.toDouble() ?? 0, - pointList[i - 1]['lon']?.toDouble() ?? 0, - ); - } else { - // use the next 2 points to calculate rotation - stopRotation = pointRotation( - pointList[i + 1]['lat']?.toDouble() ?? 0, - pointList[i + 1]['lon']?.toDouble() ?? 0, - pointList[i + 2]['lat']?.toDouble() ?? 0, - pointList[i + 2]['lon']?.toDouble() ?? 0, - ); - } - stops.add((i, BusStop.fromJson(point, routeId, stopRotation, false))); - } - } - - // Get route color and image - final routeColor = RouteColorService.getRouteColor(routeId); - final routeImageUrl = RouteColorService.getRouteImageUrl(routeId); - - routes.add( - BusRouteLine( - routeId: routeId, - points: points, - stops: stops, - color: routeColor, - imageUrl: routeImageUrl, - ), - ); - - // Handle detour points if present - if (subroute.containsKey('dtrpt')) { - final detourPoints = []; - final detourStops = <(int, BusStop)>[]; - - // Cast to list to be able to be able to get different elements - final detourPointList = subroute['dtrpt'] as List; + try { + final routes = await backendClient.getApiV4AllMbusRoutes(); + await RouteColorService.initialize(); - for (int i = 0; i < detourPointList.length; i++) { - final point = detourPointList[i]; - final isLast = i == detourPointList.length - 1; // bool to check if last + return routes + .map((r) { + final routeColor = RouteColorService.getRouteColor(r.routeId); + final routeImageUrl = RouteColorService.getRouteImageUrl(r.routeId); - detourPoints.add( - LatLng( - point['lat']?.toDouble() ?? 0, - point['lon']?.toDouble() ?? 0, - ), - ); - if (point['typ'] == 'S') { - // get rotation of stop - double stopRotation; - if (isLast) { - // use the previous 2 points to calculate rotation - stopRotation = pointRotation( - detourPointList[i - 2]['lat']?.toDouble() ?? 0, - detourPointList[i - 2]['lon']?.toDouble() ?? 0, - detourPointList[i - 1]['lat']?.toDouble() ?? 0, - detourPointList[i - 1]['lon']?.toDouble() ?? 0, - ); - - } else { - // use the next 2 points to calculate rotation - stopRotation = pointRotation( - detourPointList[i + 1]['lat']?.toDouble() ?? 0, - detourPointList[i + 1]['lon']?.toDouble() ?? 0, - detourPointList[i + 2]['lat']?.toDouble() ?? 0, - detourPointList[i + 2]['lon']?.toDouble() ?? 0, - ); - } - detourStops.add((i, BusStop.fromJson(point, routeId, stopRotation, false))); - } - } + return BusRouteLine.fromBackend(r, routeColor, routeImageUrl); + }) + .toList(); + } on DioException catch (e) { + final res = e.response; - routes.add( - BusRouteLine( - routeId: routeId, - points: detourPoints, - stops: detourStops, - color: routeColor, - imageUrl: routeImageUrl, - ), - ); - } - } catch (e) { - onError(routeId, e.toString()); - } + if (res != null && res.statusCode != 200) { + onError("", "Failed to load routes"); + throw Exception('Failed to load routes'); } - }); - return routes; + onError("", e.message ?? e.toString()); + rethrow; + } catch (e) { + onError("", e.toString()); + rethrow; + } } // Fetch all buses and their positions diff --git a/lib/constants.dart b/lib/constants.dart index b3d3eb5..3bc538e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,3 +1,5 @@ +import 'package:bluebus/backend/backend/backend_client.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -38,6 +40,13 @@ const String BACKEND_URL = String.fromEnvironment( //const String BACKEND_URL = String.fromEnvironment('BACKEND_URL', defaultValue: 'http://35.3.198.105:3000/mbus/api/v3'); //const String BACKEND_URL = String.fromEnvironment('BACKEND_URL', defaultValue: 'http://35.2.102.249:3000/mbus/api/v3/'); +// base url to use when for backend requests via openapi generated clients +const String backendOpenApiUrl = String.fromEnvironment( + 'BACKEND_OPENAPI_URL' +); + +final BackendClient backendClient = BackendClient(Dio(BaseOptions(baseUrl: backendOpenApiUrl))); + List> globalAvailableRoutes = []; // Mapping from route code to full name @@ -54,6 +63,7 @@ const Map fallback_code_to_name = { final _whitespacePattern = RegExp(r'\s+'); +// KEEP THIS IN SYNC WITH THE CORRESPONDING FUNCTION IN THE BACKEND String normalizeStopName(String rawStopName) { // Remove random characters (add them to list if needed), collapse whitespace to a single space, and trim edges. return rawStopName diff --git a/lib/models/bus_route_line.dart b/lib/models/bus_route_line.dart index ee3266c..de3e218 100644 --- a/lib/models/bus_route_line.dart +++ b/lib/models/bus_route_line.dart @@ -1,6 +1,8 @@ +import 'package:bluebus/models/lat_lng.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:flutter/material.dart'; import 'bus_stop.dart'; +import 'package:bluebus/backend/export.dart' as backend; class BusRouteLine { final String routeId; @@ -19,4 +21,14 @@ class BusRouteLine { this.color, this.imageUrl, }); + + static BusRouteLine fromBackend(backend.BusRouteLine line, Color? color, String? imageUrl) { + return BusRouteLine( + routeId: line.routeId, + points: line.points.map(latLngFromBackend).toList(), + stops: line.stops.map((x) => (x.index, BusStop.fromBackend(x.stop))).toList(), + color: color, + imageUrl: imageUrl + ); + } } diff --git a/lib/models/bus_stop.dart b/lib/models/bus_stop.dart index 5e46c0a..7a87a4f 100644 --- a/lib/models/bus_stop.dart +++ b/lib/models/bus_stop.dart @@ -1,5 +1,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import '../constants.dart'; +import 'package:bluebus/backend/export.dart' as backend; class BusStop { final String id; @@ -11,14 +12,15 @@ class BusStop { BusStop({required this.id, required this.name, required this.location, required this.routeId, required this.rotation, required this.isRide}); - factory BusStop.fromJson(Map json, String routeId, double rotation, bool isRide) { + factory BusStop.fromBackend(backend.BusStop x) { return BusStop( - id: json['stpid'] ?? '', - name: normalizeStopName(json['stpnm'] ?? ''), - location: LatLng(json['lat']?.toDouble() ?? 0, json['lon']?.toDouble() ?? 0), - routeId: routeId, - rotation: rotation, - isRide: isRide + id: x.id, + name: x.name, + location: LatLng(x.location.lat.toDouble(), + x.location.lon.toDouble()), + routeId: x.routeId, + rotation: x.rotation.toDouble(), + isRide: x.isRide ); } } diff --git a/lib/models/journey.dart b/lib/models/journey.dart index 3dba255..7bfebec 100644 --- a/lib/models/journey.dart +++ b/lib/models/journey.dart @@ -1,5 +1,8 @@ +import 'package:bluebus/models/lat_lng.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:bluebus/backend/export.dart' as backend; + class Journey { final List legs; final int departureTime; @@ -7,85 +10,96 @@ class Journey { Journey({required this.legs, required this.departureTime, required this.arrivalTime}); - factory Journey.fromJson(Map json) { + factory Journey.fromBackend(backend.ProcessedJourney journey) { return Journey( - legs: (json['legs'] as List).map((e) => Leg.fromJson(e)).toList(), - departureTime: (json['departureTime'] as num?)?.toInt() ?? 0, - arrivalTime: (json['arrivalTime'] as num?)?.toInt() ?? 0, + legs: journey.legs.map(Leg.fromBackend).toList(), + departureTime: journey.departureTime.toInt(), + arrivalTime: journey.arrivalTime.toInt(), ); } } -enum LegMode { walk, bus } - -// I really want to turn this into a sum type... (sealed class + two subclasses) -class Leg { +sealed class Leg { final String origin; final String destination; + final String originID; + final String destinationID; final double duration; final int startTime; final int endTime; - final List? stopTimes; - final Trip? trip; - final String? rt; - final String originID; - final String destinationID; - final List? pathCoords; - final Map? directions; - final LegMode mode; Leg({ required this.origin, required this.destination, + required this.originID, + required this.destinationID, required this.duration, required this.startTime, required this.endTime, - this.stopTimes, - this.trip, - this.rt, - required this.originID, - required this.destinationID, - this.pathCoords, - this.directions, - required this.mode }); - - factory Leg.fromJson(Map json) { - return Leg( - origin: json['origin'] ?? '', - destination: json['destination'] ?? '', - duration: (json['duration'] as num).toDouble(), - startTime: json['startTime'] ?? 0, - endTime: json['endTime'] ?? 0, - stopTimes: json['stopTimes'] != null - ? (json['stopTimes'] as List).map((e) => StopTime.fromJson(e)).toList() - : null, - trip: json['trip'] != null ? Trip.fromJson(json['trip']) : null, - rt: json['rt'], - originID: json['origin_id'] ?? '', - destinationID: json['destination_id'] ?? '', - pathCoords: json['path_coords'] != null? - // adding full walking path from json - (json['path_coords'] as List).map((e) { - return LatLng( - (e['lat'] as num).toDouble(), - (e['lon'] as num).toDouble(), - ); - }).toList() - : null, - directions: json['directions'] != null ? - { - for (var x in json['directions'] as List) - (x['path_index'] as num).toInt(): - (degree: (x['turn']['degrees'] as num).toDouble(), - landmark: x['turn']['landmark'] as String) - } - : null, - mode: (json['mode'] == "bus" ? LegMode.bus : LegMode.walk) - ); + + factory Leg.fromBackend(backend.FormattedLeg leg) { + switch (leg) { + case backend.FormattedLegFormattedLegWalk(): + return WalkingLeg.fromBackend(leg); + case backend.FormattedLegFormattedLegBus(): + return BusLeg.fromBackend(leg); + } } } +class BusLeg extends Leg { + final String destinationName; + final List<({ String? rt, List path })> busPathSegments; + final List<({ String? rt, LatLng location })> stopCoords; + final String mode; + final List stopTimes; + final Trip trip; + final String rt; + final String? vid; + + BusLeg.fromBackend(backend.FormattedLegFormattedLegBus leg) + : destinationName = leg.destinationName, + busPathSegments = leg.busPathSegments + .map((e) => (rt: e.rt, path: e.path.map(latLngFromBackend).toList())) + .toList(), + stopCoords = leg.stopCoords + .map((e) => (rt: e.rt, location: latLngFromBackend(e.location))) + .toList(), + mode = leg.mode, + stopTimes = leg.stopTimes.map((e) => StopTime.fromBackend(e)).toList(), + trip = Trip.fromBackend(leg.trip), + rt = leg.rt, + vid = leg.vid, + super( + origin: leg.origin, + destination: leg.destination, + originID: leg.originId, + destinationID: leg.destinationId, + duration: leg.duration.toDouble(), + startTime: leg.startTime.toInt(), + endTime: leg.endTime.toInt(), + ); +} + +class WalkingLeg extends Leg { + final List pathCoords; + // TODO: re-add turn by turn directions + + WalkingLeg.fromBackend(backend.FormattedLegFormattedLegWalk leg) + : pathCoords = leg.pathCoords.map(latLngFromBackend).toList(), + super( + origin: leg.origin, + destination: leg.destination, + originID: leg.originId, + destinationID: leg.destinationId, + duration: leg.duration.toDouble(), + startTime: leg.startTime.toInt(), + endTime: leg.endTime.toInt(), + ); + +} + typedef Turn = ({double degree, String landmark}); class StopTime { @@ -103,29 +117,29 @@ class StopTime { required this.dropOff, }); - factory StopTime.fromJson(Map json) { + factory StopTime.fromBackend(backend.StopTime st) { return StopTime( - stop: json['stop'] ?? '', - arrivalTime: json['arrivalTime'] ?? 0, - departureTime: json['departureTime'] ?? 0, - pickUp: json['pickUp'] ?? false, - dropOff: json['dropOff'] ?? false, + stop: st.stop, + arrivalTime: st.arrivalTime.toInt(), + departureTime: st.departureTime.toInt(), + pickUp: st.pickUp, + dropOff: st.dropOff, ); } } class Trip { final String tripId; - final String vid; + final String? vid; final List stopTimes; Trip({required this.tripId, required this.vid, required this.stopTimes}); - factory Trip.fromJson(Map json) { + factory Trip.fromBackend(backend.Trip trip) { return Trip( - tripId: json['tripId'] ?? '', - vid: json['vid'] ?? '', - stopTimes: (json['stopTimes'] as List).map((e) => StopTime.fromJson(e)).toList(), + tripId: trip.tripId, + vid: trip.vid, + stopTimes: trip.stopTimes.map((st) => StopTime.fromBackend(st)).toList(), ); } } \ No newline at end of file diff --git a/lib/models/lat_lng.dart b/lib/models/lat_lng.dart new file mode 100644 index 0000000..dafa0eb --- /dev/null +++ b/lib/models/lat_lng.dart @@ -0,0 +1,9 @@ + +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'package:bluebus/backend/export.dart' as backend; + +LatLng latLngFromBackend(backend.LatLon ll) { + return LatLng(ll.lat.toDouble(), ll.lon.toDouble()); +} + diff --git a/lib/services/journey_repository.dart b/lib/services/journey_repository.dart index 1a28eeb..54fa26a 100644 --- a/lib/services/journey_repository.dart +++ b/lib/services/journey_repository.dart @@ -1,5 +1,4 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; +import 'package:dio/dio.dart'; import '../constants.dart'; import '../models/journey.dart'; @@ -10,20 +9,22 @@ class JourneyRepository { required double destLat, required double destLon, }) async { - final uri = Uri.parse('$BACKEND_URL/plan-journey').replace(queryParameters: { - 'originLat': originLat.toString(), - 'originLon': originLon.toString(), - 'destLat': destLat.toString(), - 'destLon': destLon.toString(), - }); - final response = await http.get(uri); - if (response.statusCode != 200) { - throw Exception('Failed to plan journey: status ${response.statusCode}'); + try { + final response = await backendClient.getMbusApiV3PlanJourney( + originLat: originLat, + originLon: originLon, + destLat: destLat, + destLon: destLon, + ); + return response.journeys.map(Journey.fromBackend).toList(); + } on DioException catch (e) { + final res = e.response; + if (res != null && res.statusCode != 200) { + throw Exception('Failed to plan journey: status ${res.statusCode}'); + } + rethrow; + } catch (e) { + rethrow; } - final data = jsonDecode(response.body); - if (data == null || data['journeys'] == null) { - throw Exception('No journeys found in response'); - } - return (data['journeys'] as List).map((e) => Journey.fromJson(e)).toList(); } } \ No newline at end of file diff --git a/lib/services/map_layers/journey_layer.dart b/lib/services/map_layers/journey_layer.dart index 104108b..f1f5c0a 100644 --- a/lib/services/map_layers/journey_layer.dart +++ b/lib/services/map_layers/journey_layer.dart @@ -94,7 +94,7 @@ class JourneyLayer extends CompositeMapLayer { markerId: MarkerId('journey_bus_${bus.id}'), consumeTapEvents: true, position: bus.position, - icon: busIcon!, + icon: busIcon, rotation: bus.heading, anchor: const Offset(0.5, 0.5), onTap: () => _showBusSheet(bus.id), @@ -138,20 +138,19 @@ class JourneyLayer extends CompositeMapLayer { } Future addBusLegMarkersAndPolylines( - Leg leg, + BusLeg leg, Journey journey, int legIndex, ) async { // This accepts a bus leg that goes from, e.g. CCTC (C251) through several stops to a destination, e.g. Stop C251 // and adds the necessary markers and polylines to the markers and polylines Sets - if (leg.rt != null) activeJourneyRoutes.add(leg.rt!); - if (leg.trip != null) activeJourneyBusIds.add(leg.trip!.vid); + activeJourneyRoutes.add(leg.rt); + final vid = leg.trip.vid; + if (vid != null) activeJourneyBusIds.add(vid); final rt = leg.rt; - final line = rt != null - ? determineRouteOfBusLeg(routesCache, rt, leg.originID, leg.destinationID) - : null; + final line = determineRouteOfBusLeg(routesCache, rt, leg.originID, leg.destinationID); // debugPrint("Tracing path from ${leg.originID} to ${leg.destinationID}"); @@ -175,7 +174,7 @@ class JourneyLayer extends CompositeMapLayer { jointType: JointType.round, polylineId: PolylineId('journey_${journey.hashCode}_$legIndex'), points: [startLatLng, endLatLng], - color: RouteColorService.getRouteColor(leg.rt!), + color: RouteColorService.getRouteColor(leg.rt), width: 6, ); polylines.add(polyline); @@ -186,14 +185,14 @@ class JourneyLayer extends CompositeMapLayer { jointType: JointType.round, polylineId: PolylineId('journey_${journey.hashCode}_$legIndex'), points: segment, - color: RouteColorService.getRouteColor(leg.rt!), + color: RouteColorService.getRouteColor(leg.rt), width: 6, ); polylines.add(polyline); } // add stop markers at endpoints of the segment (boarding/getting off) - if ((segment?.first != null || startLatLng != null)) { + if ((segment?.first != null)) { // Making sure the marker has a valid location // BitmapDescriptor iconBitmap = await RouteIcon.small( @@ -211,13 +210,13 @@ class JourneyLayer extends CompositeMapLayer { _getOn ?? // iconBitmap ?? BitmapDescriptor.defaultMarkerWithHue( - colorToHue(RouteColorService.getRouteColor(leg.rt!)), + colorToHue(RouteColorService.getRouteColor(leg.rt)), ), anchor: Offset(0.5, 0.5), ), ); } - if ((segment?.last != null || endLatLng != null)) { + if ((segment?.last != null)) { // Making sure the marker has a valid location markers.add( Marker( @@ -227,7 +226,7 @@ class JourneyLayer extends CompositeMapLayer { icon: _getOff ?? BitmapDescriptor.defaultMarkerWithHue( - colorToHue(RouteColorService.getRouteColor(leg.rt!)), + colorToHue(RouteColorService.getRouteColor(leg.rt)), ), ), ); @@ -236,7 +235,7 @@ class JourneyLayer extends CompositeMapLayer { } void addWalkingLegMarkersAndPolylines( - Leg leg, + WalkingLeg leg, Journey journey, int legIndex, ) { @@ -253,9 +252,9 @@ class JourneyLayer extends CompositeMapLayer { startLatLng = getLatLongFromStopID(prevLeg.destinationID); } - List pathCoords = leg.pathCoords ?? []; + List pathCoords = leg.pathCoords; - if (leg.pathCoords == null) { + if (leg.pathCoords.length < 2) { if (startLatLng != null && endLatLng != null) { // If there's no path available, draw a straight line if we can pathCoords = [startLatLng, endLatLng]; @@ -318,27 +317,22 @@ class JourneyLayer extends CompositeMapLayer { activeJourneyBusIds.clear(); activeJourneyRoutes.clear(); - final allPoints = []; - // First, analyze the journey to find which legs are bus and which are walking for (int legIndex = 0; legIndex < journey.legs.length; legIndex++) { final leg = journey.legs[legIndex]; if (leg.destinationID == "VIRTUAL_DESTINATION" && - leg.pathCoords != null && - leg.pathCoords!.isNotEmpty) { - addRouteEndMarker(leg.pathCoords!.last, journey); + leg is WalkingLeg && + leg.pathCoords.isNotEmpty) { + addRouteEndMarker(leg.pathCoords.last, journey); } - // Determine if this is a walking or bus leg - walking legs don't have rt or trip - final bool isBusLeg = leg.rt != null && leg.trip != null; - // Determine leg type for processing - - if (isBusLeg) { - addBusLegMarkersAndPolylines(leg, journey, legIndex); - } else { - addWalkingLegMarkersAndPolylines(leg, journey, legIndex); + switch (leg) { + case BusLeg(): + addBusLegMarkersAndPolylines(leg, journey, legIndex); + case WalkingLeg(): + addWalkingLegMarkersAndPolylines(leg, journey, legIndex); } } diff --git a/lib/services/navigation/navigation_manager.dart b/lib/services/navigation/navigation_manager.dart index 336cc96..1471f6a 100644 --- a/lib/services/navigation/navigation_manager.dart +++ b/lib/services/navigation/navigation_manager.dart @@ -224,25 +224,16 @@ class NavOnBus extends NavigationStage { @override void initWithLeg(Leg leg) { - if (leg.mode != LegMode.bus) { + if (leg is! BusLeg) { throw ArgumentError("leg is of the wrong type"); } - final rt = leg.rt; - final trip = leg.trip; - if (rt == null || - trip == null || - // leg.stopTimes == null || - leg.originID == '' || - leg.destinationID == '') { - throw FormatException("leg was malformed"); - } - if (trip.stopTimes.length < 2) throw FormatException("trip is too short"); + if (leg.trip.stopTimes.length < 2) throw FormatException("trip is too short"); // TODO: use info from backend instead of this placeholder, check that line // has the same number of stops final points = []; final stops = <(int, BusStop)>[]; - for (final st in trip.stopTimes.skipWhile( + for (final st in leg.trip.stopTimes.skipWhile( (st) => st.stop != leg.originID, )) { final loc = getLatLongFromStopID(st.stop); @@ -254,21 +245,21 @@ class NavOnBus extends NavigationStage { id: st.stop, name: getStopNameFromID(st.stop), location: loc, - routeId: rt, + routeId: leg.rt, rotation: 0.0, - isRide: isRide(rt), + isRide: isRide(leg.rt), ), )); } final line = BusRouteLine( - routeId: rt, + routeId: leg.rt, points: points, stops: stops, - color: RouteColorService.getRouteColor(rt), + color: RouteColorService.getRouteColor(leg.rt), imageUrl: null, ); state = NavOnBusState( - trip: trip, + trip: leg.trip, departureStop: leg.originID, arrivalStop: leg.destinationID, line: line, @@ -550,7 +541,7 @@ class Walking extends NavigationStage { const LatLng(42.2775215703816, -83.73809993417933), const LatLng(42.278481544159916, -83.73811396072821), ]; - Leg? leg; + WalkingLeg? leg; Color color = Colors.black; LatLng? currWalkingPos = const LatLng(42.27831772684626, -83.73599054149456); //near cctc (replace w user's location) @@ -665,10 +656,11 @@ class Walking extends NavigationStage { // Initializes the Walking stage given a Leg. @override void initWithLeg(Leg leg_in) { + if (leg_in is! WalkingLeg) throw ArgumentError('Leg was not a walking leg'); final path = leg_in.pathCoords; leg = leg_in; - if (path == null || path.isEmpty) { + if (path.isEmpty) { throw ArgumentError( 'Walking leg from ${leg_in.origin} to ${leg_in.destination} has no path.', ); @@ -1022,16 +1014,17 @@ class NavigationManager { for (Leg leg in journey.legs) { // TODO: Call initWithLeg(leg) constructor here if it's a Bus leg - if (leg.mode == LegMode.walk) { - Walking walkingStage = Walking(); - walkingStage.setColor(walkingLineColor); // Because Flutter won't let us get a Context inside WalkingStage because it isn't a widget. Womp womp - walkingStage.initWithLeg(leg); - this.stageList.add(walkingStage); - } else if (leg.mode == LegMode.bus) { - NavOnBus onBusStage = NavOnBus(); - onBusStage.initWithLeg(leg); - this.stageList.add(onBusStage); - } + switch (leg) { + case BusLeg(): + NavOnBus onBusStage = NavOnBus(); + onBusStage.initWithLeg(leg); + this.stageList.add(onBusStage); + case WalkingLeg(): + Walking walkingStage = Walking(); + walkingStage.setColor(walkingLineColor); // Because Flutter won't let us get a Context inside WalkingStage because it isn't a widget. Womp womp + walkingStage.initWithLeg(leg); + this.stageList.add(walkingStage); + } } // this.stageList.add( diff --git a/lib/theride_api.dart b/lib/theride_api.dart index eac07a8..92ab981 100644 --- a/lib/theride_api.dart +++ b/lib/theride_api.dart @@ -1,9 +1,7 @@ import 'dart:convert'; -import 'package:bluebus/utils/geometry.dart'; +import 'package:dio/dio.dart'; import 'package:http/http.dart' as http; -import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'constants.dart'; -import 'models/bus_stop.dart'; import 'models/bus.dart'; import 'models/bus_route_line.dart'; import 'services/route_color_service.dart'; @@ -13,129 +11,31 @@ class RideAPI { // Fetch all routes and their polylines/stops static Future> fetchRoutes(Function(String route, String error) onError) async { - final response = await http.get(Uri.parse('$baseUrl/getAllRideRoutes')); - if (response.statusCode != 200) throw Exception('Failed to load routes'); - final data = jsonDecode(response.body); - final routes = []; - final routeJson = data['routes'] as Map; - - await RouteColorService.initialize(); - - routeJson.forEach((routeId, subroutes) { - for (final subroute in subroutes) { - try { - final points = []; - final stops = <(int, BusStop)>[]; - - // Cast to list to be able to be able to get different elements - final pointList = subroute['pt'] as List; - - for (int i = 0; i < pointList.length; i++) { - final point = pointList[i]; - final isLast = i == pointList.length - 1; // bool to check if last - points.add( - LatLng( - point['lat']?.toDouble() ?? 0, - point['lon']?.toDouble() ?? 0, - ), - ); - if (point['typ'] == 'S') { - // get rotation of stop - double stopRotation; - if (isLast) { - // use the previous 2 points to calculate rotation - stopRotation = pointRotation( - pointList[i - 2]['lat']?.toDouble() ?? 0, - pointList[i - 2]['lon']?.toDouble() ?? 0, - pointList[i - 1]['lat']?.toDouble() ?? 0, - pointList[i - 1]['lon']?.toDouble() ?? 0, - ); - } else { - // use the next 2 points to calculate rotation - stopRotation = pointRotation( - pointList[i + 1]['lat']?.toDouble() ?? 0, - pointList[i + 1]['lon']?.toDouble() ?? 0, - pointList[i + 2]['lat']?.toDouble() ?? 0, - pointList[i + 2]['lon']?.toDouble() ?? 0, - ); - } - stops.add((i, BusStop.fromJson(point, routeId, stopRotation, true))); - - } - } - - // Get route color and image - final routeColor = RouteColorService.getRouteColor(routeId); - final routeImageUrl = RouteColorService.getRouteImageUrl(routeId); - - routes.add( - BusRouteLine( - routeId: routeId, - points: points, - stops: stops, - color: routeColor, - imageUrl: routeImageUrl, - ), - ); - - // Handle detour points if present - if (subroute.containsKey('dtrpt')) { - final detourPoints = []; - final detourStops = <(int, BusStop)>[]; - - // Cast to list to be able to be able to get different elements - final detourPointList = subroute['dtrpt'] as List; + try { + final routes = await backendClient.getApiV4AllRideRoutes(); + await RouteColorService.initialize(); - for (int i = 0; i < detourPointList.length; i++) { - final point = detourPointList[i]; - final isLast = i == detourPointList.length - 1; // bool to check if last + return routes + .map((r) { + final routeColor = RouteColorService.getRouteColor(r.routeId); + final routeImageUrl = RouteColorService.getRouteImageUrl(r.routeId); - detourPoints.add( - LatLng( - point['lat']?.toDouble() ?? 0, - point['lon']?.toDouble() ?? 0, - ), - ); - if (point['typ'] == 'S') { - // get rotation of stop - double stopRotation; - if (isLast) { - // use the previous 2 points to calculate rotation - stopRotation = pointRotation( - detourPointList[i - 2]['lat']?.toDouble() ?? 0, - detourPointList[i - 2]['lon']?.toDouble() ?? 0, - detourPointList[i - 1]['lat']?.toDouble() ?? 0, - detourPointList[i - 1]['lon']?.toDouble() ?? 0, - ); - } else { - // use the next 2 points to calculate rotation - stopRotation = pointRotation( - detourPointList[i + 1]['lat']?.toDouble() ?? 0, - detourPointList[i + 1]['lon']?.toDouble() ?? 0, - detourPointList[i + 2]['lat']?.toDouble() ?? 0, - detourPointList[i + 2]['lon']?.toDouble() ?? 0, - ); - } - detourStops.add((i, BusStop.fromJson(point, routeId, stopRotation, true))); - } - } + return BusRouteLine.fromBackend(r, routeColor, routeImageUrl); + }) + .toList(); + } on DioException catch (e) { + final res = e.response; - routes.add( - BusRouteLine( - routeId: routeId, - points: detourPoints, - stops: detourStops, - color: routeColor, - imageUrl: routeImageUrl, - ), - ); - } - } catch (e) { - onError(routeId, e.toString()); - } + if (res != null && res.statusCode != 200) { + onError("", "Failed to load routes"); + throw Exception('Failed to load routes'); } - }); - return routes; + onError("", e.message ?? e.toString()); + rethrow; + } catch (e) { + onError("", e.toString()); + rethrow; + } } // Fetch all buses and their positions diff --git a/lib/widgets/journey_results_widget.dart b/lib/widgets/journey_results_widget.dart index 0618af0..ea4fae1 100644 --- a/lib/widgets/journey_results_widget.dart +++ b/lib/widgets/journey_results_widget.dart @@ -119,8 +119,8 @@ class _JourneyResultsWidgetState extends State { final totalDuration = journey.arrivalTime - getSecondsAfterMidnightUtc(); Set busIDs = {}; - for (Leg l in journey.legs) { - if (l.rt != null) busIDs.add(l.rt!); + for (BusLeg l in journey.legs.whereType()) { + busIDs.add(l.rt); } return GestureDetector( @@ -417,11 +417,13 @@ class _JourneyBodyState extends State { String desID, int legID, ) { + final leg = widget.journey.legs[legID]; + if (leg is! BusLeg) return (false, []); + bool foundStart = false; List<(String,int)> stopIDs = []; - // todo: add check for .trip being null - for (StopTime st in widget.journey.legs[legID].trip!.stopTimes) { + for (StopTime st in leg.trip.stopTimes) { if (st.stop == orgID) { foundStart = true; } @@ -440,11 +442,11 @@ class _JourneyBodyState extends State { } List intermediaryLocations(String orgId, String desId, int legId) { - (bool, List<(String, int)>) intermediary_stop_data = intermediaryBusStops(orgId, desId, legId); + final (_, intermediaryStopData)= intermediaryBusStops(orgId, desId, legId); List outputLocations = []; - for ((String,int) stopId in intermediary_stop_data.$2) { + for ((String,int) stopId in intermediaryStopData) { Location? loc = getLocationFromID(stopId.$1); if (loc != null) outputLocations.add(ArrivalTimeLocation(formatSecondsToTime(stopId.$2), loc)); } @@ -457,8 +459,10 @@ class _JourneyBodyState extends State { String orgID, int legID, ) { - // TODO: add check for .trip being null - for (StopTime st in widget.journey.legs[legID].trip!.stopTimes) { + final leg = widget.journey.legs[legID]; + if (leg is! BusLeg) return null; + + for (StopTime st in leg.trip.stopTimes) { if (st.stop == orgID) { return convertSecondsToFormattedTime(st.arrivalTime); } @@ -476,7 +480,7 @@ class _JourneyBodyState extends State { int index = widget.journey.legs.indexOf(leg); // walk or bus? - if (leg.rt == null) { + if (leg is! BusLeg) { // walk return Padding( padding: const EdgeInsets.only(bottom: 20), @@ -537,13 +541,13 @@ class _JourneyBodyState extends State { Row( children: [ // icon - RouteIcon.medium(leg.rt!), + RouteIcon.medium(leg.rt), SizedBox(width: 10), Expanded( child: Text( - "Take ${getPrettyRouteName(leg.rt!)}", + "Take ${getPrettyRouteName(leg.rt)}", style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, @@ -563,9 +567,9 @@ class _JourneyBodyState extends State { ), UpcomingStopsWidget( - color: RouteColorService.getRouteColor(leg.rt!), - routeId: leg.rt!, - vehicleId: leg.trip!.vid, + color: RouteColorService.getRouteColor(leg.rt), + routeId: leg.rt, + vehicleId: leg.trip.vid, stopsToDisplayOverride: intermediaryLocations(leg.originID, leg.destinationID, index), isExpanded: true, showSeeMoreButton: false, diff --git a/pubspec.yaml b/pubspec.yaml index d55fd9a..894ad70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,11 +33,24 @@ dependencies: screen_corner_radius: ^3.0.0 widget_to_marker: ^1.0.6 vector_math: ^2.2.0 + dio: ^5.9.2 + json_annotation: ^4.12.0 + retrofit: ^4.9.2 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^5.0.0 + build_runner: ^2.15.0 + carapacik_lints: ^3.12.0 + json_serializable: ^6.14.0 + retrofit_generator: ^10.2.7 + # swagger_parser: ^1.44.0 + swagger_parser: + git: + url: https://github.com/mbusdev/swagger_parser.git + path: swagger_parser + ref: main flutter: fonts: @@ -79,3 +92,10 @@ flutter_launcher_icons: adaptive_icon_foreground_inset: 24 image_path: "assets/appicon.png" min_sdk_android: 21 # android min sdk min:16, default 21 + +swagger_parser: + schema_url: https://docs.mb.thething.fyi/refs/heads/mock-backend-2/openapi/spec.json + output_directory: lib/backend/ + name: backend + language: dart + fallback_client: "backend"