Move data parsing definitions out of the client (#11)
Makes a new (dart) package called `shared`, which contains: - the code needed to parse through OCCT and BC data - any data definitions for types in that data (eg, stops) - shared utils The client will then import types from this package. This PR also re-generates the GET_STOPS json file to include OCCT
This commit is contained in:
parent
8fdb25f15e
commit
c878d08c23
29 changed files with 3628 additions and 10571 deletions
|
@ -1,149 +0,0 @@
|
||||||
import "dart:convert";
|
|
||||||
import "dart:io";
|
|
||||||
import "package:csv/csv.dart";
|
|
||||||
|
|
||||||
/// Utils on [Map].
|
|
||||||
extension MapUtils<K, V> on Map<K, V> {
|
|
||||||
/// Gets all the keys and values as 2-element records.
|
|
||||||
Iterable<(K, V)> get records => entries.map((entry) => (entry.key, entry.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on Directory {
|
|
||||||
String operator /(String child) => "$path/$child";
|
|
||||||
}
|
|
||||||
|
|
||||||
final serverDir = Directory("../server");
|
|
||||||
final dataDir = Directory(serverDir / "data/BCT");
|
|
||||||
final tripsFile = File(dataDir / "stop_times.txt");
|
|
||||||
final routesFile = File(dataDir / "trips.txt");
|
|
||||||
final stopsFile = File(dataDir / "stops.txt");
|
|
||||||
final routeNamesFile = File(dataDir / "routes.txt");
|
|
||||||
final outputFile = File(serverDir / "GET_STOPS.json");
|
|
||||||
|
|
||||||
extension type TripID(String id) { }
|
|
||||||
extension type StopID(String id) { }
|
|
||||||
extension type RouteID(String id) { }
|
|
||||||
|
|
||||||
class Stop {
|
|
||||||
final StopID id;
|
|
||||||
final double latitude;
|
|
||||||
final double longitude;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final String provider;
|
|
||||||
|
|
||||||
final Set<String> routes = {};
|
|
||||||
|
|
||||||
Stop({
|
|
||||||
required this.id,
|
|
||||||
required this.latitude,
|
|
||||||
required this.longitude,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.provider,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
|
||||||
"id": id.id,
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"latitude": latitude,
|
|
||||||
"longitude": longitude,
|
|
||||||
"provider": provider,
|
|
||||||
"routes": routes.toList(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
final converter = CsvCodec(shouldParseNumbers: false).decoder;
|
|
||||||
|
|
||||||
Future<Map<TripID, Set<StopID>>> getTrips() async {
|
|
||||||
final contents = await tripsFile.readAsString();
|
|
||||||
final csv = converter.convert(contents);
|
|
||||||
final result = <TripID, Set<StopID>>{};
|
|
||||||
for (final row in csv.skip(1)) {
|
|
||||||
final tripId = TripID(row[0]);
|
|
||||||
final stopId = StopID(row[3]);
|
|
||||||
result[tripId] ??= <StopID>{};
|
|
||||||
result[tripId]!.add(stopId);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<TripID, RouteID>> getRoutes() async {
|
|
||||||
final contents = await routesFile.readAsString();
|
|
||||||
final csv = converter.convert(contents);
|
|
||||||
final result = <TripID, RouteID>{};
|
|
||||||
for (final row in csv.skip(1)) {
|
|
||||||
final routeID = RouteID(row[0]);
|
|
||||||
final tripID = TripID(row[2]);
|
|
||||||
result[tripID] = routeID;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<RouteID, String>> getRouteNames() async {
|
|
||||||
final contents = await routeNamesFile.readAsString();
|
|
||||||
final csv = converter.convert(contents);
|
|
||||||
return {
|
|
||||||
for (final row in csv.skip(1))
|
|
||||||
RouteID(row[0]): row[3],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<StopID, Stop>> getStops() async {
|
|
||||||
final contents = await stopsFile.readAsString();
|
|
||||||
final csv = converter.convert(contents);
|
|
||||||
final result = <StopID, Stop>{};
|
|
||||||
for (final row in csv.skip(1)) {
|
|
||||||
final stopID = StopID(row[0]);
|
|
||||||
final name = row[2];
|
|
||||||
final description = row[3];
|
|
||||||
final latitude = double.parse(row[4]);
|
|
||||||
final longitude = double.parse(row[5]);
|
|
||||||
final stop = Stop(
|
|
||||||
id: stopID,
|
|
||||||
name: name,
|
|
||||||
description: description,
|
|
||||||
latitude: latitude,
|
|
||||||
longitude: longitude,
|
|
||||||
provider: "BC Transit",
|
|
||||||
);
|
|
||||||
result[stopID] = stop;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void findRoutesForStops({
|
|
||||||
required Iterable<Stop> stops,
|
|
||||||
required Map<TripID, Set<StopID>> trips,
|
|
||||||
required Map<TripID, RouteID> routes,
|
|
||||||
required Map<RouteID, String> routeNames,
|
|
||||||
}) {
|
|
||||||
for (final stop in stops) {
|
|
||||||
for (final (tripID, trip) in trips.records) {
|
|
||||||
if (!trip.contains(stop.id)) continue;
|
|
||||||
final routeID = routes[tripID]!;
|
|
||||||
final routeName = routeNames[routeID]!;
|
|
||||||
stop.routes.add(routeName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> outputJson(Iterable<Stop> stops) async {
|
|
||||||
final result = [
|
|
||||||
for (final stop in stops)
|
|
||||||
stop.toJson(),
|
|
||||||
];
|
|
||||||
const encoder = JsonEncoder.withIndent(" ");
|
|
||||||
final contents = encoder.convert(result);
|
|
||||||
await outputFile.writeAsString(contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
final trips = await getTrips();
|
|
||||||
final routes = await getRoutes();
|
|
||||||
final stops = await getStops();
|
|
||||||
final routeNames = await getRouteNames();
|
|
||||||
findRoutesForStops(stops: stops.values, trips: trips, routes: routes, routeNames: routeNames);
|
|
||||||
await outputJson(stops.values);
|
|
||||||
}
|
|
|
@ -2,4 +2,5 @@ export "src/data/utils.dart";
|
||||||
export "src/data/trip.dart";
|
export "src/data/trip.dart";
|
||||||
export "src/data/path.dart";
|
export "src/data/path.dart";
|
||||||
export "src/data/polyline.dart";
|
export "src/data/polyline.dart";
|
||||||
export "src/data/stop.dart";
|
|
||||||
|
export "package:shared/data.dart";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import "trip.dart";
|
import "trip.dart";
|
||||||
import "utils.dart";
|
import "package:shared/data.dart";
|
||||||
|
|
||||||
class PathStep {
|
class PathStep {
|
||||||
final PathTrip trip;
|
final PathTrip trip;
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
import "utils.dart";
|
|
||||||
|
|
||||||
extension type StopID(String id) { }
|
|
||||||
|
|
||||||
class Stop {
|
|
||||||
final StopID id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final Coordinates coordinates;
|
|
||||||
final String provider;
|
|
||||||
final List<String> routes;
|
|
||||||
|
|
||||||
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>();
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
import "utils.dart";
|
import "package:shared/data.dart";
|
||||||
|
|
||||||
import "package:flutter/material.dart" show TimeOfDay;
|
import "package:flutter/material.dart" show TimeOfDay;
|
||||||
|
|
||||||
|
|
|
@ -1,35 +1,6 @@
|
||||||
import "package:google_maps_flutter/google_maps_flutter.dart";
|
import "package:google_maps_flutter/google_maps_flutter.dart";
|
||||||
|
|
||||||
/// A JSON object
|
import "package:shared/data.dart";
|
||||||
typedef Json = Map<String, dynamic>;
|
|
||||||
|
|
||||||
typedef FromJson<T> = T Function(Json);
|
|
||||||
|
|
||||||
typedef Coordinates = ({double lat, double long});
|
|
||||||
|
|
||||||
/// Utils on [Map].
|
|
||||||
extension MapUtils<K, V> on Map<K, V> {
|
|
||||||
/// Gets all the keys and values as 2-element records.
|
|
||||||
Iterable<(K, V)> get records => entries.map((entry) => (entry.key, entry.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Zips two lists, like Python
|
|
||||||
Iterable<(E1, E2)> zip<E1, E2>(List<E1> list1, List<E2> list2) sync* {
|
|
||||||
if (list1.length != list2.length) throw ArgumentError("Trying to zip lists of different lengths");
|
|
||||||
for (var index = 0; index < list1.length; index++) {
|
|
||||||
yield (list1[index], list2[index]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extensions on lists
|
|
||||||
extension ListUtils<E> on List<E> {
|
|
||||||
/// Iterates over a pair of indexes and elements, like Python
|
|
||||||
Iterable<(int, E)> get enumerate sync* {
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
yield (i, this[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CoordinatesUtils on Coordinates {
|
extension CoordinatesUtils on Coordinates {
|
||||||
LatLng toLatLng() => LatLng(lat, long);
|
LatLng toLatLng() => LatLng(lat, long);
|
||||||
|
|
|
@ -26,7 +26,7 @@ mixin HomeMarkers on ChangeNotifier {
|
||||||
consumeTapEvents: true,
|
consumeTapEvents: true,
|
||||||
infoWindow: InfoWindow(
|
infoWindow: InfoWindow(
|
||||||
title: stop.name,
|
title: stop.name,
|
||||||
snippet: "${stop.description}\n\nRoutes: ${stop.routes.join("\n")}",
|
snippet: stop.summary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -304,6 +304,13 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "2.1.0"
|
||||||
|
shared:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "../shared"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "1.0.0"
|
||||||
shelf:
|
shelf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -422,5 +429,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.0-0 <4.0.0"
|
dart: ">=3.7.0 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.27.0"
|
||||||
|
|
|
@ -17,6 +17,8 @@ dependencies:
|
||||||
http: ^1.3.0
|
http: ^1.3.0
|
||||||
polyline_tools: ^0.0.2
|
polyline_tools: ^0.0.2
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
|
shared:
|
||||||
|
path: ../shared
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dhttpd: ^4.1.0
|
dhttpd: ^4.1.0
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,4 @@
|
||||||
[
|
[
|
||||||
"https://binghamtonupublic.etaspot.net/service.php?service=get_routes&token=TESTING",
|
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"name": "Westside Outbound",
|
"name": "Westside Outbound",
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -78,7 +78,7 @@ router.get(
|
||||||
"/stops",
|
"/stops",
|
||||||
async (_ctx) => {
|
async (_ctx) => {
|
||||||
try {
|
try {
|
||||||
const bytes = await Deno.readFile(`bct_stops.json`);
|
const bytes = await Deno.readFile(`GET_STOPS.json`);
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
json_mime_add(headers);
|
json_mime_add(headers);
|
||||||
cors_add(headers);
|
cors_add(headers);
|
||||||
|
|
4
src/shared/.gitignore
vendored
Normal file
4
src/shared/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# https://dart.dev/guides/libraries/private-files
|
||||||
|
# Created by `dart pub`
|
||||||
|
.dart_tool/
|
||||||
|
pubspec.lock
|
3
src/shared/CHANGELOG.md
Normal file
3
src/shared/CHANGELOG.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
2
src/shared/README.md
Normal file
2
src/shared/README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
A sample command-line application with an entrypoint in `bin/`, library code
|
||||||
|
in `lib/`, and example unit test in `test/`.
|
52
src/shared/analysis_options.yaml
Normal file
52
src/shared/analysis_options.yaml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# 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
|
6
src/shared/bin/data.dart
Normal file
6
src/shared/bin/data.dart
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import "package:shared/generator.dart";
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
final stops = StopGenerator();
|
||||||
|
await stops.generate();
|
||||||
|
}
|
2
src/shared/lib/data.dart
Normal file
2
src/shared/lib/data.dart
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export "src/utils.dart";
|
||||||
|
export "src/stops/stop.dart";
|
2
src/shared/lib/generator.dart
Normal file
2
src/shared/lib/generator.dart
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export "src/generator_utils.dart";
|
||||||
|
export "src/stops/generator.dart";
|
2
src/shared/lib/shared.dart
Normal file
2
src/shared/lib/shared.dart
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export "src/stops/stop.dart";
|
||||||
|
export "src/utils.dart";
|
56
src/shared/lib/src/generator_utils.dart
Normal file
56
src/shared/lib/src/generator_utils.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import "dart:convert";
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:csv/csv.dart";
|
||||||
|
import "package:shared/data.dart";
|
||||||
|
export "package:shared/data.dart";
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MapListUtils<K, V> on Map<K, List<V>> {
|
||||||
|
void addToList(K key, V value) {
|
||||||
|
this[key] ??= [];
|
||||||
|
this[key]!.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MapSetUtils<K, V> on Map<K, Set<V>> {
|
||||||
|
void addToSet(K key, V value) {
|
||||||
|
this[key] ??= {};
|
||||||
|
this[key]!.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Parser<T> {
|
||||||
|
Future<Iterable<T>> parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
final csvConverter = CsvCodec(shouldParseNumbers: false).decoder;
|
||||||
|
Future<Iterable<CsvRow>> readCsv(File file) async {
|
||||||
|
final contents = await file.readAsString();
|
||||||
|
final csv = csvConverter.convert(contents);
|
||||||
|
return csv.skip(1).map<CsvRow>((row) => row.cast<String>());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Json>> readJson(File file) async {
|
||||||
|
final contents = await file.readAsString();
|
||||||
|
final json = jsonDecode(contents);
|
||||||
|
return (json as List).cast<Json>();
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverDir = Directory("../server");
|
||||||
|
final _dataDir = Directory(serverDir / "data");
|
||||||
|
final occtDataDir = Directory(_dataDir / "OCCT");
|
||||||
|
final bcDataDir = Directory(_dataDir / "BCT");
|
78
src/shared/lib/src/stops/bc.dart
Normal file
78
src/shared/lib/src/stops/bc.dart
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:csv/csv.dart";
|
||||||
|
|
||||||
|
import "../generator_utils.dart";
|
||||||
|
|
||||||
|
class BcStopParser extends Parser<Stop> {
|
||||||
|
static final tripsFile = File(bcDataDir / "stop_times.txt");
|
||||||
|
static final routesFile = File(bcDataDir / "trips.txt");
|
||||||
|
static final stopsFile = File(bcDataDir / "stops.txt");
|
||||||
|
static final routeNamesFile = File(bcDataDir / "routes.txt");
|
||||||
|
|
||||||
|
static final converter = CsvCodec(shouldParseNumbers: false).decoder;
|
||||||
|
|
||||||
|
Future<Map<TripID, Set<StopID>>> getTrips() async {
|
||||||
|
final result = <TripID, Set<StopID>>{};
|
||||||
|
for (final row in await readCsv(tripsFile)) {
|
||||||
|
result.addToSet(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]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<Map<RouteID, String>> getRouteNames() async => {
|
||||||
|
for (final row in await readCsv(routeNamesFile))
|
||||||
|
RouteID(row[0]): row[3],
|
||||||
|
};
|
||||||
|
|
||||||
|
Future<Map<StopID, Stop>> getStops() async {
|
||||||
|
final result = <StopID, Stop>{};
|
||||||
|
for (final row in await readCsv(stopsFile)) {
|
||||||
|
final stopID = StopID(row[0]);
|
||||||
|
final name = row[2];
|
||||||
|
final description = row[3];
|
||||||
|
final latitude = double.parse(row[4]);
|
||||||
|
final longitude = double.parse(row[5]);
|
||||||
|
final stop = Stop(
|
||||||
|
id: stopID,
|
||||||
|
name: name,
|
||||||
|
description: description,
|
||||||
|
coordinates: (lat: latitude, long: longitude),
|
||||||
|
provider: "BC Transit",
|
||||||
|
);
|
||||||
|
result[stopID] = stop;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void findRoutesForStops({
|
||||||
|
required Iterable<Stop> stops,
|
||||||
|
required Map<TripID, Set<StopID>> trips,
|
||||||
|
required Map<TripID, RouteID> routes,
|
||||||
|
required Map<RouteID, String> routeNames,
|
||||||
|
}) {
|
||||||
|
for (final stop in stops) {
|
||||||
|
for (final (tripID, trip) in trips.records) {
|
||||||
|
if (!trip.contains(stop.id)) continue;
|
||||||
|
final routeID = routes[tripID]!;
|
||||||
|
final routeName = routeNames[routeID]!;
|
||||||
|
stop.routes.add(routeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Iterable<Stop>> parse() async {
|
||||||
|
final trips = await getTrips();
|
||||||
|
final routes = await getRoutes();
|
||||||
|
final stops = await getStops();
|
||||||
|
final routeNames = await getRouteNames();
|
||||||
|
findRoutesForStops(stops: stops.values, trips: trips, routes: routes, routeNames: routeNames);
|
||||||
|
return stops.values;
|
||||||
|
}
|
||||||
|
}
|
28
src/shared/lib/src/stops/generator.dart
Normal file
28
src/shared/lib/src/stops/generator.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
51
src/shared/lib/src/stops/occt.dart
Normal file
51
src/shared/lib/src/stops/occt.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "../generator_utils.dart";
|
||||||
|
|
||||||
|
class OcctStopParser extends Parser<Stop> {
|
||||||
|
/// Taken from: https://binghamtonupublic.etaspot.net/service.php?service=get_stops&token=TESTING
|
||||||
|
static final stopsFile = File(occtDataDir / "stops.json");
|
||||||
|
|
||||||
|
/// Taken from: https://binghamtonupublic.etaspot.net/service.php?service=get_routes&token=TESTING
|
||||||
|
static final routesFile = File(occtDataDir / "routes.json");
|
||||||
|
|
||||||
|
Future<Map<StopID, Stop>> getStops() async {
|
||||||
|
final result = <StopID, Stop>{};
|
||||||
|
for (final stopJson in await readJson(stopsFile)) {
|
||||||
|
final id = StopID.fromJson(stopJson["id"]);
|
||||||
|
result[id] ??= Stop.fromOcctJson(stopJson);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<StopID, List<RouteID>>> getRoutes() async {
|
||||||
|
final result = <StopID, List<RouteID>>{};
|
||||||
|
for (final json in await readJson(stopsFile)) {
|
||||||
|
final stopID = StopID.fromJson(json["id"]);
|
||||||
|
final routeID = RouteID.fromJson(json["rid"]);
|
||||||
|
result.addToList(stopID, routeID);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<RouteID, String>> getRouteNames() async => {
|
||||||
|
for (final json in await readJson(routesFile))
|
||||||
|
RouteID.fromJson(json["id"]): json["name"],
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Iterable<Stop>> parse() async {
|
||||||
|
final stops = await getStops();
|
||||||
|
final routes = await getRoutes();
|
||||||
|
final routeNames = await getRouteNames();
|
||||||
|
final routesToSkip = <RouteID>{RouteID("11")}; // no longer used
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stops.values;
|
||||||
|
}
|
||||||
|
}
|
60
src/shared/lib/src/stops/stop.dart
Normal file
60
src/shared/lib/src/stops/stop.dart
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import "../utils.dart";
|
||||||
|
|
||||||
|
extension type StopID(String id) {
|
||||||
|
StopID.fromJson(dynamic value) : id = value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Stop {
|
||||||
|
final StopID id;
|
||||||
|
final String name;
|
||||||
|
final String? description;
|
||||||
|
final Coordinates coordinates;
|
||||||
|
final String provider;
|
||||||
|
final Set<String> routes;
|
||||||
|
|
||||||
|
Stop({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.coordinates,
|
||||||
|
required this.provider,
|
||||||
|
}) : routes = {};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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>{};
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
"id": id.id,
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"latitude": coordinates.lat,
|
||||||
|
"longitude": coordinates.long,
|
||||||
|
"provider": provider,
|
||||||
|
"routes": routes.toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
String get summary {
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
if (description != null) {
|
||||||
|
buffer.writeln(description);
|
||||||
|
}
|
||||||
|
if (routes.isNotEmpty) {
|
||||||
|
buffer.writeln("Routes:");
|
||||||
|
routes.forEach(buffer.writeln);
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
30
src/shared/lib/src/utils.dart
Normal file
30
src/shared/lib/src/utils.dart
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/// A JSON object
|
||||||
|
typedef Json = Map<String, dynamic>;
|
||||||
|
|
||||||
|
typedef FromJson<T> = T Function(Json);
|
||||||
|
|
||||||
|
typedef Coordinates = ({double lat, double long});
|
||||||
|
|
||||||
|
/// Utils on [Map].
|
||||||
|
extension MapUtils<K, V> on Map<K, V> {
|
||||||
|
/// Gets all the keys and values as 2-element records.
|
||||||
|
Iterable<(K, V)> get records => entries.map((entry) => (entry.key, entry.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zips two lists, like Python
|
||||||
|
Iterable<(E1, E2)> zip<E1, E2>(List<E1> list1, List<E2> list2) sync* {
|
||||||
|
if (list1.length != list2.length) throw ArgumentError("Trying to zip lists of different lengths");
|
||||||
|
for (var index = 0; index < list1.length; index++) {
|
||||||
|
yield (list1[index], list2[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extensions on lists
|
||||||
|
extension ListUtils<E> on List<E> {
|
||||||
|
/// Iterates over a pair of indexes and elements, like Python
|
||||||
|
Iterable<(int, E)> get enumerate sync* {
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
yield (i, this[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
src/shared/pubspec.yaml
Normal file
17
src/shared/pubspec.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: shared
|
||||||
|
description: A sample command-line application.
|
||||||
|
version: 1.0.0
|
||||||
|
# repository: https://github.com/my_org/my_repo
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.7.0
|
||||||
|
|
||||||
|
# Add regular dependencies here.
|
||||||
|
dependencies:
|
||||||
|
csv: ^6.0.0
|
||||||
|
# path: ^1.8.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^5.0.0
|
||||||
|
test: ^1.24.0
|
||||||
|
very_good_analysis: ^7.0.0
|
Loading…
Reference in a new issue