Fix merge conflicts from main to shared_integration
This commit is contained in:
commit
e1f5b4967e
30 changed files with 2536 additions and 2366 deletions
|
@ -16,13 +16,19 @@ class HomePage extends ReactiveWidget<HomeModel> {
|
||||||
body: Row(
|
body: Row(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 300,
|
width: 325,
|
||||||
child: Sidebar(model),
|
child: Sidebar(model),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Card(
|
child: Card(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
SwitchListTile(
|
||||||
|
value: model.markerState == MarkerState.override,
|
||||||
|
onChanged: model.overrideMarkers,
|
||||||
|
title: const Text("Show stops list"),
|
||||||
|
subtitle: const Text("To select a start or end stop, use the buttons below"),
|
||||||
|
),
|
||||||
LatLongEditor(
|
LatLongEditor(
|
||||||
latitudeController: model.startLatitudeController,
|
latitudeController: model.startLatitudeController,
|
||||||
longitudeController: model.startLongitudeController,
|
longitudeController: model.startLongitudeController,
|
||||||
|
@ -65,7 +71,7 @@ class HomePage extends ReactiveWidget<HomeModel> {
|
||||||
markers: model.markers,
|
markers: model.markers,
|
||||||
onTap: model.onMapTapped,
|
onTap: model.onMapTapped,
|
||||||
polylines: {
|
polylines: {
|
||||||
for (final (index, route) in model.routes.indexed) Polyline(
|
for (final (index, route) in model.paths.indexed) Polyline(
|
||||||
polylineId: PolylineId(index.toString()),
|
polylineId: PolylineId(index.toString()),
|
||||||
color: routeColors[index],
|
color: routeColors[index],
|
||||||
points: route,
|
points: route,
|
||||||
|
|
|
@ -17,32 +17,9 @@ class ApiService extends Service {
|
||||||
|
|
||||||
Uri get _base => usingDocker
|
Uri get _base => usingDocker
|
||||||
? Uri(path: "api/")
|
? Uri(path: "api/")
|
||||||
: Uri(scheme: "http", host: "localhost", port: 80);
|
: Uri(scheme: "http", host: "localhost", port: 8001);
|
||||||
|
|
||||||
Future<List<Trip>?> getTrips() => _client.getJsonList(
|
Future<String?> getPath({
|
||||||
_base.resolve("trips"),
|
|
||||||
Trip.fromJson,
|
|
||||||
key: "path",
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<Path?> getPath({
|
|
||||||
required Coordinates start,
|
|
||||||
required Coordinates end,
|
|
||||||
}) => _client.getJsonList(
|
|
||||||
_base.resolve("path").replace(
|
|
||||||
queryParameters: {
|
|
||||||
"start_lat": start.lat.toString(),
|
|
||||||
"start_lon": start.long.toString(),
|
|
||||||
"end_lat": end.lat.toString(),
|
|
||||||
"end_lon": end.long.toString(),
|
|
||||||
"time": DateTime.now().millisecondsSinceEpoch.toString(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
key: "path",
|
|
||||||
PathStep.fromJson,
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<String?> getPath2({
|
|
||||||
required Coordinates start,
|
required Coordinates start,
|
||||||
required Coordinates end,
|
required Coordinates end,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -64,8 +41,13 @@ class ApiService extends Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<Route>?> getRoutes() => _client.getJsonList(
|
||||||
|
_base.resolve("/routes"),
|
||||||
|
Route.fromJson,
|
||||||
|
);
|
||||||
|
|
||||||
Future<List<Stop>?> getStops() => _client.getJsonList(
|
Future<List<Stop>?> getStops() => _client.getJsonList(
|
||||||
_base.resolve("stops"),
|
_base.resolve("/stops"),
|
||||||
Stop.fromJson,
|
Stop.fromJson,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ class HomeModel extends ViewModel with HomeMarkers {
|
||||||
|
|
||||||
bool isSearching = false;
|
bool isSearching = false;
|
||||||
bool isGoogleReady = false;
|
bool isGoogleReady = false;
|
||||||
List<List<LatLng>> routes = [];
|
List<List<LatLng>> paths = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
@ -39,14 +39,15 @@ class HomeModel extends ViewModel with HomeMarkers {
|
||||||
void onMapTapped(LatLng coordinates) {
|
void onMapTapped(LatLng coordinates) {
|
||||||
if (!shouldShowMarkers) return;
|
if (!shouldShowMarkers) return;
|
||||||
final marker = Marker(
|
final marker = Marker(
|
||||||
markerId: markerId,
|
markerId: markerId ?? const MarkerId("Tapped"),
|
||||||
position: coordinates,
|
position: coordinates,
|
||||||
icon: markerIcon,
|
icon: markerIcon ?? BitmapDescriptor.defaultMarker,
|
||||||
infoWindow: InfoWindow(title: "$startOrEnd here"),
|
infoWindow: InfoWindow(title: "$startOrEnd here"),
|
||||||
);
|
);
|
||||||
switch (markerState!) {
|
switch (markerState!) {
|
||||||
case MarkerState.start: updateStart(coordinates, marker);
|
case MarkerState.start: updateStart(coordinates, marker);
|
||||||
case MarkerState.end: updateEnd(coordinates, marker);
|
case MarkerState.end: updateEnd(coordinates, marker);
|
||||||
|
case MarkerState.override: return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,17 +73,11 @@ class HomeModel extends ViewModel with HomeMarkers {
|
||||||
if (end.lat == null || end.long == null) return;
|
if (end.lat == null || end.long == null) return;
|
||||||
isSearching = true;
|
isSearching = true;
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
final result = await services.api.getPath2(start: start as Coordinates, end: end as Coordinates);
|
final result = await services.api.getPath(start: start as Coordinates, end: end as Coordinates);
|
||||||
pathText = result ?? "An error occurred";
|
pathText = result ?? "An error occurred";
|
||||||
// path = result;
|
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
isSearching = false;
|
isSearching = false;
|
||||||
// routes = [
|
|
||||||
// for (final step in result) [
|
|
||||||
// ...decodePolyline(step.trip.polyline),
|
|
||||||
// ],
|
|
||||||
// ];
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,27 @@ import "package:google_maps_flutter/google_maps_flutter.dart";
|
||||||
|
|
||||||
enum MarkerState {
|
enum MarkerState {
|
||||||
start,
|
start,
|
||||||
end;
|
end,
|
||||||
|
override;
|
||||||
}
|
}
|
||||||
|
|
||||||
mixin HomeMarkers on ChangeNotifier {
|
mixin HomeMarkers on ChangeNotifier {
|
||||||
MarkerState? markerState;
|
MarkerState? markerState;
|
||||||
bool get shouldShowMarkers => markerState != null;
|
bool get shouldShowMarkers => markerState != null;
|
||||||
|
|
||||||
List<Stop>? stops;
|
// List<Stop>? stops;
|
||||||
|
Map<StopID, Stop> stops = {};
|
||||||
|
Map<RouteID, Route> routes = {};
|
||||||
Marker? _startMarker;
|
Marker? _startMarker;
|
||||||
Marker? _endMarker;
|
Marker? _endMarker;
|
||||||
|
|
||||||
|
Iterable<Stop> getStopsForRoute(RouteID routeID) => routes[routeID]!.stops
|
||||||
|
.map((stopID) => stops[stopID]!);
|
||||||
|
|
||||||
Set<Marker> get _filteredMarkers => {
|
Set<Marker> get _filteredMarkers => {
|
||||||
for (final stop in stops ?? <Stop>[])
|
// for (final stop in stops ?? <Stop>[])
|
||||||
if (stop.routes.any(routesToShow.contains))
|
for (final routeID in routesToShow)
|
||||||
|
for (final stop in getStopsForRoute(routeID))
|
||||||
Marker(
|
Marker(
|
||||||
markerId: MarkerId(stop.name),
|
markerId: MarkerId(stop.name),
|
||||||
position: stop.coordinates.toLatLng(),
|
position: stop.coordinates.toLatLng(),
|
||||||
|
@ -36,55 +43,67 @@ mixin HomeMarkers on ChangeNotifier {
|
||||||
if (_endMarker != null) _endMarker!,
|
if (_endMarker != null) _endMarker!,
|
||||||
};
|
};
|
||||||
|
|
||||||
Set<String> bcRouteNames = {};
|
List<Route> bcRouteNames = [];
|
||||||
Set<String> occtRouteNames = {};
|
List<Route> occtRouteNames = [];
|
||||||
Set<String> routesToShow = {};
|
Set<RouteID> routesToShow = {};
|
||||||
Map<String, int> stopCounts = {};
|
Iterable<(String, List<Route>)> get providers => [
|
||||||
Iterable<(String, List<String>)> get providers => [
|
("OCCT", occtRouteNames),
|
||||||
("OCCT", occtRouteNames.toList()..sort()),
|
("BC Transit", bcRouteNames),
|
||||||
("BC Transit", bcRouteNames.toList()..sort(compareNums)),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
int parseBcNumber(String routeName) {
|
int _parseBcNumber(String routeName) {
|
||||||
// eg, "53)" --> 53
|
// eg, "53)" --> 53
|
||||||
final first = routeName.split(" ").first;
|
final first = routeName.split(" ").first;
|
||||||
final withoutParen = first.substring(0, first.length - 1);
|
final withoutParen = first.substring(0, first.length - 1);
|
||||||
return int.parse(withoutParen);
|
return int.parse(withoutParen);
|
||||||
}
|
}
|
||||||
|
|
||||||
int compareNums(String a, String b) =>
|
int compareBcRoutes(Route a, Route b) =>
|
||||||
parseBcNumber(a).compareTo(parseBcNumber(b));
|
_parseBcNumber(a.shortName).compareTo(_parseBcNumber(b.shortName));
|
||||||
|
|
||||||
MarkerId get markerId => switch (markerState!) {
|
int compareOcctRoutes(Route a, Route b) =>
|
||||||
|
a.shortName.compareTo(b.shortName);
|
||||||
|
|
||||||
|
MarkerId? get markerId => switch (markerState!) {
|
||||||
MarkerState.start => const MarkerId("start"),
|
MarkerState.start => const MarkerId("start"),
|
||||||
MarkerState.end => const MarkerId("end"),
|
MarkerState.end => const MarkerId("end"),
|
||||||
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
BitmapDescriptor get markerIcon => switch (markerState!) {
|
BitmapDescriptor? get markerIcon => switch (markerState!) {
|
||||||
MarkerState.start => BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan),
|
MarkerState.start => BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan),
|
||||||
MarkerState.end => BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
MarkerState.end => BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||||
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
String get startOrEnd => switch (markerState!) {
|
String? get startOrEnd => switch (markerState!) {
|
||||||
MarkerState.start => "Start",
|
MarkerState.start => "Start",
|
||||||
MarkerState.end => "End",
|
MarkerState.end => "End",
|
||||||
|
_ => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<void> updateMarkers() async {
|
Future<void> updateMarkers() async {
|
||||||
stops = await services.api.getStops();
|
final stopsResponse = await services.api.getStops();
|
||||||
if (stops == null) return;
|
if (stopsResponse == null) return;
|
||||||
for (final stop in stops!) {
|
stops = {
|
||||||
for (final route in stop.routes) {
|
for (final stop in stopsResponse)
|
||||||
stopCounts[route] ??= 0;
|
stop.id: stop,
|
||||||
stopCounts[route] = stopCounts[route]! + 1;
|
};
|
||||||
}
|
final routesResponse = await services.api.getRoutes();
|
||||||
final namesList = switch (stop.provider) {
|
if (routesResponse == null) return;
|
||||||
|
routes = {
|
||||||
|
for (final route in routesResponse)
|
||||||
|
route.id: route,
|
||||||
|
};
|
||||||
|
for (final route in routesResponse) {
|
||||||
|
final namesList = switch (route.provider) {
|
||||||
Provider.occt => occtRouteNames,
|
Provider.occt => occtRouteNames,
|
||||||
Provider.bc => bcRouteNames,
|
Provider.bc => bcRouteNames,
|
||||||
};
|
};
|
||||||
namesList.addAll(stop.routes);
|
namesList.add(route);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
occtRouteNames.sort(compareOcctRoutes);
|
||||||
|
bcRouteNames.sort(compareBcRoutes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMarkers(MarkerState newState) {
|
void showMarkers(MarkerState newState) {
|
||||||
|
@ -100,20 +119,28 @@ mixin HomeMarkers on ChangeNotifier {
|
||||||
void onMarkerTapped(Stop stop) {
|
void onMarkerTapped(Stop stop) {
|
||||||
final coordinates = stop.coordinates.toLatLng();
|
final coordinates = stop.coordinates.toLatLng();
|
||||||
final marker = Marker(
|
final marker = Marker(
|
||||||
markerId: markerId,
|
markerId: markerId ?? MarkerId(stop.name),
|
||||||
position: coordinates,
|
position: coordinates,
|
||||||
infoWindow: InfoWindow(
|
infoWindow: InfoWindow(
|
||||||
title: "$startOrEnd at ${stop.name}",
|
title: "$startOrEnd at ${stop.name}",
|
||||||
snippet: stop.description,
|
snippet: stop.description,
|
||||||
),
|
),
|
||||||
icon: markerIcon,
|
icon: markerIcon ?? BitmapDescriptor.defaultMarker,
|
||||||
);
|
);
|
||||||
switch (markerState!) {
|
switch (markerState!) {
|
||||||
case MarkerState.start: updateStart(coordinates, marker);
|
case MarkerState.start: updateStart(coordinates, marker);
|
||||||
case MarkerState.end: updateEnd(coordinates, marker);
|
case MarkerState.end: updateEnd(coordinates, marker);
|
||||||
|
case MarkerState.override: return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flutter widget
|
||||||
|
// ignore: avoid_positional_boolean_parameters
|
||||||
|
void overrideMarkers(bool value) {
|
||||||
|
markerState = value ? MarkerState.override : null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void updateStart(LatLng coordinates, Marker marker) {
|
void updateStart(LatLng coordinates, Marker marker) {
|
||||||
_startMarker = marker;
|
_startMarker = marker;
|
||||||
|
@ -128,11 +155,11 @@ mixin HomeMarkers on ChangeNotifier {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void showRoute(String route, {required bool shouldShow}) {
|
void showRoute(Route route, {required bool shouldShow}) {
|
||||||
if (shouldShow) {
|
if (shouldShow) {
|
||||||
routesToShow.add(route);
|
routesToShow.add(route.id);
|
||||||
} else {
|
} else {
|
||||||
routesToShow.remove(route);
|
routesToShow.remove(route.id);
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ class Sidebar extends ReusableReactiveWidget<HomeModel> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
if (model.markerState != null && model.markerState != MarkerState.override) ...[
|
||||||
Text(
|
Text(
|
||||||
"Select routes",
|
"Select routes",
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
@ -25,6 +26,7 @@ class Sidebar extends ReusableReactiveWidget<HomeModel> {
|
||||||
style: context.textTheme.bodyMedium,
|
style: context.textTheme.bodyMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
if (model.shouldShowMarkers) TabBar(
|
if (model.shouldShowMarkers) TabBar(
|
||||||
tabs: [
|
tabs: [
|
||||||
|
@ -33,7 +35,7 @@ class Sidebar extends ReusableReactiveWidget<HomeModel> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (!model.shouldShowMarkers && model.pathText != null)
|
if (!model.shouldShowMarkers && model.pathText != null)
|
||||||
Center(child: Text(model.pathText!, style: context.textTheme.bodySmall)),
|
SingleChildScrollView(child: Center(child: Text(model.pathText!, style: context.textTheme.bodySmall))),
|
||||||
if (!model.shouldShowMarkers && model.pathText == null)
|
if (!model.shouldShowMarkers && model.pathText == null)
|
||||||
const Center(child: Text("Choose start or end location")),
|
const Center(child: Text("Choose start or end location")),
|
||||||
if (model.shouldShowMarkers) Expanded(
|
if (model.shouldShowMarkers) Expanded(
|
||||||
|
@ -43,12 +45,12 @@ class Sidebar extends ReusableReactiveWidget<HomeModel> {
|
||||||
ListView(
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
for (final route in routesList) CheckboxListTile(
|
for (final route in routesList) CheckboxListTile(
|
||||||
title: Text(route, maxLines: 1),
|
title: Text(route.fullName, maxLines: 1),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"${model.stopCounts[route] ?? 0} stops",
|
"${route.stops.length} stops",
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
value: model.routesToShow.contains(route),
|
value: model.routesToShow.contains(route.id),
|
||||||
onChanged: (value) => model.showRoute(route, shouldShow: value!),
|
onChanged: (value) => model.showRoute(route, shouldShow: value!),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
2
src/shared/.gitignore
vendored
2
src/shared/.gitignore
vendored
|
@ -4,3 +4,5 @@
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
client-dir
|
client-dir
|
||||||
server-data
|
server-data
|
||||||
|
path.log
|
||||||
|
notes.md
|
||||||
|
|
|
@ -39,4 +39,4 @@ WORKDIR /client/shared
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
|
||||||
CMD ["dart", "bin/path.dart"]
|
CMD ["dart", "bin/server.dart"]
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
import "package:shared/generator.dart";
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
void main() async {
|
void main(List<String> args) async {
|
||||||
|
if (args.contains("-n")) { // dry-run
|
||||||
|
await Generator.stops.parse();
|
||||||
|
await Generator.routes.parse();
|
||||||
|
} else {
|
||||||
await Generator.stops.generate();
|
await Generator.stops.generate();
|
||||||
await Generator.routes.generate();
|
await Generator.routes.generate();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
// User-facing script
|
|
||||||
// ignore_for_file: avoid_print
|
|
||||||
|
|
||||||
import "package:a_star/a_star.dart";
|
|
||||||
import "package:shared/generator.dart";
|
|
||||||
import "package:shared/graph.dart";
|
|
||||||
|
|
||||||
import "package:shelf_router/shelf_router.dart";
|
|
||||||
import "package:shelf/shelf.dart";
|
|
||||||
import "package:shelf/shelf_io.dart" as io;
|
|
||||||
|
|
||||||
// const startLat = 42.0924949645996;
|
|
||||||
// const startLong = -75.9538421630859;
|
|
||||||
// const endLat = 42.0869369506836;
|
|
||||||
// const endLong = -75.965934753418;
|
|
||||||
|
|
||||||
const startLat = 42.092083;
|
|
||||||
const startLong = -75.952271;
|
|
||||||
const endLat = 42.080822;
|
|
||||||
const endLong = -75.912529;
|
|
||||||
|
|
||||||
const numStops = 5;
|
|
||||||
|
|
||||||
void log(String message) {
|
|
||||||
// print(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<Stop> findStopsNear(Coordinates location) {
|
|
||||||
log("Finding stops near $location...");
|
|
||||||
final stopDistances = [
|
|
||||||
for (final stop in StopState.stops.values)
|
|
||||||
(stop, stop.coordinates.distanceTo(location)),
|
|
||||||
];
|
|
||||||
stopDistances.sort((a, b) => a.$2.compareTo(b.$2));
|
|
||||||
return stopDistances.take(numStops).map((pair) => pair.$1);
|
|
||||||
}
|
|
||||||
|
|
||||||
void printStops(Iterable<Stop> stops) {
|
|
||||||
log("Found stops:");
|
|
||||||
for (final stop in stops) {
|
|
||||||
log("- ${stop.routes}, ${stop.name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<RouteID> findRoutes(Iterable<Stop> stops) => {
|
|
||||||
for (final stop in stops)
|
|
||||||
...stop.routeIDs,
|
|
||||||
};
|
|
||||||
|
|
||||||
Stop findStopWithRoute(Iterable<Stop> stops, RouteID route) =>
|
|
||||||
stops.firstWhere((stop) => stop.routeIDs.contains(route));
|
|
||||||
|
|
||||||
Future<void> init() async {
|
|
||||||
print("Initializing...");
|
|
||||||
final stops = await Generator.stops.parse();
|
|
||||||
final routes = await Generator.routes.parse();
|
|
||||||
StopState.init(routes, stops);
|
|
||||||
}
|
|
||||||
|
|
||||||
final headers = {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<Response> handleRequest(Request request) async {
|
|
||||||
final startLat = double.tryParse(request.url.queryParameters["start_lat"] ?? "");
|
|
||||||
final startLong = double.tryParse(request.url.queryParameters["start_lon"] ?? "");
|
|
||||||
final endLat = double.tryParse(request.url.queryParameters["end_lat"] ?? "");
|
|
||||||
final endLong = double.tryParse(request.url.queryParameters["end_lon"] ?? "");
|
|
||||||
if (startLat == null || startLong == null || endLat == null || endLong == null) {
|
|
||||||
return Response.badRequest(body: "Could not parse coordinates", headers: headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
final start = (lat: startLat, long: startLong);
|
|
||||||
final end = (lat: endLat, long: endLong);
|
|
||||||
|
|
||||||
// 1) Find [numStops] nearest stops by the given locations
|
|
||||||
log("Finding nearest stops...");
|
|
||||||
final startStops = findStopsNear(start);
|
|
||||||
printStops(startStops);
|
|
||||||
final endStops = findStopsNear(end);
|
|
||||||
printStops(endStops);
|
|
||||||
|
|
||||||
// 2) Find all routes by those stops
|
|
||||||
log("Finding intersecting routes");
|
|
||||||
final startRoutes = findRoutes(startStops);
|
|
||||||
final endRoutes = findRoutes(endStops);
|
|
||||||
log("Start routes: $startRoutes");
|
|
||||||
log("End routes: $endRoutes");
|
|
||||||
|
|
||||||
// 3) Check for a direct route without transfers
|
|
||||||
final routesInCommon = startRoutes.intersection(endRoutes);
|
|
||||||
if (routesInCommon.isNotEmpty) {
|
|
||||||
final route = routesInCommon.first;
|
|
||||||
final startStop = findStopWithRoute(startStops, route);
|
|
||||||
final endStop = findStopWithRoute(endStops, route);
|
|
||||||
return Response.ok("\nBoard the $route at ${startStop.name} and get off at ${endStop.name}", headers: headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Use A* to find a route with transfers
|
|
||||||
log("Using A*");
|
|
||||||
final paths = <Iterable<StopState>>[];
|
|
||||||
for (final startStop in startStops) {
|
|
||||||
for (final startRoute in startStop.routeIDs) {
|
|
||||||
for (final endStop in endStops) {
|
|
||||||
final state = StopState(stopID: startStop.id, goalID: endStop.id, routeID: startRoute, depth: 0);
|
|
||||||
log("Finding a route using ${state.hash()}");
|
|
||||||
final result = aStar(state);
|
|
||||||
if (result == null) continue;
|
|
||||||
paths.add(result.reconstructPath());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (paths.isEmpty) {
|
|
||||||
return Response.ok("No routes found", headers: headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
final minPath = paths.min((path) => path.length);
|
|
||||||
final buffer = StringBuffer();
|
|
||||||
buffer.writeln("Found ${paths.length} paths, here's the best one with only ${minPath.length} steps:");
|
|
||||||
StopState? prevStep;
|
|
||||||
var index = 0;
|
|
||||||
for (final step in minPath) {
|
|
||||||
index++;
|
|
||||||
final stop = StopState.stops[step.stopID]!;
|
|
||||||
final route = StopState.routes[step.routeID]!;
|
|
||||||
if (prevStep == null) {
|
|
||||||
buffer.writeln("$index. Board the ${route.provider.humanName} ${route.fullName} bus\n at ${stop.name}");
|
|
||||||
} else if (index == minPath.length) {
|
|
||||||
buffer.writeln("$index. Get off at ${stop.name}");
|
|
||||||
} else if (prevStep.routeID != step.routeID) {
|
|
||||||
buffer.writeln("$index. Transfer to the ${route.provider.humanName} ${route.fullName}");
|
|
||||||
} else {
|
|
||||||
buffer.writeln("$index. Go to ${stop.name}");
|
|
||||||
}
|
|
||||||
prevStep = step;
|
|
||||||
}
|
|
||||||
return Response.ok(buffer.toString(), headers: headers);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
await init();
|
|
||||||
final router = Router();
|
|
||||||
router.get("/path", handleRequest);
|
|
||||||
await io.serve(router.call, "0.0.0.0", 8001);
|
|
||||||
print("Listening on 0.0.0.0:8001");
|
|
||||||
}
|
|
25
src/shared/bin/server.dart
Normal file
25
src/shared/bin/server.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// User-facing script
|
||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import "package:shared/generator.dart";
|
||||||
|
import "package:shared/graph.dart";
|
||||||
|
import "package:shared/server.dart" as handlers;
|
||||||
|
|
||||||
|
import "package:shelf_router/shelf_router.dart";
|
||||||
|
import "package:shelf/shelf_io.dart" as io;
|
||||||
|
|
||||||
|
Future<void> init() async {
|
||||||
|
print("Initializing...");
|
||||||
|
StopState.stops = await Generator.stops.parse();
|
||||||
|
StopState.routes = await Generator.routes.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
await init();
|
||||||
|
final router = Router();
|
||||||
|
router.get("/path", handlers.getPath);
|
||||||
|
router.get("/stops", handlers.getStops);
|
||||||
|
router.get("/routes", handlers.getRoutes);
|
||||||
|
await io.serve(router.call, "0.0.0.0", 8001);
|
||||||
|
print("Listening on 0.0.0.0:8001");
|
||||||
|
}
|
18
src/shared/bin/test.dart
Normal file
18
src/shared/bin/test.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// User-facing script
|
||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
|
void main(List<String> args) async {
|
||||||
|
final provider = args.first;
|
||||||
|
final routeName = args.last;
|
||||||
|
final routes = await Generator.routes.parse();
|
||||||
|
final stops = await Generator.stops.parse();
|
||||||
|
final routeID = RouteID(Provider.fromJson(provider), routeName);
|
||||||
|
final route = routes[routeID]!;
|
||||||
|
print("Here are the stops for $route:");
|
||||||
|
for (final stopID in route.stops) {
|
||||||
|
final stop = stops[stopID]!;
|
||||||
|
print("- $stop");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
export "src/graph/state.dart";
|
export "src/graph/state.dart";
|
||||||
|
export "src/graph/algorithm.dart";
|
||||||
|
|
2
src/shared/lib/server.dart
Normal file
2
src/shared/lib/server.dart
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export "src/server/misc.dart";
|
||||||
|
export "src/server/path.dart";
|
|
@ -8,4 +8,7 @@ enum Provider {
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
final String humanName;
|
final String humanName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => humanName;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class Route with Encodable {
|
||||||
fullName = json["name"],
|
fullName = json["name"],
|
||||||
stops = [
|
stops = [
|
||||||
for (final stopID in (json["stops"] as List).cast<int>())
|
for (final stopID in (json["stops"] as List).cast<int>())
|
||||||
StopID(stopID.toString()),
|
StopID(Provider.occt, stopID.toString()),
|
||||||
];
|
];
|
||||||
|
|
||||||
Route.fromBcCsv(CsvRow csv) :
|
Route.fromBcCsv(CsvRow csv) :
|
||||||
|
@ -44,6 +44,19 @@ class Route with Encodable {
|
||||||
fullName = "${csv[2]}) ${csv[3]}",
|
fullName = "${csv[2]}) ${csv[3]}",
|
||||||
stops = [];
|
stops = [];
|
||||||
|
|
||||||
|
Route.fromJson(Json json) :
|
||||||
|
id = RouteID.fromJson(json["id"]),
|
||||||
|
provider = Provider.fromJson(json["provider"]),
|
||||||
|
fullName = json["full_name"],
|
||||||
|
shortName = json["short_name"],
|
||||||
|
stops = [
|
||||||
|
for (final stopID in json["stops"])
|
||||||
|
StopID.fromJson(stopID),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => "$provider $fullName";
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Json toJson() => {
|
Json toJson() => {
|
||||||
"id": id,
|
"id": id,
|
||||||
|
|
|
@ -2,7 +2,8 @@ import "utils.dart";
|
||||||
import "route.dart";
|
import "route.dart";
|
||||||
import "provider.dart";
|
import "provider.dart";
|
||||||
|
|
||||||
extension type StopID(String id) {
|
extension type StopID._(String id) {
|
||||||
|
StopID(Provider provider, Object value) : id = "${provider.id}_$value";
|
||||||
StopID.fromJson(dynamic value) : id = value.toString();
|
StopID.fromJson(dynamic value) : id = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +25,7 @@ class Stop with Encodable {
|
||||||
}) : routes = {}, routeIDs = {};
|
}) : routes = {}, routeIDs = {};
|
||||||
|
|
||||||
Stop.fromJson(Json json) :
|
Stop.fromJson(Json json) :
|
||||||
id = StopID(json["id"]),
|
id = StopID.fromJson(json["id"]),
|
||||||
name = json["name"],
|
name = json["name"],
|
||||||
description = json["description"],
|
description = json["description"],
|
||||||
coordinates = (lat: json["latitude"], long: json["longitude"]),
|
coordinates = (lat: json["latitude"], long: json["longitude"]),
|
||||||
|
@ -36,7 +37,7 @@ class Stop with Encodable {
|
||||||
};
|
};
|
||||||
|
|
||||||
Stop.fromOcctJson(Json json) :
|
Stop.fromOcctJson(Json json) :
|
||||||
id = StopID.fromJson(json["id"]),
|
id = StopID(Provider.occt, json["id"]),
|
||||||
name = json["name"],
|
name = json["name"],
|
||||||
description = null,
|
description = null,
|
||||||
coordinates = (lat: json["lat"], long: json["lng"]),
|
coordinates = (lat: json["lat"], long: json["lng"]),
|
||||||
|
@ -44,6 +45,9 @@ class Stop with Encodable {
|
||||||
routes = <String>{},
|
routes = <String>{},
|
||||||
routeIDs = <RouteID>{};
|
routeIDs = <RouteID>{};
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => name;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id.id,
|
"id": id.id,
|
||||||
|
|
|
@ -35,16 +35,27 @@ extension ListUtils<E> on List<E> {
|
||||||
yield (i, this[i]);
|
yield (i, this[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
E max(int Function(E) count) => reduce((a, b) {
|
extension IterableUtils<E> on Iterable<E> {
|
||||||
|
E max(num Function(E) count) => reduce((a, b) {
|
||||||
final numA = count(a);
|
final numA = count(a);
|
||||||
final numB = count(b);
|
final numB = count(b);
|
||||||
return numA > numB ? a : b;
|
return numA > numB ? a : b;
|
||||||
});
|
});
|
||||||
|
|
||||||
E min(int Function(E) count) => reduce((a, b) {
|
E min(num Function(E) count) => reduce((a, b) {
|
||||||
final numA = count(a);
|
final numA = count(a);
|
||||||
final numB = count(b);
|
final numB = count(b);
|
||||||
return numA < numB ? a : b;
|
return numA < numB ? a : b;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
double sum(double Function(E) transform) => fold(
|
||||||
|
0,
|
||||||
|
(total, element) => total + transform(element),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void log(String message) {
|
||||||
|
// print(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import "dart:io";
|
||||||
|
|
||||||
import "package:shared/generator.dart";
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
class Generator<T extends Encodable> extends Parser<T> {
|
class Generator<ID, T extends Encodable> extends Parser<ID, T> {
|
||||||
final Parser<T> bc;
|
final Parser<ID, T> bc;
|
||||||
final Parser<T> occt;
|
final Parser<ID, T> occt;
|
||||||
final File outputFile;
|
final File outputFile;
|
||||||
|
|
||||||
Generator({
|
Generator({
|
||||||
|
@ -15,14 +15,15 @@ class Generator<T extends Encodable> extends Parser<T> {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<T>> parse() async => [
|
Future<Map<ID, T>> parse() async => {
|
||||||
...await bc.parse(),
|
...await bc.parse(),
|
||||||
...await occt.parse(),
|
...await occt.parse(),
|
||||||
];
|
};
|
||||||
|
|
||||||
Future<void> generate() async {
|
Future<void> generate() async {
|
||||||
|
final allData = await parse();
|
||||||
final result = [
|
final result = [
|
||||||
for (final data in await parse())
|
for (final data in allData.values)
|
||||||
data.toJson(),
|
data.toJson(),
|
||||||
];
|
];
|
||||||
const encoder = JsonEncoder.withIndent(" ");
|
const encoder = JsonEncoder.withIndent(" ");
|
||||||
|
|
|
@ -5,7 +5,7 @@ import "stops_bc.dart";
|
||||||
|
|
||||||
typedef InOutTrips = (TripID, TripID);
|
typedef InOutTrips = (TripID, TripID);
|
||||||
|
|
||||||
class BcRoutesParser extends Parser<Route> {
|
class BcRoutesParser extends Parser<RouteID, Route> {
|
||||||
final routesFile = File(bcDataDir / "routes.txt");
|
final routesFile = File(bcDataDir / "routes.txt");
|
||||||
final tripsFile = File(bcDataDir / "trips.txt");
|
final tripsFile = File(bcDataDir / "trips.txt");
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class BcRoutesParser extends Parser<Route> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Iterable<Route>> parse() async {
|
Future<Map<RouteID, Route>> parse() async {
|
||||||
final routes = await getRoutes();
|
final routes = await getRoutes();
|
||||||
final trips = await stopParser.getTrips();
|
final trips = await stopParser.getTrips();
|
||||||
final tripsForRoutes = await getTripForRoutes(trips);
|
final tripsForRoutes = await getTripForRoutes(trips);
|
||||||
|
@ -39,8 +39,9 @@ class BcRoutesParser extends Parser<Route> {
|
||||||
for (final (routeID, route) in routes.records) {
|
for (final (routeID, route) in routes.records) {
|
||||||
final longestTrip = tripsForRoutes[routeID]!;
|
final longestTrip = tripsForRoutes[routeID]!;
|
||||||
final stops = trips[longestTrip]!;
|
final stops = trips[longestTrip]!;
|
||||||
|
log("Chose $longestTrip as the longest trip for $routeID");
|
||||||
route.stops.addAll(stops);
|
route.stops.addAll(stops);
|
||||||
}
|
}
|
||||||
return routes.values;
|
return routes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,12 @@ import "dart:io";
|
||||||
|
|
||||||
import "utils.dart";
|
import "utils.dart";
|
||||||
|
|
||||||
class OcctRoutesParser extends Parser<Route> {
|
class OcctRoutesParser extends Parser<RouteID, Route> {
|
||||||
final routesFile = File(occtDataDir / "routes.json");
|
final routesFile = File(occtDataDir / "routes.json");
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Route>> parse() async => [
|
Future<Map<RouteID, Route>> parse() async => {
|
||||||
for (final routeJson in await readJson(routesFile))
|
for (final routeJson in await readJson(routesFile))
|
||||||
Route.fromOcctJson(routeJson),
|
RouteID(Provider.occt, routeJson["id"]): Route.fromOcctJson(routeJson),
|
||||||
];
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import "package:csv/csv.dart";
|
||||||
|
|
||||||
import "utils.dart";
|
import "utils.dart";
|
||||||
|
|
||||||
class BcStopParser extends Parser<Stop> {
|
class BcStopParser extends Parser<StopID, Stop> {
|
||||||
static final tripsFile = File(bcDataDir / "stop_times.txt");
|
static final tripsFile = File(bcDataDir / "stop_times.txt");
|
||||||
static final routesFile = File(bcDataDir / "trips.txt");
|
static final routesFile = File(bcDataDir / "trips.txt");
|
||||||
static final stopsFile = File(bcDataDir / "stops.txt");
|
static final stopsFile = File(bcDataDir / "stops.txt");
|
||||||
|
@ -15,7 +15,7 @@ class BcStopParser extends Parser<Stop> {
|
||||||
Future<Map<TripID, List<StopID>>> getTrips() async {
|
Future<Map<TripID, List<StopID>>> getTrips() async {
|
||||||
final result = <TripID, List<StopID>>{};
|
final result = <TripID, List<StopID>>{};
|
||||||
for (final row in await readCsv(tripsFile)) {
|
for (final row in await readCsv(tripsFile)) {
|
||||||
result.addToList(TripID(row[0]), StopID(row[3]));
|
result.addToList(TripID(row[0]), StopID(Provider.bc, row[3]));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class BcStopParser extends Parser<Stop> {
|
||||||
Future<Map<StopID, Stop>> getStops() async {
|
Future<Map<StopID, Stop>> getStops() async {
|
||||||
final result = <StopID, Stop>{};
|
final result = <StopID, Stop>{};
|
||||||
for (final row in await readCsv(stopsFile)) {
|
for (final row in await readCsv(stopsFile)) {
|
||||||
final stopID = StopID(row[0]);
|
final stopID = StopID(Provider.bc, row[0]);
|
||||||
final name = row[2];
|
final name = row[2];
|
||||||
final description = row[3];
|
final description = row[3];
|
||||||
final latitude = double.parse(row[4]);
|
final latitude = double.parse(row[4]);
|
||||||
|
@ -69,12 +69,12 @@ class BcStopParser extends Parser<Stop> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Iterable<Stop>> parse() async {
|
Future<Map<StopID, Stop>> parse() async {
|
||||||
final trips = await getTrips();
|
final trips = await getTrips();
|
||||||
final routes = await getRoutes();
|
final routes = await getRoutes();
|
||||||
final stops = await getStops();
|
final stops = await getStops();
|
||||||
final routeNames = await getRouteNames();
|
final routeNames = await getRouteNames();
|
||||||
findRoutesForStops(stops: stops.values, trips: trips, routes: routes, routeNames: routeNames);
|
findRoutesForStops(stops: stops.values, trips: trips, routes: routes, routeNames: routeNames);
|
||||||
return stops.values;
|
return stops;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import "dart:io";
|
||||||
|
|
||||||
import "utils.dart";
|
import "utils.dart";
|
||||||
|
|
||||||
class OcctStopParser extends Parser<Stop> {
|
class OcctStopParser extends Parser<StopID, Stop> {
|
||||||
/// Taken from: https://binghamtonupublic.etaspot.net/service.php?service=get_stops&token=TESTING
|
/// Taken from: https://binghamtonupublic.etaspot.net/service.php?service=get_stops&token=TESTING
|
||||||
static final stopsFile = File(occtDataDir / "stops.json");
|
static final stopsFile = File(occtDataDir / "stops.json");
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
Future<Map<StopID, Stop>> getStops() async {
|
Future<Map<StopID, Stop>> getStops() async {
|
||||||
final result = <StopID, Stop>{};
|
final result = <StopID, Stop>{};
|
||||||
for (final stopJson in await readJson(stopsFile)) {
|
for (final stopJson in await readJson(stopsFile)) {
|
||||||
final id = StopID.fromJson(stopJson["id"]);
|
final id = StopID(Provider.occt, stopJson["id"]);
|
||||||
result[id] ??= Stop.fromOcctJson(stopJson);
|
result[id] ??= Stop.fromOcctJson(stopJson);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -21,7 +21,7 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
Future<Map<StopID, List<RouteID>>> getRoutes() async {
|
Future<Map<StopID, List<RouteID>>> getRoutes() async {
|
||||||
final result = <StopID, List<RouteID>>{};
|
final result = <StopID, List<RouteID>>{};
|
||||||
for (final json in await readJson(stopsFile)) {
|
for (final json in await readJson(stopsFile)) {
|
||||||
final stopID = StopID.fromJson(json["id"]);
|
final stopID = StopID(Provider.occt, json["id"]);
|
||||||
final routeID = RouteID(Provider.occt, json["rid"]);
|
final routeID = RouteID(Provider.occt, json["rid"]);
|
||||||
result.addToList(stopID, routeID);
|
result.addToList(stopID, routeID);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Iterable<Stop>> parse() async {
|
Future<Map<StopID, Stop>> parse() async {
|
||||||
final stops = await getStops();
|
final stops = await getStops();
|
||||||
final routes = await getRoutes();
|
final routes = await getRoutes();
|
||||||
final routeNames = await getRouteNames();
|
final routeNames = await getRouteNames();
|
||||||
|
@ -46,6 +46,6 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
stop.addRoute(routeID, routeName);
|
stop.addRoute(routeID, routeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stops.values;
|
return stops;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ extension MapSetUtils<K, V> on Map<K, Set<V>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Parser<T> {
|
abstract class Parser<ID, T> {
|
||||||
Future<Iterable<T>> parse();
|
Future<Map<ID, T>> parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
final csvConverter = CsvCodec(shouldParseNumbers: false).decoder;
|
final csvConverter = CsvCodec(shouldParseNumbers: false).decoder;
|
||||||
|
|
60
src/shared/lib/src/graph/algorithm.dart
Normal file
60
src/shared/lib/src/graph/algorithm.dart
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import "package:a_star/a_star.dart";
|
||||||
|
import "package:shared/data.dart";
|
||||||
|
|
||||||
|
import "state.dart";
|
||||||
|
|
||||||
|
void printStops(Iterable<Stop> stops) {
|
||||||
|
log("Found stops:");
|
||||||
|
for (final stop in stops) {
|
||||||
|
log("- ${stop.routes}, ${stop.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<StopState>? findPath(Coordinates start, Coordinates end) {
|
||||||
|
// 1) Find [numStops] nearest stops by the given locations
|
||||||
|
log("Finding nearest stops...");
|
||||||
|
final startStops = StopState.findStopsNear(start);
|
||||||
|
printStops(startStops);
|
||||||
|
final endStops = StopState.findStopsNear(end);
|
||||||
|
printStops(endStops);
|
||||||
|
|
||||||
|
// 2) Use A* to find a route with transfers
|
||||||
|
log("Using A*");
|
||||||
|
final paths = <Iterable<StopState>>[];
|
||||||
|
for (final startStop in startStops) {
|
||||||
|
for (final startRouteID in startStop.routeIDs) {
|
||||||
|
for (final endStop in endStops) {
|
||||||
|
final state = StopState.start(
|
||||||
|
stopID: startStop.id,
|
||||||
|
goalID: endStop.id,
|
||||||
|
routeID: startRouteID,
|
||||||
|
startPoint: start,
|
||||||
|
endPoint: end,
|
||||||
|
);
|
||||||
|
log("Finding a route using ${state.hash()}");
|
||||||
|
final result = aStar(state, limit: 10000);
|
||||||
|
if (result == null) continue;
|
||||||
|
paths.add(result.reconstructPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paths.isEmpty) return null;
|
||||||
|
return paths.min(getTotalDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
double getTotalDistance(Iterable<StopState> path) =>
|
||||||
|
path.sum((step) => step.distanceWalked);
|
||||||
|
|
||||||
|
void explainPath(Iterable<StopState> path, void Function(String) printer) {
|
||||||
|
var index = 0;
|
||||||
|
for (final step in path) {
|
||||||
|
index++;
|
||||||
|
final stop = StopState.stops[step.stopID]!;
|
||||||
|
if (index == path.length) {
|
||||||
|
printer("$index. Get off at ${stop.name}");
|
||||||
|
} else {
|
||||||
|
printer("$index. ${step.explanation}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,33 +1,81 @@
|
||||||
import "package:a_star/a_star.dart";
|
import "package:a_star/a_star.dart";
|
||||||
import "package:shared/data.dart";
|
import "package:shared/data.dart";
|
||||||
|
|
||||||
class StopState extends AStarState<StopState> {
|
enum SearchMethod {
|
||||||
|
start,
|
||||||
|
bus,
|
||||||
|
transfer,
|
||||||
|
walk;
|
||||||
|
|
||||||
|
factory SearchMethod.fromJson(String json) => values.byName(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StopState extends AStarState<StopState> with Encodable {
|
||||||
static late final Map<RouteID, Route> routes;
|
static late final Map<RouteID, Route> routes;
|
||||||
static late final Map<StopID, Stop> stops;
|
static late final Map<StopID, Stop> stops;
|
||||||
|
|
||||||
static void init(Iterable<Route> allRoutes, Iterable<Stop> allStops) {
|
static Iterable<Stop> findStopsNear(Coordinates location) {
|
||||||
routes = {
|
const numStops = 5;
|
||||||
for (final route in allRoutes)
|
log("Finding stops near $location...");
|
||||||
route.id: route,
|
final stopDistances = [
|
||||||
};
|
for (final stop in stops.values)
|
||||||
stops = {
|
(stop, stop.coordinates.distanceTo(location)),
|
||||||
for (final stop in allStops)
|
];
|
||||||
stop.id: stop,
|
stopDistances.sort((a, b) => a.$2.compareTo(b.$2));
|
||||||
};
|
return stopDistances.take(numStops).map((pair) => pair.$1);
|
||||||
}
|
}
|
||||||
|
|
||||||
final StopID stopID;
|
final StopID stopID;
|
||||||
final StopID goalID;
|
final StopID goalID;
|
||||||
final RouteID routeID;
|
final RouteID routeID;
|
||||||
|
final double distanceWalked;
|
||||||
|
final SearchMethod method;
|
||||||
StopState({
|
StopState({
|
||||||
required this.stopID,
|
required this.stopID,
|
||||||
required this.goalID,
|
required this.goalID,
|
||||||
required this.routeID,
|
required this.routeID,
|
||||||
|
required this.distanceWalked,
|
||||||
|
required this.method,
|
||||||
required super.depth,
|
required super.depth,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
factory StopState.start({
|
||||||
|
required StopID stopID,
|
||||||
|
required StopID goalID,
|
||||||
|
required RouteID routeID,
|
||||||
|
required Coordinates startPoint,
|
||||||
|
required Coordinates endPoint,
|
||||||
|
}) {
|
||||||
|
final stop = stops[stopID]!;
|
||||||
|
final goal = stops[goalID]!;
|
||||||
|
final distanceToStart = startPoint.distanceTo(stop.coordinates);
|
||||||
|
final distanceToEnd = endPoint.distanceTo(goal.coordinates);
|
||||||
|
return StopState(
|
||||||
|
stopID: stopID,
|
||||||
|
goalID: goalID,
|
||||||
|
routeID: routeID,
|
||||||
|
distanceWalked: distanceToStart + distanceToEnd,
|
||||||
|
depth: 0,
|
||||||
|
method: SearchMethod.start,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory StopState.fromJson(Json json) => StopState(
|
||||||
|
stopID: StopID.fromJson(json["stop_id"]),
|
||||||
|
routeID: RouteID.fromJson(json["route_id"]),
|
||||||
|
method: SearchMethod.fromJson(json["method"]),
|
||||||
|
depth: 0,
|
||||||
|
distanceWalked: 0,
|
||||||
|
goalID: StopID.fromJson("N/A"),
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String hash() => "$stopID-$goalID-$routeID";
|
String hash() {
|
||||||
|
final stop = stops[stopID];
|
||||||
|
final goal = stops[goalID];
|
||||||
|
final route = routes[routeID];
|
||||||
|
return "${method.name}-$stop-$goal-$route";
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isGoal() => stopID == goalID;
|
bool isGoal() => stopID == goalID;
|
||||||
|
@ -44,30 +92,86 @@ class StopState extends AStarState<StopState> {
|
||||||
final route = routes[routeID]!;
|
final route = routes[routeID]!;
|
||||||
final stop = stops[stopID]!;
|
final stop = stops[stopID]!;
|
||||||
final stopIndex = route.stops.indexOf(stopID);
|
final stopIndex = route.stops.indexOf(stopID);
|
||||||
|
if (stopIndex == -1) return [];
|
||||||
final result = <StopState>[];
|
final result = <StopState>[];
|
||||||
|
|
||||||
// 1): Go forward one stop on the same route
|
// 1): Go forward one stop on the same route
|
||||||
final nextIndex = stopIndex + 1;
|
final nextIndex = stopIndex + 1;
|
||||||
if (nextIndex < route.stops.length) {
|
if (nextIndex < route.stops.length) {
|
||||||
final nextStop = route.stops[nextIndex];
|
final nextStopID = route.stops[nextIndex];
|
||||||
final state = StopState(stopID: nextStop, goalID: goalID, routeID: routeID, depth: depth + 1);
|
final state = StopState(
|
||||||
|
stopID: nextStopID,
|
||||||
|
goalID: goalID,
|
||||||
|
routeID: routeID,
|
||||||
|
depth: depth + 1,
|
||||||
|
distanceWalked: distanceWalked,
|
||||||
|
method: SearchMethod.bus,
|
||||||
|
);
|
||||||
result.add(state);
|
result.add(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Go back one stop on the same route
|
// 2) Go back one stop on the same route
|
||||||
final prevIndex = stopIndex - 1;
|
final prevIndex = stopIndex - 1;
|
||||||
if (prevIndex > 0) {
|
if (prevIndex > 0) {
|
||||||
final prevStop = route.stops[prevIndex];
|
final prevStopID = route.stops[prevIndex];
|
||||||
final state = StopState(stopID: prevStop, goalID: goalID, routeID: routeID, depth: depth + 1);
|
final state = StopState(
|
||||||
|
stopID: prevStopID,
|
||||||
|
goalID: goalID,
|
||||||
|
routeID: routeID,
|
||||||
|
depth: depth + 1,
|
||||||
|
distanceWalked: distanceWalked,
|
||||||
|
method: SearchMethod.bus,
|
||||||
|
);
|
||||||
result.add(state);
|
result.add(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Make any available transfers
|
// 3) Make any available transfers
|
||||||
for (final otherRoute in stop.routeIDs.difference({routeID})) {
|
for (final otherRouteID in stop.routeIDs.difference({routeID})) {
|
||||||
final state = StopState(stopID: stopID, goalID: goalID, routeID: otherRoute, depth: depth + 1);
|
final state = StopState(
|
||||||
|
stopID: stopID,
|
||||||
|
goalID: goalID,
|
||||||
|
routeID: otherRouteID,
|
||||||
|
depth: depth + 2,
|
||||||
|
distanceWalked: distanceWalked,
|
||||||
|
method: SearchMethod.transfer,
|
||||||
|
);
|
||||||
result.add(state);
|
result.add(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) Walk to a new stop
|
||||||
|
for (final otherStop in findStopsNear(stop.coordinates)) {
|
||||||
|
final distanceToOther = stop.coordinates.distanceTo(otherStop.coordinates);
|
||||||
|
for (final otherRouteID in otherStop.routeIDs) {
|
||||||
|
final state = StopState(
|
||||||
|
stopID: otherStop.id,
|
||||||
|
goalID: goalID,
|
||||||
|
routeID: otherRouteID,
|
||||||
|
depth: depth + 3,
|
||||||
|
distanceWalked: distanceWalked + distanceToOther,
|
||||||
|
method: SearchMethod.walk,
|
||||||
|
);
|
||||||
|
result.add(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get explanation {
|
||||||
|
final route = routes[routeID]!;
|
||||||
|
final stop = stops[stopID]!;
|
||||||
|
return switch (method) {
|
||||||
|
SearchMethod.bus => "Go one stop to $stop",
|
||||||
|
SearchMethod.start => "Walk to $stop\n and board the $route line",
|
||||||
|
SearchMethod.transfer => "Transfer to the $route line",
|
||||||
|
SearchMethod.walk => "Walk to $stop\n and board the $route line"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Json toJson() => {
|
||||||
|
"stop_id": stopID,
|
||||||
|
"route_id": routeID,
|
||||||
|
"method": method.name,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
16
src/shared/lib/src/server/misc.dart
Normal file
16
src/shared/lib/src/server/misc.dart
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import "package:shared/graph.dart";
|
||||||
|
import "package:shelf/shelf.dart";
|
||||||
|
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
Response getStops(Request _) {
|
||||||
|
final stops = StopState.stops.values;
|
||||||
|
final jsonBody = encodeJsonList(stops);
|
||||||
|
return Response.ok(jsonBody, headers: corsHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response getRoutes(Request _) {
|
||||||
|
final routes = StopState.routes.values.where((route) => !route.fullName.startsWith(")"));
|
||||||
|
final jsonBody = encodeJsonList(routes);
|
||||||
|
return Response.ok(jsonBody, headers: corsHeaders);
|
||||||
|
}
|
24
src/shared/lib/src/server/path.dart
Normal file
24
src/shared/lib/src/server/path.dart
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import "package:shared/graph.dart";
|
||||||
|
import "package:shelf/shelf.dart";
|
||||||
|
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
Response getPath(Request request) {
|
||||||
|
final startLat = double.tryParse(request.url.queryParameters["start_lat"] ?? "");
|
||||||
|
final startLong = double.tryParse(request.url.queryParameters["start_lon"] ?? "");
|
||||||
|
final endLat = double.tryParse(request.url.queryParameters["end_lat"] ?? "");
|
||||||
|
final endLong = double.tryParse(request.url.queryParameters["end_lon"] ?? "");
|
||||||
|
if (startLat == null || startLong == null || endLat == null || endLong == null) {
|
||||||
|
return Response.badRequest(body: "Could not parse coordinates", headers: corsHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 buffer = StringBuffer();
|
||||||
|
explainPath(path, buffer.writeln);
|
||||||
|
return Response.ok(buffer.toString(), headers: corsHeaders);
|
||||||
|
}
|
15
src/shared/lib/src/server/utils.dart
Normal file
15
src/shared/lib/src/server/utils.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import "dart:convert";
|
||||||
|
|
||||||
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
};
|
||||||
|
|
||||||
|
String encodeJsonList<T extends Encodable>(Iterable<T> data) {
|
||||||
|
final jsonList = [
|
||||||
|
for (final object in data)
|
||||||
|
object.toJson(),
|
||||||
|
];
|
||||||
|
return jsonEncode(jsonList);
|
||||||
|
}
|
Loading…
Reference in a new issue