diff --git a/das_client/README.md b/das_client/README.md index f4ebab91..49fec459 100644 --- a/das_client/README.md +++ b/das_client/README.md @@ -35,9 +35,23 @@ fvm flutter test --flavor dev --dart-define=MQTT_USERNAME=${MQTT_USERNAME} --dar ## Architecture -TODO - +### Test file structure +To prevent confusion, fictive train numbers with the prefix `T` are used for the test scenarios. It is desired to create new train journeys for different features. +The file structure in [test_resources](test_resources) for a test scenario looks as follows: +* base directory named `_` +* journey profile named `SFERA_JP__` +* corresponding segment profiles named `SFERA_SP__` +* corresponding train characteristics named `SFERA_TC__` + An example test scenario for train number T1 could look like this: +* T1_demo_journey/ + * SFERA_JP_T1 + * SFERA_JP_T1_without_stop + * SFERA_SP_T1_1 + * SFERA_SP_T1_2 + * SFERA_TC_T1_1 + + ## Localization The app is available in three languages: diff --git a/das_client/integration_test/test/train_journey_test.dart b/das_client/integration_test/test/train_journey_test.dart index 02fc3c99..26d42e6e 100644 --- a/das_client/integration_test/test/train_journey_test.dart +++ b/das_client/integration_test/test/train_journey_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:das_client/app/pages/journey/train_journey/widgets/header/header.dart'; import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -28,7 +30,7 @@ void main() { await tester.pumpAndSettle(); // check if station is present - expect(find.text('Solothurn'), findsOneWidget); + expect(find.text('Haltestelle B'), findsWidgets); await tester.pumpAndSettle(); }); @@ -37,7 +39,6 @@ void main() { // Load app widget. await prepareAndStartApp(tester); - // final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description); expect(trainNumberText, findsOneWidget); @@ -59,4 +60,126 @@ void main() { await tester.pumpAndSettle(); }); }); + + testWidgets('find base value when no punctuality update comes', (tester) async { + // Load app widget. + await prepareAndStartApp(tester); + + final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description); + expect(trainNumberText, findsOneWidget); + + await enterText(tester, trainNumberText, 'T6'); + + final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first; + await tester.tap(primaryButton); + + // wait for train journey to load + await tester.pumpAndSettle(); + + //find the header and check if it is existent + final headerFinder = find.byType(Header); + expect(headerFinder, findsOneWidget); + + //Find the text in the header + expect(find.descendant(of: headerFinder, matching: find.text('+00:00')), findsOneWidget); + + await tester.pumpAndSettle(); + }); + + testWidgets('check if the displayed current time is correct', (tester) async { + // Load app widget. + await prepareAndStartApp(tester); + + //Select the correct train number + final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description); + expect(trainNumberText, findsOneWidget); + + await enterText(tester, trainNumberText, 'T6'); + + //Log into the journey + final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first; + await tester.tap(primaryButton); + + // wait for train journey to load + await tester.pumpAndSettle(); + + //find the header and check if it is existent + final headerFinder = find.byType(Header); + expect(headerFinder, findsOneWidget); + + final DateTime currentTime = DateTime.now(); + final String currentHour = currentTime.hour <= 9 ? '0${currentTime.hour}' : (currentTime.hour).toString(); + final String currentMinutes = currentTime.hour <= 9 ? '0${currentTime.minute}' : (currentTime.minute).toString(); + final String currentSeconds = currentTime.hour <= 9 ? '0${currentTime.second}' : (currentTime.second).toString(); + final String nextSecond = + currentTime.hour <= 9 ? '0${currentTime.second + 1}' : (currentTime.second + 1).toString(); + final String currentWholeTime = '$currentHour:$currentMinutes:$currentSeconds'; + final String nextSecondWholeTime = '$currentHour:$currentMinutes:$nextSecond'; + + if (!find.descendant(of: headerFinder, matching: find.text(currentWholeTime)).evaluate().isNotEmpty) { + expect(find.descendant(of: headerFinder, matching: find.text(nextSecondWholeTime)), findsOneWidget); + } else { + expect(find.descendant(of: headerFinder, matching: find.text(currentWholeTime)), findsOneWidget); + } + + await tester.pumpAndSettle(); + }); + + testWidgets('check if update sent is correct', (tester) async { + // Load app widget. + await prepareAndStartApp(tester); + + // Select the correct train number + final trainNumberText = findTextFieldByLabel(l10n.p_train_selection_trainnumber_description); + expect(trainNumberText, findsOneWidget); + + await enterText(tester, trainNumberText, 'T9999'); + + // Log into the journey + final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first; + await tester.tap(primaryButton); + + // Wait for train journey to load + await tester.pumpAndSettle(); + + // Find the header and check if it is existent + final headerFinder = find.byType(Header); + expect(headerFinder, findsOneWidget); + + // Timer logic: increase timer every second, rerun the base every 100 ms and check if the UI changed + int timer = 0; + const maxTime = 10; + int millisecondsCounter = 0; + + final completer = Completer(); + + expect(find.descendant(of: headerFinder, matching: find.text('+00:00')), findsOneWidget); + + while (!completer.isCompleted) { + await tester.pumpAndSettle(); + + if (!find.descendant(of: headerFinder, matching: find.text('+00:00')).evaluate().isNotEmpty) { + expect(find.descendant(of: headerFinder, matching: find.text('+00:30')), findsOneWidget); + completer.complete(); + break; + } + + millisecondsCounter += 100; + if (millisecondsCounter % 1000 == 0) { + timer++; + } + + if (timer > maxTime) { + completer + .completeError(Exception('UI did not change from the base value to the updated value (+00:00 -> +00:30)')); + break; + } + + await Future.delayed(const Duration(milliseconds: 100)); + } + + await completer.future; + + await tester.pumpAndSettle(); + }); } diff --git a/das_client/integration_test/test/train_search_test.dart b/das_client/integration_test/test/train_search_test.dart index acb52828..5a624094 100644 --- a/das_client/integration_test/test/train_search_test.dart +++ b/das_client/integration_test/test/train_search_test.dart @@ -9,7 +9,6 @@ import '../util/test_utils.dart'; void main() { group('train search screen tests', () { - testWidgets('test default values', (tester) async { // Load app widget. await prepareAndStartApp(tester); @@ -59,7 +58,6 @@ void main() { // check that the primary button is disabled final primaryButton = find.byWidgetPredicate((widget) => widget is SBBPrimaryButton).first; expect(tester.widget(primaryButton).onPressed, isNull); - }); testWidgets('test can select yesterday', (tester) async { @@ -70,7 +68,8 @@ void main() { final yesterday = today.add(Duration(days: -1)); final todayDateTextFinder = find.text(Format.date(today)); - final yesterdayDateTextFinder = find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}'); + final yesterdayDateTextFinder = + find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}'); // Verify that today is preselected expect(todayDateTextFinder, findsOneWidget); @@ -90,7 +89,6 @@ void main() { expect(todayDateTextFinder, findsNothing); expect(yesterdayDateTextFinder, findsOneWidget); - }); testWidgets('test can not select day before yesterday', (tester) async { @@ -102,7 +100,8 @@ void main() { final dayBeforeYesterday = today.add(Duration(days: -2)); final todayDateTextFinder = find.text(Format.date(today)); - final yesterdayDateTextFinder = find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}'); + final yesterdayDateTextFinder = + find.text('${Format.date(yesterday)} ${l10n.p_train_selection_date_not_today_warning}'); final dayBeforeYesterdayDateTextFinder = find.text(Format.date(dayBeforeYesterday)); // Verify that today is preselected @@ -114,7 +113,8 @@ void main() { final sbbDatePickerFinder = find.byWidgetPredicate((widget) => widget is SBBDatePicker); final yesterdayFinder = find.descendant( of: sbbDatePickerFinder, - matching: find.byWidgetPredicate((widget) => widget is Text && widget.data == '${(dayBeforeYesterday.day)}.')); + matching: + find.byWidgetPredicate((widget) => widget is Text && widget.data == '${(dayBeforeYesterday.day)}.')); await tapElement(tester, yesterdayFinder); // tap outside dialog @@ -149,6 +149,5 @@ void main() { expect(find.text('${ErrorCode.sferaJpUnavailable.code}: ${l10n.c_error_sfera_jp_unavailable}'), findsOneWidget); }); - }); } diff --git a/das_client/ios/Podfile.lock b/das_client/ios/Podfile.lock index 2e39e64c..aaafffbc 100644 --- a/das_client/ios/Podfile.lock +++ b/das_client/ios/Podfile.lock @@ -68,4 +68,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: d9dad56c0cd0b4fd8b4fe3034a53fd42a0b990f6 -COCOAPODS: 1.16.1 +COCOAPODS: 1.16.2 diff --git a/das_client/l10n/strings_de.arb b/das_client/l10n/strings_de.arb index d4f8ae78..0493bf2b 100644 --- a/das_client/l10n/strings_de.arb +++ b/das_client/l10n/strings_de.arb @@ -43,7 +43,7 @@ "c_error_sfera_handshake_rejected": "Server hat die Verbindung abgelehnt", "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_error_sfera_invalid": "Unvollständige Daten erhalten", "c_connection_track_weiche": "Weiche", "c_button_confirm": "Übernehmen" } \ No newline at end of file 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 20c2caaf..ef598173 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,5 +1,8 @@ -import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; +import 'package:das_client/app/bloc/train_journey_cubit.dart'; import 'package:flutter/material.dart'; +import 'package:das_client/model/journey/journey.dart'; +import 'package:intl/intl.dart'; +import 'package:sbb_design_system_mobile/sbb_design_system_mobile.dart'; class TimeContainer extends StatelessWidget { const TimeContainer({super.key}); @@ -22,25 +25,56 @@ class TimeContainer extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - '05:43:00', - style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0), - ), + Flexible(child: _currentTime()), _divider(), - Text( - '+00:01:30', - style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0), - ), + Flexible(child: _punctualityDisplay(context)), ], ), ), ); } +} - Widget _divider() { - return const Padding( - padding: EdgeInsets.symmetric(vertical: sbbDefaultSpacing * 0.5), - child: Divider(height: 1.0, color: SBBColors.cloud), - ); - } +Widget _divider() { + return const Padding( + padding: EdgeInsets.symmetric(vertical: sbbDefaultSpacing * 0.5), + child: Divider(height: 1.0, color: SBBColors.cloud), + ); +} + +Widget _punctualityDisplay(BuildContext context) { + final bloc = context.trainJourneyCubit; + + return StreamBuilder( + stream: bloc.journeyStream, + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == null || snapshot.data!.metadata.delay == null) { + return Text('+00:00', style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0)); + } + + final Journey journey = snapshot.data!; + final Duration delay = journey.metadata.delay!; + + final String minutes = NumberFormat('00').format(delay.inMinutes.abs() % 60); + final String seconds = NumberFormat('00').format(delay.inSeconds.abs() % 60); + final String formattedDuration = '${delay.isNegative ? '-' : '+'}$minutes:$seconds'; + + return Text( + formattedDuration, + style: SBBTextStyles.largeLight.copyWith(fontSize: 24.0), + ); + }, + ); +} + +StreamBuilder _currentTime() { + return StreamBuilder( + stream: Stream.periodic(const Duration(milliseconds: 200)), + builder: (context, snapshot) { + return Text( + DateFormat('HH:mm:ss').format(DateTime.now().toLocal()), + style: SBBTextStyles.largeBold.copyWith(fontSize: 24.0), + ); + }, + ); } 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 ed01fc07..0704a322 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 @@ -66,7 +66,7 @@ class _TrainSelectionState extends State { onChanged: (value) => context.trainJourneyCubit.updateTrainNumber(value), controller: _trainNumberController, labelText: context.l10n.p_train_selection_trainnumber_description, - keyboardType: TextInputType.number, + keyboardType: TextInputType.text, ), ); } diff --git a/das_client/lib/model/journey/metadata.dart b/das_client/lib/model/journey/metadata.dart index a4be5262..f7302643 100644 --- a/das_client/lib/model/journey/metadata.dart +++ b/das_client/lib/model/journey/metadata.dart @@ -10,6 +10,7 @@ class Metadata { this.currentPosition, this.routeStart, this.routeEnd, + this.delay, this.breakSeries, this.additionalSpeedRestrictions = const [], this.nonStandardTrackEquipmentSegments = const [], @@ -21,6 +22,7 @@ class Metadata { final List additionalSpeedRestrictions; final BaseData? routeStart; final BaseData? routeEnd; + final Duration? delay; final List nonStandardTrackEquipmentSegments; final BreakSeries? breakSeries; final Set availableBreakSeries; 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 d65e1941..bbf3ff9d 100644 --- a/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart +++ b/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart @@ -19,6 +19,7 @@ import 'package:das_client/model/journey/track_equipment.dart'; import 'package:das_client/model/journey/velocity.dart'; import 'package:das_client/model/localized_string.dart'; import 'package:das_client/sfera/src/mapper/track_equipment_mapper.dart'; +import 'package:das_client/sfera/src/model/delay.dart'; import 'package:das_client/sfera/src/model/enums/length_type.dart'; import 'package:das_client/sfera/src/model/enums/start_end_qualifier.dart'; import 'package:das_client/sfera/src/model/enums/stop_skip_pass.dart'; @@ -27,6 +28,7 @@ import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/multilingual_text.dart'; import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; +import 'package:das_client/sfera/src/model/related_train_information.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'; @@ -42,10 +44,13 @@ class SferaModelMapper { static const String _protectionSectionNspFacultativeName = 'facultative'; static const String _protectionSectionNspLengthTypeName = 'lengthType'; - static Journey mapToJourney(JourneyProfile journeyProfile, List segmentProfiles, - List trainCharacteristics) { + static Journey mapToJourney( + {required JourneyProfile journeyProfile, + List segmentProfiles = const [], + List trainCharacteristics = const [], + RelatedTrainInformation? relatedTrainInformation}) { try { - return _mapToJourney(journeyProfile, segmentProfiles, trainCharacteristics); + return _mapToJourney(journeyProfile, segmentProfiles, trainCharacteristics, relatedTrainInformation); } catch (e, s) { Fimber.e('Error mapping journey-/segment profiles to journey:', ex: e, stacktrace: s); return Journey.invalid(); @@ -53,7 +58,7 @@ class SferaModelMapper { } static Journey _mapToJourney(JourneyProfile journeyProfile, List segmentProfiles, - List trainCharacteristics) { + List trainCharacteristics, RelatedTrainInformation? relatedTrainInformation) { final journeyData = []; final segmentProfilesLists = journeyProfile.segmentProfilesLists.toList(); @@ -130,6 +135,7 @@ class SferaModelMapper { final trainCharacteristic = _resolveFirstTrainCharacteristics(journeyProfile, trainCharacteristics); final servicePoints = journeyData.where((it) => it.type == Datatype.servicePoint).toList(); + return Journey( metadata: Metadata( nextStop: servicePoints.length > 1 ? servicePoints[1] as ServicePoint : null, @@ -137,6 +143,7 @@ class SferaModelMapper { additionalSpeedRestrictions: additionalSpeedRestrictions, routeStart: journeyData.firstOrNull, routeEnd: journeyData.lastOrNull, + delay: Delay.toDuration(relatedTrainInformation?.ownTrain.trainLocationInformation.delay.delay), nonStandardTrackEquipmentSegments: trackEquipmentSegments, availableBreakSeries: _parseAvailableBreakSeries(journeyData), breakSeries: trainCharacteristic?.tcFeatures.trainCategoryCode != null && diff --git a/das_client/lib/sfera/src/model/delay.dart b/das_client/lib/sfera/src/model/delay.dart new file mode 100644 index 00000000..01ba615e --- /dev/null +++ b/das_client/lib/sfera/src/model/delay.dart @@ -0,0 +1,23 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:iso_duration/iso_duration.dart'; + +class Delay extends SferaXmlElement { + static const String elementType = 'Delay'; + + Delay({super.type = elementType, super.attributes, super.children, super.value}); + + String get delay => attributes['Delay']!; + + @override + bool validate() { + return validateHasAttribute('Delay') && super.validate(); + } + + static Duration? toDuration(String? stringToChange) { + try { + return tryParseIso8601Duration(stringToChange); + } catch (error) { + return null; + } + } +} diff --git a/das_client/lib/sfera/src/model/g2b_event_payload.dart b/das_client/lib/sfera/src/model/g2b_event_payload.dart new file mode 100644 index 00000000..97e1f4fd --- /dev/null +++ b/das_client/lib/sfera/src/model/g2b_event_payload.dart @@ -0,0 +1,14 @@ +import 'package:das_client/sfera/src/model/journey_profile.dart'; +import 'package:das_client/sfera/src/model/related_train_information.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class G2bEventPayload extends SferaXmlElement { + static const String elementType = 'G2B_EventPayload'; + + G2bEventPayload({super.type = elementType, super.attributes, super.children, super.value}); + + RelatedTrainInformation? get relatedTrainInformation => children.whereType().firstOrNull; + + Iterable get journeyProfiles => children.whereType(); + +} 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 584d8095..7397d31e 100644 --- a/das_client/lib/sfera/src/model/g2b_reply_payload.dart +++ b/das_client/lib/sfera/src/model/g2b_reply_payload.dart @@ -1,4 +1,5 @@ import 'package:das_client/sfera/src/model/journey_profile.dart'; +import 'package:das_client/sfera/src/model/related_train_information.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'; @@ -13,4 +14,6 @@ class G2bReplyPayload extends SferaXmlElement { Iterable get segmentProfiles => children.whereType(); Iterable get trainCharacteristics => children.whereType(); + + Iterable get relatedTrainInformation => children.whereType(); } diff --git a/das_client/lib/sfera/src/model/own_train.dart b/das_client/lib/sfera/src/model/own_train.dart new file mode 100644 index 00000000..e24cc276 --- /dev/null +++ b/das_client/lib/sfera/src/model/own_train.dart @@ -0,0 +1,20 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/train_identification.dart'; +import 'package:das_client/sfera/src/model/train_location_information.dart'; + +class OwnTrain extends SferaXmlElement { + static const String elementType = 'OwnTrain'; + + OwnTrain({super.type = elementType, super.attributes, super.children, super.value}); + + TrainIdentification get trainIdentification => children.whereType().first; + + TrainLocationInformation get trainLocationInformation => children.whereType().first; + + @override + bool validate() { + return validateHasChildOfType() && + validateHasChildOfType() && + super.validate(); + } +} diff --git a/das_client/lib/sfera/src/model/related_train_information.dart b/das_client/lib/sfera/src/model/related_train_information.dart new file mode 100644 index 00000000..dea87d3f --- /dev/null +++ b/das_client/lib/sfera/src/model/related_train_information.dart @@ -0,0 +1,16 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +import 'package:das_client/sfera/src/model/own_train.dart'; + +class RelatedTrainInformation extends SferaXmlElement { + static const String elementType = 'RelatedTrainInformation'; + + RelatedTrainInformation({super.type = elementType, super.attributes, super.children, super.value}); + + OwnTrain get ownTrain => children.whereType().first; + + @override + bool validate() { + return validateHasChildOfType() && super.validate(); + } +} diff --git a/das_client/lib/sfera/src/model/sfera_g2b_event_message.dart b/das_client/lib/sfera/src/model/sfera_g2b_event_message.dart new file mode 100644 index 00000000..19831779 --- /dev/null +++ b/das_client/lib/sfera/src/model/sfera_g2b_event_message.dart @@ -0,0 +1,18 @@ +import 'package:das_client/sfera/src/model/g2b_event_payload.dart'; +import 'package:das_client/sfera/src/model/message_header.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class SferaG2bEventMessage extends SferaXmlElement { + static const String elementType = 'SFERA_G2B_EventMessage'; + + SferaG2bEventMessage({super.type = elementType, super.attributes, super.children, super.value}); + + MessageHeader get messageHeader => children.whereType().first; + + G2bEventPayload? get payload => children.whereType().firstOrNull; + + @override + bool validate() { + return validateHasChildOfType() && validateHasChildOfType() && super.validate(); + } +} diff --git a/das_client/lib/sfera/src/model/train_location_information.dart b/das_client/lib/sfera/src/model/train_location_information.dart new file mode 100644 index 00000000..21e86c97 --- /dev/null +++ b/das_client/lib/sfera/src/model/train_location_information.dart @@ -0,0 +1,15 @@ +import 'package:das_client/sfera/src/model/delay.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class TrainLocationInformation extends SferaXmlElement { + static const String elementType = 'TrainLocationInformation'; + + TrainLocationInformation({super.type = elementType, super.attributes, super.children, super.value}); + + Delay get delay => children.whereType().first; + + @override + bool validate() { + return validateHasChildOfType() && super.validate(); + } +} diff --git a/das_client/lib/sfera/src/service/event/journey_profile_event_handler.dart b/das_client/lib/sfera/src/service/event/journey_profile_event_handler.dart new file mode 100644 index 00000000..e0ad86e5 --- /dev/null +++ b/das_client/lib/sfera/src/service/event/journey_profile_event_handler.dart @@ -0,0 +1,30 @@ +import 'package:das_client/sfera/src/model/journey_profile.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_event_message.dart'; +import 'package:das_client/sfera/src/repo/sfera_repository.dart'; +import 'package:das_client/sfera/src/service/event/sfera_event_message_handler.dart'; +import 'package:fimber/fimber.dart'; + +class JourneyProfileEventHandler extends SferaEventMessageHandler { + final SferaRepository _sferaRepository; + + JourneyProfileEventHandler(super.onMessageHandled, this._sferaRepository); + + @override + Future handleMessage(SferaG2bEventMessage eventMessage) async { + if (eventMessage.payload == null || eventMessage.payload!.journeyProfiles.isEmpty) { + return false; + } + + Fimber.i('Updating journey profiles...'); + for (final journeyProfile in eventMessage.payload!.journeyProfiles) { + await _sferaRepository.saveJourneyProfile(journeyProfile); + } + + if (eventMessage.payload!.journeyProfiles.length > 1) { + Fimber.w('Received more then 1 journey profile which is not supported, using first one provided'); + } + + onMessageHandled(this, eventMessage.payload!.journeyProfiles.first); + return true; + } +} diff --git a/das_client/lib/sfera/src/service/event/related_train_information_event_handler.dart b/das_client/lib/sfera/src/service/event/related_train_information_event_handler.dart new file mode 100644 index 00000000..0171631e --- /dev/null +++ b/das_client/lib/sfera/src/service/event/related_train_information_event_handler.dart @@ -0,0 +1,19 @@ +import 'package:das_client/sfera/src/model/related_train_information.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_event_message.dart'; +import 'package:das_client/sfera/src/service/event/sfera_event_message_handler.dart'; +import 'package:fimber/fimber.dart'; + +class RelatedTrainInformationEventHandler extends SferaEventMessageHandler { + RelatedTrainInformationEventHandler(super.onMessageHandled); + + @override + Future handleMessage(SferaG2bEventMessage eventMessage) async { + if (eventMessage.payload == null || eventMessage.payload!.relatedTrainInformation == null) { + return false; + } + + Fimber.i('Received new related train information... ${eventMessage.payload!.relatedTrainInformation?.ownTrain.trainLocationInformation.delay.delay}'); + onMessageHandled(this, eventMessage.payload!.relatedTrainInformation!); + return true; + } +} diff --git a/das_client/lib/sfera/src/service/event/sfera_event_message_handler.dart b/das_client/lib/sfera/src/service/event/sfera_event_message_handler.dart new file mode 100644 index 00000000..82682dcb --- /dev/null +++ b/das_client/lib/sfera/src/service/event/sfera_event_message_handler.dart @@ -0,0 +1,11 @@ +import 'package:das_client/sfera/src/model/sfera_g2b_event_message.dart'; + +typedef MessageHandled = void Function(SferaEventMessageHandler handler, T data); + +abstract class SferaEventMessageHandler { + SferaEventMessageHandler(this.onMessageHandled); + + final MessageHandled onMessageHandled; + + Future handleMessage(SferaG2bEventMessage eventMessage); +} diff --git a/das_client/lib/sfera/src/service/handler/journey_profile_reply_handler.dart b/das_client/lib/sfera/src/service/handler/journey_profile_reply_handler.dart deleted file mode 100644 index 57dbf420..00000000 --- a/das_client/lib/sfera/src/service/handler/journey_profile_reply_handler.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; -import 'package:das_client/sfera/src/repo/sfera_repository.dart'; -import 'package:das_client/sfera/src/service/handler/sfera_message_handler.dart'; -import 'package:fimber/fimber.dart'; - -class JourneyProfileReplyHandler implements SferaMessageHandler { - final SferaRepository _sferaRepository; - JourneyProfileReplyHandler(this._sferaRepository); - - @override - Future handleMessage(SferaG2bReplyMessage message) async { - if (message.payload == null || message.payload!.journeyProfiles.isEmpty) { - return false; - } - - Fimber.i('Updating journey profiles...'); - for (final journeyProfile in message.payload!.journeyProfiles) { - await _sferaRepository.saveJourneyProfile(journeyProfile); - } - - return true; - } -} diff --git a/das_client/lib/sfera/src/service/handler/segment_profile_reply_handler.dart b/das_client/lib/sfera/src/service/handler/segment_profile_reply_handler.dart deleted file mode 100644 index 0266502f..00000000 --- a/das_client/lib/sfera/src/service/handler/segment_profile_reply_handler.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; -import 'package:das_client/sfera/src/repo/sfera_repository.dart'; -import 'package:das_client/sfera/src/service/handler/sfera_message_handler.dart'; -import 'package:fimber/fimber.dart'; - -class SegmentProfileReplyHandler implements SferaMessageHandler { - final SferaRepository _sferaRepository; - SegmentProfileReplyHandler(this._sferaRepository); - - @override - Future handleMessage(SferaG2bReplyMessage message) async { - if (message.payload == null || message.payload!.segmentProfiles.isEmpty) { - return false; - } - - Fimber.i('Updating segment profiles...'); - for (final segmentProfile in message.payload!.segmentProfiles) { - await _sferaRepository.saveSegmentProfile(segmentProfile); - } - - return true; - } -} diff --git a/das_client/lib/sfera/src/service/handler/sfera_message_handler.dart b/das_client/lib/sfera/src/service/handler/sfera_message_handler.dart deleted file mode 100644 index afc26a56..00000000 --- a/das_client/lib/sfera/src/service/handler/sfera_message_handler.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; - -abstract class SferaMessageHandler { - Future handleMessage(SferaG2bReplyMessage replyMessage); -} 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 48b1fc37..543dbd41 100644 --- a/das_client/lib/sfera/src/service/sfera_service_impl.dart +++ b/das_client/lib/sfera/src/service/sfera_service_impl.dart @@ -8,13 +8,15 @@ 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/enums/das_driving_mode.dart'; import 'package:das_client/sfera/src/model/journey_profile.dart'; +import 'package:das_client/sfera/src/model/related_train_information.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_event_message.dart'; import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.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/event/journey_profile_event_handler.dart'; +import 'package:das_client/sfera/src/service/event/related_train_information_event_handler.dart'; +import 'package:das_client/sfera/src/service/event/sfera_event_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'; @@ -30,7 +32,8 @@ class SferaServiceImpl implements SferaService { final Authenticator _authenticator; StreamSubscription? _mqttStreamSubscription; - final List _messageHandlers = []; + final List _tasks = []; + final List _eventMessageHandler = []; final _stateSubject = BehaviorSubject.seeded(SferaServiceState.disconnected); @@ -45,6 +48,7 @@ class SferaServiceImpl implements SferaService { JourneyProfile? _journeyProfile; final List _segmentProfiles = []; final List _trainCharacteristics = []; + RelatedTrainInformation? _relatedTrainInformation; @override ErrorCode? lastErrorCode; @@ -60,21 +64,31 @@ class SferaServiceImpl implements SferaService { } void _init() { + _addEventMessageHandlers(); _mqttStreamSubscription = _mqttService.messageStream.listen((xmlMessage) async { final message = SferaReplyParser.parse(xmlMessage); + if (!message.validate()) { + Fimber.w('Validation failed for MQTT response $xmlMessage'); + return; + } + if (message is SferaG2bReplyMessage) { - if (!message.validate()) { - Fimber.w('Validation failed for MQTT response $xmlMessage'); - return; + var handled = false; + for (final handler in List.from(_tasks)) { + handled |= await handler.handleMessage(message); } + if (!handled) { + Fimber.w('Could not handle sfera reply message $xmlMessage'); + } + } else if (message is SferaG2bEventMessage) { var handled = false; - for (final handler in List.from(_messageHandlers)) { + for (final handler in List.from(_eventMessageHandler)) { handled |= await handler.handleMessage(message); } if (!handled) { - Fimber.w('Could not handle sfera reply message $xmlMessage'); + Fimber.w('Could not handle sfera event message $xmlMessage'); } } }); @@ -84,7 +98,7 @@ class SferaServiceImpl implements SferaService { Future connect(OtnId otnId) async { Fimber.i('Starting new connection for $otnId'); _otnId = otnId; - _messageHandlers.clear(); + _tasks.clear(); lastErrorCode = null; _stateSubject.add(SferaServiceState.connecting); @@ -95,7 +109,7 @@ class SferaServiceImpl implements SferaService { final drivingMode = user.roles.contains(Role.driver) ? DasDrivingMode.dasNotConnected : DasDrivingMode.readOnly; final handshakeTask = HandshakeTask(mqttService: _mqttService, otnId: otnId, dasDrivingMode: drivingMode); - _messageHandlers.add(handshakeTask); + _tasks.add(handshakeTask); handshakeTask.execute(onTaskCompleted, onTaskFailed); } else { _otnId = null; @@ -105,43 +119,56 @@ class SferaServiceImpl implements SferaService { } void onTaskCompleted(SferaTask task, dynamic data) async { - _messageHandlers.remove(task); + _tasks.remove(task); Fimber.i('Task $task completed'); if (task is HandshakeTask) { _stateSubject.add(SferaServiceState.loadingJourney); final requestJourneyTask = RequestJourneyProfileTask(mqttService: _mqttService, sferaRepository: _sferaRepository, otnId: _otnId!); - _messageHandlers.add(requestJourneyTask); + _tasks.add(requestJourneyTask); requestJourneyTask.execute(onTaskCompleted, onTaskFailed); } else if (task is RequestJourneyProfileTask) { _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); - requestTrainCharacteristicsTask.execute(onTaskCompleted, onTaskFailed); + final dataList = data as List; + _journeyProfile = dataList.whereType().first; + _relatedTrainInformation = dataList.whereType().firstOrNull; + _startSegmentProfileAndTCTask(); } - if (_stateSubject.value == SferaServiceState.loadingAdditionalData && _allTaskedCompleted()) { - _addMessageHandlers(); - await _refreshSegmentProfiles(); - await _refreshTrainCharacteristics(); - final success = _updateJourney(); - if (success) { - _stateSubject.add(SferaServiceState.connected); - } else { - lastErrorCode = ErrorCode.sferaSpInvalid; - disconnect(); + if (_allTasksCompleted()) { + switch (_stateSubject.value) { + case SferaServiceState.loadingAdditionalData: + await _refreshSegmentProfiles(); + await _refreshTrainCharacteristics(); + final success = _updateJourney(); + if (success) { + _stateSubject.add(SferaServiceState.connected); + } else { + disconnect(); + } + break; + case SferaServiceState.connected: + await _refreshSegmentProfiles(); + await _refreshTrainCharacteristics(); + _updateJourney(); + default: } } } - bool _allTaskedCompleted() { - return _messageHandlers.whereType().isEmpty; + void _startSegmentProfileAndTCTask() { + final requestSegmentProfilesTask = RequestSegmentProfilesTask( + mqttService: _mqttService, sferaRepository: _sferaRepository, otnId: _otnId!, journeyProfile: _journeyProfile!); + final requestTrainCharacteristicsTask = RequestTrainCharacteristicsTask( + mqttService: _mqttService, sferaRepository: _sferaRepository, otnId: _otnId!, journeyProfile: _journeyProfile!); + _tasks.add(requestSegmentProfilesTask); + _tasks.add(requestTrainCharacteristicsTask); + requestSegmentProfilesTask.execute(onTaskCompleted, onTaskFailed); + requestTrainCharacteristicsTask.execute(onTaskCompleted, onTaskFailed); + } + + bool _allTasksCompleted() { + return _tasks.whereType().isEmpty; } Future _refreshSegmentProfiles() async { @@ -167,7 +194,7 @@ class SferaServiceImpl implements SferaService { for (final element in _journeyProfile!.trainCharactericsRefSet) { final trainCharactericsEntity = - await _sferaRepository.findTrainCharacteristics(element.tcId, element.versionMajor, element.versionMinor); + await _sferaRepository.findTrainCharacteristics(element.tcId, element.versionMajor, element.versionMinor); final trainCharacterics = trainCharactericsEntity?.toDomain(); if (trainCharacterics != null && trainCharacterics.validate()) { _trainCharacteristics.add(trainCharacterics); @@ -181,25 +208,40 @@ class SferaServiceImpl implements SferaService { bool _updateJourney() { if (_journeyProfile != null && _segmentProfiles.isNotEmpty) { Fimber.i('Updating journey stream...'); - final newJourney = SferaModelMapper.mapToJourney(_journeyProfile!, _segmentProfiles, _trainCharacteristics); + final newJourney = SferaModelMapper.mapToJourney( + journeyProfile: _journeyProfile!, + segmentProfiles: _segmentProfiles, + trainCharacteristics: _trainCharacteristics, + relatedTrainInformation: _relatedTrainInformation); if (newJourney.valid) { _journeyProfileSubject.add(newJourney); Fimber.i('Journey updates successfully.'); return true; } else { Fimber.w('Failed to update journey as it is not valid'); + lastErrorCode = ErrorCode.sferaInvalid; } } return false; } - void _addMessageHandlers() { - _messageHandlers.add(JourneyProfileReplyHandler(_sferaRepository)); - _messageHandlers.add(SegmentProfileReplyHandler(_sferaRepository)); + void _addEventMessageHandlers() { + _eventMessageHandler.add(JourneyProfileEventHandler(onJourneyProfileUpdated, _sferaRepository)); + _eventMessageHandler.add(RelatedTrainInformationEventHandler(onRelatedTrainInformationUpdated)); + } + + void onJourneyProfileUpdated(SferaEventMessageHandler handler, JourneyProfile data) async { + _journeyProfile = data; + _startSegmentProfileAndTCTask(); + } + + void onRelatedTrainInformationUpdated(SferaEventMessageHandler handler, RelatedTrainInformation data) async { + _relatedTrainInformation = data; + _updateJourney(); } void onTaskFailed(SferaTask task, ErrorCode errorCode) { - _messageHandlers.remove(task); + _tasks.remove(task); lastErrorCode = errorCode; Fimber.e('Task $task failed with error code $errorCode'); if (_stateSubject.value != SferaServiceState.connected) { diff --git a/das_client/lib/sfera/src/service/task/handshake_task.dart b/das_client/lib/sfera/src/service/task/handshake_task.dart index 2437d7b2..1415dc46 100644 --- a/das_client/lib/sfera/src/service/task/handshake_task.dart +++ b/das_client/lib/sfera/src/service/task/handshake_task.dart @@ -54,15 +54,15 @@ class HandshakeTask extends SferaTask { } @override - Future handleMessage(SferaG2bReplyMessage message) async { - if (message.handshakeAcknowledgement != null) { + Future handleMessage(SferaG2bReplyMessage replyMessage) async { + if (replyMessage.handshakeAcknowledgement != null) { stopTimeout(); Fimber.i('Received handshake acknowledgment'); _taskCompletedCallback(this, null); return true; - } else if (message.handshakeReject != null) { + } else if (replyMessage.handshakeReject != null) { stopTimeout(); - Fimber.w('Received handshake reject with reason=${message.handshakeReject?.handshakeRejectReason?.toString()}'); + Fimber.w('Received handshake reject with reason=${replyMessage.handshakeReject?.handshakeRejectReason?.toString()}'); _taskFailedCallback(this, ErrorCode.sferaHandshakeRejected); _mqttService.disconnect(); return true; 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 a955d351..4067ab84 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 @@ -1,7 +1,6 @@ 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/enums/jp_status.dart'; -import 'package:das_client/sfera/src/model/journey_profile.dart'; import 'package:das_client/sfera/src/model/jp_request.dart'; import 'package:das_client/sfera/src/model/otn_id.dart'; import 'package:das_client/sfera/src/model/sfera_b2g_request_message.dart'; @@ -13,7 +12,7 @@ import 'package:das_client/sfera/src/service/task/sfera_task.dart'; import 'package:das_client/util/error_code.dart'; import 'package:fimber/fimber.dart'; -class RequestJourneyProfileTask extends SferaTask { +class RequestJourneyProfileTask extends SferaTask> { RequestJourneyProfileTask( {required MqttService mqttService, required SferaRepository sferaRepository, required this.otnId, super.timeout}) : _mqttService = mqttService, @@ -23,11 +22,11 @@ class RequestJourneyProfileTask extends SferaTask { final OtnId otnId; final SferaRepository _sferaRepository; - late TaskCompleted _taskCompletedCallback; + late TaskCompleted> _taskCompletedCallback; late TaskFailed _taskFailedCallback; @override - Future execute(TaskCompleted onCompleted, TaskFailed onFailed) async { + Future execute(TaskCompleted> onCompleted, TaskFailed onFailed) async { _taskCompletedCallback = onCompleted; _taskFailedCallback = onFailed; @@ -78,7 +77,13 @@ class RequestJourneyProfileTask extends SferaTask { await _sferaRepository.saveJourneyProfile(journeyProfile); } - _taskCompletedCallback(this, replyMessage.payload!.journeyProfiles.first); + final result = []; + result.addAll(replyMessage.payload!.journeyProfiles); + result.addAll(replyMessage.payload!.segmentProfiles); + result.addAll(replyMessage.payload!.trainCharacteristics); + result.addAll(replyMessage.payload!.relatedTrainInformation); + + _taskCompletedCallback(this, result); return true; } return false; diff --git a/das_client/lib/sfera/src/service/task/request_segment_profiles_task.dart b/das_client/lib/sfera/src/service/task/request_segment_profiles_task.dart index 26007cd5..a3511b44 100644 --- a/das_client/lib/sfera/src/service/task/request_segment_profiles_task.dart +++ b/das_client/lib/sfera/src/service/task/request_segment_profiles_task.dart @@ -101,7 +101,7 @@ class RequestSegmentProfilesTask extends SferaTask> { if (allValid) { _taskCompletedCallback(this, replyMessage.payload!.segmentProfiles.toList()); } else { - _taskFailedCallback(this, ErrorCode.sferaSpInvalid); + _taskFailedCallback(this, ErrorCode.sferaInvalid); } return true; diff --git a/das_client/lib/sfera/src/service/task/sfera_task.dart b/das_client/lib/sfera/src/service/task/sfera_task.dart index ede1d366..fd89eb48 100644 --- a/das_client/lib/sfera/src/service/task/sfera_task.dart +++ b/das_client/lib/sfera/src/service/task/sfera_task.dart @@ -1,13 +1,13 @@ import 'dart:async'; -import 'package:das_client/sfera/src/service/handler/sfera_message_handler.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; import 'package:das_client/util/error_code.dart'; import 'package:fimber/fimber.dart'; typedef TaskFailed = void Function(SferaTask task, ErrorCode errorCode); typedef TaskCompleted = void Function(SferaTask task, T? data); -abstract class SferaTask implements SferaMessageHandler { +abstract class SferaTask { SferaTask({Duration? timeout}) : _timeout = timeout ?? const Duration(seconds: 15); final Duration _timeout; @@ -15,6 +15,8 @@ abstract class SferaTask implements SferaMessageHandler { Future execute(TaskCompleted onCompleted, TaskFailed onFailed); + Future handleMessage(SferaG2bReplyMessage replyMessage); + void startTimeout( TaskFailed onFailed, ) { diff --git a/das_client/lib/sfera/src/sfera_reply_parser.dart b/das_client/lib/sfera/src/sfera_reply_parser.dart index 2a5dfdd1..f1bec1a3 100644 --- a/das_client/lib/sfera/src/sfera_reply_parser.dart +++ b/das_client/lib/sfera/src/sfera_reply_parser.dart @@ -5,6 +5,8 @@ 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/delay.dart'; +import 'package:das_client/sfera/src/model/g2b_event_payload.dart'; import 'package:das_client/sfera/src/model/g2b_reply_payload.dart'; import 'package:das_client/sfera/src/model/handshake_acknowledgement.dart'; import 'package:das_client/sfera/src/model/handshake_reject.dart'; @@ -19,8 +21,11 @@ import 'package:das_client/sfera/src/model/network_specific_area.dart'; import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; import 'package:das_client/sfera/src/model/network_specific_point.dart'; import 'package:das_client/sfera/src/model/otn_id.dart'; +import 'package:das_client/sfera/src/model/own_train.dart'; +import 'package:das_client/sfera/src/model/related_train_information.dart'; import 'package:das_client/sfera/src/model/segment_profile.dart'; import 'package:das_client/sfera/src/model/segment_profile_list.dart'; +import 'package:das_client/sfera/src/model/sfera_g2b_event_message.dart'; import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; import 'package:das_client/sfera/src/model/signal.dart'; @@ -52,6 +57,7 @@ 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/train_location_information.dart'; import 'package:das_client/sfera/src/model/velocity.dart'; import 'package:das_client/sfera/src/model/virtual_balise.dart'; import 'package:das_client/sfera/src/model/virtual_balise_position.dart'; @@ -200,6 +206,18 @@ class SferaReplyParser { return TrainCharacteristics(type: type, attributes: attributes, children: children, value: value); case TcFeatures.elementType: return TcFeatures(type: type, attributes: attributes, children: children, value: value); + case SferaG2bEventMessage.elementType: + return SferaG2bEventMessage(type: type, attributes: attributes, children: children, value: value); + case G2bEventPayload.elementType: + return G2bEventPayload(type: type, attributes: attributes, children: children, value: value); + case RelatedTrainInformation.elementType: + return RelatedTrainInformation(type: type, attributes: attributes, children: children, value: value); + case OwnTrain.elementType: + return OwnTrain(type: type, attributes: attributes, children: children, value: value); + case TrainLocationInformation.elementType: + return TrainLocationInformation(type: type, attributes: attributes, children: children, value: value); + case Delay.elementType: + return Delay(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/error_code.dart b/das_client/lib/util/error_code.dart index f5b74289..11de18cb 100644 --- a/das_client/lib/util/error_code.dart +++ b/das_client/lib/util/error_code.dart @@ -9,7 +9,7 @@ enum ErrorCode { sferaHandshakeRejected(code: 10001), sferaRequestTimeout(code: 10002), sferaJpUnavailable(code: 10003), - sferaSpInvalid(code: 10004); + sferaInvalid(code: 10004); const ErrorCode({ required this.code, @@ -36,8 +36,8 @@ extension ErrorCodeExtension on ErrorCode { return context.l10n.c_error_sfera_request_timeout; case ErrorCode.sferaJpUnavailable: return context.l10n.c_error_sfera_jp_unavailable; - case ErrorCode.sferaSpInvalid: - return context.l10n.c_error_sfera_sp_invalid; + case ErrorCode.sferaInvalid: + return context.l10n.c_error_sfera_invalid; } } } \ No newline at end of file diff --git a/das_client/pubspec.lock b/das_client/pubspec.lock index 7f8c4506..73b770af 100644 --- a/das_client/pubspec.lock +++ b/das_client/pubspec.lock @@ -608,6 +608,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0-dev.14" + iso_duration: + dependency: "direct main" + description: + name: iso_duration + sha256: "0233fed5e650c0256c1e37f8427f28cb37bb680b948ee845e2c8f1befb0f899b" + url: "https://pub.dev" + source: hosted + version: "0.1.1" js: dependency: transitive description: diff --git a/das_client/pubspec.yaml b/das_client/pubspec.yaml index 532aaea3..c27c54ed 100644 --- a/das_client/pubspec.yaml +++ b/das_client/pubspec.yaml @@ -60,6 +60,8 @@ dependencies: synchronized: ^3.3.0 # https://pub.dev/packages/flutter_svg flutter_svg: ^2.0.14 + # https://pub.dev/packages/iso_duration + iso_duration: ^0.1.1 dev_dependencies: integration_test: diff --git a/das_client/test/sfera/mapper/sfera_mapper_test.dart b/das_client/test/sfera/mapper/sfera_mapper_test.dart index cb55e0cb..d5f4be0a 100644 --- a/das_client/test/sfera/mapper/sfera_mapper_test.dart +++ b/das_client/test/sfera/mapper/sfera_mapper_test.dart @@ -15,6 +15,7 @@ import 'package:das_client/model/journey/track_equipment.dart'; import 'package:das_client/model/journey/train_series.dart'; 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/delay.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'; @@ -74,7 +75,8 @@ void main() { trainCharacteristics.add(trainCharacteristic); } - return SferaModelMapper.mapToJourney(journeyProfile, segmentProfiles, trainCharacteristics); + return SferaModelMapper.mapToJourney( + journeyProfile: journeyProfile, segmentProfiles: segmentProfiles, trainCharacteristics: trainCharacteristics); } test('Test invalid journey on SP missing', () async { @@ -578,4 +580,58 @@ void main() { expect(journey.metadata.breakSeries!.trainSeries, TrainSeries.R); expect(journey.metadata.breakSeries!.breakSeries, 115); }); + + test('Test correct conversion from String to duration with the delay being PT0M25S', () async { + final String delayAsString = 'PT0M25S'; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNotNull); + expect(convertedDelay!.isNegative, false); + expect(convertedDelay.inMinutes, 0); + expect(convertedDelay.inSeconds, 25); + }); + + test('Test correct conversion from String to duration with negative delay', () async { + final String delayAsString = '-PT3M5S'; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNotNull); + expect(convertedDelay!.isNegative, true); + expect(convertedDelay.inMinutes, -3); + expect(convertedDelay.inSeconds, -185); + }); + + test('Test null String conversion to null duration', () async { + final String? delayAsString = null; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNull); + }); + + test('Test empty String conversion to null duration', () async { + final String delayAsString = ''; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNull); + }); + + test('Test big delay String over one hour conversion to correct duration', () async { + final String delayAsString = 'PT5H45M20S'; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNotNull); + expect(convertedDelay!.isNegative, false); + expect(convertedDelay.inHours, 5); + expect(convertedDelay.inMinutes, 345); + expect(convertedDelay.inSeconds, 20720); + }); + + test('Test only seconds conversion to correct duration', () async { + final String delayAsString = 'PT14S'; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNotNull); + expect(convertedDelay!.isNegative, false); + expect(convertedDelay.inSeconds, 14); + }); + + test('Test wrong ISO 8601 format String conversion to null duration', () async { + final String delayAsString = '+PTH45S3434M334'; + final Duration? convertedDelay = Delay.toDuration(delayAsString); + expect(convertedDelay, isNull); + }); } 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 6a3d4782..7c7101f3 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 @@ -2,6 +2,7 @@ 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/journey_profile.dart'; import 'package:das_client/sfera/src/model/sfera_g2b_reply_message.dart'; import 'package:das_client/sfera/src/service/task/request_journey_profile_task.dart'; import 'package:das_client/util/error_code.dart'; @@ -38,7 +39,7 @@ void main() { await journeyTask.execute((task, data) { expect(task, journeyTask); - expect(data, sferaG2bReplyMessage.payload!.journeyProfiles.first); + expect(data!.whereType().first, sferaG2bReplyMessage.payload!.journeyProfiles.first); }, (task, errorCode) { fail('Task failed with error code $errorCode'); }); 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 7141178a..3722e270 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 @@ -96,7 +96,7 @@ void main() { fail('Test should not call success'); }, (task, errorCode) { expect(task, segmentTask); - expect(errorCode, ErrorCode.sferaSpInvalid); + expect(errorCode, ErrorCode.sferaInvalid); }); verify(mqttService.publishMessage(any, any, any)).called(1); 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 deleted file mode 100644 index 59c002c1..00000000 --- a/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_JP_T5.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - 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 deleted file mode 100644 index 778f64ca..00000000 --- a/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_SP_T5_1.xml +++ /dev/null @@ -1,252 +0,0 @@ - - - - 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 deleted file mode 100644 index 51ce7bf2..00000000 --- a/sfera-mock/src/main/resources/static_sfera_resources/T5_breaking_series/SFERA_TC_T5_1.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - 1185 - - - diff --git a/sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_1000.xml b/sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_2000.xml similarity index 100% rename from sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_1000.xml rename to sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_2000.xml diff --git a/sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_60000.xml b/sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_60000.xml new file mode 100644 index 00000000..20241ef8 --- /dev/null +++ b/sfera-mock/src/main/resources/static_sfera_resources/T9999_mixed_journey/SFERA_Event_T9999_60000.xml @@ -0,0 +1,22 @@ + + + + + + + 1085 + T9999 + 2022-01-04 + + + + + + 0085 + + + + + + +