This commit is contained in:
ImBenji
2024-02-27 16:04:12 +00:00
parent 72ef70901d
commit 2d2dcdaecb
44 changed files with 71333 additions and 27 deletions

View File

@@ -28,8 +28,15 @@ class AnnouncementCache extends AudioCache {
final archive = ZipDecoder().decodeBytes(bytes.buffer.asUint8List());
for (final file in archive) {
if (Announcements.contains(file.name)) {
_audioCache[file.name] = file.content;
String filename = file.name;
if (filename.contains("/")) {
filename = filename.split("/").last;
}
if (Announcements.contains(filename)) {
_audioCache[filename] = file.content;
print("Loaded announcement: ${filename}");
}
}
}

View File

@@ -1,11 +1,21 @@
import 'dart:io';
import 'package:bus_infotainment/pages/audio_cache_test.dart';
import 'package:bus_infotainment/pages/tfl_dataset_test.dart';
import 'package:bus_infotainment/singletons/live_information.dart';
import 'package:flutter/material.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
LiveInformation liveInformation = LiveInformation();
await liveInformation.LoadDatasets();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@@ -14,27 +24,19 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// TRY THIS: Try running your application with "flutter run". You'll see
// the application has a purple toolbar. Then, without quitting the app,
// try changing the seedColor in the colorScheme below to Colors.green
// and then invoke "hot reload" (save your changes or press the "hot
// reload" button in a Flutter-supported IDE, or press "r" if you used
// the command line to start the app).
//
// Notice that the counter didn't reset back to zero; the application
// state is not lost during the reload. To reset the state, use hot
// restart instead.
//
// This works for code too, not just values: Most code changes can be
// tested with just a hot reload.
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
brightness: Brightness.light,
/* light theme settings */
),
darkTheme: ThemeData(
brightness: Brightness.dark,
/* dark theme settings */
),
themeMode: ThemeMode.dark,
routes: {
'/home': (context) => const MyHomePage(title: 'Flutter Demo Home Page'),
'/': (context) => AudioCacheTest(),
'/audiocachetest': (context) => AudioCacheTest(),
'/': (context) => TfL_Dataset_Test(),
},
);

View File

@@ -1,6 +1,6 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:bus_infotainment/audio_cache.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -38,8 +38,8 @@ class AudioCacheTest extends StatelessWidget {
announcements.add(
ElevatedButton(
onPressed: () {
AudioPlayer player = AudioPlayer();
player.play(BytesSource(_announcementCache[key]));
// AudioPlayer player = AudioPlayer();
// player.play(BytesSource(_announcementCache[key]));
},
child: Text(
key

View File

@@ -0,0 +1,156 @@
import 'dart:async';
import 'package:bus_infotainment/singletons/live_information.dart';
import 'package:bus_infotainment/utils/delegates.dart';
import 'package:flutter/material.dart';
import 'package:text_scroll/text_scroll.dart';
class ibus_display extends StatefulWidget {
bool hasBorder = true;
ibus_display({
this.hasBorder = true
});
@override
State<ibus_display> createState() => _ibus_displayState();
}
class _ibus_displayState extends State<ibus_display> {
String topLine = "*** NO MESSAGE ***";
late final ListenerReceipt<AnnouncementQueueEntry> _receipt;
_ibus_displayState(){
LiveInformation liveInformation = LiveInformation();
_receipt = liveInformation.announcementDelegate.addListener((value) {
topLine = value.displayText;
setState(() {
});
});
topLine = liveInformation.CurrentAnnouncement;
}
Timer _timer() => Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
topLine = LiveInformation().CurrentAnnouncement;
});
});
String _padString(String input){
if (input.length < 40){
print("Input is too short");
return input;
}
String prefix = "";
String suffix = "";
for (int i = 0; i < 80; i++){
prefix += " ";
}
return prefix + input + suffix;
}
@override
void dispose() {
LiveInformation().announcementDelegate.removeListener(_receipt);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
child: FittedBox(
alignment: Alignment.center,
child: Stack(
children: [
Container(
// width: double.infinity,
// height: 100,
decoration: BoxDecoration(
color: Colors.black,
border: widget.hasBorder ? Border.all(color: Colors.grey.shade900, width: 2) : null,
),
clipBehavior: Clip.hardEdge,
child: Transform.scale(
scale: 1.3,
transformHitTests: false,
child: Transform.translate(
offset: Offset(0, 4),
child: Column(
children: [
Transform.translate(
offset: Offset(0, 5),
child: Container(
alignment: Alignment.center,
width: 32*4*3,
child: TextScroll(
_padString(topLine),
velocity: Velocity(pixelsPerSecond: Offset(120, 0)),
style: const TextStyle(
fontSize: 20,
color: Colors.orange,
fontFamily: "ibus"
),
),
),
),
Transform.translate(
offset: Offset(0, -7),
child: Text(
"Bus Stopping",
style: const TextStyle(
fontSize: 20,
color: Colors.orange,
fontFamily: "ibus",
height: 1.5
),
),
)
],
),
),
)
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
border: widget.hasBorder ? Border.all(color: Colors.grey.shade900, width: 2) : null,
),
),
)
],
),
),
);
}
}

85
lib/pages/display.dart Normal file
View File

@@ -0,0 +1,85 @@
import 'package:bus_infotainment/pages/components/ibus_display.dart';
import 'package:bus_infotainment/pages/tfl_dataset_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class pages_Display extends StatefulWidget {
final TfL_Dataset_TestState _tfL_Dataset_TestState;
pages_Display(this._tfL_Dataset_TestState, {Key? key}) : super(key: key);
@override
State<pages_Display> createState() => _pages_DisplayState();
}
class _pages_DisplayState extends State<pages_Display> {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: widget._tfL_Dataset_TestState.hideUI ? Colors.black : Theme.of(context).colorScheme.background
),
width: double.infinity,
child: Stack(
children: [
Positioned.fill(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (MediaQuery.of(context).size.width < 600)
Expanded(
child: RotatedBox(
quarterTurns: 1,
child: ibus_display(
hasBorder: false,
),
),
)
else
Expanded(
child: ibus_display(
hasBorder: false,
),
),
],
),
),
Positioned.fill(
child: Container(
alignment: Alignment.bottomRight,
child: IconButton(
icon: Icon(Icons.fullscreen),
onPressed: () {
// Hide the app bar and nav bar
widget._tfL_Dataset_TestState.setState(() {
widget._tfL_Dataset_TestState.hideUI = !widget._tfL_Dataset_TestState.hideUI;
});
setState(() {
});
// Hide the notification bar and make the app full screen and display over notch
if (widget._tfL_Dataset_TestState.hideUI) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
}
},
),
),
)
],
)
);
}
}

410
lib/pages/home.dart Normal file
View File

@@ -0,0 +1,410 @@
import 'package:bus_infotainment/pages/components/ibus_display.dart';
import 'package:bus_infotainment/singletons/live_information.dart';
import 'package:flutter/material.dart';
class pages_Home extends StatelessWidget {
const pages_Home({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text("Home Page"),
ibus_display(),
SizedBox(
height: 10,
),
_QuickAnnouncements(),
],
)
);
}
}
class _QuickAnnouncements extends StatefulWidget {
_QuickAnnouncements({super.key});
@override
State<_QuickAnnouncements> createState() => _QuickAnnouncementsState();
}
class _QuickAnnouncementsState extends State<_QuickAnnouncements> {
List<Widget> announcements = [];
int _currentIndex = 0;
_QuickAnnouncementsState() {
LiveInformation liveInformation = LiveInformation();
for (ManualAnnouncementEntry announcement in liveInformation.manualAnnouncements) {
announcements.add(
_QuickAnnouncement(announcement: announcement, index: liveInformation.manualAnnouncements.indexOf(announcement))
);
}
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: Border.all(
color: Colors.black,
width: 2
),
),
padding: const EdgeInsets.all(2),
child: Column(
children: [
Container(
height: 2,
color: Colors.black,
),
if (_currentIndex < announcements.length)
announcements[_currentIndex + 0]
else
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
),
Container(
height: 2,
color: Colors.black,
),
if (_currentIndex + 1 < announcements.length)
announcements[_currentIndex + 1]
else
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
),
Container(
height: 2,
color: Colors.black,
),
if (_currentIndex + 2 < announcements.length)
announcements[_currentIndex + 2]
else
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
),
Container(
height: 2,
color: Colors.black,
),
if (_currentIndex + 3 < announcements.length)
announcements[_currentIndex + 3]
else
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
),
Container(
height: 2,
color: Colors.black,
),
Container(
height: 40,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
alignment: Alignment.centerRight,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
margin: const EdgeInsets.symmetric(
horizontal: 4
),
child: Container(
child: Stack(
children: [
Container(
width: 40,
height: 40,
child: Icon(
Icons.arrow_upward,
color: Colors.black,
),
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
_currentIndex = wrap(_currentIndex - 4, 0, announcements.length);
setState(() {});
print(_currentIndex);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text(""),
),
)
],
),
)
),
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
margin: const EdgeInsets.symmetric(
horizontal: 4
),
child: Container(
child: Stack(
children: [
Container(
width: 40,
height: 40,
child: Icon(
Icons.arrow_downward,
color: Colors.black,
),
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
_currentIndex = wrap(_currentIndex + 4, 0, announcements.length);
setState(() {});
print(_currentIndex);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text(""),
),
)
],
),
)
),
]
),
),
Container(
height: 2,
color: Colors.black,
),
]
),
);
}
}
int wrap(int i, int j, int length) {
return ((i - j) % length + length) % length;
}
class _QuickAnnouncement extends StatelessWidget {
final ManualAnnouncementEntry announcement;
final int index;
const _QuickAnnouncement({super.key, required this.announcement, required this.index});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
border: const Border.symmetric(
vertical: BorderSide(
color: Colors.black,
width: 2
)
),
),
padding: const EdgeInsets.all(5),
width: double.infinity,
height: 50,
child: Row(
children: [
Text(
announcement.shortName,
style: const TextStyle(
fontSize: 20,
color: Colors.black,
fontFamily: "lcd",
height: 1,
),
),
Expanded(
child: Container(
alignment: Alignment.centerRight,
child: Text(
(index+1).toString(),
style: const TextStyle(
fontSize: 20,
color: Colors.black,
fontFamily: "lcd",
height: 1,
),
),
),
)
],
)
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
LiveInformation liveInformation = LiveInformation();
liveInformation.queueAnnouncement(announcement);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: const Text("More"),
),
)
],
);
}
}

397
lib/pages/routes.dart Normal file
View File

@@ -0,0 +1,397 @@
import 'package:bus_infotainment/singletons/live_information.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:text_scroll/text_scroll.dart';
class pages_Routes extends StatefulWidget {
@override
State<pages_Routes> createState() => _pages_RoutesState();
}
class _pages_RoutesState extends State<pages_Routes> {
final TextEditingController _controller = TextEditingController(text: "");
@override
Widget build(BuildContext context) {
LiveInformation liveInformation = LiveInformation();
List<Widget> routes = [];
routes.add(SizedBox(height: 10));
for (BusRoute route in liveInformation.busSequences!.routes.values) {
if (!route.routeNumber.toLowerCase().contains(_controller.text.toLowerCase())) {
continue;
}
routes.add(_Route(route, this));
routes.add(SizedBox(height: 10));
}
return Container(
color: Theme.of(context).colorScheme.background,
width: double.infinity,
// padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 4,
),
],
),
child: TextField(
controller: _controller,
onChanged: (String value) {
setState(() {});
},
),
),
Expanded(
child: ListView(
children: routes,
),
),
],
)
);
}
}
class _Route extends StatelessWidget {
final BusRoute route;
final _pages_RoutesState tfL_Dataset_TestState;
const _Route(this.route, this.tfL_Dataset_TestState);
@override
Widget build(BuildContext context) {
List<Widget> Variants = [];
for (BusRouteVariant variant in route.routeVariants.values) {
Variants.add(const SizedBox(height: 10));
Variants.add(_Variant(route, variant, tfL_Dataset_TestState));
}
return Container(
alignment: Alignment.center,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 4,
),
],
color: Colors.grey.shade900,
),
margin: const EdgeInsets.symmetric(horizontal: 10),
padding: const EdgeInsets.all(10),
width: 100,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Colors.grey.shade800,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 2,
),
],
),
padding: const EdgeInsets.all(5),
child: Text(
"Route: ${route.routeNumber}",
style: GoogleFonts.montserrat(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
height: 1
),
),
),
ListView(
children: Variants,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
),
],
),
);
}
}
class _Variant extends StatelessWidget {
final BusRoute route;
final BusRouteVariant variant;
final _pages_RoutesState tfL_Dataset_TestState;
const _Variant(this.route, this.variant, this.tfL_Dataset_TestState);
@override
Widget build(BuildContext context) {
return Container(
child: Stack(
children: [
Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.5),
spreadRadius: 2,
blurRadius: 2,
),
],
color: Colors.grey.shade800,
),
padding: const EdgeInsets.all(5),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
"Start:",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(width: 5),
Expanded(
child: TextScroll(
"${variant.busStops.first.formattedStopName}",
mode: TextScrollMode.bouncing,
pauseBetween: const Duration(seconds: 2),
pauseOnBounce: const Duration(seconds: 2),
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.normal,
color: Colors.white,
height: 1,
),
),
),
],
),
SizedBox(height: 5),
Row(
children: [
Text(
"End:",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(width: 5),
Expanded(
child: TextScroll(
"${variant.busStops.last.formattedStopName}",
mode: TextScrollMode.bouncing,
pauseBetween: const Duration(seconds: 2),
pauseOnBounce: const Duration(seconds: 2),
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.normal,
color: Colors.white,
height: 1,
),
),
),
],
),
],
)
),
Positioned.fill(
child: ElevatedButton(
onPressed: () {
print("Variant: ${variant.routeVariant}");
// Open dialog
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
contentPadding: const EdgeInsets.only(
top: 15,
left: 15,
right: 15,
bottom: 8,
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Select the following route?",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.w700,
color: Colors.black,
),
),
Text(
"${route.routeNumber} to ${variant.busStops.last.formattedStopName}",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
],
),
actionsPadding: const EdgeInsets.only(
left: 15,
right: 15,
bottom: 15,
),
actions: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
child: const Text("Cancel"),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
LiveInformation liveInformation = LiveInformation();
liveInformation.setRouteVariant(variant);
tfL_Dataset_TestState.setState(() {});
},
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
),
child: const Text("Confirm"),
),
],
);
}
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
foregroundColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(0),
),
),
child: Container()
),
)
],
)
);
}
}

16
lib/pages/settings.dart Normal file
View File

@@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class pages_Settings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
],
)
);
}
}

View File

@@ -0,0 +1,148 @@
import 'dart:math';
import 'package:bus_infotainment/pages/display.dart';
import 'package:bus_infotainment/pages/home.dart';
import 'package:bus_infotainment/pages/routes.dart';
import 'package:bus_infotainment/pages/settings.dart';
import 'package:bus_infotainment/singletons/live_information.dart';
import 'package:bus_infotainment/tfl_datasets.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:text_scroll/text_scroll.dart';
class TfL_Dataset_Test extends StatefulWidget {
@override
State<TfL_Dataset_Test> createState() => TfL_Dataset_TestState();
}
class TfL_Dataset_TestState extends State<TfL_Dataset_Test> {
int _selectedIndex = 0;
bool hideUI = false;
late final List<Widget> Pages;
TfL_Dataset_TestState() {
Pages = [
pages_Home(),
pages_Routes(),
pages_Display(this),
pages_Settings(),
];
}
@override
Widget build(BuildContext context) {
LiveInformation liveInformation = LiveInformation();
_selectedIndex = min(_selectedIndex, Pages.length - 1);
_selectedIndex = max(_selectedIndex, 0);
return Scaffold(
appBar: !hideUI ? AppBar(
surfaceTintColor: Colors.transparent,
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Bus Infotainment",
style: GoogleFonts.montserrat(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Row(
children: [
Text(
"Selected: ",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
if (liveInformation.getRouteVariant() != null)
Container(
decoration: BoxDecoration(
color: Colors.black,
),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
child: Text(
"${liveInformation.getRouteVariant()!.busRoute.routeNumber} to ${liveInformation.getRouteVariant()!.busStops.last.formattedStopName}",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.orange.shade900,
),
),
)
else
Text(
"None",
style: GoogleFonts.montserrat(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
)
],
),
) : null,
body: Pages[_selectedIndex],
bottomNavigationBar: !hideUI ? NavigationBar(
selectedIndex: _selectedIndex,
destinations: const [
NavigationDestination(
icon: Icon(Icons.home),
label: "Home",
),
NavigationDestination(
icon: Icon(Icons.bus_alert),
label: "Routes",
),
NavigationDestination(
icon: Icon(Icons.tv),
label: "Display",
),
NavigationDestination(
icon: Icon(Icons.settings),
label: "Settings",
),
],
onDestinationSelected: (int index) {
setState(() {
_selectedIndex = index;
});
},
) : null,
);
}
}

View File

@@ -1,5 +1,221 @@
// Singleton
import 'dart:async';
import 'package:bus_infotainment/audio_cache.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:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart';
class LiveInformation {
static final LiveInformation _singleton = LiveInformation._internal();
factory LiveInformation() {
return _singleton;
}
LiveInformation._internal();
Future<void> LoadDatasets() async {
{
// Load the bus sequences
try {
http.Response response = await http.get(Uri.parse('https://tfl.gov.uk/bus-sequences.csv'));
busSequences = BusSequences.fromCSV(response.body);
print("Loaded bus sequences from TFL");
} catch (e) {
String csv = await rootBundle.loadString("assets/datasets/bus-sequences.csv");
busSequences = BusSequences.fromCSV(csv);
print("Loaded bus sequences from assets");
}
}
refreshTimer();
}
Timer refreshTimer() => Timer.periodic(Duration(seconds: 1), (timer) {
_handleAnnouncementQueue();
});
AudioWrapper audioPlayer = AudioWrapper();
AnnouncementCache announcementCache = AnnouncementCache();
List<AnnouncementQueueEntry> announcementQueue = [];
EventDelegate<AnnouncementQueueEntry> announcementDelegate = EventDelegate();
String CurrentAnnouncement = "*** NO MESSAGE ***";
void _handleAnnouncementQueue() async {
print("Handling announcement queue");
if (audioPlayer.state != AudioWrapper_State.Playing) {
if (announcementQueue.isNotEmpty) {
AnnouncementQueueEntry announcement = announcementQueue.first;
announcementDelegate.trigger(announcement);
CurrentAnnouncement = announcement.displayText;
for (AudioWrapperSource source in announcement.audioSources) {
Duration? duration = await audioPlayer.play(source);
if (source == announcement.audioSources.last) {
announcementQueue.removeAt(0);
}
await Future.delayed(duration!);
await Future.delayed(Duration(milliseconds: 150));
}
audioPlayer.stop();
print("Popped announcement queue");
}
}
}
void announceRouteVariant(BusRouteVariant routeVariant) async {
if (routeVariant == null) {
return;
}
String display = "${routeVariant.busRoute.routeNumber} to ${routeVariant.busStops.last.formattedStopName}";
String audio_route = "R_${routeVariant.busRoute.routeNumber}_001.mp3";
String audio_destination = routeVariant.busStops.last.getAudioFileName();
print("Audio file: $audio_route");
await announcementCache.loadAnnouncements([audio_route, audio_destination]);
AudioWrapperSource source_route = AudioWrapperByteSource(announcementCache[audio_route]);
AudioWrapperSource source_destination = AudioWrapperByteSource(announcementCache[audio_destination]);
queueAnnouncement(AnnouncementQueueEntry(
displayText: display,
audioSources: [source_route, AudioWrapperAssetSource("audio/to_destination.wav"), source_destination]
));
}
late BusSequences busSequences;
BusRouteVariant? _currentRouteVariant;
void setRouteVariant(BusRouteVariant routeVariant) {
_currentRouteVariant = routeVariant;
announceRouteVariant(routeVariant);
}
BusRouteVariant? getRouteVariant() {
return _currentRouteVariant;
}
void queueAnnouncement(AnnouncementQueueEntry announcement) {
announcementQueue.add(announcement);
}
List<ManualAnnouncementEntry> manualAnnouncements = [
ManualAnnouncementEntry(
shortName: "Driver Change",
informationText: "Driver Change",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/driverchange.mp3")],
),
ManualAnnouncementEntry(
shortName: "No Standing Upr Deck",
informationText: "No standing on the upper deck",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nostanding.mp3")],
),
ManualAnnouncementEntry(
shortName: "Face Covering",
informationText: "Please wear a face covering!",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/facecovering.mp3")],
),
ManualAnnouncementEntry(
shortName: "Seats Upstairs",
informationText: "Seats are available upstairs",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/seatsupstairs.mp3")],
),
ManualAnnouncementEntry(
shortName: "Bus Terminates Here",
informationText: "Bus terminates here. Please take your belongings with you",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busterminateshere.mp3")],
),
ManualAnnouncementEntry(
shortName: "Bus On Diversion",
informationText: "Bus on diversion. Please listen for further announcements",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/busondiversion.mp3")],
),
ManualAnnouncementEntry(
shortName: "Destination Change",
informationText: "Destination Changed - please listen for further instructions",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/destinationchange.mp3")],
),
ManualAnnouncementEntry(
shortName: "Wheelchair Space",
informationText: "Wheelchair space requested",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace1.mp3")],
),
ManualAnnouncementEntry(
shortName: "Move Down The Bus",
informationText: "Please move down the bus",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/movedownthebus.mp3")],
),
ManualAnnouncementEntry(
shortName: "Next Stop Closed",
informationText: "The next bus stop is closed",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/nextstopclosed.wav")],
),
ManualAnnouncementEntry(
shortName: "CCTV In Operation",
informationText: "CCTV is in operation on this bus",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/cctvoperation.mp3")],
),
ManualAnnouncementEntry(
shortName: "Safe Door Opening",
informationText: "Driver will open the doors when it is safe to do so",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/safedooropening.mp3")],
),
ManualAnnouncementEntry(
shortName: "Buggy Safety",
informationText: "For your child's safety, please remain with your buggy",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/buggysafety.mp3")],
),
ManualAnnouncementEntry(
shortName: "Wheelchair Space 2",
informationText: "Wheelchair priority space required",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/wheelchairspace2.mp3")],
),
ManualAnnouncementEntry(
shortName: "Service Regulation",
informationText: "Regulating service - please listen for further information",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/serviceregulation.mp3")],
),
ManualAnnouncementEntry(
shortName: "Bus Ready To Depart",
informationText: "This bus is ready to depart",
audioSources: [AudioWrapperAssetSource("audio/manual_announcements/readytodepart.mp3")],
),
];
}
class AnnouncementQueueEntry {
final String displayText;
final List<AudioWrapperSource> audioSources;
AnnouncementQueueEntry({required this.displayText, required this.audioSources});
}
class ManualAnnouncementEntry extends AnnouncementQueueEntry {
final String shortName;
ManualAnnouncementEntry({required this.shortName, required String informationText, required List<AudioWrapperSource> audioSources}) : super(displayText: informationText, audioSources: audioSources);
}

125
lib/tfl_datasets.dart Normal file
View File

@@ -0,0 +1,125 @@
import 'dart:typed_data';
import 'package:bus_infotainment/audio_cache.dart';
import 'package:bus_infotainment/utils/NameBeautify.dart';
import 'package:csv/csv.dart';
class BusSequences {
Map<String, BusRoute> routes = {};
BusSequences.fromCSV(String csv) {
List<List<dynamic>> rowsAsListOfValues = const CsvToListConverter().convert(csv);
rowsAsListOfValues.removeAt(0);
for (int i = 0; i < rowsAsListOfValues.length; i++) {
try {
List<dynamic> entries = rowsAsListOfValues[i];
String routeNumber = entries[0].toString();
BusRoute route = routes.containsKey(routeNumber) ? routes[routeNumber]! : BusRoute(routeNumber: routeNumber);
int routeVariant = entries[1];
BusRouteVariant variant = route.routeVariants.containsKey(routeVariant) ? route.routeVariants[routeVariant]! : BusRouteVariant(routeVariant: routeVariant, busRoute: route);
BusRouteStops stop = BusRouteStops();
stop.stopName = entries[6].toString();
stop.stopCode = entries[4].toString();
stop.easting = entries[7];
stop.northing = entries[8];
variant.busStops.add(stop);
route.routeVariants[routeVariant] = variant;
routes[routeNumber] = route;
} catch (e) {
// print("Error parsing bus sequence: $e");
}
}
}
}
class BusRoute {
String routeNumber = "";
AnnouncementCache? announcementCache;
Map<int, BusRouteVariant> routeVariants = {};
BusRoute({
this.routeNumber = "-1",
this.announcementCache,
});
}
class BusRouteVariant {
int routeVariant = -1;
List<BusRouteStops> busStops = [];
late BusRoute busRoute;
BusRouteVariant({
this.routeVariant = -1,
required this.busRoute,
});
}
class BusRouteStops {
String stopName = "";
int easting = -1;
int northing = -1;
String stopCode = "";
String get formattedStopName {
return NameBeautify.beautifyStopName(stopName);
}
String getAudioFileName() {
// Convert the stop name to all caps
String stopName = this.stopName.toUpperCase();
stopName = NameBeautify.beautifyStopName(stopName);
// replace & with N
stopName = stopName.replaceAll('&', 'N');
stopName = stopName.replaceAll('/', '');
stopName = stopName.replaceAll('\'', '');
stopName = stopName.replaceAll(' ', ' ');
// Replace space with underscore
stopName = stopName.replaceAll(' ', '_');
// convert to all caps
stopName = stopName.toUpperCase();
stopName = "S_${stopName}_001.mp3";
return stopName;
}
}

View File

@@ -0,0 +1,69 @@
import 'dart:math';
import 'package:intl/intl.dart';
class NameBeautify {
static final Map<String, String> Longify = {
"ctr": "Centre",
"stn": "Station",
"tn": "Town",
};
static String beautifyStopName(String label) {
String stopName = label.toUpperCase();
// remove <>
stopName = stopName.replaceAll("<>", "");
// remove any parathesese pairs as well as the contents (), [], {}, <>, ><
stopName = stopName.replaceAll(RegExp(r'\(.*\)'), '');
stopName = stopName.replaceAll(RegExp(r'\[.*\]'), '');
stopName = stopName.replaceAll(RegExp(r'\{.*\}'), '');
// stopName = stopName.replaceAll(RegExp(r'\<.*\>'), '');
stopName = stopName.replaceAll(RegExp(r'\>.*\<'), '');
// remove any special characters except & and /
stopName = stopName.replaceAll(RegExp(r'[^a-zA-Z0-9&/ ]'), '');
// remove any double spaces
stopName = stopName.replaceAll(RegExp(r' '), ' ');
// remove any spaces at the start or end of the string
stopName = stopName.trim();
// replace any short words with their long form
for (String phrase in Longify.keys) {
stopName = stopName.replaceAll(RegExp(phrase, caseSensitive: false), Longify[phrase]!);
}
stopName = stopName.toLowerCase();
// Capitalify the first letter of each word
try {
stopName = stopName.split(' ').map((word) => word[0].toUpperCase() + word.substring(1)).join(' ');
} catch (e) {}
return stopName;
}
static String getShortTime(){
// return the HH:MM with AM and PM and make sure that the hour is 12 hour format and it always double digits. IE 01, 02 etc
DateTime now = DateTime.now();
String formatted = DateFormat('hh:mm a').format(now);
return formatted;
}
static String getLongTime() {
DateTime now = DateTime.now();
String formattedTime = DateFormat('HH:mm:ss dd.MM.yyyy').format(now);
return formattedTime;
}
}

View File

@@ -0,0 +1,125 @@
import 'package:audioplayers/audioplayers.dart' as audioplayers;
import 'package:flutter/foundation.dart';
import 'package:just_audio/just_audio.dart' as justaudio;
enum AudioWrapper_State {
Playing,
NotPlaying
}
class AudioWrapper {
audioplayers.AudioPlayer _audioPlayer_AudioPlayer = audioplayers.AudioPlayer();
justaudio.AudioPlayer _justAudio_AudioPlayer = justaudio.AudioPlayer();
justaudio.AudioSource _convertSource_JustAudio(AudioWrapperSource source){
if (source is AudioWrapperByteSource){
return _ByteSource(source.bytes);
} else if (source is AudioWrapperAssetSource){
return justaudio.AudioSource.asset(source.assetPath);
} else {
throw Exception("Unknown source type");
}
}
audioplayers.Source _convertSource_AudioPlayers(AudioWrapperSource source){
if (source is AudioWrapperByteSource){
return audioplayers.BytesSource(source.bytes);
} else if (source is AudioWrapperAssetSource){
return audioplayers.AssetSource(source.assetPath);
} else {
throw Exception("Unknown source type");
}
}
Future<Duration?> play(AudioWrapperSource source) async {
if (kIsWeb) {
// Use just_audio
justaudio.AudioSource audioSource = _convertSource_JustAudio(source);
Duration? duration = await _justAudio_AudioPlayer.setAudioSource(audioSource);
_justAudio_AudioPlayer.play();
return duration;
} else {
// Use audioplayers
audioplayers.Source audioSource = _convertSource_AudioPlayers(source);
await _audioPlayer_AudioPlayer.play(audioSource);
return await _audioPlayer_AudioPlayer.getDuration();
}
}
void stop(){
if (kIsWeb) {
_justAudio_AudioPlayer.stop();
} else {
_audioPlayer_AudioPlayer.stop();
}
}
AudioWrapper_State get state {
if (kIsWeb) {
if (_justAudio_AudioPlayer.playing){
return AudioWrapper_State.Playing;
} else {
return AudioWrapper_State.NotPlaying;
}
} else {
if (_audioPlayer_AudioPlayer.state == audioplayers.PlayerState.playing){
return AudioWrapper_State.Playing;
} else {
return AudioWrapper_State.NotPlaying;
}
}
}
}
class AudioWrapperSource {
}
class AudioWrapperByteSource extends AudioWrapperSource {
Uint8List bytes = Uint8List(0);
AudioWrapperByteSource(Uint8List bytes){
this.bytes = bytes;
}
}
class AudioWrapperAssetSource extends AudioWrapperSource {
String assetPath = "";
AudioWrapperAssetSource(String assetPath){
this.assetPath = assetPath;
}
}
class _ByteSource extends justaudio.StreamAudioSource {
final List<int> bytes;
_ByteSource(this.bytes);
@override
Future<justaudio.StreamAudioResponse> request([int? start, int? end]) async {
start ??= 0;
end ??= bytes.length;
return justaudio.StreamAudioResponse(
sourceLength: bytes.length,
contentLength: end - start,
offset: start,
stream: Stream.value(bytes.sublist(start, end)),
contentType: 'audio/mpeg',
);
}
}

90
lib/utils/delegates.dart Normal file
View File

@@ -0,0 +1,90 @@
import 'package:flutter/cupertino.dart';
/// Event system
class ListenerReceipt<T> {
Function(T) listener;
ListenerReceipt(this.listener);
}
class EventDelegate<T> {
final List<ListenerReceipt<T>> _receipts = [];
ListenerReceipt<T> addListener(Function(T) listener) {
final receipt = ListenerReceipt(listener);
_receipts.add(receipt);
return receipt;
}
void removeListener(ListenerReceipt<T> receipt) {
_receipts.remove(receipt);
print("removed listener");
}
void trigger(T event) {
print("triggering event");
for (var receipt in _receipts) {
print("triggering listener");
try {
receipt.listener(event);
} catch (e) {
print("Error in listener: $e");
removeListener(receipt);
}
}
}
}
// flutter integration
class DelegateBuilder<T> extends StatefulWidget {
final EventDelegate<T> delegate;
final Widget Function(BuildContext, T) builder;
final Widget Function(BuildContext)? defaultBuilder;
DelegateBuilder({required this.delegate, required this.builder, this.defaultBuilder}) : super(key: UniqueKey())
{
print("created delegate builder widget");
}
@override
State<StatefulWidget> createState() => _DelegateBuilderState<T>();
}
class _DelegateBuilderState<T> extends State<DelegateBuilder<T>> {
late ListenerReceipt<T> _receipt;
T? lastEvent;
@override
void initState() {
super.initState();
print("init delegate builder widget");
_receipt = widget.delegate.addListener((event) {
lastEvent = event;
print("triggered");
setState(() {
});
});
}
@override
void dispose() {
super.dispose();
widget.delegate.removeListener(_receipt);
print("disposed");
}
@override
Widget build(BuildContext context) {
print("rebuilt");
print("Valid: ${lastEvent != null}");
return lastEvent == null ? widget.defaultBuilder == null ? Container() : widget.defaultBuilder!(context) : widget.builder(context, lastEvent!);
}
}