diff --git a/src/client/lib/src/pages/home.dart b/src/client/lib/src/pages/home.dart index 08cbd35..f9032db 100644 --- a/src/client/lib/src/pages/home.dart +++ b/src/client/lib/src/pages/home.dart @@ -1,5 +1,5 @@ import "package:client/data.dart"; -import "package:flutter/material.dart"; +import "package:flutter/material.dart" hide Route; import "package:google_maps_flutter/google_maps_flutter.dart"; import "package:client/view_models.dart"; @@ -52,30 +52,36 @@ class HomePage extends ReactiveWidget { const Divider(), const SizedBox(height: 8), if (model.isLoading) - const LinearProgressIndicator() - else if (model.isSearching && model.path == null) - const Text("Could not connect to API") - else - for (final step in model.path ?? []) Text( - "Get on at ${step.enter.lat}, ${step.enter.long}\n" - "Get off at ${step.exit.lat}, ${step.exit.long}", - ), + const LinearProgressIndicator(), + if (model.errorText != null) + Text(model.errorText!), Expanded( child: model.isGoogleReady ? GoogleMap( onMapCreated: (controller) => model.mapController = controller, + onTap: model.onMapTapped, initialCameraPosition: const CameraPosition( target: LatLng(42.10125081757972, -75.94181323552698), zoom: 14, ), - markers: model.markers, - onTap: model.onMapTapped, + markers: { + ...model.markers, + for (final (stop, reason) in model.pathWaypoint) Marker( + markerId: MarkerId(stop.id.id), + position: stop.coordinates.toLatLng(), + infoWindow: InfoWindow( + title: reason, + snippet: stop.description, + ), + ), + }, circles: { for (final route in model.pathStops) for (final stop in route) Circle( circleId: CircleId(stop.hashCode.toString()), center: stop.toLatLng(), fillColor: Colors.white, + zIndex: 1, radius: 2, ), }, @@ -83,11 +89,22 @@ class HomePage extends ReactiveWidget { for (final (index, route) in model.pathStops.indexed) Polyline( polylineId: PolylineId(index.toString()), color: routeColors[index], + jointType: JointType.round, points: [ for (final stop in route) stop.toLatLng(), ], ), + for (final (index, walkingPath) in model.pathWalking.indexed) Polyline( + polylineId: PolylineId((model.pathStops.length + index).toString()), + jointType: JointType.round, + width: 8, + patterns: List.filled(20, PatternItem.gap(3)), + points: [ + for (final step in walkingPath) + step.toLatLng(), + ], + ), }, ) : const Center(child: CircularProgressIndicator()), @@ -101,4 +118,4 @@ class HomePage extends ReactiveWidget { ); } -const routeColors = [Colors.blue, Colors.red, Colors.green, Colors.yellow, Colors.orange]; +final routeColors = [Colors.red, Colors.orange, Colors.amber, Colors.green, Colors.blue, Colors.deepPurple, Colors.grey, Colors.pink, Colors.teal, ...List.filled(10, Colors.black)]; diff --git a/src/client/lib/src/view_models/home.dart b/src/client/lib/src/view_models/home.dart index 1a0f28e..1e88548 100644 --- a/src/client/lib/src/view_models/home.dart +++ b/src/client/lib/src/view_models/home.dart @@ -2,7 +2,7 @@ import "dart:async"; import "package:client/data.dart"; import "package:client/services.dart"; -import "package:flutter/widgets.dart" hide Path; +import "package:flutter/widgets.dart" hide Path, Route; import "package:google_maps_flutter/google_maps_flutter.dart"; import "package:shared/graph.dart"; @@ -10,6 +10,8 @@ import "package:shared/graph.dart"; import "home_markers.dart"; import "view_model.dart"; +typedef PathStep = (Stop, String); + /// The view model for the home page. class HomeModel extends ViewModel with HomeMarkers { final startLatitudeController = TextEditingController(); @@ -17,7 +19,6 @@ class HomeModel extends ViewModel with HomeMarkers { final endLatitudeController = TextEditingController(); final endLongitudeController = TextEditingController(); - Path? path; GoogleMapController? mapController; double? get startLatitude => double.tryParse(startLatitudeController.text); @@ -66,8 +67,8 @@ class HomeModel extends ViewModel with HomeMarkers { super.updateEnd(coordinates, marker); } - String? pathText; - List pathWaypoint = []; + List? pathText; + List pathWaypoint = []; List> pathWalking = []; List> pathStops = []; @@ -87,10 +88,11 @@ class HomeModel extends ViewModel with HomeMarkers { String explainStep(StopState step) { final route = routes[step.routeID]!; final stop = stops[step.stopID]!; + if (step.isGoal()) return "Get off at $stop"; return switch (step.method) { - SearchMethod.bus => "Go one stop to $stop", + SearchMethod.bus => "Ride the bus to $stop", SearchMethod.start => "Walk to $stop\n and board the $route line", - SearchMethod.transfer => "Transfer to the $route line", + SearchMethod.transfer => "Transfer at $stop\n to the $route line", SearchMethod.walk => "Walk to $stop\n and board the $route line" }; } @@ -99,6 +101,7 @@ class HomeModel extends ViewModel with HomeMarkers { final start = (lat: startLatitude, long: startLongitude); final end = (lat: endLatitude, long: endLongitude); pathText = null; + errorText = null; if (start.lat == null || start.long == null) return; if (end.lat == null || end.long == null) return; isSearching = true; @@ -108,6 +111,9 @@ class HomeModel extends ViewModel with HomeMarkers { if (path == null) { errorText = "An error occcurred"; return; + } else if (path.isEmpty) { + errorText = "No path found"; + return; } pathWalking.clear(); pathWaypoint.clear(); @@ -118,12 +124,12 @@ class HomeModel extends ViewModel with HomeMarkers { switch (step.method) { case SearchMethod.start: pathWalking.add([start, position]); - pathWaypoint.add(position); + pathWaypoint.add( (stop, explainStep(step)) ); pathStops.add([position]); case SearchMethod.bus: pathStops.last.add(position); case SearchMethod.transfer: - pathWaypoint.add(position); + pathWaypoint.add( (stop, explainStep(step)) ); pathStops.last.add(position); pathStops.add([position]); case SearchMethod.walk: @@ -131,13 +137,19 @@ class HomeModel extends ViewModel with HomeMarkers { final prevStopID = prevStep.stopID; final prevStop = stops[prevStopID]!; pathWalking.add([prevStop.coordinates, position]); - pathWaypoint.add(stop.coordinates); + pathWaypoint.add( (prevStop, explainStep(prevStep)) ); + pathWaypoint.add( (stop, explainStep(step)) ); pathStops.add([position]); } + if (index == (path.length - 1)) { + if (step.method == SearchMethod.bus) { + pathWaypoint.add( (stop, explainStep(step)) ); + } + pathWalking.add([stop.coordinates, end]); + } } - final buffer = StringBuffer(); - explainPath(path, buffer.writeln); - pathText = buffer.toString(); + pathText = []; + explainPath(path, pathText!.add); isSearching = false; notifyListeners(); } diff --git a/src/client/lib/src/widgets/sidebar.dart b/src/client/lib/src/widgets/sidebar.dart index f641190..0e2f7c0 100644 --- a/src/client/lib/src/widgets/sidebar.dart +++ b/src/client/lib/src/widgets/sidebar.dart @@ -34,10 +34,18 @@ class Sidebar extends ReusableReactiveWidget { Tab(text: provider), ], ), - if (!model.shouldShowMarkers && model.pathText != null) - SingleChildScrollView(child: Center(child: Text(model.pathText!, style: context.textTheme.bodySmall))), - if (!model.shouldShowMarkers && model.pathText == null) - const Center(child: Text("Choose start or end location")), + if (!model.shouldShowMarkers) Expanded( + child: ListView( + children: [ + if (model.pathText != null) ...[ + for (final line in model.pathText!) + Text(line, style: context.textTheme.bodySmall), + ] + else + const Center(child: Text("Choose start or end location")), + ], + ), + ), if (model.shouldShowMarkers) Expanded( child: TabBarView( children: [ diff --git a/src/server/GET_STOPS.json b/src/server/GET_STOPS.json index 00dfdc4..f4ab79a 100644 --- a/src/server/GET_STOPS.json +++ b/src/server/GET_STOPS.json @@ -12055,7 +12055,6 @@ "Westside Inbound", "Downtown Center Leroy Outbound", "Downtown Center Leroy Inbound", - "UCLUB", "Campus Shuttle", "Riviera Ridge - Town Square Mall", "Oakdale Commons", @@ -12070,7 +12069,6 @@ "OCCT_2", "OCCT_3", "OCCT_4", - "OCCT_5", "OCCT_8", "OCCT_9", "OCCT_10", @@ -12385,7 +12383,6 @@ "routes": [ "Westside Inbound", "Downtown Center Leroy Inbound", - "UCLUB", "Campus Shuttle", "Riviera Ridge - Town Square Mall", "Oakdale Commons", @@ -12396,7 +12393,6 @@ "route_ids": [ "OCCT_2", "OCCT_4", - "OCCT_5", "OCCT_8", "OCCT_9", "OCCT_10", @@ -12721,12 +12717,10 @@ "longitude": -75.9657592773438, "provider": "OCCT", "routes": [ - "UCLUB", "Campus Shuttle", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_8", "OCCT_16" ] @@ -12739,12 +12733,10 @@ "longitude": -75.9673309326172, "provider": "OCCT", "routes": [ - "UCLUB", "Campus Shuttle", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_8", "OCCT_16" ] @@ -12757,11 +12749,9 @@ "longitude": -75.9564208984375, "provider": "OCCT", "routes": [ - "UCLUB", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_16" ] }, @@ -12773,11 +12763,9 @@ "longitude": -75.9558868408203, "provider": "OCCT", "routes": [ - "UCLUB", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_16" ] }, @@ -12789,12 +12777,10 @@ "longitude": -75.9628753662109, "provider": "OCCT", "routes": [ - "UCLUB", "Campus Shuttle", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_8", "OCCT_16" ] @@ -12807,11 +12793,9 @@ "longitude": -75.959098815918, "provider": "OCCT", "routes": [ - "UCLUB", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_16" ] }, @@ -12823,11 +12807,9 @@ "longitude": -75.9538421630859, "provider": "OCCT", "routes": [ - "UCLUB", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_16" ] }, @@ -12839,11 +12821,9 @@ "longitude": -75.9595413208008, "provider": "OCCT", "routes": [ - "UCLUB", "ITC - UCLUB" ], "route_ids": [ - "OCCT_5", "OCCT_16" ] }, diff --git a/src/server/GET_routes.json b/src/server/GET_routes.json index e01ec43..4fb360f 100644 --- a/src/server/GET_routes.json +++ b/src/server/GET_routes.json @@ -1242,24 +1242,6 @@ "OCCT_1" ] }, - { - "id": "OCCT_5", - "provider": "OCCT", - "full_name": "UCLUB", - "short_name": "UC", - "stops": [ - "OCCT_61", - "OCCT_63", - "OCCT_64", - "OCCT_65", - "OCCT_66", - "OCCT_67", - "OCCT_68", - "OCCT_69", - "OCCT_134", - "OCCT_1" - ] - }, { "id": "OCCT_8", "provider": "OCCT", diff --git a/src/shared/lib/src/generator/routes_bc.dart b/src/shared/lib/src/generator/routes_bc.dart index 64a7b7e..21b45f4 100644 --- a/src/shared/lib/src/generator/routes_bc.dart +++ b/src/shared/lib/src/generator/routes_bc.dart @@ -10,10 +10,12 @@ class BcRoutesParser extends Parser { final tripsFile = File(bcDataDir / "trips.txt"); final stopParser = BcStopParser(); + static final routesToSkip = ["48", "91"]; Future> getRoutes() async => { for (final csv in await readCsv(routesFile)) - RouteID.fromBcCsv(csv): Route.fromBcCsv(csv), + if (!routesToSkip.contains(csv[0])) + RouteID.fromBcCsv(csv): Route.fromBcCsv(csv), }; Future> getTripForRoutes(Map> trips) async { diff --git a/src/shared/lib/src/generator/routes_occt.dart b/src/shared/lib/src/generator/routes_occt.dart index d049151..e99d8d6 100644 --- a/src/shared/lib/src/generator/routes_occt.dart +++ b/src/shared/lib/src/generator/routes_occt.dart @@ -4,10 +4,12 @@ import "utils.dart"; class OcctRoutesParser extends Parser { final routesFile = File(occtDataDir / "routes.json"); + static final routesToSkip = {5}; @override Future> parse() async => { for (final routeJson in await readJson(routesFile)) - RouteID(Provider.occt, routeJson["id"]): Route.fromOcctJson(routeJson), + if (!routesToSkip.contains(routeJson["id"])) + RouteID(Provider.occt, routeJson["id"]): Route.fromOcctJson(routeJson), }; } diff --git a/src/shared/lib/src/generator/stops_occt.dart b/src/shared/lib/src/generator/stops_occt.dart index f879388..47f1211 100644 --- a/src/shared/lib/src/generator/stops_occt.dart +++ b/src/shared/lib/src/generator/stops_occt.dart @@ -38,7 +38,7 @@ class OcctStopParser extends Parser { final stops = await getStops(); final routes = await getRoutes(); final routeNames = await getRouteNames(); - final routesToSkip = {RouteID(Provider.occt, "11")}; + final routesToSkip = {RouteID(Provider.occt, "11"), RouteID(Provider.occt, "5")}; for (final (stopID, stop) in stops.records) { for (final routeID in routes[stopID]!) { if (routesToSkip.contains(routeID)) continue; diff --git a/src/shared/lib/src/graph/algorithm.dart b/src/shared/lib/src/graph/algorithm.dart index 741f854..f18bc3e 100644 --- a/src/shared/lib/src/graph/algorithm.dart +++ b/src/shared/lib/src/graph/algorithm.dart @@ -32,7 +32,7 @@ Iterable? findPath(Coordinates start, Coordinates end) { endPoint: end, ); log("Finding a route using ${state.hash()}"); - final result = aStar(state, limit: 10000); + final result = aStar(state, limit: 1000, verbose: false); if (result == null) continue; paths.add(result.reconstructPath()); } diff --git a/src/shared/lib/src/graph/state.dart b/src/shared/lib/src/graph/state.dart index 5432f3c..3e6ca92 100644 --- a/src/shared/lib/src/graph/state.dart +++ b/src/shared/lib/src/graph/state.dart @@ -62,11 +62,11 @@ class StopState extends AStarState with Encodable { factory StopState.fromJson(Json json) => StopState( stopID: StopID.fromJson(json["stop_id"]), + goalID: StopID.fromJson(json["goal_id"]), routeID: RouteID.fromJson(json["route_id"]), method: SearchMethod.fromJson(json["method"]), depth: 0, distanceWalked: 0, - goalID: StopID.fromJson("N/A"), ); @override @@ -74,7 +74,7 @@ class StopState extends AStarState with Encodable { final stop = stops[stopID]; final goal = stops[goalID]; final route = routes[routeID]; - return "${method.name}-$stop-$goal-$route"; + return "$distanceWalked-${method.name}-$stop-$goal-$route"; } @override @@ -89,7 +89,8 @@ class StopState extends AStarState with Encodable { @override Iterable expand() { - final route = routes[routeID]!; + final route = routes[routeID]; + if (route == null) return []; final stop = stops[stopID]!; final stopIndex = route.stops.indexOf(stopID); if (stopIndex == -1) return []; @@ -99,11 +100,13 @@ class StopState extends AStarState with Encodable { final nextIndex = stopIndex + 1; if (nextIndex < route.stops.length) { final nextStopID = route.stops[nextIndex]; + final nextStop = stops[nextStopID]!; + final distanceDriven = stop.coordinates.distanceTo(nextStop.coordinates); final state = StopState( stopID: nextStopID, goalID: goalID, routeID: routeID, - depth: depth + 1, + depth: depth + distanceDriven, distanceWalked: distanceWalked, method: SearchMethod.bus, ); @@ -112,13 +115,15 @@ class StopState extends AStarState with Encodable { // 2) Go back one stop on the same route final prevIndex = stopIndex - 1; - if (prevIndex > 0) { + if (prevIndex >= 0) { final prevStopID = route.stops[prevIndex]; + final prevStop = stops[prevStopID]!; + final distanceDriven = stop.coordinates.distanceTo(prevStop.coordinates); final state = StopState( stopID: prevStopID, goalID: goalID, routeID: routeID, - depth: depth + 1, + depth: depth + distanceDriven, distanceWalked: distanceWalked, method: SearchMethod.bus, ); @@ -131,7 +136,7 @@ class StopState extends AStarState with Encodable { stopID: stopID, goalID: goalID, routeID: otherRouteID, - depth: depth + 2, + depth: depth + 5, distanceWalked: distanceWalked, method: SearchMethod.transfer, ); @@ -146,7 +151,7 @@ class StopState extends AStarState with Encodable { stopID: otherStop.id, goalID: goalID, routeID: otherRouteID, - depth: depth + 3, + depth: depth + 5 + 10*distanceToOther, distanceWalked: distanceWalked + distanceToOther, method: SearchMethod.walk, ); @@ -160,6 +165,7 @@ class StopState extends AStarState with Encodable { @override Json toJson() => { "stop_id": stopID, + "goal_id": goalID, "route_id": routeID, "method": method.name, }; diff --git a/src/shared/lib/src/server/path.dart b/src/shared/lib/src/server/path.dart index 98a52bf..0dcce77 100644 --- a/src/shared/lib/src/server/path.dart +++ b/src/shared/lib/src/server/path.dart @@ -14,10 +14,7 @@ Response getPath(Request request) { final start = (lat: startLat, long: startLong); final end = (lat: endLat, long: endLong); - final path = findPath(start, end); - if (path == null) { - return Response.ok("No routes found", headers: corsHeaders); - } + final path = findPath(start, end) ?? []; final jsonBody = encodeJsonList(path); return Response.ok(jsonBody, headers: corsHeaders); }