Files
Bus-Infotainment--IBus-/lib/backend/live_information.dart
2024-05-17 17:38:34 +01:00

566 lines
16 KiB
Dart

// Singleton
import 'dart:async';
import 'dart:convert';
import 'package:appwrite/appwrite.dart' as appwrite;
import 'package:appwrite/models.dart' as models;
import 'package:bus_infotainment/audio_cache.dart';
import 'package:bus_infotainment/auth/api_constants.dart';
import 'package:bus_infotainment/auth/auth_api.dart';
import 'package:bus_infotainment/backend/modules/announcement.dart';
import 'package:bus_infotainment/backend/modules/commands.dart';
import 'package:bus_infotainment/backend/modules/synced_time.dart';
import 'package:bus_infotainment/backend/modules/tracker.dart';
import 'package:bus_infotainment/backend/modules/tube_info.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:bus_infotainment/utils/audio%20wrapper.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:bus_infotainment/workaround/keepalive_realtime.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:ntp/ntp.dart';
import 'package:permission_handler/permission_handler.dart';
class LiveInformation {
static final LiveInformation _singleton = LiveInformation._internal();
factory LiveInformation() {
return _singleton;
}
LiveInformation._internal();
Future<void> initialize() async {
{
// By default, load the bus sequences from the assets
print("Loading bus sequences from assets");
busSequences = BusSequences.fromCSV(
await rootBundle.loadString("assets/datasets/bus-blinds.csv"),
await rootBundle.loadString("assets/datasets/bus-sequences.csv")
);
print("Loaded bus sequences from assets");
try {
http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv'));
busSequences = BusSequences.fromCSV(
await rootBundle.loadString("assets/datasets/bus-blinds.csv"),
response.body
);
print("Loaded bus sequences from TFL");
} catch (e) {
print("Failed to load bus sequences from TFL. Using local copy.");
}
// Load tube stations
print("Loading tube stations from assets");
tubeStations = TubeStations.fromJson(json.decode(await rootBundle.loadString("assets/datasets/tube_stations.json")));
print("Loaded tube stations from assets");
}
// Initialise modules
syncedTimeModule = SyncedTimeModule();
announcementModule = AnnouncementModule();
initTrackerModule();
print("Initialised LiveInformation");
if (!auth.isAuthenticated()) {
auth.loadAnonymousUser();
}
}
Future<void> initTrackerModule() async {
if (await Permission.location.isGranted) {
trackerModule = TrackerModule();
}
}
// Auth
AuthAPI auth = AuthAPI(
autoLoad: false,
);
String? roomCode;
String? roomDocumentID;
bool isHost = false;
appwrite.RealtimeSubscription? _subscription;
RealtimeKeepAliveConnection? _keepAliveConnection; // This is a workaround for a bug in the appwrite SDK
// Modules
// late CommandModule commandModule; This needs to be deprecated
late BusSequences busSequences;
late AnnouncementModule announcementModule;
late SyncedTimeModule syncedTimeModule;
late TrackerModule trackerModule;
late TubeStations tubeStations;
// Important variables
BusRouteVariant? _currentRouteVariant;
// Events
EventDelegate<BusRouteVariant?> routeVariantDelegate = EventDelegate();
// Internal methods
Future<void> setRouteVariant(BusRouteVariant? routeVariant) async {
if (routeVariant == null) {
_currentRouteVariant = null;
await announcementModule.queueAnnounceByAudioName(
displayText: "*** NO MESSAGE ***",
);
routeVariantDelegate.trigger(null);
return;
}
if (roomCode != null) {
try {
final client = auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
final document = response.documents.first;
// Check if the route is not the same
if (document.data["CurrentRoute"] != routeVariant.busRoute.routeNumber || document.data["CurrentRouteVariant"] != routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant)) {
final updatedDocument = await databases.updateDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id,
data: {
"CurrentRoute": routeVariant.busRoute.routeNumber,
"CurrentRouteVariant": routeVariant.busRoute.routeVariants.values.toList().indexOf(routeVariant),
"LastUpdater": auth.userID,
}
);
print("Updated route on server");
}
} catch (e) {
print("Failed to update route on server");
}
}
Continuation:
// Set the current route variant
_currentRouteVariant = routeVariant;
// Let everyone know that the route variant has been set/changed
routeVariantDelegate.trigger(routeVariant);
// Get all of the files that need to be cached
List<String> audioFiles = [];
for (BusRouteStop stop in routeVariant.busStops) {
audioFiles.add(stop.getAudioFileName());
}
// Cache/Load the audio files
await announcementModule
.announcementCache
.loadAnnouncementsFromBytes(await LiveInformation().announcementModule.getBundleBytes(), [
...audioFiles,
if (!routeVariant.busRoute.routeNumber.toLowerCase().startsWith("ul"))
"R_${routeVariant.busRoute.routeNumber}_001.mp3"
else
"R_RAIL_REPLACEMENT_SERVICE_001.mp3",
]
);
// Display the route variant
announcementModule.queueAnnouncementByRouteVariant(routeVariant: routeVariant);
}
// Public methods
BusRouteVariant? getRouteVariant() {
return _currentRouteVariant;
}
Future<void> setRouteVariantQuery(String routeNumber, int routeVariantIndex) async {
BusRoute route = busSequences.routes[routeNumber]!;
BusRouteVariant routeVariant = route.routeVariants.values.toList()[routeVariantIndex];
await setRouteVariant(
routeVariant
);
}
// Multi device support
Future<void> createRoom(String roomCode) async {
print("Creating room with code $roomCode");
// Update the room code
this.roomCode = roomCode;
// Enable host mode
isHost = true;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Remove any existing documents
final existingDocuments = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
for (var document in existingDocuments.documents) {
await databases.deleteDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: document.$id
);
}
// Create the document
final document = await databases.createDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: appwrite.ID.unique(),
data: {
"SessionID": roomCode,
"LastUpdater": auth.userID,
}
);
// Listen for changes
// { Breaks due to bug in appwrite
// final realtime = appwrite.Realtime(client);
//
// if (_subscription != null) {
// _subscription!.close();
// }
//
// _subscription = realtime.subscribe(
// ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}']
// );
// _subscription!.stream.listen(ServerListener);
// }
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
print("Created room with code $roomCode");
}
Future<void> JoinRoom(String roomCode) async {
print("Joining room with code $roomCode");
// Disable host mode
isHost = false;
// Update the room code
this.roomCode = roomCode;
// Access the database
final client = auth.client;
final databases = appwrite.Databases(client);
// Get the document
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode)
]
);
if (response.documents.isEmpty) {
throw Exception("Room not found");
}
final document = response.documents.first;
// Listen for changes
// {
// final realtime = appwrite.Realtime(client);
//
// if (_subscription != null) {
// _subscription!.close();
// }
//
// _subscription = realtime.subscribe([
// 'databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'
// ]);
//
// _subscription!.stream.listen(ServerListener);
// }
// Listen for changes
if (_keepAliveConnection != null) {
try {
_keepAliveConnection!.close();
} catch (e) {
print("Failed to close connection... oh well");
}
}
String APPWRITE_ENDPOINT_URL = "https://cloud.appwrite.io/v1";
String domain = APPWRITE_ENDPOINT_URL.replaceAll("https://", "").trim();
_keepAliveConnection = RealtimeKeepAliveConnection(
channels: ['databases.6633e85400036415ab0f.collections.6633e85d0020f52f3771.documents.${document.$id}'],
onData: ServerListener,
domain: domain,
client: auth.client,
onError: (e) {
print("Workarround Error: $e");
},
);
_keepAliveConnection!.initialize();
// Update the room document ID
roomDocumentID = document.$id;
// Get the current route
try {
String routeNumber = document.data["CurrentRoute"];
int routeVariantIndex = document.data["CurrentRouteVariant"];
await setRouteVariantQuery(routeNumber, routeVariantIndex);
print("Set route to $routeNumber $routeVariantIndex");
} catch (e) {
print("Failed to set route");
}
print("Joined room with code $roomCode");
}
String? lastCommand;
Future<void> ServerListener(appwrite.RealtimeMessage response) async {
print("Session update");
// Only do something if the document was created or updated
if (!(response.events.first.contains("create") || response.events.first.contains("update"))) {
return;
}
// Get the user that caused the event
String senderID = response.payload["LastUpdater"];
// If the sender is the same as the client, then ignore the event
if (senderID == auth.userID) {
print("Ignoring event");
return;
}
// Check to see if the commands are updated
try {
// Get the new route
String routeNumber = response.payload["CurrentRoute"];
int routeVariantIndex = response.payload["CurrentRouteVariant"];
// If the route arent the same, then update the route
if (routeNumber != _currentRouteVariant!.busRoute.routeNumber || routeVariantIndex != _currentRouteVariant!.busRoute.routeVariants.values.toList().indexOf(_currentRouteVariant!)) {
// Set the route
await setRouteVariantQuery(routeNumber, routeVariantIndex);
// announce the route
// announcementModule.queueAnnouncementByRouteVariant(routeVariant: _currentRouteVariant!);
}
} catch (e) {
print("Failed to set route");
}
// Execute the command
List<String> commands = response.payload["Commands"].cast<String>();
String? command = commands.last;
if (command == lastCommand) {
return;
}
lastCommand = command;
List<String> commandParts = _splitCommand(command);
String commandName = commandParts[0];
if (commandName == "announce") {
print("Announce command received");
String mode = commandParts[1];
print ("Command: $command");
if (mode == "info") {
print("Announce info command received");
announcementModule.queueAnnouncementByInfoIndex(
sendToServer: false,
infoIndex: int.parse(commandParts[2]),
scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[3])),
);
} else if (mode == "dest") {
print("Announce dest command received");
String routeNumber = commandParts[2];
int routeVariantIndex = int.parse(commandParts[3]);
announcementModule.queueAnnouncementByRouteVariant(
sendToServer: false,
routeVariant: busSequences.routes[routeNumber]!.routeVariants.values.toList()[routeVariantIndex],
scheduledTime: DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts[4])),
);
} else if (mode == "manual") {
print("Announce manual command received");
final displayText = commandParts[2];
List<String> audioFileNames = commandParts.sublist(3);
try {
if (int.parse(audioFileNames.last) != null) {
audioFileNames.removeLast();
}
} catch (e) {}
DateTime scheduledTime = LiveInformation().syncedTimeModule.Now().add(Duration(seconds: 1));
try {
if (int.parse(commandParts.last) != null) {
scheduledTime = DateTime.fromMillisecondsSinceEpoch(int.parse(commandParts.last));
}
} catch (e) {}
announcementModule.queueAnnounceByAudioName(
displayText: displayText,
audioNames: audioFileNames,
scheduledTime: scheduledTime,
sendToServer: false
);
}
}
}
String _extractId(String input) {
RegExp regExp = RegExp(r'\("user:(.*)"\)');
Match match = regExp.firstMatch(input)!;
return match.group(1)!;
}
Future<void> SendCommand(String command) async {
final client = auth.client;
final databases = appwrite.Databases(client);
final response = await databases.listDocuments(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
queries: [
appwrite.Query.search("SessionID", roomCode!)
]
);
List<String> pastCommands = [];
response.documents.first.data["Commands"].forEach((element) {
pastCommands.add(element);
});
pastCommands.add(command);
final document = await databases.updateDocument(
databaseId: "6633e85400036415ab0f",
collectionId: "6633e85d0020f52f3771",
documentId: roomDocumentID!,
data: {
"Commands": pastCommands,
"LastUpdater": auth.userID,
}
);
}
List<String> _splitCommand(String command) {
var regex = RegExp(r'([^\s"]+)|"([^"]*)"');
var matches = regex.allMatches(command);
return matches.map((match) => match.group(0)!.replaceAll('"', '')).toList();
}
/*
Everything under this will be considered legacy code
*/
}
class AnnouncementQueueEntry {
final String displayText;
final List<AudioWrapperSource> audioSources;
bool sendToServer = true;
DateTime? scheduledTime;
DateTime? timestamp;
AnnouncementQueueEntry({required this.displayText, required this.audioSources, this.sendToServer = true, this.scheduledTime, this.timestamp});
}
class NamedAnnouncementQueueEntry extends AnnouncementQueueEntry {
final String shortName;
NamedAnnouncementQueueEntry({
required this.shortName,
required String displayText,
required List<AudioWrapperSource> audioSources,
DateTime? scheduledTime,
DateTime? timestamp,
bool sendToServer = true,
}) : super(
displayText: displayText,
audioSources: audioSources,
sendToServer: sendToServer,
scheduledTime: scheduledTime,
timestamp: timestamp,
);
}
var abs = (int value) => value < 0 ? -value : value;