Working "find a path" code (#13)
The client first requests stops from the TS server (`0.0.0.0:80`), then the path from the Dart server (`localhost:8001`). It will be trivial to move the TS logic to the Dart server since the Dart server is the one that parsed all that data, and it's anyway in a `JSON` file. There is a `server/GET_ROUTES.json` file, but it's not needed. I just included it for easier debugging. `server/GET_STOPS.json` includes both the stops and the routes, and the client does some (inefficient) work to re-organize it. The Dart server uses a basic A* implementation to sort find direct routes and routes with transfers. The A* logic is in `src/shared/lib/src/graph/graph.dart`. This file defines the state itself, and `package:a_star` on Pub.dev (mine) defines the algorithm. `src/shared/bin/path.dart` is the whole of the Dart server. TODO: - add unit tests - this PR breaks almost nothing, so there is duplicate work being done on the client and some unused logic - return a structured path object for the client to use as it wants. This PR just sends a well-formatted string
This commit is contained in:
parent
5e24c8ad30
commit
d0387c7aa2
33 changed files with 5921 additions and 1420 deletions
2
src/.gitignore
vendored
2
src/.gitignore
vendored
|
@ -1 +1,3 @@
|
||||||
*.env
|
*.env
|
||||||
|
pubspec.lock
|
||||||
|
.dart_tool
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
|
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
|
||||||
|
|
||||||
import "package:client/data.dart";
|
import "package:client/data.dart";
|
||||||
|
import "package:http/http.dart" as http;
|
||||||
|
|
||||||
import "api_client.dart";
|
import "api_client.dart";
|
||||||
import "service.dart";
|
import "service.dart";
|
||||||
|
@ -41,6 +42,28 @@ class ApiService extends Service {
|
||||||
PathStep.fromJson,
|
PathStep.fromJson,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Future<String?> getPath2({
|
||||||
|
required Coordinates start,
|
||||||
|
required Coordinates end,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse("http://localhost:8001/path").replace(
|
||||||
|
queryParameters: {
|
||||||
|
"start_lat": start.lat.toString(),
|
||||||
|
"start_lon": start.long.toString(),
|
||||||
|
"end_lat": end.lat.toString(),
|
||||||
|
"end_lon": end.long.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
final response = await http.get(uri);
|
||||||
|
return response.body;
|
||||||
|
// Any error should show null
|
||||||
|
// ignore: avoid_catches_without_on_clauses
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Stop>?> getStops() => _client.getJsonList(
|
Future<List<Stop>?> getStops() => _client.getJsonList(
|
||||||
_base.resolve("stops"),
|
_base.resolve("stops"),
|
||||||
Stop.fromJson,
|
Stop.fromJson,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
|
||||||
import "package:client/data.dart";
|
import "package:client/data.dart";
|
||||||
|
import "package:flutter/foundation.dart";
|
||||||
import "package:http/http.dart" as http;
|
import "package:http/http.dart" as http;
|
||||||
|
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
|
@ -19,9 +20,10 @@ class ApiClient {
|
||||||
for (final json in listOfJson)
|
for (final json in listOfJson)
|
||||||
fromJson(json),
|
fromJson(json),
|
||||||
];
|
];
|
||||||
|
// Want to catch all errors for the UI
|
||||||
|
// ignore: avoid_catches_without_on_clauses
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
// ignore: avoid_print
|
debugPrint("Error: $error\n$stackTrace");
|
||||||
print("Error: $error\n$stackTrace");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,7 @@ class HomeModel extends ViewModel with HomeMarkers {
|
||||||
super.updateEnd(coordinates, marker);
|
super.updateEnd(coordinates, marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String? pathText;
|
||||||
Future<void> search() async {
|
Future<void> search() async {
|
||||||
final start = (lat: startLatitude, long: startLongitude);
|
final start = (lat: startLatitude, long: startLongitude);
|
||||||
final end = (lat: endLatitude, long: endLongitude);
|
final end = (lat: endLatitude, long: endLongitude);
|
||||||
|
@ -71,16 +72,17 @@ 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.getPath(start: start as Coordinates, end: end as Coordinates);
|
final result = await services.api.getPath2(start: start as Coordinates, end: end as Coordinates);
|
||||||
path = result;
|
pathText = result ?? "An error occurred";
|
||||||
|
// path = result;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
if (result == null) return;
|
if (result == null) return;
|
||||||
isSearching = false;
|
isSearching = false;
|
||||||
routes = [
|
// routes = [
|
||||||
for (final step in result) [
|
// for (final step in result) [
|
||||||
...decodePolyline(step.trip.polyline),
|
// ...decodePolyline(step.trip.polyline),
|
||||||
],
|
// ],
|
||||||
];
|
// ];
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,12 +78,11 @@ mixin HomeMarkers on ChangeNotifier {
|
||||||
stopCounts[route] ??= 0;
|
stopCounts[route] ??= 0;
|
||||||
stopCounts[route] = stopCounts[route]! + 1;
|
stopCounts[route] = stopCounts[route]! + 1;
|
||||||
}
|
}
|
||||||
|
final namesList = switch (stop.provider) {
|
||||||
if (stop.provider.contains("OCCT")) {
|
Provider.occt => occtRouteNames,
|
||||||
occtRouteNames.addAll(stop.routes);
|
Provider.bc => bcRouteNames,
|
||||||
} else {
|
};
|
||||||
bcRouteNames.addAll(stop.routes);
|
namesList.addAll(stop.routes);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,12 @@ class Sidebar extends ReusableReactiveWidget<HomeModel> {
|
||||||
Tab(text: provider),
|
Tab(text: provider),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Expanded(
|
if (!model.shouldShowMarkers && model.pathText != null)
|
||||||
child: !model.shouldShowMarkers
|
Center(child: Text(model.pathText!, style: context.textTheme.bodySmall)),
|
||||||
? const Center(child: Text("Choose start or end location"))
|
if (!model.shouldShowMarkers && model.pathText == null)
|
||||||
: TabBarView(
|
const Center(child: Text("Choose start or end location")),
|
||||||
|
if (model.shouldShowMarkers) Expanded(
|
||||||
|
child: TabBarView(
|
||||||
children: [
|
children: [
|
||||||
for (final (_, routesList) in model.providers)
|
for (final (_, routesList) in model.providers)
|
||||||
ListView(
|
ListView(
|
||||||
|
|
|
@ -1,433 +0,0 @@
|
||||||
# Generated by pub
|
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
|
||||||
packages:
|
|
||||||
args:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: args
|
|
||||||
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.7.0"
|
|
||||||
async:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: async
|
|
||||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.12.0"
|
|
||||||
boolean_selector:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: boolean_selector
|
|
||||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.2"
|
|
||||||
build_cli_annotations:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: build_cli_annotations
|
|
||||||
sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
characters:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: characters
|
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
clock:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: clock
|
|
||||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.2"
|
|
||||||
collection:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: collection
|
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.19.1"
|
|
||||||
convert:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: convert
|
|
||||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.1.2"
|
|
||||||
csslib:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: csslib
|
|
||||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
csv:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: csv
|
|
||||||
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.0.0"
|
|
||||||
dhttpd:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: dhttpd
|
|
||||||
sha256: "2e24765d7569b8e0a02a441e3cf96f09cca69dfecba646e7e9f6b3ab45a2f3fe"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
fake_async:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: fake_async
|
|
||||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.2"
|
|
||||||
fixnum:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: fixnum
|
|
||||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_plugin_android_lifecycle:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: flutter_plugin_android_lifecycle
|
|
||||||
sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.27"
|
|
||||||
flutter_test:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
flutter_web_plugins:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
go_router:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: go_router
|
|
||||||
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "14.8.1"
|
|
||||||
google_maps:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_maps
|
|
||||||
sha256: "4d6e199c561ca06792c964fa24b2bac7197bf4b401c2e1d23e345e5f9939f531"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "8.1.1"
|
|
||||||
google_maps_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: google_maps_flutter
|
|
||||||
sha256: b42ff7f3875a5eedbe388d883100561b85c62beed1c39ad66dd60537c75bb424
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.12.0"
|
|
||||||
google_maps_flutter_android:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_maps_flutter_android
|
|
||||||
sha256: "0ede4ae8326335c0c007c8c7a8c9737449263123385e2bdf49f3e71103b2dc2e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.16.0"
|
|
||||||
google_maps_flutter_ios:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_maps_flutter_ios
|
|
||||||
sha256: ef72c822930ce69515cb91c10cd88cfb8b26296f765808a43cbc9a10eaffacfe
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.15.0"
|
|
||||||
google_maps_flutter_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_maps_flutter_platform_interface
|
|
||||||
sha256: "970c8f766c02909c7be282dea923c971f83a88adaf07f8871d0aacebc3b07bb2"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.11.1"
|
|
||||||
google_maps_flutter_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: google_maps_flutter_web
|
|
||||||
sha256: a45786ea6691cc7cdbe2cf3ce2c2daf4f82a885745666b4a36baada3a4e12897
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.5.12"
|
|
||||||
html:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: html
|
|
||||||
sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.15.5"
|
|
||||||
http:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: http
|
|
||||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
http_parser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: http_parser
|
|
||||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.2"
|
|
||||||
leak_tracker:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker
|
|
||||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "10.0.8"
|
|
||||||
leak_tracker_flutter_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_flutter_testing
|
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.9"
|
|
||||||
leak_tracker_testing:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: leak_tracker_testing
|
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "3.0.1"
|
|
||||||
logging:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: logging
|
|
||||||
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.3.0"
|
|
||||||
matcher:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: matcher
|
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.12.17"
|
|
||||||
material_color_utilities:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: material_color_utilities
|
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.11.1"
|
|
||||||
meta:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: meta
|
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.16.0"
|
|
||||||
mime:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: mime
|
|
||||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.0"
|
|
||||||
path:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: path
|
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.9.1"
|
|
||||||
plugin_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: plugin_platform_interface
|
|
||||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.8"
|
|
||||||
polyline_tools:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: polyline_tools
|
|
||||||
sha256: "8c523335bb8d16fb0c7835916ed94d97d9a063a4386e9193bd6e8fe233591df9"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.0.2"
|
|
||||||
sanitize_html:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sanitize_html
|
|
||||||
sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
shared:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: "../shared"
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "1.0.0"
|
|
||||||
shelf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf
|
|
||||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.2"
|
|
||||||
shelf_static:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: shelf_static
|
|
||||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.3"
|
|
||||||
sky_engine:
|
|
||||||
dependency: transitive
|
|
||||||
description: flutter
|
|
||||||
source: sdk
|
|
||||||
version: "0.0.0"
|
|
||||||
source_span:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: source_span
|
|
||||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.10.1"
|
|
||||||
stack_trace:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stack_trace
|
|
||||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.12.1"
|
|
||||||
stream_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_channel
|
|
||||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
stream_transform:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_transform
|
|
||||||
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.1"
|
|
||||||
string_scanner:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: string_scanner
|
|
||||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.1"
|
|
||||||
term_glyph:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: term_glyph
|
|
||||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.2.2"
|
|
||||||
test_api:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: test_api
|
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.7.4"
|
|
||||||
typed_data:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: typed_data
|
|
||||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
vector_math:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vector_math
|
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.4"
|
|
||||||
very_good_analysis:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: very_good_analysis
|
|
||||||
sha256: "1fb637c0022034b1f19ea2acb42a3603cbd8314a470646a59a2fb01f5f3a8629"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.0.0"
|
|
||||||
vm_service:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: vm_service
|
|
||||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "14.3.1"
|
|
||||||
web:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: web
|
|
||||||
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.1.1"
|
|
||||||
sdks:
|
|
||||||
dart: ">=3.7.0 <4.0.0"
|
|
||||||
flutter: ">=3.27.0"
|
|
|
@ -6,6 +6,7 @@ version: 0.1.0+1
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.5.0
|
sdk: ^3.5.0
|
||||||
|
|
||||||
|
resolution: workspace
|
||||||
dependencies:
|
dependencies:
|
||||||
fixnum: ^1.1.1
|
fixnum: ^1.1.1
|
||||||
csv: ^6.0.0
|
csv: ^6.0.0
|
||||||
|
@ -24,7 +25,6 @@ dev_dependencies:
|
||||||
dhttpd: ^4.1.0
|
dhttpd: ^4.1.0
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
very_good_analysis: ^6.0.0
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
// This is a basic Flutter widget test.
|
|
||||||
//
|
|
||||||
// To perform an interaction with a widget in your test, use the WidgetTester
|
|
||||||
// utility in the flutter_test package. For example, you can send tap and scroll
|
|
||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import 'package:client/main.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
|
||||||
// Build our app and trigger a frame.
|
|
||||||
await tester.pumpWidget(const MyApp());
|
|
||||||
|
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
|
||||||
}
|
|
10
src/pubspec.yaml
Normal file
10
src/pubspec.yaml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
name: cs445
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.7.0
|
||||||
|
|
||||||
|
workspace:
|
||||||
|
- shared
|
||||||
|
- client
|
||||||
|
dev_dependencies:
|
||||||
|
very_good_analysis: ^7.0.0
|
File diff suppressed because it is too large
Load diff
1454
src/server/GET_routes.json
Normal file
1454
src/server/GET_routes.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,52 +0,0 @@
|
||||||
# This file configures the analyzer, which statically analyzes Dart code to
|
|
||||||
# check for errors, warnings, and lints. See the following for docs:
|
|
||||||
# https://dart.dev/guides/language/analysis-options
|
|
||||||
#
|
|
||||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
|
||||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
|
||||||
# invoked from the command line by running `flutter analyze`.
|
|
||||||
include: package:very_good_analysis/analysis_options.yaml # has more lints
|
|
||||||
|
|
||||||
analyzer:
|
|
||||||
language:
|
|
||||||
# Strict casts isn't helpful with null safety. It only notifies you on `dynamic`,
|
|
||||||
# which happens all the time in JSON.
|
|
||||||
#
|
|
||||||
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-casts.md
|
|
||||||
strict-casts: false
|
|
||||||
|
|
||||||
# Don't let any types be inferred as `dynamic`.
|
|
||||||
#
|
|
||||||
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-inference.md
|
|
||||||
strict-inference: true
|
|
||||||
|
|
||||||
# Don't let Dart infer the wrong type on the left side of an assignment.
|
|
||||||
#
|
|
||||||
# See https://github.com/dart-lang/language/blob/main/resources/type-system/strict-raw-types.md
|
|
||||||
strict-raw-types: true
|
|
||||||
|
|
||||||
exclude:
|
|
||||||
- lib/generated/**.dart
|
|
||||||
- test/**.dart
|
|
||||||
- example/**.dart
|
|
||||||
|
|
||||||
linter:
|
|
||||||
rules:
|
|
||||||
# Rules NOT in package:very_good_analysis
|
|
||||||
prefer_double_quotes: true
|
|
||||||
prefer_expression_function_bodies: true
|
|
||||||
|
|
||||||
# Rules to be disabled from package:very_good_analysis
|
|
||||||
prefer_single_quotes: false # prefer_double_quotes
|
|
||||||
lines_longer_than_80_chars: false # lines should be at most 100 chars
|
|
||||||
sort_pub_dependencies: false # Sort dependencies by function
|
|
||||||
use_key_in_widget_constructors: false # not in Flutter apps
|
|
||||||
directives_ordering: false # sort dart, then flutter, then package imports
|
|
||||||
always_use_package_imports: false # not when importing sibling files
|
|
||||||
sort_constructors_first: false # final properties, then constructor
|
|
||||||
avoid_dynamic_calls: false # this lint takes over errors in the IDE
|
|
||||||
one_member_abstracts: false # abstract classes are good for interfaces
|
|
||||||
cascade_invocations: false # cascades are often harder to read
|
|
||||||
|
|
||||||
# Temporarily disabled until we are ready to document
|
|
||||||
public_member_api_docs: false
|
|
|
@ -1,6 +1,6 @@
|
||||||
import "package:shared/generator.dart";
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
final stops = StopGenerator();
|
await Generator.stops.generate();
|
||||||
await stops.generate();
|
await Generator.routes.generate();
|
||||||
}
|
}
|
||||||
|
|
147
src/shared/bin/path.dart
Normal file
147
src/shared/bin/path.dart
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
// 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, "localhost", 8001);
|
||||||
|
print("Listening on localhost:8001");
|
||||||
|
}
|
|
@ -1,2 +1,4 @@
|
||||||
export "src/utils.dart";
|
export "src/data/provider.dart";
|
||||||
export "src/stops/stop.dart";
|
export "src/data/route.dart";
|
||||||
|
export "src/data/stop.dart";
|
||||||
|
export "src/data/utils.dart";
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
export "src/generator_utils.dart";
|
export "src/generator/generator.dart";
|
||||||
export "src/stops/generator.dart";
|
export "src/generator/routes_bc.dart";
|
||||||
|
export "src/generator/routes_occt.dart";
|
||||||
|
export "src/generator/stops_bc.dart";
|
||||||
|
export "src/generator/stops_occt.dart";
|
||||||
|
export "src/generator/utils.dart";
|
||||||
|
|
1
src/shared/lib/graph.dart
Normal file
1
src/shared/lib/graph.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export "src/graph/state.dart";
|
|
@ -1,2 +0,0 @@
|
||||||
export "src/stops/stop.dart";
|
|
||||||
export "src/utils.dart";
|
|
11
src/shared/lib/src/data/provider.dart
Normal file
11
src/shared/lib/src/data/provider.dart
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
enum Provider {
|
||||||
|
bc("BCT", "BC Transit"),
|
||||||
|
occt("OCCT", "OCC Transport");
|
||||||
|
|
||||||
|
const Provider(this.id, this.humanName);
|
||||||
|
|
||||||
|
factory Provider.fromJson(String json) => values.firstWhere((value) => value.id == json);
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String humanName;
|
||||||
|
}
|
55
src/shared/lib/src/data/route.dart
Normal file
55
src/shared/lib/src/data/route.dart
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import "provider.dart";
|
||||||
|
import "stop.dart";
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
extension type RouteID._(String id) {
|
||||||
|
RouteID(Provider provider, Object json) :
|
||||||
|
id = "${provider.id}_$json";
|
||||||
|
|
||||||
|
RouteID.fromJson(dynamic json) : id = json.toString();
|
||||||
|
factory RouteID.fromBcCsv(CsvRow csv) => RouteID(Provider.bc, csv[0]);
|
||||||
|
|
||||||
|
RouteID withDirection(String direction) => RouteID._("${id}_$direction");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Route with Encodable {
|
||||||
|
final RouteID id;
|
||||||
|
final Provider provider;
|
||||||
|
final String fullName;
|
||||||
|
final String shortName;
|
||||||
|
final List<StopID> stops;
|
||||||
|
|
||||||
|
const Route({
|
||||||
|
required this.id,
|
||||||
|
required this.provider,
|
||||||
|
required this.stops,
|
||||||
|
required this.fullName,
|
||||||
|
required this.shortName,
|
||||||
|
});
|
||||||
|
|
||||||
|
Route.fromOcctJson(Json json) :
|
||||||
|
id = RouteID(Provider.occt, json["id"]),
|
||||||
|
provider = Provider.occt,
|
||||||
|
shortName = json["abbr"],
|
||||||
|
fullName = json["name"],
|
||||||
|
stops = [
|
||||||
|
for (final stopID in (json["stops"] as List).cast<int>())
|
||||||
|
StopID(stopID.toString()),
|
||||||
|
];
|
||||||
|
|
||||||
|
Route.fromBcCsv(CsvRow csv) :
|
||||||
|
id = RouteID.fromBcCsv(csv),
|
||||||
|
provider = Provider.bc,
|
||||||
|
shortName = csv[2],
|
||||||
|
fullName = "${csv[2]}) ${csv[3]}",
|
||||||
|
stops = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Json toJson() => {
|
||||||
|
"id": id,
|
||||||
|
"provider": provider.id,
|
||||||
|
"full_name": fullName,
|
||||||
|
"short_name": shortName,
|
||||||
|
"stops": stops,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,16 +1,19 @@
|
||||||
import "../utils.dart";
|
import "utils.dart";
|
||||||
|
import "route.dart";
|
||||||
|
import "provider.dart";
|
||||||
|
|
||||||
extension type StopID(String id) {
|
extension type StopID(String id) {
|
||||||
StopID.fromJson(dynamic value) : id = value.toString();
|
StopID.fromJson(dynamic value) : id = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
class Stop {
|
class Stop with Encodable {
|
||||||
final StopID id;
|
final StopID id;
|
||||||
final String name;
|
final String name;
|
||||||
final String? description;
|
final String? description;
|
||||||
final Coordinates coordinates;
|
final Coordinates coordinates;
|
||||||
final String provider;
|
final Provider provider;
|
||||||
final Set<String> routes;
|
final Set<String> routes;
|
||||||
|
final Set<RouteID> routeIDs;
|
||||||
|
|
||||||
Stop({
|
Stop({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -18,32 +21,39 @@ class Stop {
|
||||||
required this.description,
|
required this.description,
|
||||||
required this.coordinates,
|
required this.coordinates,
|
||||||
required this.provider,
|
required this.provider,
|
||||||
}) : routes = {};
|
}) : routes = {}, routeIDs = {};
|
||||||
|
|
||||||
Stop.fromJson(Json json) :
|
Stop.fromJson(Json json) :
|
||||||
id = StopID(json["id"]),
|
id = StopID(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"]),
|
||||||
provider = json["provider"],
|
provider = Provider.fromJson(json["provider"]),
|
||||||
routes = (json["routes"] as List).cast<String>().toSet();
|
routes = (json["routes"] as List).cast<String>().toSet(),
|
||||||
|
routeIDs = {
|
||||||
|
for (final routeID in (json["route_ids"] as List))
|
||||||
|
RouteID.fromJson(routeID),
|
||||||
|
};
|
||||||
|
|
||||||
Stop.fromOcctJson(Json json) :
|
Stop.fromOcctJson(Json json) :
|
||||||
id = StopID.fromJson(json["id"]),
|
id = StopID.fromJson(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"]),
|
||||||
provider = "OCCT",
|
provider = Provider.occt,
|
||||||
routes = <String>{};
|
routes = <String>{},
|
||||||
|
routeIDs = <RouteID>{};
|
||||||
|
|
||||||
|
@override
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
"id": id.id,
|
"id": id.id,
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"latitude": coordinates.lat,
|
"latitude": coordinates.lat,
|
||||||
"longitude": coordinates.long,
|
"longitude": coordinates.long,
|
||||||
"provider": provider,
|
"provider": provider.id,
|
||||||
"routes": routes.toList(),
|
"routes": routes.toList(),
|
||||||
|
"route_ids": routeIDs.toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
String get summary {
|
String get summary {
|
||||||
|
@ -57,4 +67,9 @@ class Stop {
|
||||||
}
|
}
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void addRoute(RouteID id, String name) {
|
||||||
|
routeIDs.add(id);
|
||||||
|
routes.add(name);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,18 @@
|
||||||
/// A JSON object
|
import "dart:math";
|
||||||
|
|
||||||
typedef Json = Map<String, dynamic>;
|
typedef Json = Map<String, dynamic>;
|
||||||
|
typedef CsvRow = List<String>;
|
||||||
typedef FromJson<T> = T Function(Json);
|
typedef FromJson<T> = T Function(Json);
|
||||||
|
|
||||||
typedef Coordinates = ({double lat, double long});
|
typedef Coordinates = ({double lat, double long});
|
||||||
|
|
||||||
|
mixin Encodable {
|
||||||
|
Json toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CoordinateUtils on Coordinates {
|
||||||
|
double distanceTo(Coordinates other) => sqrt(pow(lat - other.lat, 2) + pow(long - other.long, 2));
|
||||||
|
}
|
||||||
|
|
||||||
/// Utils on [Map].
|
/// Utils on [Map].
|
||||||
extension MapUtils<K, V> on Map<K, V> {
|
extension MapUtils<K, V> on Map<K, V> {
|
||||||
/// Gets all the keys and values as 2-element records.
|
/// Gets all the keys and values as 2-element records.
|
||||||
|
@ -27,4 +35,16 @@ extension ListUtils<E> on List<E> {
|
||||||
yield (i, this[i]);
|
yield (i, this[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
E max(int Function(E) count) => reduce((a, b) {
|
||||||
|
final numA = count(a);
|
||||||
|
final numB = count(b);
|
||||||
|
return numA > numB ? a : b;
|
||||||
|
});
|
||||||
|
|
||||||
|
E min(int Function(E) count) => reduce((a, b) {
|
||||||
|
final numA = count(a);
|
||||||
|
final numB = count(b);
|
||||||
|
return numA < numB ? a : b;
|
||||||
|
});
|
||||||
}
|
}
|
44
src/shared/lib/src/generator/generator.dart
Normal file
44
src/shared/lib/src/generator/generator.dart
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import "dart:convert";
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
|
class Generator<T extends Encodable> extends Parser<T> {
|
||||||
|
final Parser<T> bc;
|
||||||
|
final Parser<T> occt;
|
||||||
|
final File outputFile;
|
||||||
|
|
||||||
|
Generator({
|
||||||
|
required this.bc,
|
||||||
|
required this.occt,
|
||||||
|
required this.outputFile,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<T>> parse() async => [
|
||||||
|
...await bc.parse(),
|
||||||
|
...await occt.parse(),
|
||||||
|
];
|
||||||
|
|
||||||
|
Future<void> generate() async {
|
||||||
|
final result = [
|
||||||
|
for (final data in await parse())
|
||||||
|
data.toJson(),
|
||||||
|
];
|
||||||
|
const encoder = JsonEncoder.withIndent(" ");
|
||||||
|
final contents = encoder.convert(result);
|
||||||
|
await outputFile.writeAsString(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final stops = Generator(
|
||||||
|
bc: BcStopParser(),
|
||||||
|
occt: OcctStopParser(),
|
||||||
|
outputFile: File(serverDir / "GET_STOPS.json"),
|
||||||
|
);
|
||||||
|
|
||||||
|
static final routes = Generator(
|
||||||
|
bc: BcRoutesParser(),
|
||||||
|
occt: OcctRoutesParser(),
|
||||||
|
outputFile: File(serverDir / "GET_routes.json"),
|
||||||
|
);
|
||||||
|
}
|
46
src/shared/lib/src/generator/routes_bc.dart
Normal file
46
src/shared/lib/src/generator/routes_bc.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "utils.dart";
|
||||||
|
import "stops_bc.dart";
|
||||||
|
|
||||||
|
typedef InOutTrips = (TripID, TripID);
|
||||||
|
|
||||||
|
class BcRoutesParser extends Parser<Route> {
|
||||||
|
final routesFile = File(bcDataDir / "routes.txt");
|
||||||
|
final tripsFile = File(bcDataDir / "trips.txt");
|
||||||
|
|
||||||
|
final stopParser = BcStopParser();
|
||||||
|
|
||||||
|
Future<Map<RouteID, Route>> getRoutes() async => {
|
||||||
|
for (final csv in await readCsv(routesFile))
|
||||||
|
RouteID.fromBcCsv(csv): Route.fromBcCsv(csv),
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<Map<RouteID, TripID>> getTripForRoutes(Map<TripID, List<StopID>> trips) async {
|
||||||
|
final tripsForRoutes = <RouteID, List<TripID>>{};
|
||||||
|
for (final row in await readCsv(tripsFile)) {
|
||||||
|
final routeID = RouteID(Provider.bc, row[0]);
|
||||||
|
final trip = TripID(row[2]);
|
||||||
|
// print(trip);
|
||||||
|
tripsForRoutes.addToList(routeID, trip);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
for (final (routeID, tripsForRoute) in tripsForRoutes.records)
|
||||||
|
routeID: tripsForRoute.max((trip) => trips[trip]!.length),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Iterable<Route>> parse() async {
|
||||||
|
final routes = await getRoutes();
|
||||||
|
final trips = await stopParser.getTrips();
|
||||||
|
final tripsForRoutes = await getTripForRoutes(trips);
|
||||||
|
|
||||||
|
for (final (routeID, route) in routes.records) {
|
||||||
|
final longestTrip = tripsForRoutes[routeID]!;
|
||||||
|
final stops = trips[longestTrip]!;
|
||||||
|
route.stops.addAll(stops);
|
||||||
|
}
|
||||||
|
return routes.values;
|
||||||
|
}
|
||||||
|
}
|
13
src/shared/lib/src/generator/routes_occt.dart
Normal file
13
src/shared/lib/src/generator/routes_occt.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "utils.dart";
|
||||||
|
|
||||||
|
class OcctRoutesParser extends Parser<Route> {
|
||||||
|
final routesFile = File(occtDataDir / "routes.json");
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Route>> parse() async => [
|
||||||
|
for (final routeJson in await readJson(routesFile))
|
||||||
|
Route.fromOcctJson(routeJson),
|
||||||
|
];
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import "dart:io";
|
||||||
|
|
||||||
import "package:csv/csv.dart";
|
import "package:csv/csv.dart";
|
||||||
|
|
||||||
import "../generator_utils.dart";
|
import "utils.dart";
|
||||||
|
|
||||||
class BcStopParser extends Parser<Stop> {
|
class BcStopParser extends Parser<Stop> {
|
||||||
static final tripsFile = File(bcDataDir / "stop_times.txt");
|
static final tripsFile = File(bcDataDir / "stop_times.txt");
|
||||||
|
@ -12,23 +12,23 @@ class BcStopParser extends Parser<Stop> {
|
||||||
|
|
||||||
static final converter = CsvCodec(shouldParseNumbers: false).decoder;
|
static final converter = CsvCodec(shouldParseNumbers: false).decoder;
|
||||||
|
|
||||||
Future<Map<TripID, Set<StopID>>> getTrips() async {
|
Future<Map<TripID, List<StopID>>> getTrips() async {
|
||||||
final result = <TripID, Set<StopID>>{};
|
final result = <TripID, List<StopID>>{};
|
||||||
for (final row in await readCsv(tripsFile)) {
|
for (final row in await readCsv(tripsFile)) {
|
||||||
result.addToSet(TripID(row[0]), StopID(row[3]));
|
result.addToList(TripID(row[0]), StopID(row[3]));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<TripID, RouteID>> getRoutes() async => {
|
Future<Map<TripID, RouteID>> getRoutes() async => {
|
||||||
for (final row in await readCsv(routesFile))
|
for (final row in await readCsv(routesFile))
|
||||||
TripID(row[2]): RouteID(row[0]),
|
TripID(row[2]): RouteID(Provider.bc, row[0]),
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<Map<RouteID, String>> getRouteNames() async => {
|
Future<Map<RouteID, String>> getRouteNames() async => {
|
||||||
for (final row in await readCsv(routeNamesFile))
|
for (final row in await readCsv(routeNamesFile))
|
||||||
if (row[2].isNotEmpty)
|
if (row[2].isNotEmpty)
|
||||||
RouteID(row[0]): "${row[2]}) ${row[3]}",
|
RouteID(Provider.bc, row[0]): "${row[2]}) ${row[3]}",
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<Map<StopID, Stop>> getStops() async {
|
Future<Map<StopID, Stop>> getStops() async {
|
||||||
|
@ -44,7 +44,7 @@ class BcStopParser extends Parser<Stop> {
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
coordinates: (lat: latitude, long: longitude),
|
coordinates: (lat: latitude, long: longitude),
|
||||||
provider: "BC Transit",
|
provider: Provider.bc,
|
||||||
);
|
);
|
||||||
result[stopID] = stop;
|
result[stopID] = stop;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ class BcStopParser extends Parser<Stop> {
|
||||||
|
|
||||||
void findRoutesForStops({
|
void findRoutesForStops({
|
||||||
required Iterable<Stop> stops,
|
required Iterable<Stop> stops,
|
||||||
required Map<TripID, Set<StopID>> trips,
|
required Map<TripID, List<StopID>> trips,
|
||||||
required Map<TripID, RouteID> routes,
|
required Map<TripID, RouteID> routes,
|
||||||
required Map<RouteID, String> routeNames,
|
required Map<RouteID, String> routeNames,
|
||||||
}) {
|
}) {
|
||||||
|
@ -63,7 +63,7 @@ class BcStopParser extends Parser<Stop> {
|
||||||
final routeID = routes[tripID]!;
|
final routeID = routes[tripID]!;
|
||||||
final routeName = routeNames[routeID];
|
final routeName = routeNames[routeID];
|
||||||
if (routeName == null) continue; // old route
|
if (routeName == null) continue; // old route
|
||||||
stop.routes.add(routeName);
|
stop.addRoute(routeID, routeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import "dart:io";
|
import "dart:io";
|
||||||
|
|
||||||
import "../generator_utils.dart";
|
import "utils.dart";
|
||||||
|
|
||||||
class OcctStopParser extends Parser<Stop> {
|
class OcctStopParser extends Parser<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
|
||||||
|
@ -22,7 +22,7 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
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.fromJson(json["id"]);
|
||||||
final routeID = RouteID.fromJson(json["rid"]);
|
final routeID = RouteID(Provider.occt, json["rid"]);
|
||||||
result.addToList(stopID, routeID);
|
result.addToList(stopID, routeID);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -30,7 +30,7 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
|
|
||||||
Future<Map<RouteID, String>> getRouteNames() async => {
|
Future<Map<RouteID, String>> getRouteNames() async => {
|
||||||
for (final json in await readJson(routesFile))
|
for (final json in await readJson(routesFile))
|
||||||
RouteID.fromJson(json["id"]): json["name"],
|
RouteID(Provider.occt, json["id"]): json["name"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -38,12 +38,12 @@ class OcctStopParser extends Parser<Stop> {
|
||||||
final stops = await getStops();
|
final stops = await getStops();
|
||||||
final routes = await getRoutes();
|
final routes = await getRoutes();
|
||||||
final routeNames = await getRouteNames();
|
final routeNames = await getRouteNames();
|
||||||
final routesToSkip = <RouteID>{RouteID("11")}; // no longer used
|
final routesToSkip = <RouteID>{RouteID(Provider.occt, "11")};
|
||||||
for (final (stopID, stop) in stops.records) {
|
for (final (stopID, stop) in stops.records) {
|
||||||
for (final routeID in routes[stopID]!) {
|
for (final routeID in routes[stopID]!) {
|
||||||
if (routesToSkip.contains(routeID)) continue;
|
if (routesToSkip.contains(routeID)) continue;
|
||||||
final routeName = routeNames[routeID]!;
|
final routeName = routeNames[routeID]!;
|
||||||
stop.routes.add(routeName);
|
stop.addRoute(routeID, routeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stops.values;
|
return stops.values;
|
|
@ -9,12 +9,6 @@ extension type TripID(String id) {
|
||||||
TripID.fromJson(dynamic value) : id = value.toString();
|
TripID.fromJson(dynamic value) : id = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
extension type RouteID(String id) {
|
|
||||||
RouteID.fromJson(dynamic value) : id = value.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef CsvRow = List<String>;
|
|
||||||
|
|
||||||
extension DirectoryUtils on Directory {
|
extension DirectoryUtils on Directory {
|
||||||
String operator /(String child) => "$path/$child";
|
String operator /(String child) => "$path/$child";
|
||||||
}
|
}
|
73
src/shared/lib/src/graph/state.dart
Normal file
73
src/shared/lib/src/graph/state.dart
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import "package:a_star/a_star.dart";
|
||||||
|
import "package:shared/data.dart";
|
||||||
|
|
||||||
|
class StopState extends AStarState<StopState> {
|
||||||
|
static late final Map<RouteID, Route> routes;
|
||||||
|
static late final Map<StopID, Stop> stops;
|
||||||
|
|
||||||
|
static void init(Iterable<Route> allRoutes, Iterable<Stop> allStops) {
|
||||||
|
routes = {
|
||||||
|
for (final route in allRoutes)
|
||||||
|
route.id: route,
|
||||||
|
};
|
||||||
|
stops = {
|
||||||
|
for (final stop in allStops)
|
||||||
|
stop.id: stop,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
final StopID stopID;
|
||||||
|
final StopID goalID;
|
||||||
|
final RouteID routeID;
|
||||||
|
StopState({
|
||||||
|
required this.stopID,
|
||||||
|
required this.goalID,
|
||||||
|
required this.routeID,
|
||||||
|
required super.depth,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String hash() => "$stopID-$goalID-$routeID";
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isGoal() => stopID == goalID;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double heuristic() {
|
||||||
|
final stop = stops[stopID]!;
|
||||||
|
final goal = stops[goalID]!;
|
||||||
|
return stop.coordinates.distanceTo(goal.coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<StopState> expand() {
|
||||||
|
final route = routes[routeID]!;
|
||||||
|
final stop = stops[stopID]!;
|
||||||
|
final stopIndex = route.stops.indexOf(stopID);
|
||||||
|
final result = <StopState>[];
|
||||||
|
|
||||||
|
// 1): Go forward one stop on the same route
|
||||||
|
final nextIndex = stopIndex + 1;
|
||||||
|
if (nextIndex < route.stops.length) {
|
||||||
|
final nextStop = route.stops[nextIndex];
|
||||||
|
final state = StopState(stopID: nextStop, goalID: goalID, routeID: routeID, depth: depth + 1);
|
||||||
|
result.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Go back one stop on the same route
|
||||||
|
final prevIndex = stopIndex - 1;
|
||||||
|
if (prevIndex > 0) {
|
||||||
|
final prevStop = route.stops[prevIndex];
|
||||||
|
final state = StopState(stopID: prevStop, goalID: goalID, routeID: routeID, depth: depth + 1);
|
||||||
|
result.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Make any available transfers
|
||||||
|
for (final otherRoute in stop.routeIDs.difference({routeID})) {
|
||||||
|
final state = StopState(stopID: stopID, goalID: goalID, routeID: otherRoute, depth: depth + 1);
|
||||||
|
result.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,28 +0,0 @@
|
||||||
import "dart:convert";
|
|
||||||
import "dart:io";
|
|
||||||
|
|
||||||
import "../generator_utils.dart";
|
|
||||||
|
|
||||||
import "bc.dart";
|
|
||||||
import "occt.dart";
|
|
||||||
|
|
||||||
class StopGenerator {
|
|
||||||
static final serverDir = Directory("../server");
|
|
||||||
static final outputFile = File(serverDir / "GET_STOPS.json");
|
|
||||||
|
|
||||||
final Parser<Stop> bc = BcStopParser();
|
|
||||||
final Parser<Stop> occt = OcctStopParser();
|
|
||||||
|
|
||||||
Future<void> generate() async {
|
|
||||||
final bcStops = await bc.parse();
|
|
||||||
final occtStops = await occt.parse();
|
|
||||||
final stops = [...bcStops, ...occtStops];
|
|
||||||
final result = [
|
|
||||||
for (final stop in stops)
|
|
||||||
stop.toJson(),
|
|
||||||
];
|
|
||||||
const encoder = JsonEncoder.withIndent(" ");
|
|
||||||
final contents = encoder.convert(result);
|
|
||||||
await outputFile.writeAsString(contents);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,9 +6,14 @@ version: 1.0.0
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.0
|
sdk: ^3.7.0
|
||||||
|
|
||||||
|
resolution: workspace
|
||||||
|
|
||||||
# Add regular dependencies here.
|
# Add regular dependencies here.
|
||||||
dependencies:
|
dependencies:
|
||||||
csv: ^6.0.0
|
csv: ^6.0.0
|
||||||
|
a_star: ^3.0.1
|
||||||
|
shelf: ^1.4.2
|
||||||
|
shelf_router: ^1.1.4
|
||||||
# path: ^1.8.0
|
# path: ^1.8.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
Loading…
Reference in a new issue