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
|
||||
pubspec.lock
|
||||
.dart_tool
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import "package:flutter/foundation.dart" show debugPrint, kDebugMode;
|
||||
|
||||
import "package:client/data.dart";
|
||||
import "package:http/http.dart" as http;
|
||||
|
||||
import "api_client.dart";
|
||||
import "service.dart";
|
||||
|
@ -41,6 +42,28 @@ class ApiService extends Service {
|
|||
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(
|
||||
_base.resolve("stops"),
|
||||
Stop.fromJson,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import "dart:convert";
|
||||
|
||||
import "package:client/data.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:http/http.dart" as http;
|
||||
|
||||
class ApiClient {
|
||||
|
@ -19,9 +20,10 @@ class ApiClient {
|
|||
for (final json in listOfJson)
|
||||
fromJson(json),
|
||||
];
|
||||
// Want to catch all errors for the UI
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (error, stackTrace) {
|
||||
// ignore: avoid_print
|
||||
print("Error: $error\n$stackTrace");
|
||||
debugPrint("Error: $error\n$stackTrace");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ class HomeModel extends ViewModel with HomeMarkers {
|
|||
super.updateEnd(coordinates, marker);
|
||||
}
|
||||
|
||||
String? pathText;
|
||||
Future<void> search() async {
|
||||
final start = (lat: startLatitude, long: startLongitude);
|
||||
final end = (lat: endLatitude, long: endLongitude);
|
||||
|
@ -71,16 +72,17 @@ class HomeModel extends ViewModel with HomeMarkers {
|
|||
if (end.lat == null || end.long == null) return;
|
||||
isSearching = true;
|
||||
isLoading = true;
|
||||
final result = await services.api.getPath(start: start as Coordinates, end: end as Coordinates);
|
||||
path = result;
|
||||
final result = await services.api.getPath2(start: start as Coordinates, end: end as Coordinates);
|
||||
pathText = result ?? "An error occurred";
|
||||
// path = result;
|
||||
isLoading = false;
|
||||
if (result == null) return;
|
||||
isSearching = false;
|
||||
routes = [
|
||||
for (final step in result) [
|
||||
...decodePolyline(step.trip.polyline),
|
||||
],
|
||||
];
|
||||
// routes = [
|
||||
// for (final step in result) [
|
||||
// ...decodePolyline(step.trip.polyline),
|
||||
// ],
|
||||
// ];
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,12 +78,11 @@ mixin HomeMarkers on ChangeNotifier {
|
|||
stopCounts[route] ??= 0;
|
||||
stopCounts[route] = stopCounts[route]! + 1;
|
||||
}
|
||||
|
||||
if (stop.provider.contains("OCCT")) {
|
||||
occtRouteNames.addAll(stop.routes);
|
||||
} else {
|
||||
bcRouteNames.addAll(stop.routes);
|
||||
}
|
||||
final namesList = switch (stop.provider) {
|
||||
Provider.occt => occtRouteNames,
|
||||
Provider.bc => bcRouteNames,
|
||||
};
|
||||
namesList.addAll(stop.routes);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
|
|
@ -32,27 +32,29 @@ class Sidebar extends ReusableReactiveWidget<HomeModel> {
|
|||
Tab(text: provider),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: !model.shouldShowMarkers
|
||||
? const Center(child: Text("Choose start or end location"))
|
||||
: TabBarView(
|
||||
children: [
|
||||
for (final (_, routesList) in model.providers)
|
||||
ListView(
|
||||
children: [
|
||||
for (final route in routesList) CheckboxListTile(
|
||||
title: Text(route, maxLines: 1),
|
||||
subtitle: Text(
|
||||
"${model.stopCounts[route] ?? 0} stops",
|
||||
maxLines: 1,
|
||||
),
|
||||
value: model.routesToShow.contains(route),
|
||||
onChanged: (value) => model.showRoute(route, shouldShow: value!),
|
||||
),
|
||||
],
|
||||
if (!model.shouldShowMarkers && model.pathText != null)
|
||||
Center(child: Text(model.pathText!, style: context.textTheme.bodySmall)),
|
||||
if (!model.shouldShowMarkers && model.pathText == null)
|
||||
const Center(child: Text("Choose start or end location")),
|
||||
if (model.shouldShowMarkers) Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
for (final (_, routesList) in model.providers)
|
||||
ListView(
|
||||
children: [
|
||||
for (final route in routesList) CheckboxListTile(
|
||||
title: Text(route, maxLines: 1),
|
||||
subtitle: Text(
|
||||
"${model.stopCounts[route] ?? 0} stops",
|
||||
maxLines: 1,
|
||||
),
|
||||
value: model.routesToShow.contains(route),
|
||||
onChanged: (value) => model.showRoute(route, shouldShow: value!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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:
|
||||
sdk: ^3.5.0
|
||||
|
||||
resolution: workspace
|
||||
dependencies:
|
||||
fixnum: ^1.1.1
|
||||
csv: ^6.0.0
|
||||
|
@ -24,7 +25,6 @@ dev_dependencies:
|
|||
dhttpd: ^4.1.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
very_good_analysis: ^6.0.0
|
||||
|
||||
flutter:
|
||||
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";
|
||||
|
||||
void main() async {
|
||||
final stops = StopGenerator();
|
||||
await stops.generate();
|
||||
await Generator.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/stops/stop.dart";
|
||||
export "src/data/provider.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/stops/generator.dart";
|
||||
export "src/generator/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) {
|
||||
StopID.fromJson(dynamic value) : id = value.toString();
|
||||
}
|
||||
|
||||
class Stop {
|
||||
class Stop with Encodable {
|
||||
final StopID id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final Coordinates coordinates;
|
||||
final String provider;
|
||||
final Provider provider;
|
||||
final Set<String> routes;
|
||||
final Set<RouteID> routeIDs;
|
||||
|
||||
Stop({
|
||||
required this.id,
|
||||
|
@ -18,32 +21,39 @@ class Stop {
|
|||
required this.description,
|
||||
required this.coordinates,
|
||||
required this.provider,
|
||||
}) : routes = {};
|
||||
}) : routes = {}, routeIDs = {};
|
||||
|
||||
Stop.fromJson(Json json) :
|
||||
id = StopID(json["id"]),
|
||||
name = json["name"],
|
||||
description = json["description"],
|
||||
coordinates = (lat: json["latitude"], long: json["longitude"]),
|
||||
provider = json["provider"],
|
||||
routes = (json["routes"] as List).cast<String>().toSet();
|
||||
provider = Provider.fromJson(json["provider"]),
|
||||
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) :
|
||||
id = StopID.fromJson(json["id"]),
|
||||
name = json["name"],
|
||||
description = null,
|
||||
coordinates = (lat: json["lat"], long: json["lng"]),
|
||||
provider = "OCCT",
|
||||
routes = <String>{};
|
||||
provider = Provider.occt,
|
||||
routes = <String>{},
|
||||
routeIDs = <RouteID>{};
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id.id,
|
||||
"name": name,
|
||||
"description": description,
|
||||
"latitude": coordinates.lat,
|
||||
"longitude": coordinates.long,
|
||||
"provider": provider,
|
||||
"provider": provider.id,
|
||||
"routes": routes.toList(),
|
||||
"route_ids": routeIDs.toList(),
|
||||
};
|
||||
|
||||
String get summary {
|
||||
|
@ -57,4 +67,9 @@ class Stop {
|
|||
}
|
||||
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 CsvRow = List<String>;
|
||||
typedef FromJson<T> = T Function(Json);
|
||||
|
||||
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].
|
||||
extension MapUtils<K, V> on Map<K, V> {
|
||||
/// Gets all the keys and values as 2-element records.
|
||||
|
@ -27,4 +35,16 @@ extension ListUtils<E> on List<E> {
|
|||
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 "../generator_utils.dart";
|
||||
import "utils.dart";
|
||||
|
||||
class BcStopParser extends Parser<Stop> {
|
||||
static final tripsFile = File(bcDataDir / "stop_times.txt");
|
||||
|
@ -12,23 +12,23 @@ class BcStopParser extends Parser<Stop> {
|
|||
|
||||
static final converter = CsvCodec(shouldParseNumbers: false).decoder;
|
||||
|
||||
Future<Map<TripID, Set<StopID>>> getTrips() async {
|
||||
final result = <TripID, Set<StopID>>{};
|
||||
Future<Map<TripID, List<StopID>>> getTrips() async {
|
||||
final result = <TripID, List<StopID>>{};
|
||||
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;
|
||||
}
|
||||
|
||||
Future<Map<TripID, RouteID>> getRoutes() async => {
|
||||
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 => {
|
||||
for (final row in await readCsv(routeNamesFile))
|
||||
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 {
|
||||
|
@ -44,7 +44,7 @@ class BcStopParser extends Parser<Stop> {
|
|||
name: name,
|
||||
description: description,
|
||||
coordinates: (lat: latitude, long: longitude),
|
||||
provider: "BC Transit",
|
||||
provider: Provider.bc,
|
||||
);
|
||||
result[stopID] = stop;
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class BcStopParser extends Parser<Stop> {
|
|||
|
||||
void findRoutesForStops({
|
||||
required Iterable<Stop> stops,
|
||||
required Map<TripID, Set<StopID>> trips,
|
||||
required Map<TripID, List<StopID>> trips,
|
||||
required Map<TripID, RouteID> routes,
|
||||
required Map<RouteID, String> routeNames,
|
||||
}) {
|
||||
|
@ -63,7 +63,7 @@ class BcStopParser extends Parser<Stop> {
|
|||
final routeID = routes[tripID]!;
|
||||
final routeName = routeNames[routeID];
|
||||
if (routeName == null) continue; // old route
|
||||
stop.routes.add(routeName);
|
||||
stop.addRoute(routeID, routeName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import "dart:io";
|
||||
|
||||
import "../generator_utils.dart";
|
||||
import "utils.dart";
|
||||
|
||||
class OcctStopParser extends Parser<Stop> {
|
||||
/// 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>>{};
|
||||
for (final json in await readJson(stopsFile)) {
|
||||
final stopID = StopID.fromJson(json["id"]);
|
||||
final routeID = RouteID.fromJson(json["rid"]);
|
||||
final routeID = RouteID(Provider.occt, json["rid"]);
|
||||
result.addToList(stopID, routeID);
|
||||
}
|
||||
return result;
|
||||
|
@ -30,7 +30,7 @@ class OcctStopParser extends Parser<Stop> {
|
|||
|
||||
Future<Map<RouteID, String>> getRouteNames() async => {
|
||||
for (final json in await readJson(routesFile))
|
||||
RouteID.fromJson(json["id"]): json["name"],
|
||||
RouteID(Provider.occt, json["id"]): json["name"],
|
||||
};
|
||||
|
||||
@override
|
||||
|
@ -38,12 +38,12 @@ class OcctStopParser extends Parser<Stop> {
|
|||
final stops = await getStops();
|
||||
final routes = await getRoutes();
|
||||
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 routeID in routes[stopID]!) {
|
||||
if (routesToSkip.contains(routeID)) continue;
|
||||
final routeName = routeNames[routeID]!;
|
||||
stop.routes.add(routeName);
|
||||
stop.addRoute(routeID, routeName);
|
||||
}
|
||||
}
|
||||
return stops.values;
|
|
@ -9,12 +9,6 @@ extension type TripID(String id) {
|
|||
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 {
|
||||
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:
|
||||
sdk: ^3.7.0
|
||||
|
||||
resolution: workspace
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
csv: ^6.0.0
|
||||
a_star: ^3.0.1
|
||||
shelf: ^1.4.2
|
||||
shelf_router: ^1.1.4
|
||||
# path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
|
|
Loading…
Reference in a new issue