diff --git a/das_client/assets/icons/icon_indicator_checked.svg b/das_client/assets/icons/icon_indicator_checked.svg new file mode 100644 index 00000000..2f755ac8 --- /dev/null +++ b/das_client/assets/icons/icon_indicator_checked.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/das_client/integration_test/test/train_journey_table_test.dart b/das_client/integration_test/test/train_journey_table_test.dart index 9bf97a06..6ef812b9 100644 --- a/das_client/integration_test/test/train_journey_table_test.dart +++ b/das_client/integration_test/test/train_journey_table_test.dart @@ -6,17 +6,159 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/table/curve_p import 'package:das_client/app/pages/journey/train_journey/widgets/table/protection_section_row.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/service_point_row.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/signal_row.dart'; +import 'package:das_client/app/pages/journey/train_journey/widgets/train_journey.dart'; import 'package:das_client/app/pages/profile/profile_page.dart'; import 'package:das_client/app/widgets/table/das_table.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import '../app_test.dart'; import '../util/test_utils.dart'; void main() { group('train journey table test', () { + testWidgets('test breaking series defaults to ??', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: '4816'); + + final breakingSeriesHeaderCell = find.byKey(TrainJourney.breakingSeriesHeaderKey); + expect(breakingSeriesHeaderCell, findsOneWidget); + expect(find.descendant(of: breakingSeriesHeaderCell, matching: find.text('??')), findsNWidgets(1)); + }); + + testWidgets('test default breaking series is taken from train characteristics (R115)', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: 'T5'); + + final breakingSeriesHeaderCell = find.byKey(TrainJourney.breakingSeriesHeaderKey); + expect(breakingSeriesHeaderCell, findsOneWidget); + expect(find.descendant(of: breakingSeriesHeaderCell, matching: find.text('R115')), findsNWidgets(1)); + }); + + testWidgets('test all breakseries options are displayed', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: 'T5'); + + // Open break series bottom sheet + await tapElement(tester, find.byKey(TrainJourney.breakingSeriesHeaderKey)); + + final expectedCategories = {'R', 'A', 'D'}; + + for (final entry in expectedCategories) { + expect(find.text(entry), findsOneWidget); + } + + final expectedOptions = { + 'R105', + 'R115', + 'R125', + 'R135', + 'R150', + 'A50', + 'A60', + 'A65', + 'A70', + 'A75', + 'A80', + 'A85', + 'A95', + 'A105', + 'A115', + 'D30' + }; + + for (final entry in expectedOptions) { + expect(find.text(entry), findsAtLeast(1)); + } + }); + + testWidgets('test message when no breakseries are defined', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: '7839'); + + // Open break series bottom sheet + await tapElement(tester, find.byKey(TrainJourney.breakingSeriesHeaderKey)); + + expect(find.text(l10n.p_train_journey_break_series_empty), findsOneWidget); + }); + + testWidgets('test speed values of default breakSeries (R115)', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: 'T5'); + + final expectedSpeeds = { + 'Genève-Aéroport': '60', + '65.3': '44', // 1. Curve + 'New Line Speed All': '60', + 'Genève': '60', + 'New Line Speed A Missing': '60', + '42.5': '44', // 2. Curve + '40.5': null, // 3. Curve + 'Gland': '60', + }; + + for (final entry in expectedSpeeds.entries) { + final tableRow = findDASTableRowByText(entry.key); + expect(tableRow, findsOneWidget); + + if (entry.value != null) { + final speedText = find.descendant(of: tableRow, matching: find.text(entry.value!)); + expect(speedText, findsOneWidget); + } else { + final textWidgets = find.descendant(of: tableRow, matching: find.byWidgetPredicate((it) => it is Text)); + expect(textWidgets, findsNWidgets(2)); // KM and Kurve text widgets + } + } + }); + + testWidgets('test speed values of missing break Series', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: 'T5'); + + await _selectBreakSeries(tester, breakSeries: 'A85'); + + final breakingSeriesHeaderCell = find.byKey(TrainJourney.breakingSeriesHeaderKey); + expect(breakingSeriesHeaderCell, findsOneWidget); + expect(find.descendant(of: breakingSeriesHeaderCell, matching: find.text('A85')), findsNWidgets(1)); + + final expectedSpeeds = { + 'Genève-Aéroport': '90', + '65.3': '55', // 1. Curve + 'New Line Speed All': '90', + 'Genève': 'XX', + 'New Line Speed A Missing': 'XX', + '42.5': 'XX', // 2. Curve + '40.5': null, // 3. Curve + 'Gland': '90', + }; + + for (final entry in expectedSpeeds.entries) { + final tableRow = findDASTableRowByText(entry.key); + expect(tableRow, findsOneWidget); + + if (entry.value != null) { + final speedText = find.descendant(of: tableRow, matching: find.text(entry.value!)); + expect(speedText, findsOneWidget); + } else { + final textWidgets = find.descendant(of: tableRow, matching: find.byWidgetPredicate((it) => it is Text)); + expect(textWidgets, findsNWidgets(2)); // KM and Kurve text widgets + } + } + }); + testWidgets('test connection track is displayed correctly', (tester) async { await prepareAndStartApp(tester); @@ -131,7 +273,6 @@ void main() { l10n.p_train_journey_table_journey_information_label, l10n.p_train_journey_table_time_label, l10n.p_train_journey_table_advised_speed_label, - l10n.p_train_journey_table_braked_weight_speed_label, l10n.p_train_journey_table_graduated_speed_label, ]; @@ -437,7 +578,8 @@ void main() { final rowsAtKm33_8 = findDASTableRowByText('33.8'); expect(rowsAtKm33_8, findsExactly(2)); final segment1CABStop = rowsAtKm33_8.last; // end should be after other elements at same location - final segment1CABStopIcon = find.descendant(of: segment1CABStop, matching: find.byKey(CABSignalingRow.cabSignalingEndIconKey)); + final segment1CABStopIcon = + find.descendant(of: segment1CABStop, matching: find.byKey(CABSignalingRow.cabSignalingEndIconKey)); expect(segment1CABStopIcon, findsOneWidget); // Track equipment segment without ETCS level 2 should be ignored @@ -449,7 +591,8 @@ void main() { final rowsAtKm12_5 = findDASTableRowByText('12.5'); expect(rowsAtKm12_5, findsExactly(2)); final segment2CABStart = rowsAtKm12_5.first; // start should be before other elements at same location - final segment2CABStartIcon = find.descendant(of: segment2CABStart, matching: find.byKey(CABSignalingRow.cabSignalingStartIconKey)); + final segment2CABStartIcon = + find.descendant(of: segment2CABStart, matching: find.byKey(CABSignalingRow.cabSignalingStartIconKey)); expect(segment2CABStartIcon, findsOneWidget); await tester.dragUntilVisible(find.text('75.3'), scrollableFinder, const Offset(0, -50)); final trackEquipmentTypeChange = findDASTableRowByText('56.8'); @@ -458,13 +601,15 @@ void main() { final rothristServicePointRow = findDASTableRowByText('46.2'); expect(rothristServicePointRow, findsOneWidget); // no CAB signaling at connecting ETCS L2 segments final segment2CABEnd = findDASTableRowByText('39.9'); - final segment2CABEndIcon = find.descendant(of: segment2CABEnd, matching: find.byKey(CABSignalingRow.cabSignalingEndIconKey)); + final segment2CABEndIcon = + find.descendant(of: segment2CABEnd, matching: find.byKey(CABSignalingRow.cabSignalingEndIconKey)); expect(segment2CABEndIcon, findsOneWidget); // CAB segment with end outside train journey and start at 8.3 km await tester.dragUntilVisible(find.text('9.5'), scrollableFinder, const Offset(0, -50)); final segment3CABStart = findDASTableRowByText('8.3'); - final segment3CABStartIcon = find.descendant(of: segment3CABStart, matching: find.byKey(CABSignalingRow.cabSignalingStartIconKey)); + final segment3CABStartIcon = + find.descendant(of: segment3CABStart, matching: find.byKey(CABSignalingRow.cabSignalingStartIconKey)); expect(segment3CABStartIcon, findsOneWidget); }); }); @@ -487,3 +632,15 @@ Future _loadTrainJourney(WidgetTester tester, {required String trainNumber // wait for train journey to load await tester.pumpAndSettle(); } + +Future _selectBreakSeries(WidgetTester tester, {required String breakSeries}) async { + // Open break series bottom sheet + await tapElement(tester, find.byKey(TrainJourney.breakingSeriesHeaderKey)); + + // Check if the bottom sheeet is opened + expect(find.text(l10n.p_train_journey_break_series), findsOneWidget); + await tapElement(tester, find.text(breakSeries)); + + // confirm button + await tapElement(tester, find.text(l10n.c_button_confirm)); +} diff --git a/das_client/integration_test/test/train_journey_test.dart b/das_client/integration_test/test/train_journey_test.dart index cccd7f8d..31aeb7cd 100644 --- a/das_client/integration_test/test/train_journey_test.dart +++ b/das_client/integration_test/test/train_journey_test.dart @@ -1,5 +1,5 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/header/header.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter_test/flutter_test.dart'; import '../app_test.dart'; diff --git a/das_client/integration_test/test/train_search_test.dart b/das_client/integration_test/test/train_search_test.dart index 194ee324..acb52828 100644 --- a/das_client/integration_test/test/train_search_test.dart +++ b/das_client/integration_test/test/train_search_test.dart @@ -1,6 +1,6 @@ import 'package:das_client/util/error_code.dart'; import 'package:das_client/util/format.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/das_client/integration_test/util/test_utils.dart b/das_client/integration_test/util/test_utils.dart index 11f6cf72..d0e26eee 100644 --- a/das_client/integration_test/util/test_utils.dart +++ b/das_client/integration_test/util/test_utils.dart @@ -1,5 +1,5 @@ import 'package:das_client/app/widgets/table/das_table.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/das_client/l10n/strings_de.arb b/das_client/l10n/strings_de.arb index 2e74c26a..d4f8ae78 100644 --- a/das_client/l10n/strings_de.arb +++ b/das_client/l10n/strings_de.arb @@ -1,6 +1,5 @@ { "c_app_name": "DAS Client", - "p_train_selection_load": "Übernehmen", "p_train_selection_trainnumber_description": "Zugnummer", "p_train_selection_ru_description": "EVU", "p_train_selection_date_description": "Datum", @@ -13,7 +12,8 @@ "p_train_journey_table_journey_information_label": "Streckeninformationen", "p_train_journey_table_advised_speed_label": "FE", "p_train_journey_table_graduated_speed_label": "OG", - "p_train_journey_table_braked_weight_speed_label": "R150", + "p_train_journey_break_series": "Bremsreihe", + "p_train_journey_break_series_empty": "Es sind keine Bremsreihen vorhanden", "p_train_journey_table_curve_type_curve": "Kurve", "p_train_journey_table_curve_type_station_exit_curve": "Kurve Ausfahrt", "p_train_journey_table_curve_type_curve_after_halt": "Kurve nach Haltestelle", @@ -44,5 +44,6 @@ "c_error_sfera_request_timeout": "Timeout bei der Anfrage", "c_error_sfera_jp_unavailable": "Fahrordnung nicht vorhanden", "c_error_sfera_sp_invalid": "Unvollständige Daten erhalten", - "c_connection_track_weiche": "Weiche" + "c_connection_track_weiche": "Weiche", + "c_button_confirm": "Übernehmen" } \ No newline at end of file diff --git a/das_client/lib/app.dart b/das_client/lib/app.dart index 8c0724bd..a1f87929 100644 --- a/das_client/lib/app.dart +++ b/das_client/lib/app.dart @@ -1,6 +1,6 @@ import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/nav/app_router.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class App extends StatelessWidget { diff --git a/das_client/lib/app/bloc/train_journey_cubit.dart b/das_client/lib/app/bloc/train_journey_cubit.dart index 916e4da0..7261e76c 100644 --- a/das_client/lib/app/bloc/train_journey_cubit.dart +++ b/das_client/lib/app/bloc/train_journey_cubit.dart @@ -1,12 +1,15 @@ import 'dart:async'; import 'package:das_client/app/model/ru.dart'; +import 'package:das_client/app/model/train_journey_settings.dart'; +import 'package:das_client/model/journey/break_series.dart'; import 'package:das_client/model/journey/journey.dart'; import 'package:das_client/sfera/sfera_component.dart'; import 'package:das_client/util/error_code.dart'; import 'package:fimber/fimber.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:rxdart/rxdart.dart'; part 'train_journey_state.dart'; @@ -20,11 +23,16 @@ class TrainJourneyCubit extends Cubit { Stream get journeyStream => _sferaService.journeyStream; + final _settingsSubject = BehaviorSubject.seeded(TrainJourneySettings()); + + Stream get settingsStream => _settingsSubject.stream; + StreamSubscription? _stateSubscription; void loadTrainJourney() async { final currentState = state; if (currentState is SelectingTrainJourneyState) { + _resetSettings(); final date = currentState.date; final ru = currentState.ru; final trainNumber = currentState.trainNumber; @@ -43,7 +51,7 @@ class TrainJourneyCubit extends Cubit { case SferaServiceState.connecting: case SferaServiceState.handshaking: case SferaServiceState.loadingJourney: - case SferaServiceState.loadingSegments: + case SferaServiceState.loadingAdditionalData: emit(ConnectingState(ru, trainNumber, date)); break; case SferaServiceState.disconnected: @@ -57,6 +65,10 @@ class TrainJourneyCubit extends Cubit { } } + void _resetSettings() { + _settingsSubject.add(TrainJourneySettings()); + } + void updateTrainNumber(String? trainNumber) { if (state is SelectingTrainJourneyState) { emit(SelectingTrainJourneyState( @@ -101,6 +113,10 @@ class TrainJourneyCubit extends Cubit { _stateSubscription?.cancel(); _stateSubscription = null; } + + void updateBreakSeries(BreakSeries selectedBreakSeries) { + _settingsSubject.add(_settingsSubject.value.copyWith(selectedBreakSeries: selectedBreakSeries)); + } } extension ContextBlocExtension on BuildContext { diff --git a/das_client/lib/app/model/train_journey_settings.dart b/das_client/lib/app/model/train_journey_settings.dart new file mode 100644 index 00000000..0df6482f --- /dev/null +++ b/das_client/lib/app/model/train_journey_settings.dart @@ -0,0 +1,15 @@ +import 'package:das_client/model/journey/break_series.dart'; + +class TrainJourneySettings { + TrainJourneySettings({ + this.selectedBreakSeries, + }); + + final BreakSeries? selectedBreakSeries; + + TrainJourneySettings copyWith({BreakSeries? selectedBreakSeries}) { + return TrainJourneySettings( + selectedBreakSeries: selectedBreakSeries ?? this.selectedBreakSeries, + ); + } +} diff --git a/das_client/lib/app/nav/das_navigation_drawer.dart b/das_client/lib/app/nav/das_navigation_drawer.dart index aee710c8..96ee9fbd 100644 --- a/das_client/lib/app/nav/das_navigation_drawer.dart +++ b/das_client/lib/app/nav/das_navigation_drawer.dart @@ -3,7 +3,7 @@ import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/nav/app_router.dart'; import 'package:das_client/app/widgets/app_version_text.dart'; import 'package:das_client/app/widgets/device_id_text.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class DASNavigationDrawer extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/journey_page.dart b/das_client/lib/app/pages/journey/journey_page.dart index f7c4deb6..f5550acb 100644 --- a/das_client/lib/app/pages/journey/journey_page.dart +++ b/das_client/lib/app/pages/journey/journey_page.dart @@ -7,7 +7,7 @@ import 'package:das_client/app/pages/journey/train_journey/train_journey_overvie import 'package:das_client/app/pages/journey/train_selection/train_selection.dart'; import 'package:das_client/auth/authentication_component.dart'; import 'package:das_client/di.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart b/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart index 2ec6d6bb..23ae9000 100644 --- a/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart +++ b/das_client/lib/app/pages/journey/train_journey/train_journey_overview.dart @@ -1,7 +1,7 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/header/adl_notification.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/header/header.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/train_journey.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; // TODO: handle extraLarge font sizes (diff to figma) globally. diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/break_series_selection.dart b/das_client/lib/app/pages/journey/train_journey/widgets/break_series_selection.dart new file mode 100644 index 00000000..aff897ce --- /dev/null +++ b/das_client/lib/app/pages/journey/train_journey/widgets/break_series_selection.dart @@ -0,0 +1,115 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:das_client/app/i18n/i18n.dart'; +import 'package:das_client/app/pages/journey/train_journey/widgets/break_series_selection_button.dart'; +import 'package:das_client/model/journey/break_series.dart'; +import 'package:das_client/model/journey/train_series.dart'; +import 'package:flutter/material.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; + +class BreakSeriesSelection extends StatefulWidget { + const BreakSeriesSelection({required this.availableBreakSeries, required this.selectedBreakSeries, super.key}); + + final Set availableBreakSeries; + final BreakSeries? selectedBreakSeries; + + @override + State createState() => _BreakSeriesSelectionState(); +} + +class _BreakSeriesSelectionState extends State { + BreakSeries? selectedBreakSeries; + + @override + void initState() { + selectedBreakSeries = widget.selectedBreakSeries; + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (widget.availableBreakSeries.isEmpty) { + return SizedBox( + height: 100, + child: Center( + child: Text(context.l10n.p_train_journey_break_series_empty), + ), + ); + } + + final canConfirm = selectedBreakSeries != null && selectedBreakSeries != widget.selectedBreakSeries; + + return Padding( + padding: const EdgeInsets.fromLTRB(sbbDefaultSpacing, 0, sbbDefaultSpacing, 21), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(sbbDefaultSpacing, 0, sbbDefaultSpacing, 0), + child: SizedBox( + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _rows(context), + ), + ), + ), + ), + SizedBox(height: 46), + Padding( + padding: const EdgeInsets.fromLTRB(sbbDefaultSpacing, 0, sbbDefaultSpacing, 0), + child: SBBPrimaryButton( + label: context.l10n.c_button_confirm, + onPressed: canConfirm ? () => context.router.maybePop(selectedBreakSeries) : null), + ) + ], + ), + ); + } + + List _rows(BuildContext context) { + return widget.availableBreakSeries + .map((it) => it.trainSeries) + .toSet() + .map((it) => _trainSeriesRows(context, it)) + .expand((it) => it) + .toList(); + } + + List _trainSeriesRows(BuildContext context, TrainSeries trainSeries) { + final breakSeries = widget.availableBreakSeries.where((it) => it.trainSeries == trainSeries).toList(); + breakSeries.sort((a, b) => a.breakSeries.compareTo(b.breakSeries)); + + return [ + Padding( + padding: const EdgeInsets.fromLTRB(0, sbbDefaultSpacing, 0, sbbDefaultSpacing), + child: Text( + trainSeries.name, + style: SBBTextStyles.mediumBold, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, sbbDefaultSpacing), + child: Wrap( + spacing: sbbDefaultSpacing * 0.75, + runSpacing: sbbDefaultSpacing, + children: List.generate( + breakSeries.length, + (index) { + final breakSerie = breakSeries[index]; + return BreakSeriesSelectionButton( + label: breakSerie.toString(), + currentlySelected: breakSerie == selectedBreakSeries, + onTap: () { + setState(() { + selectedBreakSeries = breakSerie; + }); + }); + }, + ), + ), + ), + ]; + } +} diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/break_series_selection_button.dart b/das_client/lib/app/pages/journey/train_journey/widgets/break_series_selection_button.dart new file mode 100644 index 00000000..45425ba2 --- /dev/null +++ b/das_client/lib/app/pages/journey/train_journey/widgets/break_series_selection_button.dart @@ -0,0 +1,40 @@ +import 'package:das_client/app/widgets/assets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; + +class BreakSeriesSelectionButton extends StatelessWidget { + const BreakSeriesSelectionButton( + {required this.label, required this.currentlySelected, required this.onTap, super.key}); + + final String label; + final bool currentlySelected; + final GestureTapCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Stack( + clipBehavior: Clip.none, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: currentlySelected ? SBBColors.granite : SBBColors.cloud, + ), + width: 72, + height: 48, + child: Center( + child: Text( + label, + style: SBBTextStyles.mediumBold.copyWith(color: currentlySelected ? SBBColors.white : SBBColors.black), + ), + ), + ), + if (currentlySelected) Positioned(top: -6, right: -6, child: SvgPicture.asset(AppAssets.iconIndicatorChecked)) + ], + ), + ); + } +} diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/header/adl_notification.dart b/das_client/lib/app/pages/journey/train_journey/widgets/header/adl_notification.dart index 53c097f9..df382bac 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/header/adl_notification.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/header/adl_notification.dart @@ -1,5 +1,5 @@ import 'package:das_client/app/i18n/i18n.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class ADLNotification extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/header/departure_authorization.dart b/das_client/lib/app/pages/journey/train_journey/widgets/header/departure_authorization.dart index 0b766889..e5eafbd0 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/header/departure_authorization.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/header/departure_authorization.dart @@ -1,4 +1,4 @@ -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class DepartureAuthorization extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/header/header.dart b/das_client/lib/app/pages/journey/train_journey/widgets/header/header.dart index b1629fcb..256e61fb 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/header/header.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/header/header.dart @@ -1,6 +1,6 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/header/main_container.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/header/time_container.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class Header extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/header/main_container.dart b/das_client/lib/app/pages/journey/train_journey/widgets/header/main_container.dart index 9e79eaf8..1390e46a 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/header/main_container.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/header/main_container.dart @@ -4,7 +4,7 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/header/depart import 'package:das_client/app/pages/journey/train_journey/widgets/header/radio_channel.dart'; import 'package:das_client/app/widgets/assets.dart'; import 'package:das_client/app/widgets/widget_extensions.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:das_client/model/journey/journey.dart'; diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/header/radio_channel.dart b/das_client/lib/app/pages/journey/train_journey/widgets/header/radio_channel.dart index c2f959e3..89f00d47 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/header/radio_channel.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/header/radio_channel.dart @@ -1,4 +1,4 @@ -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class RadioChannel extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/header/time_container.dart b/das_client/lib/app/pages/journey/train_journey/widgets/header/time_container.dart index 131faa7a..20c2caaf 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/header/time_container.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/header/time_container.dart @@ -1,4 +1,4 @@ -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class TimeContainer extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/additional_speed_restriction_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/additional_speed_restriction_row.dart index cab347ef..039ca951 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/additional_speed_restriction_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/additional_speed_restriction_row.dart @@ -3,9 +3,9 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/table/base_ro import 'package:das_client/app/widgets/assets.dart'; import 'package:das_client/app/widgets/table/das_table_cell.dart'; import 'package:das_client/model/journey/additional_speed_restriction_data.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; class AdditionalSpeedRestrictionRow extends BaseRowBuilder { static const Key additionalSpeedRestrictionIconKey = Key('addition_speed_restriction_icon_key'); @@ -15,6 +15,7 @@ class AdditionalSpeedRestrictionRow extends BaseRowBuilder extends DASTableRowBuilder { const BaseRowBuilder({ required this.metadata, required this.data, + required this.settings, super.height = rowHeight, this.defaultAlignment = Alignment.bottomCenter, this.rowColor, @@ -22,6 +24,7 @@ class BaseRowBuilder extends DASTableRowBuilder { final Color? rowColor; final Metadata metadata; final T data; + final TrainJourneySettings settings; @override DASTableRow build(BuildContext context) { @@ -84,14 +87,7 @@ class BaseRowBuilder extends DASTableRowBuilder { } DASTableCell graduatedSpeedCell(BuildContext context) { - if (data.speedData == null) { - return DASTableCell.empty(); - } - - return DASTableCell( - child: Text(data.speedData!.resolvedSpeed(metadata.trainSeries, metadata.breakSeries) ?? ''), - alignment: Alignment.center, - ); + return DASTableCell.empty(); } DASTableCell advisedSpeedCell(BuildContext context) { @@ -99,7 +95,17 @@ class BaseRowBuilder extends DASTableRowBuilder { } DASTableCell brakedWeightSpeedCell(BuildContext context) { - return DASTableCell.empty(); + if (data.speedData == null) { + return DASTableCell.empty(); + } + + final currentTrainSeries = settings.selectedBreakSeries?.trainSeries ?? metadata.breakSeries?.trainSeries; + final currentBreakSeries = settings.selectedBreakSeries?.breakSeries ?? metadata.breakSeries?.breakSeries; + + return DASTableCell( + child: Text(data.speedData!.resolvedSpeed(currentTrainSeries, currentBreakSeries) ?? 'XX'), + alignment: Alignment.center, + ); } // TODO: clarify use of different icon cells and set appropriate name diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/cab_signaling_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/cab_signaling_row.dart index 2a407c32..1600b78d 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/cab_signaling_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/cab_signaling_row.dart @@ -12,6 +12,7 @@ class CABSignalingRow extends BaseRowBuilder { CABSignalingRow({ required super.metadata, required super.data, + required super.settings, }); @override diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/bracket_station_body.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/bracket_station_body.dart index 78ca13d8..8ce5df70 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/bracket_station_body.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/bracket_station_body.dart @@ -1,5 +1,5 @@ import 'package:das_client/model/journey/bracket_station.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class BracketStationBody extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart index f98fd752..d8df1811 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart @@ -1,5 +1,5 @@ import 'package:das_client/app/widgets/table/das_table_theme.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class RouteCellBody extends StatelessWidget { diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/connection_track_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/connection_track_row.dart index b0df0a83..55213881 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/connection_track_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/connection_track_row.dart @@ -8,6 +8,7 @@ class ConnectionTrackRow extends BaseRowBuilder { ConnectionTrackRow({ required super.metadata, required super.data, + required super.settings, }); @override diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/curve_point_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/curve_point_row.dart index 75eb904f..3ce40ac0 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/curve_point_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/curve_point_row.dart @@ -12,6 +12,7 @@ class CurvePointRow extends BaseRowBuilder { CurvePointRow({ required super.metadata, required super.data, + required super.settings, }); @override diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/protection_section_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/protection_section_row.dart index 21387a1d..3e787341 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/protection_section_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/protection_section_row.dart @@ -3,7 +3,7 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/table/base_ro import 'package:das_client/app/widgets/assets.dart'; import 'package:das_client/app/widgets/table/das_table_cell.dart'; import 'package:das_client/model/journey/protection_section.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -14,6 +14,7 @@ class ProtectionSectionRow extends BaseRowBuilder { ProtectionSectionRow({ required super.metadata, required super.data, + required super.settings, super.height = rowHeight, }) : super(rowColor: SBBColors.peach); diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart index 9ae98616..00997af5 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/service_point_row.dart @@ -4,7 +4,7 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/table/cells/r import 'package:das_client/app/widgets/assets.dart'; import 'package:das_client/app/widgets/table/das_table_cell.dart'; import 'package:das_client/model/journey/service_point.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -16,6 +16,7 @@ class ServicePointRow extends BaseRowBuilder { ServicePointRow({ required super.metadata, required super.data, + required super.settings, super.height = rowHeight, }) : super(rowColor: metadata.nextStop == data ? SBBColors.royal.withOpacity(0.2) : Colors.transparent); diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/signal_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/signal_row.dart index e0de615f..03a993e6 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/signal_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/signal_row.dart @@ -12,6 +12,7 @@ class SignalRow extends BaseRowBuilder { SignalRow({ required super.metadata, required super.data, + required super.settings, }); @override diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/speed_change_row.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/speed_change_row.dart index 141cba44..65b0f512 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/speed_change_row.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/speed_change_row.dart @@ -7,6 +7,7 @@ class SpeedChangeRow extends BaseRowBuilder { SpeedChangeRow({ required super.metadata, required super.data, + required super.settings, }); @override diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart b/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart index c6d94c3c..c7c4ee5d 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/train_journey.dart @@ -1,8 +1,10 @@ import 'package:das_client/app/bloc/train_journey_cubit.dart'; import 'package:das_client/app/i18n/i18n.dart'; +import 'package:das_client/app/model/train_journey_settings.dart'; +import 'package:das_client/app/pages/journey/train_journey/widgets/break_series_selection.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/additional_speed_restriction_row.dart'; -import 'package:das_client/app/pages/journey/train_journey/widgets/table/connection_track_row.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/cab_signaling_row.dart'; +import 'package:das_client/app/pages/journey/train_journey/widgets/table/connection_track_row.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/curve_point_row.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/protection_section_row.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/service_point_row.dart'; @@ -12,8 +14,9 @@ import 'package:das_client/app/widgets/table/das_table.dart'; import 'package:das_client/app/widgets/table/das_table_column.dart'; import 'package:das_client/app/widgets/table/das_table_row.dart'; import 'package:das_client/model/journey/additional_speed_restriction_data.dart'; -import 'package:das_client/model/journey/connection_track.dart'; +import 'package:das_client/model/journey/break_series.dart'; import 'package:das_client/model/journey/cab_signaling.dart'; +import 'package:das_client/model/journey/connection_track.dart'; import 'package:das_client/model/journey/curve_point.dart'; import 'package:das_client/model/journey/datatype.dart'; import 'package:das_client/model/journey/journey.dart'; @@ -21,70 +24,83 @@ import 'package:das_client/model/journey/protection_section.dart'; import 'package:das_client/model/journey/service_point.dart'; import 'package:das_client/model/journey/signal.dart'; import 'package:das_client/model/journey/speed_change.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; class TrainJourney extends StatelessWidget { const TrainJourney({super.key}); + static const Key breakingSeriesHeaderKey = Key('breaking_series_header_key'); + @override Widget build(BuildContext context) { final bloc = context.trainJourneyCubit; - return StreamBuilder( - stream: bloc.journeyStream, + return StreamBuilder>( + stream: CombineLatestStream.list([bloc.journeyStream, bloc.settingsStream]), builder: (context, snapshot) { - final Journey? journey = snapshot.data; - if (journey == null) { + if (!snapshot.hasData || snapshot.data?[0] == null) { return Container(); } - return _body(context, journey); + final journey = snapshot.data![0] as Journey; + final settings = snapshot.data![1] as TrainJourneySettings; + + return _body(context, journey, settings); }, ); } - Widget _body(BuildContext context, Journey journey) { + Widget _body(BuildContext context, Journey journey, TrainJourneySettings settings) { return Padding( padding: const EdgeInsets.symmetric(horizontal: sbbDefaultSpacing * 0.5), child: DASTable( - columns: _columns(context), - rows: _rows(context, journey), + columns: _columns(context, journey, settings), + rows: _rows(context, journey, settings), ), ); } - List _rows(BuildContext context, Journey journey) { + List _rows(BuildContext context, Journey journey, TrainJourneySettings settings) { return List.generate(journey.data.length, (index) { final rowData = journey.data[index]; switch (rowData.type) { case Datatype.servicePoint: - return ServicePointRow(metadata: journey.metadata, data: rowData as ServicePoint).build(context); + return ServicePointRow(metadata: journey.metadata, data: rowData as ServicePoint, settings: settings) + .build(context); case Datatype.protectionSection: - return ProtectionSectionRow(metadata: journey.metadata, data: rowData as ProtectionSection).build(context); + return ProtectionSectionRow( + metadata: journey.metadata, data: rowData as ProtectionSection, settings: settings) + .build(context); case Datatype.curvePoint: - return CurvePointRow(metadata: journey.metadata, data: rowData as CurvePoint).build(context); + return CurvePointRow(metadata: journey.metadata, data: rowData as CurvePoint, settings: settings) + .build(context); case Datatype.signal: - return SignalRow(metadata: journey.metadata, data: rowData as Signal).build(context); + return SignalRow(metadata: journey.metadata, data: rowData as Signal, settings: settings).build(context); case Datatype.additionalSpeedRestriction: return AdditionalSpeedRestrictionRow( - metadata: journey.metadata, data: rowData as AdditionalSpeedRestrictionData) + metadata: journey.metadata, data: rowData as AdditionalSpeedRestrictionData, settings: settings) .build(context); case Datatype.connectionTrack: - return ConnectionTrackRow(metadata: journey.metadata, data: rowData as ConnectionTrack).build(context); + return ConnectionTrackRow(metadata: journey.metadata, data: rowData as ConnectionTrack, settings: settings) + .build(context); case Datatype.speedChange: - return SpeedChangeRow(metadata: journey.metadata, data: rowData as SpeedChange).build(context); + return SpeedChangeRow(metadata: journey.metadata, data: rowData as SpeedChange, settings: settings) + .build(context); case Datatype.cabSignaling: - return CABSignalingRow( - metadata: journey.metadata, - data: rowData as CABSignaling, - ).build(context); + return CABSignalingRow(metadata: journey.metadata, data: rowData as CABSignaling, settings: settings) + .build(context); } }); } - List _columns(BuildContext context) { + List _columns(BuildContext context, Journey journey, TrainJourneySettings settings) { + final speedLabel = settings.selectedBreakSeries != null + ? '${settings.selectedBreakSeries!.trainSeries.name}${settings.selectedBreakSeries!.breakSeries}' + : '${journey.metadata.breakSeries?.trainSeries.name ?? '?'}${journey.metadata.breakSeries?.breakSeries ?? '?'}'; + return [ DASTableColumn(child: Text(context.l10n.p_train_journey_table_kilometre_label), width: 64.0), DASTableColumn(child: Text(context.l10n.p_train_journey_table_time_label), width: 100.0), @@ -105,9 +121,31 @@ class TrainJourney extends StatelessWidget { end: BorderSide(color: SBBColors.cloud, width: 2.0), ), ), - DASTableColumn(child: Text(context.l10n.p_train_journey_table_braked_weight_speed_label), width: 62.0), + // TODO: find out what to do when break series is not defined + DASTableColumn( + child: Text(speedLabel), + width: 62.0, + onTap: () => _onBreakSeriesTap(context, journey, settings), + headerKey: breakingSeriesHeaderKey), DASTableColumn(child: Text(context.l10n.p_train_journey_table_advised_speed_label), width: 62.0), DASTableColumn(width: 40.0), // actions ]; } + + void _onBreakSeriesTap(BuildContext context, Journey journey, TrainJourneySettings settings) { + final trainJourneyCubit = context.trainJourneyCubit; + + showSBBModalSheet( + context: context, + title: context.l10n.p_train_journey_break_series, + constraints: BoxConstraints(), + child: BreakSeriesSelection( + availableBreakSeries: journey.metadata.availableBreakSeries, + selectedBreakSeries: settings.selectedBreakSeries ?? journey.metadata.breakSeries)) + .then( + (newValue) => { + if (newValue != null) {trainJourneyCubit.updateBreakSeries(newValue)} + }, + ); + } } diff --git a/das_client/lib/app/pages/journey/train_selection/train_selection.dart b/das_client/lib/app/pages/journey/train_selection/train_selection.dart index 1e6a0361..4bb54aa2 100644 --- a/das_client/lib/app/pages/journey/train_selection/train_selection.dart +++ b/das_client/lib/app/pages/journey/train_selection/train_selection.dart @@ -4,7 +4,7 @@ import 'package:das_client/app/model/ru.dart'; import 'package:das_client/app/widgets/header.dart'; import 'package:das_client/util/error_code.dart'; import 'package:das_client/util/format.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -115,7 +115,7 @@ class _TrainSelectionState extends State { return Padding( padding: const EdgeInsets.symmetric(vertical: sbbDefaultSpacing, horizontal: sbbDefaultSpacing / 2), child: SBBPrimaryButton( - label: context.l10n.p_train_selection_load, + label: context.l10n.c_button_confirm, onPressed: _canContinue(state) ? () => context.trainJourneyCubit.loadTrainJourney() : null, ), ); diff --git a/das_client/lib/app/pages/links/links_page.dart b/das_client/lib/app/pages/links/links_page.dart index dc678125..ffe96a05 100644 --- a/das_client/lib/app/pages/links/links_page.dart +++ b/das_client/lib/app/pages/links/links_page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/nav/das_navigation_drawer.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; @RoutePage() diff --git a/das_client/lib/app/pages/login/login_page.dart b/das_client/lib/app/pages/login/login_page.dart index e9ab8012..6e63e0b7 100644 --- a/das_client/lib/app/pages/login/login_page.dart +++ b/das_client/lib/app/pages/login/login_page.dart @@ -4,7 +4,7 @@ import 'package:das_client/di.dart'; import 'package:das_client/flavor.dart'; import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/nav/app_router.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:fimber/fimber.dart'; import 'package:flutter/material.dart'; diff --git a/das_client/lib/app/pages/profile/profile_page.dart b/das_client/lib/app/pages/profile/profile_page.dart index c1e1e668..8f52cda8 100644 --- a/das_client/lib/app/pages/profile/profile_page.dart +++ b/das_client/lib/app/pages/profile/profile_page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/nav/das_navigation_drawer.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; @RoutePage() diff --git a/das_client/lib/app/pages/settings/settings_page.dart b/das_client/lib/app/pages/settings/settings_page.dart index 6e3b6438..68f1610f 100644 --- a/das_client/lib/app/pages/settings/settings_page.dart +++ b/das_client/lib/app/pages/settings/settings_page.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/nav/das_navigation_drawer.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; @RoutePage() diff --git a/das_client/lib/app/widgets/app_version_text.dart b/das_client/lib/app/widgets/app_version_text.dart index 802e361a..139670f1 100644 --- a/das_client/lib/app/widgets/app_version_text.dart +++ b/das_client/lib/app/widgets/app_version_text.dart @@ -1,4 +1,4 @@ -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; diff --git a/das_client/lib/app/widgets/assets.dart b/das_client/lib/app/widgets/assets.dart index ab95aca0..5f6020b1 100644 --- a/das_client/lib/app/widgets/assets.dart +++ b/das_client/lib/app/widgets/assets.dart @@ -15,4 +15,5 @@ class AppAssets { static const iconSignalLaneChange = '$_iconsDir/icon_signal_line_change.svg'; static const iconCabStart = '$_iconsDir/icon_cab_start.svg'; static const iconCabEnd = '$_iconsDir/icon_cab_end.svg'; + static const iconIndicatorChecked = '$_iconsDir/icon_indicator_checked.svg'; } diff --git a/das_client/lib/app/widgets/device_id_text.dart b/das_client/lib/app/widgets/device_id_text.dart index cb959d57..8b417bb3 100644 --- a/das_client/lib/app/widgets/device_id_text.dart +++ b/das_client/lib/app/widgets/device_id_text.dart @@ -1,5 +1,5 @@ import 'package:das_client/util/device_id_info.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class DeviceIdText extends StatelessWidget { diff --git a/das_client/lib/app/widgets/header.dart b/das_client/lib/app/widgets/header.dart index fa60d162..b4f4dbc8 100644 --- a/das_client/lib/app/widgets/header.dart +++ b/das_client/lib/app/widgets/header.dart @@ -1,4 +1,4 @@ -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; class Header extends StatelessWidget { diff --git a/das_client/lib/app/widgets/table/das_table.dart b/das_client/lib/app/widgets/table/das_table.dart index c00244b4..0388bfd2 100644 --- a/das_client/lib/app/widgets/table/das_table.dart +++ b/das_client/lib/app/widgets/table/das_table.dart @@ -5,7 +5,7 @@ import 'package:das_client/app/widgets/table/das_table_cell.dart'; import 'package:das_client/app/widgets/table/das_table_column.dart'; import 'package:das_client/app/widgets/table/das_table_row.dart'; import 'package:das_client/app/widgets/table/das_table_theme.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; /// [DASTable] provides a the basic structure for a train journey table. @@ -107,10 +107,11 @@ class DASTable extends StatelessWidget { Widget _headerCell(DASTableColumn column) { return Builder(builder: (context) { final tableThemeData = DASTableTheme.of(context)?.data; - return _TableCellWrapper( + final headerCell = _TableCellWrapper( expanded: column.expanded, width: column.width, child: Container( + key: column.headerKey, decoration: BoxDecoration( border: tableThemeData?.headingRowBorder ?? column.border, color: column.color ?? tableThemeData?.headingRowColor, @@ -126,6 +127,12 @@ class DASTable extends StatelessWidget { ), ), ); + return column.onTap != null + ? GestureDetector( + onTap: column.onTap, + child: headerCell, + ) + : headerCell; }); } diff --git a/das_client/lib/app/widgets/table/das_table_column.dart b/das_client/lib/app/widgets/table/das_table_column.dart index 0af7e041..729acdf9 100644 --- a/das_client/lib/app/widgets/table/das_table_column.dart +++ b/das_client/lib/app/widgets/table/das_table_column.dart @@ -1,5 +1,5 @@ import 'package:das_client/app/widgets/table/das_table_cell.dart'; -import 'package:design_system_flutter/design_system_flutter.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter/material.dart'; /// Represents a column in the [DASTable] with optional styling and width constraints. @@ -17,6 +17,8 @@ class DASTableColumn { this.width, this.hidden = false, this.alignment = Alignment.center, + this.onTap, + this.headerKey, }) : assert((width != null && width > 0) || expanded); /// The content of the column header as a widget. @@ -42,5 +44,11 @@ class DASTableColumn { /// Whether the column is visible or not. final bool hidden; + /// Callback for tap events on the column header. + final GestureTapCallback? onTap; + + /// Key for the header cell + final Key? headerKey; + get isVisible => !hidden; } diff --git a/das_client/lib/model/journey/break_series.dart b/das_client/lib/model/journey/break_series.dart new file mode 100644 index 00000000..13536302 --- /dev/null +++ b/das_client/lib/model/journey/break_series.dart @@ -0,0 +1,27 @@ +import 'package:das_client/model/journey/train_series.dart'; + +class BreakSeries { + BreakSeries({ + required this.trainSeries, + required this.breakSeries, + }); + + final TrainSeries trainSeries; + final int breakSeries; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is BreakSeries && + runtimeType == other.runtimeType && + trainSeries == other.trainSeries && + breakSeries == other.breakSeries; + + @override + int get hashCode => trainSeries.hashCode ^ breakSeries.hashCode; + + @override + String toString() { + return '${trainSeries.name}$breakSeries'; + } +} diff --git a/das_client/lib/model/journey/curve_point.dart b/das_client/lib/model/journey/curve_point.dart index aa6d9b0f..de4f7dcd 100644 --- a/das_client/lib/model/journey/curve_point.dart +++ b/das_client/lib/model/journey/curve_point.dart @@ -5,18 +5,21 @@ class CurvePoint extends BaseData { CurvePoint({ required super.order, required super.kilometre, + super.speedData, this.curvePointType, this.curveType, + this.text, this.comment, }) : super(type: Datatype.curvePoint); final CurvePointType? curvePointType; final CurveType? curveType; + final String? text; final String? comment; @override String toString() { - return "CurvePoint(order: $order, kilometre: $kilometre, curvePointType: $curvePointType, curveType: $curveType, comment: '$comment')"; + return "CurvePoint(order: $order, kilometre: $kilometre, curvePointType: $curvePointType, curveType: $curveType, text: '$text', comment: '$comment')"; } } diff --git a/das_client/lib/model/journey/metadata.dart b/das_client/lib/model/journey/metadata.dart index 09c7eab7..a4be5262 100644 --- a/das_client/lib/model/journey/metadata.dart +++ b/das_client/lib/model/journey/metadata.dart @@ -1,8 +1,8 @@ import 'package:das_client/model/journey/additional_speed_restriction.dart'; import 'package:das_client/model/journey/base_data.dart'; +import 'package:das_client/model/journey/break_series.dart'; import 'package:das_client/model/journey/service_point.dart'; import 'package:das_client/model/journey/track_equipment.dart'; -import 'package:das_client/model/journey/train_series.dart'; class Metadata { Metadata({ @@ -10,10 +10,10 @@ class Metadata { this.currentPosition, this.routeStart, this.routeEnd, - this.trainSeries = TrainSeries.R, - this.breakSeries = 150, + this.breakSeries, this.additionalSpeedRestrictions = const [], this.nonStandardTrackEquipmentSegments = const [], + this.availableBreakSeries = const {}, }); final ServicePoint? nextStop; @@ -22,6 +22,6 @@ class Metadata { final BaseData? routeStart; final BaseData? routeEnd; final List nonStandardTrackEquipmentSegments; - final TrainSeries trainSeries; - final int breakSeries; + final BreakSeries? breakSeries; + final Set availableBreakSeries; } diff --git a/das_client/lib/model/journey/service_point.dart b/das_client/lib/model/journey/service_point.dart index 06e27a17..67f20859 100644 --- a/das_client/lib/model/journey/service_point.dart +++ b/das_client/lib/model/journey/service_point.dart @@ -11,6 +11,7 @@ class ServicePoint extends BaseData { required this.isStation, required super.order, required super.kilometre, + super.speedData, this.bracketStation, }) : super(type: Datatype.servicePoint); @@ -22,6 +23,6 @@ class ServicePoint extends BaseData { @override String toString() { - return 'ServicePoint(order: $order, kilometre: $kilometre, name: $name, mandatoryStop: $mandatoryStop, isStop: $isStop, isStation: $isStation, bracketStation: $bracketStation)'; + return 'ServicePoint(order: $order, kilometre: $kilometre, name: $name, mandatoryStop: $mandatoryStop, isStop: $isStop, isStation: $isStation, bracketStation: $bracketStation, speedData: $speedData)'; } } diff --git a/das_client/lib/model/journey/speed_data.dart b/das_client/lib/model/journey/speed_data.dart index a47d9a56..9d37436d 100644 --- a/das_client/lib/model/journey/speed_data.dart +++ b/das_client/lib/model/journey/speed_data.dart @@ -7,7 +7,9 @@ class SpeedData { final List velocities; - String? resolvedSpeed(TrainSeries trainSeries, int breakSeries) { + String? resolvedSpeed(TrainSeries? trainSeries, int? breakSeries) { + if (trainSeries == null) return null; + final trainSeriesVelocities = velocities.where((it) => it.trainSeries == trainSeries); final exactMatchingVelocity = trainSeriesVelocities.firstWhereOrNull((it) => it.breakSeries == breakSeries); return exactMatchingVelocity?.speed ?? diff --git a/das_client/lib/model/journey/train_series.dart b/das_client/lib/model/journey/train_series.dart index 80eba9f7..160646fb 100644 --- a/das_client/lib/model/journey/train_series.dart +++ b/das_client/lib/model/journey/train_series.dart @@ -1,3 +1,5 @@ +import 'package:collection/collection.dart'; + enum TrainSeries { A, D, @@ -12,4 +14,10 @@ enum TrainSeries { (e) => e.name.toLowerCase() == value.toLowerCase(), ); } + + static TrainSeries? fromOptional(String? value) { + return values.firstWhereOrNull( + (e) => e.name.toLowerCase() == value?.toLowerCase(), + ); + } } diff --git a/das_client/lib/mqtt/src/mqtt_service_impl.dart b/das_client/lib/mqtt/src/mqtt_service_impl.dart index 79fa3578..3e571536 100644 --- a/das_client/lib/mqtt/src/mqtt_service_impl.dart +++ b/das_client/lib/mqtt/src/mqtt_service_impl.dart @@ -50,6 +50,7 @@ class MqttServiceImpl implements MqttService { } if (await _mqttClientConnector.connect(_client, company, train)) { _client.subscribe('${prefix}90940/2/event/$company/$train', MqttQos.exactlyOnce); + _client.subscribe('${prefix}90940/2/event/$company/$train/$_deviceId', MqttQos.exactlyOnce); _client.subscribe('${prefix}90940/2/G2B/$company/$train/$_deviceId', MqttQos.exactlyOnce); Fimber.i("Subscribed to topic with prefix='$prefix'..."); _startUpdateListener(); diff --git a/das_client/lib/sfera/src/db/train_characteristics_entity.dart b/das_client/lib/sfera/src/db/train_characteristics_entity.dart new file mode 100644 index 00000000..dbc99b54 --- /dev/null +++ b/das_client/lib/sfera/src/db/train_characteristics_entity.dart @@ -0,0 +1,36 @@ +import 'package:das_client/sfera/src/model/train_characteristics.dart'; +import 'package:das_client/sfera/src/sfera_reply_parser.dart'; +import 'package:isar/isar.dart'; + +part 'train_characteristics_entity.g.dart'; + +@Collection(accessor: 'trainCharacteristics') +class TrainCharacteristicsEntity { + TrainCharacteristicsEntity( + {required this.id, + required this.tcId, + required this.majorVersion, + required this.minorVersion, + required this.xmlData}); + + final int id; + final String tcId; + final String majorVersion; + final String minorVersion; + final String xmlData; + + TrainCharacteristics toDomain() { + return SferaReplyParser.parse(xmlData); + } +} + +extension TrainCharacteristicsMapperX on TrainCharacteristics { + TrainCharacteristicsEntity toEntity({required int isarId}) { + return TrainCharacteristicsEntity( + id: isarId, + tcId: tcId, + majorVersion: versionMajor, + minorVersion: versionMinor, + xmlData: buildDocument().toString()); + } +} diff --git a/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart b/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart index 1284faec..d65e1941 100644 --- a/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart +++ b/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart @@ -3,6 +3,7 @@ import 'package:das_client/model/journey/additional_speed_restriction.dart'; import 'package:das_client/model/journey/additional_speed_restriction_data.dart'; import 'package:das_client/model/journey/base_data.dart'; import 'package:das_client/model/journey/bracket_station.dart'; +import 'package:das_client/model/journey/break_series.dart'; import 'package:das_client/model/journey/cab_signaling.dart'; import 'package:das_client/model/journey/connection_track.dart'; import 'package:das_client/model/journey/curve_point.dart'; @@ -29,6 +30,7 @@ import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; import 'package:das_client/sfera/src/model/speeds.dart'; import 'package:das_client/sfera/src/model/taf_tap_location.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; import 'package:fimber/fimber.dart'; class SferaModelMapper { @@ -40,16 +42,18 @@ class SferaModelMapper { static const String _protectionSectionNspFacultativeName = 'facultative'; static const String _protectionSectionNspLengthTypeName = 'lengthType'; - static Journey mapToJourney(JourneyProfile journeyProfile, List segmentProfiles) { + static Journey mapToJourney(JourneyProfile journeyProfile, List segmentProfiles, + List trainCharacteristics) { try { - return _mapToJourney(journeyProfile, segmentProfiles); + return _mapToJourney(journeyProfile, segmentProfiles, trainCharacteristics); } catch (e, s) { Fimber.e('Error mapping journey-/segment profiles to journey:', ex: e, stacktrace: s); return Journey.invalid(); } } - static Journey _mapToJourney(JourneyProfile journeyProfile, List segmentProfiles) { + static Journey _mapToJourney(JourneyProfile journeyProfile, List segmentProfiles, + List trainCharacteristics) { final journeyData = []; final segmentProfilesLists = journeyProfile.segmentProfilesLists.toList(); @@ -75,7 +79,8 @@ class SferaModelMapper { // Remove new line speeds that are already present as connection tracks newLineSpeeds.removeWhere((speedChange) => - connectionTracks.firstWhereOrNull((connectionTrack) => connectionTrack.speedData == speedChange.speedData) != null); + connectionTracks.firstWhereOrNull((connectionTrack) => connectionTrack.speedData == speedChange.speedData) != + null); journeyData.addAll(connectionTracks); journeyData.addAll(newLineSpeeds); @@ -92,14 +97,14 @@ class SferaModelMapper { .first; journeyData.add(ServicePoint( - name: _localizedStringFromMultilingualText(tafTapLocation.locationNames), - order: calculateOrder(segmentIndex, timingPoint.location), - mandatoryStop: tpConstraint.stoppingPointInformation?.stopType?.mandatoryStop ?? true, - isStop: tpConstraint.stopSkipPass == StopSkipPass.stoppingPoint, - isStation: tafTapLocation.locationType != TafTapLocationType.stoppingLocation, - bracketStation: _parseBracketStation(tafTapLocations, tafTapLocation), - kilometre: kilometreMap[timingPoint.location] ?? [], - )); + name: _localizedStringFromMultilingualText(tafTapLocation.locationNames), + order: calculateOrder(segmentIndex, timingPoint.location), + mandatoryStop: tpConstraint.stoppingPointInformation?.stopType?.mandatoryStop ?? true, + isStop: tpConstraint.stopSkipPass == StopSkipPass.stoppingPoint, + isStation: tafTapLocation.locationType != TafTapLocationType.stoppingLocation, + bracketStation: _parseBracketStation(tafTapLocations, tafTapLocation), + kilometre: kilometreMap[timingPoint.location] ?? [], + speedData: _speedDataFromSpeeds(tafTapLocation.stationSpeed?.xmlStationSpeed.element))); } _parseAndAddProtectionSections(journeyData, segmentIndex, segmentProfile, kilometreMap); @@ -123,6 +128,7 @@ class SferaModelMapper { journeyData.sort(); + final trainCharacteristic = _resolveFirstTrainCharacteristics(journeyProfile, trainCharacteristics); final servicePoints = journeyData.where((it) => it.type == Datatype.servicePoint).toList(); return Journey( metadata: Metadata( @@ -132,6 +138,13 @@ class SferaModelMapper { routeStart: journeyData.firstOrNull, routeEnd: journeyData.lastOrNull, nonStandardTrackEquipmentSegments: trackEquipmentSegments, + availableBreakSeries: _parseAvailableBreakSeries(journeyData), + breakSeries: trainCharacteristic?.tcFeatures.trainCategoryCode != null && + trainCharacteristic?.tcFeatures.brakedWeightPercentage != null + ? BreakSeries( + trainSeries: trainCharacteristic!.tcFeatures.trainCategoryCode!, + breakSeries: trainCharacteristic.tcFeatures.brakedWeightPercentage!) + : null, ), data: journeyData, ); @@ -281,9 +294,8 @@ class SferaModelMapper { static BracketStation? _parseBracketStation(List allLocations, TafTapLocation tafTapLocation) { for (final tafTapLocationNsp in tafTapLocation.nsp) { if (tafTapLocationNsp.name == _bracketStationNspName) { - final mainStationNsp = tafTapLocationNsp.networkSpecificParameters - .where((it) => it.name == _bracketStationMainStationNspName) - .firstOrNull; + final mainStationNsp = + tafTapLocationNsp.parameters.where((it) => it.name == _bracketStationMainStationNspName).firstOrNull; if (mainStationNsp == null) { Fimber.w('Encountered bracket station without main station NSP declaration: $tafTapLocation'); } else { @@ -312,12 +324,15 @@ class SferaModelMapper { return curvePointsNsp.map((curvePointNsp) { final curvePointTypeValue = curvePointNsp.parameters.withName('curvePointType')?.nspValue; final curveTypeValue = curvePointNsp.parameters.withName('curveType')?.nspValue; + final curveSpeed = curvePointNsp.xmlCurveSpeed?.element; return CurvePoint( order: calculateOrder(segmentIndex, curvePointNsp.location), kilometre: kilometreMap[curvePointNsp.location] ?? [], curvePointType: curvePointTypeValue != null ? CurvePointType.from(curvePointTypeValue) : null, curveType: curveTypeValue != null ? CurveType.from(curveTypeValue) : null, - comment: curvePointNsp.parameters.withName('comment')?.nspValue, + text: curveSpeed?.text, + comment: curveSpeed?.comment, + speedData: _speedDataFromSpeeds(curveSpeed?.speeds), ); }).toList(); } @@ -358,14 +373,34 @@ class SferaModelMapper { }).toList(); } - static SpeedData _speedDataFromSpeeds(Speeds? speeds) { - if (speeds == null) { - return SpeedData(); - } + static SpeedData? _speedDataFromSpeeds(Speeds? speeds) { + if (speeds == null) return null; return SpeedData( velocities: speeds.velocities .map((it) => Velocity( trainSeries: it.trainSeries, reduced: it.reduced, speed: it.speed, breakSeries: it.brakeSeries)) .toList()); } + + static Set _parseAvailableBreakSeries(List journeyData) { + return journeyData + .where((it) => it.speedData != null) + .map((it) => it.speedData!.velocities) + .expand((it) => it) + .where((it) => it.breakSeries != null) + .map((it) => BreakSeries(trainSeries: it.trainSeries, breakSeries: it.breakSeries!)) + .toSet(); + } + + static TrainCharacteristics? _resolveFirstTrainCharacteristics( + JourneyProfile journey, List trainCharacteristics) { + final firstTrainRef = journey.trainCharactericsRefSet.firstOrNull; + if (firstTrainRef == null) return null; + + return trainCharacteristics.firstWhereOrNull((it) => + it.tcId == firstTrainRef.tcId && + it.ruId == firstTrainRef.ruId && + it.versionMajor == firstTrainRef.versionMajor && + it.versionMinor == firstTrainRef.versionMinor); + } } diff --git a/das_client/lib/sfera/src/model/b2g_request.dart b/das_client/lib/sfera/src/model/b2g_request.dart index 4b9e348a..75d4ecee 100644 --- a/das_client/lib/sfera/src/model/b2g_request.dart +++ b/das_client/lib/sfera/src/model/b2g_request.dart @@ -1,6 +1,7 @@ import 'package:das_client/sfera/src/model/jp_request.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; import 'package:das_client/sfera/src/model/sp_request.dart'; +import 'package:das_client/sfera/src/model/tc_request.dart'; class B2gRequest extends SferaXmlElement { static const String elementType = 'B2G_Request'; @@ -15,9 +16,13 @@ class B2gRequest extends SferaXmlElement { factory B2gRequest.createSPRequest(List spRequests) { final request = B2gRequest(); - for (final spRequest in spRequests) { - request.children.add(spRequest); - } + request.children.addAll(spRequests); + return request; + } + + factory B2gRequest.createTCRequest(List tcRequests) { + final request = B2gRequest(); + request.children.addAll(tcRequests); return request; } } diff --git a/das_client/lib/sfera/src/model/curve_point_network_specific_point.dart b/das_client/lib/sfera/src/model/curve_point_network_specific_point.dart new file mode 100644 index 00000000..0a763678 --- /dev/null +++ b/das_client/lib/sfera/src/model/curve_point_network_specific_point.dart @@ -0,0 +1,10 @@ +import 'package:das_client/sfera/src/model/network_specific_point.dart'; +import 'package:das_client/sfera/src/model/xml_curve_speed.dart'; + +class CurvePointNetworkSpecificPoint extends NetworkSpecificPoint { + static const String elementName = 'curvePoint'; + + CurvePointNetworkSpecificPoint({super.type, super.attributes, super.children, super.value}); + + XmlCurveSpeed? get xmlCurveSpeed => parameters.whereType().firstOrNull; +} diff --git a/das_client/lib/sfera/src/model/curve_speed.dart b/das_client/lib/sfera/src/model/curve_speed.dart new file mode 100644 index 00000000..45475909 --- /dev/null +++ b/das_client/lib/sfera/src/model/curve_speed.dart @@ -0,0 +1,14 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/speeds.dart'; + +class CurveSpeed extends SferaXmlElement { + static const String elementType = 'curveSpeed'; + + CurveSpeed({super.type = elementType, super.attributes, super.children, super.value}); + + Speeds? get speeds => children.whereType().firstOrNull; + + String? get text => attributes['text']; + + String? get comment => attributes['comment']; +} diff --git a/das_client/lib/sfera/src/model/g2b_reply_payload.dart b/das_client/lib/sfera/src/model/g2b_reply_payload.dart index 1d7ecdc9..584d8095 100644 --- a/das_client/lib/sfera/src/model/g2b_reply_payload.dart +++ b/das_client/lib/sfera/src/model/g2b_reply_payload.dart @@ -1,6 +1,7 @@ import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; class G2bReplyPayload extends SferaXmlElement { static const String elementType = 'G2B_ReplyPayload'; @@ -10,4 +11,6 @@ class G2bReplyPayload extends SferaXmlElement { Iterable get journeyProfiles => children.whereType(); Iterable get segmentProfiles => children.whereType(); + + Iterable get trainCharacteristics => children.whereType(); } diff --git a/das_client/lib/sfera/src/model/journey_profile.dart b/das_client/lib/sfera/src/model/journey_profile.dart index b363682a..c27b3aac 100644 --- a/das_client/lib/sfera/src/model/journey_profile.dart +++ b/das_client/lib/sfera/src/model/journey_profile.dart @@ -2,6 +2,7 @@ import 'package:das_client/sfera/src/model/enums/jp_status.dart'; import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; import 'package:das_client/sfera/src/model/segment_profile_list.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/train_characteristics_ref.dart'; import 'package:das_client/sfera/src/model/train_identification.dart'; class JourneyProfile extends SferaXmlElement { @@ -13,6 +14,9 @@ class JourneyProfile extends SferaXmlElement { Iterable get segmentProfilesLists => children.whereType(); + Set get trainCharactericsRefSet => + children.whereType().map((it) => it.trainCharacteristicsRef).expand((it) => it).toSet(); + JpStatus get status => XmlEnum.valueOf(JpStatus.values, attributes['JP_Status']) ?? JpStatus.valid; @override diff --git a/das_client/lib/sfera/src/model/network_specific_parameter.dart b/das_client/lib/sfera/src/model/network_specific_parameter.dart index f3d45810..ea536adf 100644 --- a/das_client/lib/sfera/src/model/network_specific_parameter.dart +++ b/das_client/lib/sfera/src/model/network_specific_parameter.dart @@ -1,6 +1,8 @@ import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; -import 'package:das_client/sfera/src/model/xml_new_line_speed.dart'; import 'package:das_client/sfera/src/model/track_equipment_type_wrapper.dart'; +import 'package:das_client/sfera/src/model/xml_curve_speed.dart'; +import 'package:das_client/sfera/src/model/xml_new_line_speed.dart'; +import 'package:das_client/sfera/src/model/xml_station_speed.dart'; class NetworkSpecificParameter extends SferaXmlElement { static const String elementType = 'NetworkSpecificParameter'; @@ -11,8 +13,12 @@ class NetworkSpecificParameter extends SferaXmlElement { {Map? attributes, List? children, String? value}) { if (attributes?['name'] == XmlNewLineSpeed.elementName) { return XmlNewLineSpeed(attributes: attributes, children: children, value: value); - } else if (attributes?['name'] == TrackEquipmentTypeWrapper.elementName) { + } else if (attributes?['name'] == TrackEquipmentTypeWrapper.elementName) { return TrackEquipmentTypeWrapper(attributes: attributes, children: children, value: value); + } else if (attributes?['name'] == XmlCurveSpeed.elementName) { + return XmlCurveSpeed(attributes: attributes, children: children, value: value); + } else if (attributes?['name'] == XmlStationSpeed.elementName) { + return XmlStationSpeed(attributes: attributes, children: children, value: value); } return NetworkSpecificParameter(attributes: attributes, children: children, value: value); } @@ -31,4 +37,4 @@ class NetworkSpecificParameter extends SferaXmlElement { extension NetworkSpecificParametersExtension on Iterable { NetworkSpecificParameter? withName(String name) => where((it) => it.name == name).firstOrNull; -} \ No newline at end of file +} diff --git a/das_client/lib/sfera/src/model/network_specific_point.dart b/das_client/lib/sfera/src/model/network_specific_point.dart index c8851bf6..8229ca3c 100644 --- a/das_client/lib/sfera/src/model/network_specific_point.dart +++ b/das_client/lib/sfera/src/model/network_specific_point.dart @@ -1,3 +1,4 @@ +import 'package:das_client/sfera/src/model/curve_point_network_specific_point.dart'; import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; import 'package:das_client/sfera/src/model/new_line_speed_network_specific_point.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; @@ -11,6 +12,8 @@ class NetworkSpecificPoint extends SpGenericPoint { factory NetworkSpecificPoint.from({Map? attributes, List? children, String? value}) { if (attributes?['name'] == NewLineSpeedNetworkSpecificPoint.elementName) { return NewLineSpeedNetworkSpecificPoint(attributes: attributes, children: children, value: value); + } else if (attributes?['name'] == CurvePointNetworkSpecificPoint.elementName) { + return CurvePointNetworkSpecificPoint(attributes: attributes, children: children, value: value); } return NetworkSpecificPoint(attributes: attributes, children: children, value: value); } diff --git a/das_client/lib/sfera/src/model/nsp.dart b/das_client/lib/sfera/src/model/nsp.dart index a0e1b41e..b7f5dd46 100644 --- a/das_client/lib/sfera/src/model/nsp.dart +++ b/das_client/lib/sfera/src/model/nsp.dart @@ -8,7 +8,7 @@ abstract class Nsp extends SferaXmlElement { String get name => attributes['name']!; - Iterable get networkSpecificParameters => children.whereType(); + Iterable get parameters => children.whereType(); @override bool validate() { diff --git a/das_client/lib/sfera/src/model/segment_profile_list.dart b/das_client/lib/sfera/src/model/segment_profile_list.dart index 67851e12..7d6d372f 100644 --- a/das_client/lib/sfera/src/model/segment_profile_list.dart +++ b/das_client/lib/sfera/src/model/segment_profile_list.dart @@ -3,6 +3,7 @@ import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; import 'package:das_client/sfera/src/model/sp_zone.dart'; import 'package:das_client/sfera/src/model/temporary_constraints.dart'; import 'package:das_client/sfera/src/model/timing_point_constraints.dart'; +import 'package:das_client/sfera/src/model/train_characteristics_ref.dart'; class SegmentProfileList extends SferaXmlElement { static const String elementType = 'SegmentProfileList'; @@ -19,6 +20,8 @@ class SegmentProfileList extends SferaXmlElement { Iterable get timingPointsContraints => children.whereType(); + Iterable get trainCharacteristicsRef => children.whereType(); + Iterable get asrTemporaryConstrains => children .whereType() .where((it) => it.temporaryConstraintType == TemporaryConstraintType.asr); diff --git a/das_client/lib/sfera/src/model/sp_points.dart b/das_client/lib/sfera/src/model/sp_points.dart index 8fd11d63..67ba0f89 100644 --- a/das_client/lib/sfera/src/model/sp_points.dart +++ b/das_client/lib/sfera/src/model/sp_points.dart @@ -1,3 +1,4 @@ +import 'package:das_client/sfera/src/model/curve_point_network_specific_point.dart'; import 'package:das_client/sfera/src/model/network_specific_point.dart'; import 'package:das_client/sfera/src/model/new_line_speed_network_specific_point.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; @@ -8,7 +9,6 @@ import 'package:das_client/sfera/src/model/virtual_balise.dart'; class SpPoints extends SferaXmlElement { static const String elementType = 'SP_Points'; static const String _protectionSectionNspName = 'protectionSection'; - static const String _curvePointName = 'curvePoint'; SpPoints({super.type = elementType, super.attributes, super.children, super.value}); @@ -16,9 +16,6 @@ class SpPoints extends SferaXmlElement { Iterable get signals => children.whereType(); - Iterable get curvePointsNsp => - children.whereType().where((it) => it.name == _curvePointName); - Iterable get balise => children.whereType(); Iterable get protectionSectionNsp => @@ -26,4 +23,7 @@ class SpPoints extends SferaXmlElement { Iterable get newLineSpeedsNsp => children.whereType(); + + Iterable get curvePointsNsp => + children.whereType(); } diff --git a/das_client/lib/sfera/src/model/station_speed.dart b/das_client/lib/sfera/src/model/station_speed.dart new file mode 100644 index 00000000..0fa45716 --- /dev/null +++ b/das_client/lib/sfera/src/model/station_speed.dart @@ -0,0 +1,7 @@ +import 'package:das_client/sfera/src/model/speeds.dart'; + +class StationSpeed extends Speeds { + static const String elementType = 'stationSpeed'; + + StationSpeed({super.type = elementType, super.attributes, super.children, super.value}); +} diff --git a/das_client/lib/sfera/src/model/station_speed_nsp.dart b/das_client/lib/sfera/src/model/station_speed_nsp.dart new file mode 100644 index 00000000..804c1c8f --- /dev/null +++ b/das_client/lib/sfera/src/model/station_speed_nsp.dart @@ -0,0 +1,15 @@ +import 'package:das_client/sfera/src/model/taf_tap_location_nsp.dart'; +import 'package:das_client/sfera/src/model/xml_station_speed.dart'; + +class StationSpeedNsp extends TafTapLocationNsp { + static const String elementName = 'stationSpeed'; + + StationSpeedNsp({super.type, super.attributes, super.children, super.value}); + + XmlStationSpeed get xmlStationSpeed => children.whereType().first; + + @override + bool validate() { + return validateHasChildOfType() && super.validate(); + } +} diff --git a/das_client/lib/sfera/src/model/taf_tap_location.dart b/das_client/lib/sfera/src/model/taf_tap_location.dart index 576c3570..b66c7bd3 100644 --- a/das_client/lib/sfera/src/model/taf_tap_location.dart +++ b/das_client/lib/sfera/src/model/taf_tap_location.dart @@ -1,6 +1,7 @@ import 'package:das_client/sfera/src/model/enums/taf_tap_location_type.dart'; import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/station_speed_nsp.dart'; import 'package:das_client/sfera/src/model/taf_tap_location_ident.dart'; import 'package:das_client/sfera/src/model/taf_tap_location_name.dart'; import 'package:das_client/sfera/src/model/taf_tap_location_nsp.dart'; @@ -21,6 +22,8 @@ class TafTapLocation extends SferaXmlElement { Iterable get nsp => children.whereType(); + StationSpeedNsp? get stationSpeed => children.whereType().firstOrNull; + @override bool validate() { return validateHasChildOfType() && super.validate(); diff --git a/das_client/lib/sfera/src/model/taf_tap_location_nsp.dart b/das_client/lib/sfera/src/model/taf_tap_location_nsp.dart index 0063b020..bbc7b0d8 100644 --- a/das_client/lib/sfera/src/model/taf_tap_location_nsp.dart +++ b/das_client/lib/sfera/src/model/taf_tap_location_nsp.dart @@ -1,7 +1,16 @@ import 'package:das_client/sfera/src/model/nsp.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/station_speed_nsp.dart'; class TafTapLocationNsp extends Nsp { static const String elementType = 'TAF_TAP_Location_NSP'; TafTapLocationNsp({super.type = elementType, super.attributes, super.children, super.value}); + + factory TafTapLocationNsp.from({Map? attributes, List? children, String? value}) { + if (attributes?['name'] == StationSpeedNsp.elementName) { + return StationSpeedNsp(attributes: attributes, children: children, value: value); + } + return TafTapLocationNsp(attributes: attributes, children: children, value: value); + } } diff --git a/das_client/lib/sfera/src/model/tc_features.dart b/das_client/lib/sfera/src/model/tc_features.dart new file mode 100644 index 00000000..2e426704 --- /dev/null +++ b/das_client/lib/sfera/src/model/tc_features.dart @@ -0,0 +1,13 @@ +import 'package:das_client/model/journey/train_series.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/util/util.dart'; + +class TcFeatures extends SferaXmlElement { + static const String elementType = 'TC_Features'; + + TcFeatures({super.type = elementType, super.attributes, super.children, super.value}); + + int? get brakedWeightPercentage => Util.tryParseInt(attributes['brakedWeightPercentage']); + + TrainSeries? get trainCategoryCode => TrainSeries.fromOptional(attributes['trainCategoryCode']); +} diff --git a/das_client/lib/sfera/src/model/tc_request.dart b/das_client/lib/sfera/src/model/tc_request.dart new file mode 100644 index 00000000..a7b12bbe --- /dev/null +++ b/das_client/lib/sfera/src/model/tc_request.dart @@ -0,0 +1,16 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class TcRequest extends SferaXmlElement { + static const String elementType = 'TC_Request'; + + TcRequest({super.type = elementType, super.attributes, super.children, super.value}); + + factory TcRequest.create({required String id, required String versionMajor, required String versionMinor, required String ruId}) { + final request = TcRequest(); + request.attributes['TC_ID'] = id; + request.attributes['TC_VersionMajor'] = versionMajor; + request.attributes['TC_VersionMinor'] = versionMinor; + request.children.add(SferaXmlElement(type: 'TC_RU_ID', value: ruId)); + return request; + } +} diff --git a/das_client/lib/sfera/src/model/train_characteristics.dart b/das_client/lib/sfera/src/model/train_characteristics.dart new file mode 100644 index 00000000..d19e8bc4 --- /dev/null +++ b/das_client/lib/sfera/src/model/train_characteristics.dart @@ -0,0 +1,28 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/tc_features.dart'; + +class TrainCharacteristics extends SferaXmlElement { + static const String elementType = 'TrainCharacteristics'; + + TrainCharacteristics({super.type = elementType, super.attributes, super.children, super.value}); + + String get tcId => attributes['TC_ID']!; + + String get ruId => childrenWithType('TC_RU_ID').first.value!; + + String get versionMajor => attributes['TC_VersionMajor']!; + + String get versionMinor => attributes['TC_VersionMinor']!; + + TcFeatures get tcFeatures => children.whereType().first; + + @override + bool validate() { + return validateHasChildOfType() && + validateHasAttribute('TC_ID') && + validateHasChild('TC_RU_ID') && + validateHasAttribute('TC_VersionMajor') && + validateHasAttribute('TC_VersionMinor') && + super.validate(); + } +} diff --git a/das_client/lib/sfera/src/model/train_characteristics_ref.dart b/das_client/lib/sfera/src/model/train_characteristics_ref.dart new file mode 100644 index 00000000..9b2d8420 --- /dev/null +++ b/das_client/lib/sfera/src/model/train_characteristics_ref.dart @@ -0,0 +1,46 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class TrainCharacteristicsRef extends SferaXmlElement { + static const String elementType = 'TrainCharacteristicsRef'; + + TrainCharacteristicsRef({super.type = elementType, super.attributes, super.children, super.value}); + + String get tcId => attributes['TC_ID']!; + + String get ruId => childrenWithType('TC_RU_ID').first.value!; + + String get versionMajor => attributes['TC_VersionMajor']!; + + String get versionMinor => attributes['TC_VersionMinor']!; + + double get location => double.parse(attributes['location']!); + + @override + bool validate() { + return validateHasAttribute('TC_ID') && + validateHasChild('TC_RU_ID') && + validateHasAttributeDouble('location') && + validateHasAttribute('TC_VersionMajor') && + validateHasAttribute('TC_VersionMinor') && + super.validate(); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is TrainCharacteristicsRef && + other.tcId == tcId && + other.versionMajor == versionMajor && + other.versionMinor == versionMinor && + other.ruId == ruId; + } + + @override + int get hashCode => tcId.hashCode ^ versionMajor.hashCode ^ versionMinor.hashCode ^ ruId.hashCode; + + @override + String toString() { + return 'TrainCharacteristicsRef(tcId: $tcId, versionMajor: $versionMajor, versionMinor: $versionMinor, ruId: $ruId)'; + } +} diff --git a/das_client/lib/sfera/src/model/xml_curve_speed.dart b/das_client/lib/sfera/src/model/xml_curve_speed.dart new file mode 100644 index 00000000..6abc7094 --- /dev/null +++ b/das_client/lib/sfera/src/model/xml_curve_speed.dart @@ -0,0 +1,9 @@ +import 'package:das_client/sfera/src/model/curve_speed.dart'; +import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; +import 'package:das_client/sfera/src/model/nsp_xml_element.dart'; + +class XmlCurveSpeed extends NetworkSpecificParameter with NspXmlElement { + static const String elementName = 'xmlCurveSpeed'; + + XmlCurveSpeed({super.attributes, super.children, super.value}); +} diff --git a/das_client/lib/sfera/src/model/xml_station_speed.dart b/das_client/lib/sfera/src/model/xml_station_speed.dart new file mode 100644 index 00000000..c30dd931 --- /dev/null +++ b/das_client/lib/sfera/src/model/xml_station_speed.dart @@ -0,0 +1,9 @@ +import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; +import 'package:das_client/sfera/src/model/nsp_xml_element.dart'; +import 'package:das_client/sfera/src/model/station_speed.dart'; + +class XmlStationSpeed extends NetworkSpecificParameter with NspXmlElement { + static const String elementName = 'xmlStationSpeed'; + + XmlStationSpeed({super.attributes, super.children, super.value}); +} diff --git a/das_client/lib/sfera/src/repo/sfera_repository.dart b/das_client/lib/sfera/src/repo/sfera_repository.dart index 050c211a..70aa3dda 100644 --- a/das_client/lib/sfera/src/repo/sfera_repository.dart +++ b/das_client/lib/sfera/src/repo/sfera_repository.dart @@ -1,7 +1,9 @@ import 'package:das_client/sfera/src/db/journey_profile_entity.dart'; import 'package:das_client/sfera/src/db/segment_profile_entity.dart'; +import 'package:das_client/sfera/src/db/train_characteristics_entity.dart'; import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; abstract class SferaRepository { const SferaRepository._(); @@ -10,10 +12,14 @@ abstract class SferaRepository { Future saveSegmentProfile(SegmentProfile segmentProfile); + Future saveTrainCharacteristics(TrainCharacteristics trainCharacteristics); + Future findJourneyProfile(String company, String operationalTrainNumber, DateTime startDate); Future findSegmentProfile(String spId, String majorVersion, String minorVersion); + Future findTrainCharacteristics(String tcId, String majorVersion, String minorVersion); + Stream> observeJourneyProfile( String company, String operationalTrainNumber, DateTime startDate); } diff --git a/das_client/lib/sfera/src/repo/sfera_repository_impl.dart b/das_client/lib/sfera/src/repo/sfera_repository_impl.dart index e65e5fd4..3fd00634 100644 --- a/das_client/lib/sfera/src/repo/sfera_repository_impl.dart +++ b/das_client/lib/sfera/src/repo/sfera_repository_impl.dart @@ -1,8 +1,10 @@ import 'package:das_client/sfera/sfera_component.dart'; import 'package:das_client/sfera/src/db/journey_profile_entity.dart'; import 'package:das_client/sfera/src/db/segment_profile_entity.dart'; +import 'package:das_client/sfera/src/db/train_characteristics_entity.dart'; import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; import 'package:fimber/fimber.dart'; import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; @@ -19,7 +21,9 @@ class SferaRepositoryImpl implements SferaRepository { Fimber.i('Initializing SferaStore...'); final dir = await getApplicationDocumentsDirectory(); _db = await Isar.openAsync( - schemas: [JourneyProfileEntitySchema, SegmentProfileEntitySchema], directory: dir.path, name: 'das'); + schemas: [JourneyProfileEntitySchema, SegmentProfileEntitySchema, TrainCharacteristicsEntitySchema], + directory: dir.path, + name: 'das'); } @override @@ -96,4 +100,36 @@ class SferaRepositoryImpl implements SferaRepository { .startDateEqualTo(startDate) .watch(fireImmediately: true); } + + @override + Future findTrainCharacteristics( + String tcId, String majorVersion, String minorVersion) async { + await _initialized; + return _db.trainCharacteristics + .where() + .tcIdEqualTo(tcId) + .majorVersionEqualTo(majorVersion) + .minorVersionEqualTo(minorVersion) + .findFirst(); + } + + @override + Future saveTrainCharacteristics(TrainCharacteristics trainCharacteristics) async { + await _initialized; + + final existingTrainCharacteristics = await findTrainCharacteristics( + trainCharacteristics.tcId, trainCharacteristics.versionMajor, trainCharacteristics.versionMinor); + if (existingTrainCharacteristics == null) { + final trainCharacteristicsEntity = + trainCharacteristics.toEntity(isarId: _db.trainCharacteristics.autoIncrement()); + Fimber.i( + 'Writing train characteristics to db tcId=${trainCharacteristicsEntity.tcId} majorVersion=${trainCharacteristicsEntity.majorVersion} minorVersion=${trainCharacteristicsEntity.minorVersion}'); + _db.write((isar) { + isar.trainCharacteristics.put(trainCharacteristicsEntity); + }); + } else { + Fimber.i( + 'train characteristics already exists in db tcId=${existingTrainCharacteristics.tcId} majorVersion=${existingTrainCharacteristics.majorVersion} minorVersion=${existingTrainCharacteristics.minorVersion}'); + } + } } diff --git a/das_client/lib/sfera/src/service/sfera_service_impl.dart b/das_client/lib/sfera/src/service/sfera_service_impl.dart index a75d33d2..a8821244 100644 --- a/das_client/lib/sfera/src/service/sfera_service_impl.dart +++ b/das_client/lib/sfera/src/service/sfera_service_impl.dart @@ -10,12 +10,14 @@ import 'package:das_client/sfera/src/model/enums/das_driving_mode.dart'; import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; import 'package:das_client/sfera/src/service/handler/journey_profile_reply_handler.dart'; import 'package:das_client/sfera/src/service/handler/segment_profile_reply_handler.dart'; import 'package:das_client/sfera/src/service/handler/sfera_message_handler.dart'; import 'package:das_client/sfera/src/service/task/handshake_task.dart'; import 'package:das_client/sfera/src/service/task/request_journey_profile_task.dart'; import 'package:das_client/sfera/src/service/task/request_segment_profiles_task.dart'; +import 'package:das_client/sfera/src/service/task/request_train_characteristics_task.dart'; import 'package:das_client/sfera/src/service/task/sfera_task.dart'; import 'package:das_client/util/error_code.dart'; import 'package:fimber/fimber.dart'; @@ -41,6 +43,7 @@ class SferaServiceImpl implements SferaService { OtnId? _otnId; JourneyProfile? _journeyProfile; final List _segmentProfiles = []; + final List _trainCharacteristics = []; @override ErrorCode? lastErrorCode; @@ -108,15 +111,22 @@ class SferaServiceImpl implements SferaService { _messageHandlers.add(requestJourneyTask); requestJourneyTask.execute(onTaskCompleted, onTaskFailed); } else if (task is RequestJourneyProfileTask) { - _stateSubject.add(SferaServiceState.loadingSegments); + _stateSubject.add(SferaServiceState.loadingAdditionalData); final requestSegmentProfilesTask = RequestSegmentProfilesTask( mqttService: _mqttService, sferaRepository: _sferaRepository, otnId: _otnId!, journeyProfile: data); + final requestTrainCharacteristicsTask = RequestTrainCharacteristicsTask( + mqttService: _mqttService, sferaRepository: _sferaRepository, otnId: _otnId!, journeyProfile: data); _journeyProfile = data; _messageHandlers.add(requestSegmentProfilesTask); + _messageHandlers.add(requestTrainCharacteristicsTask); requestSegmentProfilesTask.execute(onTaskCompleted, onTaskFailed); - } else if (task is RequestSegmentProfilesTask) { + requestTrainCharacteristicsTask.execute(onTaskCompleted, onTaskFailed); + } + + if (_stateSubject.value == SferaServiceState.loadingAdditionalData && _allTaskedCompleted()) { _addMessageHandlers(); await _refreshSegmentProfiles(); + await _refreshTrainCharacteristics(); final success = _updateJourney(); if (success) { _stateSubject.add(SferaServiceState.connected); @@ -127,6 +137,10 @@ class SferaServiceImpl implements SferaService { } } + bool _allTaskedCompleted() { + return _messageHandlers.whereType().isEmpty; + } + Future _refreshSegmentProfiles() async { if (_journeyProfile != null) { _segmentProfiles.clear(); @@ -144,12 +158,29 @@ class SferaServiceImpl implements SferaService { } } + Future _refreshTrainCharacteristics() async { + if (_journeyProfile != null) { + _trainCharacteristics.clear(); + + for (final element in _journeyProfile!.trainCharactericsRefSet) { + final trainCharactericsEntity = + await _sferaRepository.findTrainCharacteristics(element.tcId, element.versionMajor, element.versionMinor); + final trainCharacterics = trainCharactericsEntity?.toDomain(); + if (trainCharacterics != null && trainCharacterics.validate()) { + _trainCharacteristics.add(trainCharacterics); + } else { + Fimber.w('Could not find and validate $element'); + } + } + } + } + bool _updateJourney() { if (_journeyProfile != null && _segmentProfiles.isNotEmpty) { Fimber.i('Updating journey stream...'); - final newJourney = SferaModelMapper.mapToJourney(_journeyProfile!, _segmentProfiles); + final newJourney = SferaModelMapper.mapToJourney(_journeyProfile!, _segmentProfiles, _trainCharacteristics); if (newJourney.valid) { - _journeyProfileSubject.add(SferaModelMapper.mapToJourney(_journeyProfile!, _segmentProfiles)); + _journeyProfileSubject.add(newJourney); Fimber.i('Journey updates successfully.'); return true; } else { @@ -168,7 +199,7 @@ class SferaServiceImpl implements SferaService { _messageHandlers.remove(task); lastErrorCode = errorCode; Fimber.e('Task $task failed with error code $errorCode'); - if (task is HandshakeTask || task is RequestJourneyProfileTask || task is RequestSegmentProfilesTask) { + if (_stateSubject.value != SferaServiceState.connected) { disconnect(); } } diff --git a/das_client/lib/sfera/src/service/sfera_service_state.dart b/das_client/lib/sfera/src/service/sfera_service_state.dart index 9ff782aa..b24d61d7 100644 --- a/das_client/lib/sfera/src/service/sfera_service_state.dart +++ b/das_client/lib/sfera/src/service/sfera_service_state.dart @@ -3,7 +3,7 @@ enum SferaServiceState { connecting, handshaking, loadingJourney, - loadingSegments, + loadingAdditionalData, connected, offline } diff --git a/das_client/lib/sfera/src/service/task/request_journey_profile_task.dart b/das_client/lib/sfera/src/service/task/request_journey_profile_task.dart index 7d2479d4..a955d351 100644 --- a/das_client/lib/sfera/src/service/task/request_journey_profile_task.dart +++ b/das_client/lib/sfera/src/service/task/request_journey_profile_task.dart @@ -61,13 +61,19 @@ class RequestJourneyProfileTask extends SferaTask { } Fimber.i( - 'Received G2bReplyPayload response with ${replyMessage.payload!.journeyProfiles.length} JourneyProfiles and ${replyMessage.payload!.segmentProfiles.length} SegmentProfiles...', + 'Received G2bReplyPayload response with ${replyMessage.payload!.journeyProfiles.length} JourneyProfiles, ' + '${replyMessage.payload!.segmentProfiles.length} SegmentProfiles and ' + '${replyMessage.payload!.trainCharacteristics.length} TrainCharacteristics...', ); for (final element in replyMessage.payload!.segmentProfiles) { await _sferaRepository.saveSegmentProfile(element); } + for (final trainCharacteristics in replyMessage.payload!.trainCharacteristics) { + await _sferaRepository.saveTrainCharacteristics(trainCharacteristics); + } + for (final journeyProfile in replyMessage.payload!.journeyProfiles) { await _sferaRepository.saveJourneyProfile(journeyProfile); } diff --git a/das_client/lib/sfera/src/service/task/request_train_characteristics_task.dart b/das_client/lib/sfera/src/service/task/request_train_characteristics_task.dart new file mode 100644 index 00000000..644e9023 --- /dev/null +++ b/das_client/lib/sfera/src/service/task/request_train_characteristics_task.dart @@ -0,0 +1,102 @@ +import 'package:das_client/mqtt/mqtt_component.dart'; +import 'package:das_client/sfera/src/model/b2g_request.dart'; +import 'package:das_client/sfera/src/model/journey_profile.dart'; +import 'package:das_client/sfera/src/model/otn_id.dart'; +import 'package:das_client/sfera/src/model/sfera_b2g_request_message.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; +import 'package:das_client/sfera/src/model/tc_request.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; +import 'package:das_client/sfera/src/model/train_characteristics_ref.dart'; +import 'package:das_client/sfera/src/model/train_identification.dart'; +import 'package:das_client/sfera/src/repo/sfera_repository.dart'; +import 'package:das_client/sfera/src/service/sfera_service.dart'; +import 'package:das_client/sfera/src/service/task/sfera_task.dart'; +import 'package:fimber/fimber.dart'; + +class RequestTrainCharacteristicsTask extends SferaTask> { + RequestTrainCharacteristicsTask( + {required MqttService mqttService, + required SferaRepository sferaRepository, + required this.otnId, + required this.journeyProfile, + super.timeout}) + : _mqttService = mqttService, + _sferaRepository = sferaRepository; + + final MqttService _mqttService; + final OtnId otnId; + final SferaRepository _sferaRepository; + final JourneyProfile journeyProfile; + + late TaskCompleted> _taskCompletedCallback; + late TaskFailed _taskFailedCallback; + + @override + Future execute(TaskCompleted> onCompleted, TaskFailed onFailed) async { + _taskCompletedCallback = onCompleted; + _taskFailedCallback = onFailed; + + await _requestSegmentProfiles(); + } + + Future _requestSegmentProfiles() async { + final missingTrainCharacteristics = await findMissingTrainCharacteristics(); + if (missingTrainCharacteristics.isEmpty) { + Fimber.i('No missing train characteristics found...'); + _taskCompletedCallback(this, []); + return; + } + + final List tcRequests = []; + for (final missingTrainRef in missingTrainCharacteristics) { + tcRequests.add(TcRequest.create( + id: missingTrainRef.tcId, + versionMajor: missingTrainRef.versionMajor, + versionMinor: missingTrainRef.versionMinor, + ruId: missingTrainRef.ruId)); + } + + final trainIdentification = TrainIdentification.create(otnId: otnId); + final sferaB2gRequestMessage = SferaB2gRequestMessage.create( + await SferaService.messageHeader(trainIdentification: trainIdentification, sender: otnId.company), + b2gRequest: B2gRequest.createTCRequest(tcRequests)); + Fimber.i('Sending train characteristics request...'); + + startTimeout(_taskFailedCallback); + _mqttService.publishMessage(otnId.company, SferaService.sferaTrain(otnId.operationalTrainNumber, otnId.startDate), + sferaB2gRequestMessage.buildDocument().toString()); + } + + Future> findMissingTrainCharacteristics() async { + final missingTrainCharacteristics = {}; + + for (final trainRef in journeyProfile.trainCharactericsRefSet) { + final existingTrainCharacteristic = + await _sferaRepository.findTrainCharacteristics(trainRef.tcId, trainRef.versionMajor, trainRef.versionMinor); + if (existingTrainCharacteristic == null) { + missingTrainCharacteristics.add(trainRef); + } + } + + return missingTrainCharacteristics; + } + + @override + Future handleMessage(SferaG2bReplyMessage replyMessage) async { + if (replyMessage.payload != null && replyMessage.payload!.trainCharacteristics.isNotEmpty) { + stopTimeout(); + Fimber.i( + 'Received G2bReplyPayload response with ${replyMessage.payload!.trainCharacteristics.length} TrainCharacteristics...', + ); + + for (final element in replyMessage.payload!.trainCharacteristics) { + await _sferaRepository.saveTrainCharacteristics(element); + } + + _taskCompletedCallback(this, replyMessage.payload!.trainCharacteristics.toList()); + + return true; + } + return false; + } +} diff --git a/das_client/lib/sfera/src/sfera_reply_parser.dart b/das_client/lib/sfera/src/sfera_reply_parser.dart index 9a184274..2a5dfdd1 100644 --- a/das_client/lib/sfera/src/sfera_reply_parser.dart +++ b/das_client/lib/sfera/src/sfera_reply_parser.dart @@ -3,6 +3,7 @@ import 'package:das_client/sfera/src/model/connection_track.dart'; import 'package:das_client/sfera/src/model/current_limitation.dart'; import 'package:das_client/sfera/src/model/current_limitation_change.dart'; import 'package:das_client/sfera/src/model/current_limitation_start.dart'; +import 'package:das_client/sfera/src/model/curve_speed.dart'; import 'package:das_client/sfera/src/model/das_operating_modes_selected.dart'; import 'package:das_client/sfera/src/model/g2b_reply_payload.dart'; import 'package:das_client/sfera/src/model/handshake_acknowledgement.dart'; @@ -32,6 +33,7 @@ import 'package:das_client/sfera/src/model/sp_context_information.dart'; import 'package:das_client/sfera/src/model/sp_points.dart'; import 'package:das_client/sfera/src/model/sp_zone.dart'; import 'package:das_client/sfera/src/model/speeds.dart'; +import 'package:das_client/sfera/src/model/station_speed.dart'; import 'package:das_client/sfera/src/model/stop_type.dart'; import 'package:das_client/sfera/src/model/stopping_point_information.dart'; import 'package:das_client/sfera/src/model/taf_tap_location.dart'; @@ -39,6 +41,7 @@ import 'package:das_client/sfera/src/model/taf_tap_location_ident.dart'; import 'package:das_client/sfera/src/model/taf_tap_location_name.dart'; import 'package:das_client/sfera/src/model/taf_tap_location_nsp.dart'; import 'package:das_client/sfera/src/model/taf_tap_location_reference.dart'; +import 'package:das_client/sfera/src/model/tc_features.dart'; import 'package:das_client/sfera/src/model/temporary_constraint_reason.dart'; import 'package:das_client/sfera/src/model/temporary_constraints.dart'; import 'package:das_client/sfera/src/model/timing_point.dart'; @@ -46,6 +49,8 @@ import 'package:das_client/sfera/src/model/timing_point_constraints.dart'; import 'package:das_client/sfera/src/model/timing_point_reference.dart'; import 'package:das_client/sfera/src/model/tp_id_reference.dart'; import 'package:das_client/sfera/src/model/tp_name.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; +import 'package:das_client/sfera/src/model/train_characteristics_ref.dart'; import 'package:das_client/sfera/src/model/train_identification.dart'; import 'package:das_client/sfera/src/model/velocity.dart'; import 'package:das_client/sfera/src/model/virtual_balise.dart'; @@ -156,7 +161,7 @@ class SferaReplyParser { case StopType.elementType: return StopType(type: type, attributes: attributes, children: children, value: value); case TafTapLocationNsp.elementType: - return TafTapLocationNsp(type: type, attributes: attributes, children: children, value: value); + return TafTapLocationNsp.from(attributes: attributes, children: children, value: value); case NetworkSpecificParameter.elementType: return NetworkSpecificParameter.from(attributes: attributes, children: children, value: value); case CurrentLimitation.elementType: @@ -173,18 +178,28 @@ class SferaReplyParser { return NetworkSpecificArea(type: type, attributes: attributes, children: children, value: value); case AdditionalSpeedRestriction.elementType: return AdditionalSpeedRestriction(type: type, attributes: attributes, children: children, value: value); - case TemporaryConstraints.elementType: + case TemporaryConstraints.elementType: return TemporaryConstraints(type: type, attributes: attributes, children: children, value: value); - case TemporaryConstraintReason.elementType: + case TemporaryConstraintReason.elementType: return TemporaryConstraintReason(type: type, attributes: attributes, children: children, value: value); - case Velocity.elementType: + case Velocity.elementType: return Velocity(type: type, attributes: attributes, children: children, value: value); - case Speeds.elementType: + case Speeds.elementType: return Speeds(type: type, attributes: attributes, children: children, value: value); - case LineSpeed.elementType: + case LineSpeed.elementType: return LineSpeed(type: type, attributes: attributes, children: children, value: value); - case ConnectionTrack.elementType: + case ConnectionTrack.elementType: return ConnectionTrack(type: type, attributes: attributes, children: children, value: value); + case CurveSpeed.elementType: + return CurveSpeed(type: type, attributes: attributes, children: children, value: value); + case StationSpeed.elementType: + return StationSpeed(type: type, attributes: attributes, children: children, value: value); + case TrainCharacteristicsRef.elementType: + return TrainCharacteristicsRef(type: type, attributes: attributes, children: children, value: value); + case TrainCharacteristics.elementType: + return TrainCharacteristics(type: type, attributes: attributes, children: children, value: value); + case TcFeatures.elementType: + return TcFeatures(type: type, attributes: attributes, children: children, value: value); default: return SferaXmlElement(type: type, attributes: attributes, children: children, value: value); } diff --git a/das_client/lib/util/util.dart b/das_client/lib/util/util.dart new file mode 100644 index 00000000..33670724 --- /dev/null +++ b/das_client/lib/util/util.dart @@ -0,0 +1,5 @@ +class Util { + static int? tryParseInt(String? value) { + return value != null ? int.tryParse(value) : null; + } +} diff --git a/das_client/pubspec.lock b/das_client/pubspec.lock index ab727e30..7f8c4506 100644 --- a/das_client/pubspec.lock +++ b/das_client/pubspec.lock @@ -293,15 +293,6 @@ packages: relative: true source: path version: "0.0.1" - design_system_flutter: - dependency: "direct main" - description: - path: "." - ref: "1.4.0" - resolved-ref: "13197f60141d0bb0fb4ee512b5ff34e68e6c5e35" - url: "https://github.com/SchweizerischeBundesbahnen/design_system_flutter.git" - source: git - version: "1.4.0" device_info_plus: dependency: "direct main" description: @@ -905,6 +896,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + sbb_design_system_mobile: + dependency: "direct main" + description: + name: sbb_design_system_mobile + sha256: "9231baa5e51b5bda186e3bdfe2949ea932141557793ffb7aeb2062a9d88b790e" + url: "https://pub.dev" + source: hosted + version: "2.1.1" sbb_oidc: dependency: "direct main" description: diff --git a/das_client/pubspec.yaml b/das_client/pubspec.yaml index a5b2b7d6..532aaea3 100644 --- a/das_client/pubspec.yaml +++ b/das_client/pubspec.yaml @@ -18,11 +18,8 @@ dependencies: path: sbb_oidc ref: 3.4.0 - design_system_flutter: - git: - url: https://github.com/SchweizerischeBundesbahnen/design_system_flutter.git - ref: 1.4.0 - + # https://pub.dev/packages/sbb_design_system_mobile + sbb_design_system_mobile: ^2.1.1 # https://pub.dev/packages/collection collection: ^1.18.0 # https://pub.dev/packages/meta diff --git a/das_client/test/sfera/mapper/sfera_mapper_test.dart b/das_client/test/sfera/mapper/sfera_mapper_test.dart index 70b8b711..861ec74f 100644 --- a/das_client/test/sfera/mapper/sfera_mapper_test.dart +++ b/das_client/test/sfera/mapper/sfera_mapper_test.dart @@ -16,6 +16,7 @@ import 'package:das_client/sfera/sfera_component.dart'; import 'package:das_client/sfera/src/mapper/sfera_model_mapper.dart'; import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; +import 'package:das_client/sfera/src/model/train_characteristics.dart'; import 'package:fimber/fimber.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -30,11 +31,20 @@ void main() { return files; } - Journey getJourney(String trainNumber, int spCount, {String? spTrainNumber}) { + List getFilesForTc(String baseName, int count) { + final files = []; + for (var i = 1; i <= count; i++) { + files.add(File('test_resources/tc/${baseName}_$i.xml')); + } + return files; + } + + Journey getJourney(String trainNumber, int spCount, {String? spTrainNumber, int tcCount = 0}) { final journeyFile = File('test_resources/jp/SFERA_JP_$trainNumber.xml'); final journeyProfile = SferaReplyParser.parse(journeyFile.readAsStringSync()); expect(journeyProfile.validate(), true); final List segmentProfiles = []; + final List trainCharacteristics = []; for (final File file in getFilesForSp('SFERA_SP_${spTrainNumber ?? trainNumber}', spCount)) { final segmentProfile = SferaReplyParser.parse(file.readAsStringSync()); @@ -42,7 +52,13 @@ void main() { segmentProfiles.add(segmentProfile); } - return SferaModelMapper.mapToJourney(journeyProfile, segmentProfiles); + for (final File file in getFilesForTc('SFERA_TC_$trainNumber', tcCount)) { + final trainCharacteristic = SferaReplyParser.parse(file.readAsStringSync()); + expect(trainCharacteristic.validate(), true); + trainCharacteristics.add(trainCharacteristic); + } + + return SferaModelMapper.mapToJourney(journeyProfile, segmentProfiles, trainCharacteristics); } test('Test invalid journey on SP missing', () async { @@ -472,4 +488,54 @@ void main() { expect(connectionTracks[2].speedData!.velocities[1].reduced, false); expect(connectionTracks[2].speedData!.velocities[1].breakSeries, isNull); }); + + test('Test available break series are parsed correctly', () async { + var journey = getJourney('9999', 5); + expect(journey.valid, true); + expect(journey.metadata.availableBreakSeries, hasLength(2)); + expect(journey.metadata.availableBreakSeries.elementAt(0).trainSeries, TrainSeries.R); + expect(journey.metadata.availableBreakSeries.elementAt(0).breakSeries, 100); + expect(journey.metadata.availableBreakSeries.elementAt(1).trainSeries, TrainSeries.A); + expect(journey.metadata.availableBreakSeries.elementAt(1).breakSeries, 30); + + journey = getJourney('T5', 1); + expect(journey.valid, true); + expect(journey.metadata.availableBreakSeries, hasLength(16)); + expect(journey.metadata.availableBreakSeries.elementAt(0).trainSeries, TrainSeries.R); + expect(journey.metadata.availableBreakSeries.elementAt(0).breakSeries, 105); + expect(journey.metadata.availableBreakSeries.elementAt(5).trainSeries, TrainSeries.A); + expect(journey.metadata.availableBreakSeries.elementAt(5).breakSeries, 50); + expect(journey.metadata.availableBreakSeries.elementAt(15).trainSeries, TrainSeries.D); + expect(journey.metadata.availableBreakSeries.elementAt(15).breakSeries, 30); + }); + + test('Test station/curve speeds are parsed correctly', () async { + final journey = getJourney('T5', 1); + expect(journey.valid, true); + + final curvePoints = journey.data.where((it) => it.type == Datatype.curvePoint).cast().toList(); + expect(curvePoints, hasLength(3)); + expect(curvePoints[0].speedData, isNotNull); + expect(curvePoints[0].speedData!.velocities, hasLength(3)); + expect(curvePoints[1].speedData, isNotNull); + expect(curvePoints[1].speedData!.velocities, hasLength(2)); + expect(curvePoints[2].speedData, isNull); + + final servicePoints = journey.data.where((it) => it.type == Datatype.servicePoint).cast().toList(); + expect(servicePoints, hasLength(3)); + expect(servicePoints[0].speedData, isNotNull); + expect(servicePoints[0].speedData!.velocities, hasLength(16)); + expect(servicePoints[1].speedData, isNotNull); + expect(servicePoints[1].speedData!.velocities, hasLength(6)); + expect(servicePoints[2].speedData, isNotNull); + expect(servicePoints[2].speedData!.velocities, hasLength(16)); + }); + + test('Test train characterists break series is parsed correctly', () async { + final journey = getJourney('T5', 1, tcCount: 1); + expect(journey.valid, true); + expect(journey.metadata.breakSeries, isNotNull); + expect(journey.metadata.breakSeries!.trainSeries, TrainSeries.R); + expect(journey.metadata.breakSeries!.breakSeries, 115); + }); } diff --git a/das_client/test/sfera/service/sfera_request_journey_profile_task_test.dart b/das_client/test/sfera/service/sfera_request_journey_profile_task_test.dart index 6c9ff625..6a3d4782 100644 --- a/das_client/test/sfera/service/sfera_request_journey_profile_task_test.dart +++ b/das_client/test/sfera/service/sfera_request_journey_profile_task_test.dart @@ -165,4 +165,27 @@ void main() { await Future.delayed(const Duration(milliseconds: 1200)); expect(timeoutReached, true); }); + + test('Test JP request saves train characteristic to sfera repository', () async { + when(mqttService.publishMessage(any, any, any)).thenReturn(true); + + final file = File('test_resources/SFERA_G2B_Reply_TC_request_T5.xml'); + final sferaG2bReplyMessage = SferaReplyParser.parse(file.readAsStringSync()); + + final journeyTask = RequestJourneyProfileTask(mqttService: mqttService, sferaRepository: sferaRepository, otnId: otnId); + + await journeyTask.execute((task, data) { + expect(task, journeyTask); + }, (task, errorCode) { + fail('Task failed with error code $errorCode'); + }); + + verify(mqttService.publishMessage(any, any, any)).called(1); + + final result = await journeyTask.handleMessage(sferaG2bReplyMessage); + expect(result, true); + + verify(sferaRepository.saveJourneyProfile(any)).called(1); + verify(sferaRepository.saveTrainCharacteristics(any)).called(1); + }); } diff --git a/das_client/test/sfera/service/sfera_request_segment_profile_task_test.dart b/das_client/test/sfera/service/sfera_request_segment_profile_task_test.dart index 84bfb1cd..7141178a 100644 --- a/das_client/test/sfera/service/sfera_request_segment_profile_task_test.dart +++ b/das_client/test/sfera/service/sfera_request_segment_profile_task_test.dart @@ -140,7 +140,7 @@ void main() { final file = File('test_resources/SFERA_G2B_Reply_JP_request_9232.xml'); final sferaG2bReplyMessage = SferaReplyParser.parse(file.readAsStringSync()); - final journeyTask = RequestSegmentProfilesTask( + final spTask = RequestSegmentProfilesTask( mqttService: mqttService, sferaRepository: sferaRepository, otnId: otnId, @@ -149,7 +149,7 @@ void main() { ); var timeoutReached = false; - await journeyTask.execute((task, data) { + await spTask.execute((task, data) { fail('Test should not call success'); }, (task, errorCode) { expect(errorCode, ErrorCode.sferaRequestTimeout); diff --git a/das_client/test/sfera/service/sfera_request_train_characteristic_task_test.dart b/das_client/test/sfera/service/sfera_request_train_characteristic_task_test.dart new file mode 100644 index 00000000..7617ca2c --- /dev/null +++ b/das_client/test/sfera/service/sfera_request_train_characteristic_task_test.dart @@ -0,0 +1,135 @@ +import 'dart:io'; + +import 'package:das_client/mqtt/mqtt_component.dart'; +import 'package:das_client/sfera/sfera_component.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; +import 'package:das_client/sfera/src/service/task/request_train_characteristics_task.dart'; +import 'package:das_client/util/error_code.dart'; +import 'package:fimber/fimber.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'sfera_request_train_characteristic_task_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + late MockMqttService mqttService; + late MockSferaRepository sferaRepository; + late OtnId otnId; + Fimber.plantTree(DebugTree()); + + setUp(() { + mqttService = MockMqttService(); + sferaRepository = MockSferaRepository(); + otnId = OtnId.create('1085', '719', DateTime.now()); + }); + + test('Test TC request successful', () async { + when(mqttService.publishMessage(any, any, any)).thenReturn(true); + + final file = File('test_resources/SFERA_G2B_Reply_TC_request_T5.xml'); + final sferaG2bReplyMessage = SferaReplyParser.parse(file.readAsStringSync()); + + final tcTask = RequestTrainCharacteristicsTask( + mqttService: mqttService, + sferaRepository: sferaRepository, + otnId: otnId, + journeyProfile: sferaG2bReplyMessage.payload!.journeyProfiles.first); + + await tcTask.execute((task, data) { + expect(task, tcTask); + expect(data, sferaG2bReplyMessage.payload!.trainCharacteristics); + }, (task, errorCode) { + fail('Task failed with error code $errorCode'); + }); + + verify(mqttService.publishMessage(any, any, any)).called(1); + + final result = await tcTask.handleMessage(sferaG2bReplyMessage); + expect(result, true); + }); + + test('Test TC request saves to sfera repository', () async { + when(mqttService.publishMessage(any, any, any)).thenReturn(true); + + final file = File('test_resources/SFERA_G2B_Reply_TC_request_T5.xml'); + final sferaG2bReplyMessage = SferaReplyParser.parse(file.readAsStringSync()); + + final tcTask = RequestTrainCharacteristicsTask( + mqttService: mqttService, + sferaRepository: sferaRepository, + otnId: otnId, + journeyProfile: sferaG2bReplyMessage.payload!.journeyProfiles.first); + + await tcTask.execute((task, data) { + expect(task, tcTask); + }, (task, errorCode) { + fail('Task failed with error code $errorCode'); + }); + + verify(mqttService.publishMessage(any, any, any)).called(1); + + final result = await tcTask.handleMessage(sferaG2bReplyMessage); + expect(result, true); + + verify(sferaRepository.saveTrainCharacteristics(any)).called(1); + }); + + test('Test TC request ignores other messages', () async { + when(mqttService.publishMessage(any, any, any)).thenReturn(true); + + final file = File('test_resources/SFERA_G2B_Reply_TC_request_T5.xml'); + final sferaG2bReplyMessage = SferaReplyParser.parse(file.readAsStringSync()); + + final tcTask = RequestTrainCharacteristicsTask( + mqttService: mqttService, + sferaRepository: sferaRepository, + otnId: otnId, + journeyProfile: sferaG2bReplyMessage.payload!.journeyProfiles.first); + + await tcTask.execute((task, data) { + fail('Test should not call success'); + }, (task, errorCode) { + fail('Test should not call error'); + }); + + verify(mqttService.publishMessage(any, any, any)).called(1); + + final handShakefile = File('test_resources/SFERA_G2B_ReplyMessage_handshake.xml'); + final handshakeSferaG2bReplyMessage = SferaReplyParser.parse(handShakefile.readAsStringSync()); + final result = await tcTask.handleMessage(handshakeSferaG2bReplyMessage); + expect(result, false); + }); + + test('Test request segment profile timeout', () async { + when(mqttService.publishMessage(any, any, any)).thenReturn(true); + + final file = File('test_resources/SFERA_G2B_Reply_TC_request_T5.xml'); + final sferaG2bReplyMessage = SferaReplyParser.parse(file.readAsStringSync()); + + final tcTask = RequestTrainCharacteristicsTask( + mqttService: mqttService, + sferaRepository: sferaRepository, + otnId: otnId, + journeyProfile: sferaG2bReplyMessage.payload!.journeyProfiles.first, + timeout: const Duration(seconds: 1), + ); + + var timeoutReached = false; + await tcTask.execute((task, data) { + fail('Test should not call success'); + }, (task, errorCode) { + expect(errorCode, ErrorCode.sferaRequestTimeout); + timeoutReached = true; + }); + + verify(mqttService.publishMessage(any, any, any)).called(1); + + await Future.delayed(const Duration(milliseconds: 1200)); + expect(timeoutReached, true); + }); +} diff --git a/das_client/test_resources/SFERA_G2B_Reply_TC_request_T5.xml b/das_client/test_resources/SFERA_G2B_Reply_TC_request_T5.xml new file mode 100644 index 00000000..f2bfd6db --- /dev/null +++ b/das_client/test_resources/SFERA_G2B_Reply_TC_request_T5.xml @@ -0,0 +1,45 @@ + + + + 0088 + 1088 + + + + + + 1085 + T5 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + 1185 + + + + + 1185 + + + + diff --git a/das_client/test_resources/jp/SFERA_JP_T5.xml b/das_client/test_resources/jp/SFERA_JP_T5.xml new file mode 100644 index 00000000..59c002c1 --- /dev/null +++ b/das_client/test_resources/jp/SFERA_JP_T5.xml @@ -0,0 +1,35 @@ + + + + + 1085 + T5 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + 1185 + + + diff --git a/das_client/test_resources/sp/SFERA_SP_9999_1.xml b/das_client/test_resources/sp/SFERA_SP_9999_1.xml index 9fe83e57..67af050b 100644 --- a/das_client/test_resources/sp/SFERA_SP_9999_1.xml +++ b/das_client/test_resources/sp/SFERA_SP_9999_1.xml @@ -30,7 +30,10 @@ - + @@ -49,18 +52,6 @@ - - - - - - - - - - - - @@ -81,6 +72,9 @@ + + + diff --git a/das_client/test_resources/sp/SFERA_SP_9999_2.xml b/das_client/test_resources/sp/SFERA_SP_9999_2.xml index 96738c76..cb0ee7b2 100644 --- a/das_client/test_resources/sp/SFERA_SP_9999_2.xml +++ b/das_client/test_resources/sp/SFERA_SP_9999_2.xml @@ -29,7 +29,10 @@ - + diff --git a/das_client/test_resources/sp/SFERA_SP_9999_3.xml b/das_client/test_resources/sp/SFERA_SP_9999_3.xml index 5b25ddd9..8e2cedea 100644 --- a/das_client/test_resources/sp/SFERA_SP_9999_3.xml +++ b/das_client/test_resources/sp/SFERA_SP_9999_3.xml @@ -24,14 +24,20 @@ - + - + diff --git a/das_client/test_resources/sp/SFERA_SP_T5_1.xml b/das_client/test_resources/sp/SFERA_SP_T5_1.xml new file mode 100644 index 00000000..778f64ca --- /dev/null +++ b/das_client/test_resources/sp/SFERA_SP_T5_1.xml @@ -0,0 +1,252 @@ + + + + 0085 + + + + + + CH + 3002 + + + + + + + CH + 3003 + + + + + + + CH + 3004 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CH + 3002 + + + + + + + + + + CH + 3003 + + + + + + + + + + CH + 3004 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/tc/SFERA_TC_T5_1.xml b/das_client/test_resources/tc/SFERA_TC_T5_1.xml new file mode 100644 index 00000000..51ce7bf2 --- /dev/null +++ b/das_client/test_resources/tc/SFERA_TC_T5_1.xml @@ -0,0 +1,8 @@ + + + 1185 + + + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_JP_9999.xml b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_JP_9999.xml index 36902805..69ddc7b3 100644 --- a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_JP_9999.xml +++ b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_JP_9999.xml @@ -18,6 +18,9 @@ + + 1085 + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_1.xml b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_1.xml index e3a0f659..67af050b 100644 --- a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_1.xml +++ b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_1.xml @@ -30,7 +30,10 @@ - + @@ -49,18 +52,6 @@ - - - - - - - - - - - - @@ -68,10 +59,6 @@ - - - - diff --git a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_2.xml b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_2.xml index 96738c76..cb0ee7b2 100644 --- a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_2.xml +++ b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_2.xml @@ -29,7 +29,10 @@ - + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_3.xml b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_3.xml index 5b25ddd9..8e2cedea 100644 --- a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_3.xml +++ b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_SP_9999_3.xml @@ -24,14 +24,20 @@ - + - + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_TC_9999_1.xml b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_TC_9999_1.xml new file mode 100644 index 00000000..3c4e44b9 --- /dev/null +++ b/sfera-mock/src/main/resources/static_sfera_resources/9999_Mixed_journey/SFERA_TC_9999_1.xml @@ -0,0 +1,8 @@ + + + 1085 + + + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_JP_T5.xml b/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_JP_T5.xml new file mode 100644 index 00000000..59c002c1 --- /dev/null +++ b/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_JP_T5.xml @@ -0,0 +1,35 @@ + + + + + 1085 + T5 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + 1185 + + + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_SP_T5_1.xml b/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_SP_T5_1.xml new file mode 100644 index 00000000..778f64ca --- /dev/null +++ b/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_SP_T5_1.xml @@ -0,0 +1,252 @@ + + + + 0085 + + + + + + CH + 3002 + + + + + + + CH + 3003 + + + + + + + CH + 3004 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CH + 3002 + + + + + + + + + + CH + 3003 + + + + + + + + + + CH + 3004 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_TC_T5_1.xml b/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_TC_T5_1.xml new file mode 100644 index 00000000..51ce7bf2 --- /dev/null +++ b/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_TC_T5_1.xml @@ -0,0 +1,8 @@ + + + 1185 + + +