diff --git a/das_client/assets/icons/icon_additional_speed_restriction.svg b/das_client/assets/icons/icon_additional_speed_restriction.svg new file mode 100644 index 00000000..f327dec5 --- /dev/null +++ b/das_client/assets/icons/icon_additional_speed_restriction.svg @@ -0,0 +1,5 @@ + + + + \ 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 36d362cc..5f3990a7 100644 --- a/das_client/integration_test/test/train_journey_table_test.dart +++ b/das_client/integration_test/test/train_journey_table_test.dart @@ -1,3 +1,4 @@ +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/cells/bracket_station_body.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/protection_section_row.dart'; @@ -12,6 +13,63 @@ import '../util/test_utils.dart'; void main() { group('train journey table test', () { + testWidgets('test additional speed restriction row is displayed correctly', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: '500'); + + final scrollableFinder = find.byType(ListView); + expect(scrollableFinder, findsOneWidget); + + final asrRow = findDASTableRowByText('km 64.200 - km 47.200'); + expect(asrRow, findsOneWidget); + + final asrIcon = find.descendant( + of: asrRow, matching: find.byKey(AdditionalSpeedRestrictionRow.additionalSpeedRestrictionIconKey)); + expect(asrIcon, findsOneWidget); + + final asrSpeed = find.descendant(of: asrRow, matching: find.text('60')); + expect(asrSpeed, findsOneWidget); + + // check all cells are colored + final coloredCells = find.descendant( + of: asrRow, + matching: find.byWidgetPredicate((it) => + it is Container && + it.decoration is BoxDecoration && + (it.decoration as BoxDecoration).color == AdditionalSpeedRestrictionRow.additionalSpeedRestrictionColor)); + expect(coloredCells, findsNWidgets(11)); + }); + + testWidgets('test other rows are displayed correctly', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: '500'); + + final scrollableFinder = find.byType(ListView); + expect(scrollableFinder, findsOneWidget); + + final testRows = ['Genève', 'km 32.2', 'Lengnau', 'WANZ']; + + for (final rowText in testRows) { + await tester.dragUntilVisible(find.text(rowText), scrollableFinder, const Offset(0, -50)); + + final testRow = findDASTableRowByText(rowText); + expect(testRow, findsOneWidget); + + // check first 3 cells are colored + final coloredCells = find.descendant( + of: testRow, + matching: find.byWidgetPredicate((it) => + it is Container && + it.decoration is BoxDecoration && + (it.decoration as BoxDecoration).color == AdditionalSpeedRestrictionRow.additionalSpeedRestrictionColor)); + expect(coloredCells, findsNWidgets(3)); + } + }); + testWidgets('check if all table columns with header are present', (tester) async { await prepareAndStartApp(tester); @@ -34,6 +92,38 @@ void main() { } }); + testWidgets('test route is displayed correctly', (tester) async { + await prepareAndStartApp(tester); + + // load train journey by filling out train selection page + await _loadTrainJourney(tester, trainNumber: '9999'); + + final scrollableFinder = find.byType(ListView); + expect(scrollableFinder, findsOneWidget); + + final stopRouteRow = findDASTableRowByText('Bahnhof A'); + final nonStoppingPassRouteRow = findDASTableRowByText('Haltestelle B'); + expect(stopRouteRow, findsOneWidget); + expect(nonStoppingPassRouteRow, findsOneWidget); + + // check stop circles + final stopRoute = find.descendant(of: stopRouteRow, matching: find.byKey(RouteCellBody.stopKey)); + final nonStoppingPassRoute = + find.descendant(of: nonStoppingPassRouteRow, matching: find.byKey(RouteCellBody.stopKey)); + expect(stopRoute, findsOneWidget); + expect(nonStoppingPassRoute, findsNothing); + + // check route start + final routeStart = find.byKey(RouteCellBody.routeStartKey); + expect(routeStart, findsOneWidget); + + await tester.dragUntilVisible(find.byKey(RouteCellBody.routeEndKey), scrollableFinder, const Offset(0, -50)); + + // check route end + final routeEnd = find.byKey(RouteCellBody.routeEndKey); + expect(routeEnd, findsOneWidget); + }); + testWidgets('test protection secions are displayed correctly', (tester) async { await prepareAndStartApp(tester); @@ -54,7 +144,8 @@ void main() { expect(find.descendant(of: protectionSectionRow, matching: find.text('FL')), findsOneWidget); expect(find.descendant(of: protectionSectionRow, matching: find.text('32.2')), findsOneWidget); // Verify icon is displayed - expect(find.descendant(of: protectionSectionRow, matching: find.byKey(ProtectionSectionRow.protectionSectionKey)), findsOneWidget); + expect(find.descendant(of: protectionSectionRow, matching: find.byKey(ProtectionSectionRow.protectionSectionKey)), + findsOneWidget); // Scroll to next protection section await tester.dragUntilVisible(find.text('Yverdon-les-Bains'), scrollableFinder, const Offset(0, -20)); @@ -157,6 +248,11 @@ void main() { // load train journey by filling out train selection page await _loadTrainJourney(tester, trainNumber: '9999'); + final scrollableFinder = find.byType(ListView); + expect(scrollableFinder, findsOneWidget); + + await tester.dragUntilVisible(find.text('Klammerbahnhof D1'), scrollableFinder, const Offset(0, -50)); + final bracketStationD = findDASTableRowByText('Klammerbahnhof D'); final bracketStationD1 = findDASTableRowByText('Klammerbahnhof D1'); expect(bracketStationD, findsOneWidget); @@ -181,6 +277,11 @@ void main() { // load train journey by filling out train selection page await _loadTrainJourney(tester, trainNumber: '9999'); + final scrollableFinder = find.byType(ListView); + expect(scrollableFinder, findsOneWidget); + + await tester.dragUntilVisible(find.text('Halt auf Verlangen C'), scrollableFinder, const Offset(0, -50)); + final stopOnDemandRow = findDASTableRowByText('Halt auf Verlangen C'); expect(stopOnDemandRow, findsOneWidget); @@ -195,34 +296,6 @@ void main() { expect(stopRoute, findsNothing); }); - testWidgets('test route is displayed correctly', (tester) async { - await prepareAndStartApp(tester); - - // load train journey by filling out train selection page - await _loadTrainJourney(tester, trainNumber: '9999'); - - final stopRouteRow = findDASTableRowByText('Bahnhof A'); - final nonStoppingPassRouteRow = findDASTableRowByText('Haltestelle B'); - expect(stopRouteRow, findsOneWidget); - expect(nonStoppingPassRouteRow, findsOneWidget); - - // check stop circles - final stopRoute = find.descendant(of: stopRouteRow, matching: find.byKey(RouteCellBody.stopKey)); - final nonStoppingPassRoute = find.descendant(of: nonStoppingPassRouteRow, matching: find.byKey(RouteCellBody.stopKey)); - expect(stopRoute, findsOneWidget); - expect(nonStoppingPassRoute, findsNothing); - - // check route start - final startStationRow = findDASTableRowByText('Bahnhof A'); - final routeStart = find.descendant(of: startStationRow, matching: find.byKey(RouteCellBody.routeStartKey)); - expect(routeStart, findsOneWidget); - - // check route end - final endStationRow = findDASTableRowByText('Klammerbahnhof D1'); - final routeEnd = find.descendant(of: endStationRow, matching: find.byKey(RouteCellBody.routeEndKey)); - expect(routeEnd, findsOneWidget); - }); - testWidgets('test halt is displayed italic', (tester) async { await prepareAndStartApp(tester); @@ -237,8 +310,6 @@ void main() { .byWidgetPredicate((it) => it is Text && it.data == 'Schlieren' && it.style?.fontStyle != FontStyle.italic); expect(schlierenText, findsOneWidget); }); - - }); } 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 new file mode 100644 index 00000000..ac3f72bf --- /dev/null +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/additional_speed_restriction_row.dart @@ -0,0 +1,50 @@ +import 'package:das_client/app/i18n/i18n.dart'; +import 'package:das_client/app/pages/journey/train_journey/widgets/table/base_row_builder.dart'; +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'; + +class AdditionalSpeedRestrictionRow extends BaseRowBuilder { + static const Key additionalSpeedRestrictionIconKey = Key('addition_speed_restrction_icon_key'); + static const Color additionalSpeedRestrictionColor = SBBColors.orange; + + AdditionalSpeedRestrictionRow({ + super.height = 44.0, + required super.metadata, + required super.data, + }) : super(rowColor: additionalSpeedRestrictionColor); + + @override + DASTableCell informationCell(BuildContext context) { + return DASTableCell( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text('${context.l10n.p_train_journey_table_kilometre_label} ${data.restriction.kmFrom.toStringAsFixed(3)} ' + '- ${context.l10n.p_train_journey_table_kilometre_label} ${data.restriction.kmTo.toStringAsFixed(3)}'), + ], + ), + ); + } + + @override + DASTableCell iconsCell2(BuildContext context) { + return DASTableCell( + child: SvgPicture.asset( + AppAssets.iconAdditionalSpeedRestriction, + key: additionalSpeedRestrictionIconKey, + ), + alignment: Alignment.center); + } + + @override + DASTableCell graduatedSpeedCell(BuildContext context) { + return DASTableCell( + child: Text(data.restriction.speed.toString()), + alignment: Alignment.center, + ); + } +} diff --git a/das_client/lib/app/pages/journey/train_journey/widgets/table/base_row_builder.dart b/das_client/lib/app/pages/journey/train_journey/widgets/table/base_row_builder.dart index 2862e0df..00cbe211 100644 --- a/das_client/lib/app/pages/journey/train_journey/widgets/table/base_row_builder.dart +++ b/das_client/lib/app/pages/journey/train_journey/widgets/table/base_row_builder.dart @@ -1,21 +1,25 @@ +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/cells/route_cell_body.dart'; import 'package:das_client/app/widgets/table/das_table_cell.dart'; import 'package:das_client/app/widgets/table/das_table_row.dart'; +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/metadata.dart'; import 'package:flutter/material.dart'; -abstract class BaseRowBuilder extends DASTableRowBuilder { +class BaseRowBuilder extends DASTableRowBuilder { const BaseRowBuilder({ - super.height, - this.kilometre, + super.height = 44.0, this.defaultAlignment = Alignment.bottomCenter, this.rowColor, - this.isCurrentPosition = false, + required this.metadata, + required this.data, }); - final List? kilometre; final Alignment defaultAlignment; final Color? rowColor; - final bool isCurrentPosition; + final Metadata metadata; + final T data; @override DASTableRow build(BuildContext context) { @@ -39,31 +43,37 @@ abstract class BaseRowBuilder extends DASTableRowBuilder { } DASTableCell kilometreCell(BuildContext context) { - if (kilometre == null || kilometre!.isEmpty) { + if (data.kilometre.isEmpty) { return DASTableCell.empty(); } return DASTableCell( + color: getSpecialCellColor(), child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(kilometre![0].toStringAsFixed(1)), - if (kilometre!.length > 1) Text(kilometre![1].toStringAsFixed(1)) + Text(data.kilometre[0].toStringAsFixed(1)), + if (data.kilometre.length > 1) Text(data.kilometre[1].toStringAsFixed(1)) ], ), alignment: Alignment.centerLeft); } DASTableCell timeCell(BuildContext context) { - return DASTableCell(child: Text('06:05:52'), alignment: defaultAlignment); + return DASTableCell(color: getSpecialCellColor(), child: Text('06:05:52'), alignment: defaultAlignment); } DASTableCell routeCell(BuildContext context) { return DASTableCell( + color: getSpecialCellColor(), padding: EdgeInsets.all(0.0), alignment: null, - child: RouteCellBody(isCurrentPosition: isCurrentPosition), + child: RouteCellBody( + isCurrentPosition: metadata.currentPosition == data, + isRouteStart: metadata.routeStart == data, + isRouteEnd: metadata.routeEnd == data, + ), ); } @@ -72,15 +82,15 @@ abstract class BaseRowBuilder extends DASTableRowBuilder { } DASTableCell graduatedSpeedCell(BuildContext context) { - return DASTableCell(child: Text('85'), alignment: defaultAlignment); + return DASTableCell.empty(); } DASTableCell advisedSpeedCell(BuildContext context) { - return DASTableCell(child: Text('100'), alignment: defaultAlignment); + return DASTableCell.empty(); } DASTableCell brakedWeightSpeedCell(BuildContext context) { - return DASTableCell(child: Text('95'), alignment: defaultAlignment); + return DASTableCell.empty(); } // TODO: clarify use of different icon cells and set appropriate name @@ -101,4 +111,16 @@ abstract class BaseRowBuilder extends DASTableRowBuilder { DASTableCell actionsCell(BuildContext context) { return DASTableCell.empty(); } + + Color? getSpecialCellColor() { + return getAdditionalSpeedRestriction() != null + ? AdditionalSpeedRestrictionRow.additionalSpeedRestrictionColor + : null; + } + + AdditionalSpeedRestriction? getAdditionalSpeedRestriction() { + return metadata.additionalSpeedRestrictions + .where((it) => it.orderFrom <= data.order && it.orderTo >= data.order) + .firstOrNull; + } } 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 c195b6e1..05de9df6 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 @@ -54,7 +54,11 @@ class RouteCellBody extends StatelessWidget { final isDarkTheme = SBBBaseStyle.of(context).brightness == Brightness.dark; final lineColor = isDarkTheme ? SBBColors.white : SBBColors.black; return Positioned( - key: isRouteStart ? routeStartKey : isRouteEnd ? routeEndKey : null, + key: isRouteStart + ? routeStartKey + : isRouteEnd + ? routeEndKey + : null, top: isRouteStart ? height - sbbDefaultSpacing : -sbbDefaultSpacing, bottom: isRouteEnd ? sbbDefaultSpacing : -sbbDefaultSpacing, right: 0, 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 e8c8dcea..32645ed5 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 @@ -2,23 +2,19 @@ import 'package:das_client/app/i18n/i18n.dart'; import 'package:das_client/app/pages/journey/train_journey/widgets/table/base_row_builder.dart'; 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/metadata.dart'; import 'package:das_client/model/journey/protection_section.dart'; import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -class ProtectionSectionRow extends BaseRowBuilder { +class ProtectionSectionRow extends BaseRowBuilder { static const Key protectionSectionKey = Key('protection_section_key'); ProtectionSectionRow({ super.height = 44.0, - required this.metadata, - required this.protectionSection, - }) : super(rowColor: SBBColors.peach, kilometre: protectionSection.kilometre); - - final Metadata metadata; - final ProtectionSection protectionSection; + required super.metadata, + required super.data, + }) : super(rowColor: SBBColors.peach); @override DASTableCell informationCell(BuildContext context) { @@ -26,10 +22,9 @@ class ProtectionSectionRow extends BaseRowBuilder { child: Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text( - '${context.l10n.p_train_journey_table_kilometre_label} ${protectionSection.kilometre[0].toStringAsFixed(1)}'), + Text('${context.l10n.p_train_journey_table_kilometre_label} ${data.kilometre[0].toStringAsFixed(1)}'), Spacer(), - Text('${protectionSection.isOptional ? 'F' : ''}${protectionSection.isLong ? 'L' : ''}'), + Text('${data.isOptional ? 'F' : ''}${data.isLong ? 'L' : ''}'), ], ), ); 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 38c35ea2..511dee8b 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 @@ -3,38 +3,24 @@ import 'package:das_client/app/pages/journey/train_journey/widgets/table/cells/b import 'package:das_client/app/pages/journey/train_journey/widgets/table/cells/route_cell_body.dart'; 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/metadata.dart'; import 'package:das_client/model/journey/service_point.dart'; import 'package:design_system_flutter/design_system_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -// TODO: Extract real values from SFERA objects. -class ServicePointRow extends BaseRowBuilder { +class ServicePointRow extends BaseRowBuilder { static const Key stopOnRequestKey = Key('stop_on_request_key'); ServicePointRow({ super.height = 64.0, - this.isRouteStart = false, - this.isRouteEnd = false, - required this.metadata, - required this.servicePoint, - }) : super( - rowColor: metadata.nextStop == servicePoint ? SBBColors.royal.withOpacity(0.2) : Colors.transparent, - kilometre: servicePoint.kilometre, - isCurrentPosition: metadata.currentPosition == servicePoint); - - final Metadata metadata; - final ServicePoint servicePoint; - - - final bool isRouteStart; - final bool isRouteEnd; + required super.metadata, + required super.data, + }) : super(rowColor: metadata.nextStop == data ? SBBColors.royal.withOpacity(0.2) : Colors.transparent); @override DASTableCell informationCell(BuildContext context) { - final servicePointName = servicePoint.name.localized; - final textStyle = servicePoint.isStation + final servicePointName = data.name.localized; + final textStyle = data.isStation ? SBBTextStyles.largeBold.copyWith(fontSize: 24.0) : SBBTextStyles.largeLight.copyWith(fontSize: 24.0, fontStyle: FontStyle.italic); return DASTableCell( @@ -57,12 +43,12 @@ class ServicePointRow extends BaseRowBuilder { child: Stack( clipBehavior: Clip.none, children: [ - if (servicePoint.bracketStation != null) + if (data.bracketStation != null) BracketStationBody( - bracketStation: servicePoint.bracketStation!, + bracketStation: data.bracketStation!, height: height!, ), - if (!servicePoint.mandatoryStop) + if (!data.mandatoryStop) Align( alignment: Alignment.bottomCenter, child: SvgPicture.asset( @@ -77,14 +63,15 @@ class ServicePointRow extends BaseRowBuilder { @override DASTableCell routeCell(BuildContext context) { return DASTableCell( + color: getSpecialCellColor(), padding: EdgeInsets.all(0.0), alignment: null, child: RouteCellBody( - isStop: servicePoint.isStop, - isCurrentPosition: isCurrentPosition, - isRouteStart: isRouteStart, - isRouteEnd: isRouteEnd, - isStopOnRequest: !servicePoint.mandatoryStop, + isStop: data.isStop, + isCurrentPosition: metadata.currentPosition == data, + isRouteStart: metadata.routeStart == data, + isRouteEnd: metadata.routeEnd == data, + isStopOnRequest: !data.mandatoryStop, ), ); } 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 ed914d23..613d31cb 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,9 +1,12 @@ import 'package:das_client/app/bloc/train_journey_cubit.dart'; import 'package:das_client/app/i18n/i18n.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/base_row_builder.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'; 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/model/journey/additional_speed_restriction_data.dart'; import 'package:das_client/model/journey/datatype.dart'; import 'package:das_client/model/journey/journey.dart'; import 'package:das_client/model/journey/protection_section.dart'; @@ -47,12 +50,20 @@ class TrainJourney extends StatelessWidget { case Datatype.servicePoint: return ServicePointRow( metadata: journey.metadata, - servicePoint: rowData as ServicePoint, - isRouteStart: index == 0, - isRouteEnd: index == journey.data.length - 1, + data: rowData as ServicePoint, ).build(context); case Datatype.protectionSection: - return ProtectionSectionRow(metadata: journey.metadata, protectionSection: rowData as ProtectionSection) + return ProtectionSectionRow(metadata: journey.metadata, data: rowData as ProtectionSection) + .build(context); + case Datatype.curvePoint: + // TODO: + return BaseRowBuilder(metadata: journey.metadata, data: rowData).build(context); + case Datatype.signal: + // TODO: + return BaseRowBuilder(metadata: journey.metadata, data: rowData).build(context); + case Datatype.additionalSpeedRestriction: + return AdditionalSpeedRestrictionRow( + metadata: journey.metadata, data: rowData as AdditionalSpeedRestrictionData) .build(context); } }) diff --git a/das_client/lib/app/widgets/assets.dart b/das_client/lib/app/widgets/assets.dart index e596d010..f82e233d 100644 --- a/das_client/lib/app/widgets/assets.dart +++ b/das_client/lib/app/widgets/assets.dart @@ -10,4 +10,5 @@ class AppAssets { static const iconHeaderStop = '$_iconsDir/icon_header_stop.svg'; static const iconStopOnRequest = '$_iconsDir/icon_stop_on_request.svg'; static const iconProtectionSection = '$_iconsDir/icon_protection_section.svg'; + static const iconAdditionalSpeedRestriction = '$_iconsDir/icon_additional_speed_restriction.svg'; } diff --git a/das_client/lib/model/journey/additional_speed_restriction.dart b/das_client/lib/model/journey/additional_speed_restriction.dart new file mode 100644 index 00000000..f11d6938 --- /dev/null +++ b/das_client/lib/model/journey/additional_speed_restriction.dart @@ -0,0 +1,16 @@ +import 'package:das_client/model/journey/base_data.dart'; + +class AdditionalSpeedRestriction { + AdditionalSpeedRestriction( + {required this.kmFrom, required this.kmTo, required this.orderFrom, required this.orderTo, this.speed}); + + final double kmFrom; + final double kmTo; + final int orderFrom; + final int orderTo; + final int? speed; + + bool needsEndMarker(List journeyData) { + return journeyData.where((it) => it.order >= orderFrom && it.order <= orderTo).length > 1; + } +} diff --git a/das_client/lib/model/journey/additional_speed_restriction_data.dart b/das_client/lib/model/journey/additional_speed_restriction_data.dart new file mode 100644 index 00000000..3995aa2e --- /dev/null +++ b/das_client/lib/model/journey/additional_speed_restriction_data.dart @@ -0,0 +1,10 @@ +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/datatype.dart'; + +class AdditionalSpeedRestrictionData extends BaseData { + AdditionalSpeedRestrictionData({required this.restriction, required super.order, required super.kilometre}) + : super(type: Datatype.additionalSpeedRestriction); + + final AdditionalSpeedRestriction restriction; +} diff --git a/das_client/lib/model/journey/base_data.dart b/das_client/lib/model/journey/base_data.dart index 8035118c..2966c377 100644 --- a/das_client/lib/model/journey/base_data.dart +++ b/das_client/lib/model/journey/base_data.dart @@ -1,9 +1,16 @@ import 'package:das_client/model/journey/datatype.dart'; +import 'package:das_client/model/journey/track_equipment.dart'; abstract class BaseData { - BaseData({required this.type, required this.order, required this.kilometre}); + BaseData({ + required this.type, + required this.order, + required this.kilometre, + this.trackEquipment = const [], + }); final Datatype type; final int order; final List kilometre; + final List trackEquipment; } diff --git a/das_client/lib/model/journey/bracket_station.dart b/das_client/lib/model/journey/bracket_station.dart index 3a3ccada..96268be8 100644 --- a/das_client/lib/model/journey/bracket_station.dart +++ b/das_client/lib/model/journey/bracket_station.dart @@ -1,5 +1,3 @@ - - class BracketStation { BracketStation({this.mainStationAbbreviation}); diff --git a/das_client/lib/model/journey/curve_point.dart b/das_client/lib/model/journey/curve_point.dart new file mode 100644 index 00000000..53687e32 --- /dev/null +++ b/das_client/lib/model/journey/curve_point.dart @@ -0,0 +1,51 @@ +import 'package:das_client/model/journey/base_data.dart'; +import 'package:das_client/model/journey/datatype.dart'; + +class CurvePoint extends BaseData { + CurvePoint({ + required super.order, + required super.kilometre, + super.trackEquipment, + this.curvePointType, + this.curveType, + this.comment, + }) : super(type: Datatype.curvePoint); + + final CurvePointType? curvePointType; + final CurveType? curveType; + final String? comment; +} + +/// marks the beginning and the end of a curve +enum CurvePointType { + begin, + end, + unknown; + + factory CurvePointType.from(String value) { + return values.firstWhere( + (e) => e.name.toLowerCase() == value.toLowerCase(), + orElse: () => CurvePointType.unknown, + ); + } +} + +/// Type of curve. Is provided only if curvePointType = 'begin' +enum CurveType { + /// begins on the line and ends on the line or a station or a halt + curve, + + /// begins in a station + stationExitCurve, + + /// begins at an halt + curveAfterHalt, + unknown; + + factory CurveType.from(String value) { + return values.firstWhere( + (e) => e.name.toLowerCase() == value.toLowerCase(), + orElse: () => CurveType.unknown, + ); + } +} diff --git a/das_client/lib/model/journey/datatype.dart b/das_client/lib/model/journey/datatype.dart index 0f435d7a..46f36983 100644 --- a/das_client/lib/model/journey/datatype.dart +++ b/das_client/lib/model/journey/datatype.dart @@ -1,4 +1,7 @@ enum Datatype { servicePoint, - protectionSection; + protectionSection, + signal, + curvePoint, + additionalSpeedRestriction; } diff --git a/das_client/lib/model/journey/metadata.dart b/das_client/lib/model/journey/metadata.dart index 949a3fb2..7d83f109 100644 --- a/das_client/lib/model/journey/metadata.dart +++ b/das_client/lib/model/journey/metadata.dart @@ -1,9 +1,19 @@ +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/service_point.dart'; class Metadata { - const Metadata({this.nextStop, this.currentPosition}); + Metadata( + {this.nextStop, + this.currentPosition, + this.routeStart, + this.routeEnd, + List? additionalSpeedRestrictions}) + : additionalSpeedRestrictions = additionalSpeedRestrictions ?? []; final ServicePoint? nextStop; final BaseData? currentPosition; + final List additionalSpeedRestrictions; + final BaseData? routeStart; + final BaseData? routeEnd; } diff --git a/das_client/lib/model/journey/service_point.dart b/das_client/lib/model/journey/service_point.dart index ed829a03..099ee620 100644 --- a/das_client/lib/model/journey/service_point.dart +++ b/das_client/lib/model/journey/service_point.dart @@ -4,15 +4,16 @@ import 'package:das_client/model/journey/datatype.dart'; import 'package:das_client/model/localized_string.dart'; class ServicePoint extends BaseData { - ServicePoint( - {required this.name, - required this.mandatoryStop, - required this.isStop, - required this.isStation, - this.bracketStation, - required super.order, - required super.kilometre}) - : super(type: Datatype.servicePoint); + ServicePoint({ + required this.name, + required this.mandatoryStop, + required this.isStop, + required this.isStation, + this.bracketStation, + required super.order, + required super.kilometre, + super.trackEquipment, + }) : super(type: Datatype.servicePoint); final LocalizedString name; final bool mandatoryStop; diff --git a/das_client/lib/model/journey/signal.dart b/das_client/lib/model/journey/signal.dart new file mode 100644 index 00000000..cc55d94d --- /dev/null +++ b/das_client/lib/model/journey/signal.dart @@ -0,0 +1,32 @@ +import 'package:das_client/model/journey/base_data.dart'; +import 'package:das_client/model/journey/datatype.dart'; + +class Signal extends BaseData { + Signal({ + required super.order, + required super.kilometre, + this.visualIdentifier, + this.functions = const [], + super.trackEquipment, + }) : super(type: Datatype.signal); + + final List functions; + final String? visualIdentifier; +} + +enum SignalFunction { + entry, + exit, + intermediate, + block, + protection, + laneChange, + unknown; + + factory SignalFunction.from(String value) { + return values.firstWhere( + (e) => e.name.toLowerCase() == value.toLowerCase(), + orElse: () => SignalFunction.unknown, + ); + } +} \ No newline at end of file diff --git a/das_client/lib/model/journey/track_equipment.dart b/das_client/lib/model/journey/track_equipment.dart new file mode 100644 index 00000000..ebefbcd9 --- /dev/null +++ b/das_client/lib/model/journey/track_equipment.dart @@ -0,0 +1,52 @@ +import 'package:collection/collection.dart'; + +class TrackEquipment { + TrackEquipment({ + required this.type, + this.startLocation, + this.endLocation, + this.appliesToWholeSp = false, + }); + + final double? startLocation; + final double? endLocation; + final bool appliesToWholeSp; + final TrackEquipmentType type; + + bool isOnLocation(double location) { + if (appliesToWholeSp) { + return true; + } else if (startLocation != null && endLocation != null) { + return startLocation! <= location && location <= endLocation!; + } else if (startLocation != null) { + return startLocation! <= location; + } else if (endLocation != null) { + return location <= endLocation!; + } + return false; + } +} + +enum TrackEquipmentType { + etcsL1ls2TracksWithSingleTrackEquipment('ETCS-L1LS-2TracksWithSingleTrackEquipment'), + etcsL2ConvSpeedReversingImpossible('ETCS-L2-convSpeedReversingImpossible'), + etcsL2ExtSpeedReversingPossible('ETCS-L2-extSpeedReversingPossible'), + etcsL2ExtSpeedReversingImpossible('ETCS-L2-extSpeedReversingImpossible'); + + const TrackEquipmentType(this.value); + + final String value; + + static TrackEquipmentType? from(String value) { + return values.firstWhereOrNull( + (e) => e.value.toLowerCase() == value.toLowerCase() + ); + } +} + +// extensions + +extension TrackEquipmentsExtension on Iterable { + Iterable whereOnLocation(double location) => + where((trackEquipment) => trackEquipment.isOnLocation(location)); +} 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 0c67d4e9..44482353 100644 --- a/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart +++ b/das_client/lib/sfera/src/mapper/sfera_model_mapper.dart @@ -1,19 +1,27 @@ import 'package:collection/collection.dart'; +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/curve_point.dart'; import 'package:das_client/model/journey/datatype.dart'; import 'package:das_client/model/journey/journey.dart'; import 'package:das_client/model/journey/metadata.dart'; 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/track_equipment.dart'; import 'package:das_client/model/localized_string.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'; 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/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/segment_profile.dart'; +import 'package:das_client/sfera/src/model/segment_profile_list.dart'; import 'package:das_client/sfera/src/model/taf_tap_location.dart'; import 'package:fimber/fimber.dart'; @@ -39,7 +47,6 @@ class SferaModelMapper { final journeyData = []; final segmentProfilesLists = journeyProfile.segmentProfilesLists.toList(); - final tafTapLocations = segmentProfiles.map((it) => it.areas).whereNotNull().expand((it) => it.tafTapLocations).toList(); @@ -52,7 +59,16 @@ class SferaModelMapper { it.versionMinor == segmentProfileList.versionMinor) .first; + final trackEquipments = _parseTrackEquipments(segmentProfile); + final kilometreMap = _parseKilometre(segmentProfile); + + final curvePoints = _parseCurvePoints(segmentProfile, segmentIndex, kilometreMap, trackEquipments); + journeyData.addAll(curvePoints); + + final signals = _parseSignals(segmentProfile, segmentIndex, kilometreMap, trackEquipments); + journeyData.addAll(signals); + final timingPoints = segmentProfile.points?.timingPoints.toList() ?? []; for (final tpConstraint in segmentProfileList.timingPointsContraints) { @@ -65,25 +81,167 @@ 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] ?? [], + trackEquipment: trackEquipments.whereOnLocation(timingPoint.location).toList(), + )); } _parseAndAddProtectionSections(journeyData, segmentIndex, segmentProfile, kilometreMap); } + final additionalSpeedRestrictions = _parseAdditionalSpeedRestrictions(journeyProfile, segmentProfiles); + for (final restriction in additionalSpeedRestrictions) { + journeyData.add(AdditionalSpeedRestrictionData( + restriction: restriction, order: restriction.orderFrom, kilometre: [restriction.kmFrom])); + + if (restriction.needsEndMarker(journeyData)) { + journeyData.add(AdditionalSpeedRestrictionData( + restriction: restriction, order: restriction.orderTo, kilometre: [restriction.kmTo])); + } + } + journeyData.sort((a, b) => a.order.compareTo(b.order)); + final servicePoints = journeyData.where((it) => it.type == Datatype.servicePoint).toList(); return Journey( - metadata: Metadata( - nextStop: servicePoints.length > 1 ? servicePoints[1] as ServicePoint : null, - currentPosition: journeyData.first), - data: journeyData); + metadata: Metadata( + nextStop: servicePoints.length > 1 ? servicePoints[1] as ServicePoint : null, + currentPosition: journeyData.first, + additionalSpeedRestrictions: additionalSpeedRestrictions, + routeStart: journeyData.firstOrNull, + routeEnd: journeyData.lastOrNull), + data: journeyData, + ); + } + + static List _parseAdditionalSpeedRestrictions( + JourneyProfile journeyProfile, List segmentProfiles) { + final List result = []; + final now = DateTime.now(); + final segmentProfilesLists = journeyProfile.segmentProfilesLists.toList(); + + int? startSegmentIndex; + int? endSegmentIndex; + double? startLocation; + double? endLocation; + + for (int segmentIndex = 0; segmentIndex < segmentProfilesLists.length; segmentIndex++) { + final segmentProfileList = segmentProfilesLists[segmentIndex]; + + for (final asrTemporaryConstrain in segmentProfileList.asrTemporaryConstrains) { + // TODO: Es werden Langsamfahrstellen von 30min vor Start der Fahrt (betriebliche Zeit) bis 30min nach Ende der Fahrt (betriebliche Zeit) angezeigt. + if (asrTemporaryConstrain.startTime != null && asrTemporaryConstrain.startTime!.isAfter(now) || + asrTemporaryConstrain.endTime != null && asrTemporaryConstrain.endTime!.isBefore(now)) { + continue; + } + + switch (asrTemporaryConstrain.startEndQualifier) { + case StartEndQualifier.starts: + startLocation = asrTemporaryConstrain.startLocation; + startSegmentIndex = segmentIndex; + break; + case StartEndQualifier.startsEnds: + startLocation = asrTemporaryConstrain.startLocation; + startSegmentIndex = segmentIndex; + continue next; + next: + case StartEndQualifier.ends: + endLocation = asrTemporaryConstrain.endLocation; + endSegmentIndex = segmentIndex; + break; + case StartEndQualifier.wholeSp: + break; + } + + if (startSegmentIndex != null && endSegmentIndex != null && startLocation != null && endLocation != null) { + final startSegment = _findSegmentProfile(segmentProfiles, segmentProfilesLists[startSegmentIndex]); + final endSegment = _findSegmentProfile(segmentProfiles, segmentProfilesLists[endSegmentIndex]); + + final startKilometreMap = _parseKilometre(startSegment); + final endKilometreMap = _parseKilometre(endSegment); + + final startOrder = _calculateOrder(startSegmentIndex, startLocation); + final endOrder = _calculateOrder(endSegmentIndex, endLocation); + + result.add(AdditionalSpeedRestriction( + kmFrom: startKilometreMap[startLocation]!.first, + kmTo: endKilometreMap[endLocation]!.first, + orderFrom: startOrder, + orderTo: endOrder, + speed: asrTemporaryConstrain.additionalSpeedRestriction?.asrSpeed)); + + startSegmentIndex = null; + endSegmentIndex = null; + startLocation = null; + endLocation = null; + } + } + } + + if (startSegmentIndex != null || endSegmentIndex != null || startLocation != null || endLocation != null) { + Fimber.w('Incomplete additional speed restriction found: ' + 'startSegmentIndex: $startSegmentIndex, endSegmentIndex: $endSegmentIndex, ' + 'startLocation: $startLocation, endLocation: $endLocation'); + } + + return result; + } + + static SegmentProfile _findSegmentProfile( + List segmentProfiles, SegmentProfileList segmentProfileList) { + return segmentProfiles + .where((it) => + it.id == segmentProfileList.spId && + it.versionMajor == segmentProfileList.versionMajor && + it.versionMinor == segmentProfileList.versionMinor) + .first; + } + + static Iterable _parseSignals(SegmentProfile segmentProfile, int segmentIndex, + Map> kilometreMap, List trackEquipments) { + final signals = segmentProfile.points?.signals ?? []; + return signals.map((signal) { + return Signal( + visualIdentifier: signal.physicalCharacteristics?.visualIdentifier, + functions: signal.functions.map((function) => SignalFunction.from(function.value!)).toList(), + order: _calculateOrder(segmentIndex, signal.id.location), + kilometre: kilometreMap[signal.id.location] ?? [], + trackEquipment: trackEquipments.whereOnLocation(signal.id.location).toList(), + ); + }); + } + + static List _parseTrackEquipments(SegmentProfile segmentProfile) { + final nonStandardTrackEquipments = segmentProfile.areas?.nonStandardTrackEquipments ?? []; + return nonStandardTrackEquipments + .map((element) { + final trackEquipmentType = TrackEquipmentType.from(element.trackEquipmentType!.nspValue); + if (trackEquipmentType == null) { + Fimber.w( + 'Encountered nonStandardTrackEquipment without main station NSP declaration: ${element.trackEquipmentType}'); + return null; + } else { + final hasStartLocation = element.startEndQualifier == StartEndQualifier.starts || + element.startEndQualifier == StartEndQualifier.startsEnds; + final hasEndLocation = element.startEndQualifier == StartEndQualifier.ends || + element.startEndQualifier == StartEndQualifier.startsEnds; + return TrackEquipment( + type: trackEquipmentType, + startLocation: hasStartLocation ? element.startLocation! : null, + endLocation: hasEndLocation ? element.endLocation! : null, + appliesToWholeSp: element.startEndQualifier == StartEndQualifier.wholeSp, + ); + } + }) + .where((e) => e != null) + .cast() + .toList(); } static int _calculateOrder(int segmentIndex, double location) { @@ -168,4 +326,21 @@ class SferaModelMapper { return null; } + + static List _parseCurvePoints(SegmentProfile segmentProfile, int segmentIndex, + Map> kilometreMap, List trackEquipments) { + final curvePointsNsp = segmentProfile.points?.curvePointsNsp ?? []; + return curvePointsNsp.map((curvePointNsp) { + final curvePointTypeValue = curvePointNsp.parameters.withName('curvePointType')?.nspValue; + final curveTypeValue = curvePointNsp.parameters.withName('curveType')?.nspValue; + 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, + trackEquipment: trackEquipments.whereOnLocation(curvePointNsp.location).toList(), + ); + }).toList(); + } } diff --git a/das_client/lib/sfera/src/model/additional_speed_restriction.dart b/das_client/lib/sfera/src/model/additional_speed_restriction.dart new file mode 100644 index 00000000..9f4f8e01 --- /dev/null +++ b/das_client/lib/sfera/src/model/additional_speed_restriction.dart @@ -0,0 +1,11 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class AdditionalSpeedRestriction extends SferaXmlElement { + static const String elementType = 'AdditionalSpeedRestriction'; + + AdditionalSpeedRestriction({super.type = elementType, super.attributes, super.children, super.value}); + + bool get asrFront => attributes['ASR_Front'] != null ? bool.tryParse(attributes['ASR_Front']!) ?? false : false; + + int? get asrSpeed => attributes['ASR_Speed'] != null ? int.tryParse(attributes['ASR_Speed']!) : null; +} diff --git a/das_client/lib/sfera/src/model/enums/start_end_qualifier.dart b/das_client/lib/sfera/src/model/enums/start_end_qualifier.dart new file mode 100644 index 00000000..80b2fdad --- /dev/null +++ b/das_client/lib/sfera/src/model/enums/start_end_qualifier.dart @@ -0,0 +1,15 @@ +import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; + +enum StartEndQualifier implements XmlEnum { + starts(xmlValue: 'Starts'), + ends(xmlValue: 'Ends'), + startsEnds(xmlValue: 'StartsEnds'), + wholeSp(xmlValue: 'WholeSP'); + + const StartEndQualifier({ + required this.xmlValue, + }); + + @override + final String xmlValue; +} diff --git a/das_client/lib/sfera/src/model/enums/temporary_constraint_type.dart b/das_client/lib/sfera/src/model/enums/temporary_constraint_type.dart new file mode 100644 index 00000000..3379ff03 --- /dev/null +++ b/das_client/lib/sfera/src/model/enums/temporary_constraint_type.dart @@ -0,0 +1,21 @@ +import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; + +enum TemporaryConstraintType implements XmlEnum { + asr(xmlValue: 'ASR'), + lowAdhesion(xmlValue: 'Low_Adhesion'), + tractionTotalCurrent(xmlValue: 'TractionTotalCurrent'), + regenerationTotalCurrent(xmlValue: 'RegenerationTotalCurrent'), + powerAdvice(xmlValue: 'PowerAdvice'), + estimatedVoltage(xmlValue: 'EstimatedVoltage'), + wind(xmlValue: 'Wind'), + unavailableDasOperatingModes(xmlValue: 'Unavailable_DAS_OperatingModes'), + advisedSpeed(xmlValue: 'AdvisedSpeed'), + networkSpecificConstraint(xmlValue: 'NetworkSpecificConstraint'); + + const TemporaryConstraintType({ + required this.xmlValue, + }); + + @override + final String xmlValue; +} diff --git a/das_client/lib/sfera/src/model/network_specific_area.dart b/das_client/lib/sfera/src/model/network_specific_area.dart new file mode 100644 index 00000000..62dfd3df --- /dev/null +++ b/das_client/lib/sfera/src/model/network_specific_area.dart @@ -0,0 +1,34 @@ +import 'package:das_client/sfera/src/model/enums/start_end_qualifier.dart'; +import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; +import 'package:das_client/sfera/src/model/network_specific_parameter.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class NetworkSpecificArea extends SferaXmlElement { + static const String elementType = 'NetworkSpecificArea'; + static const String _trackEquipmentTypeName = 'trackEquipmentType'; + + NetworkSpecificArea({super.type = elementType, super.attributes, super.children, super.value}); + + String get name => attributes['name']!; + + StartEndQualifier? get startEndQualifier => + XmlEnum.valueOf(StartEndQualifier.values, attributes['startEndQualifier']); + + double? get startLocation => _parseOrNull(attributes['startLocation']); + + double? get endLocation => _parseOrNull(attributes['endLocation']); + + Iterable get networkSpecificParameters => children.whereType(); + + NetworkSpecificParameter? get trackEquipmentType => + children.whereType().where((it) => it.name == _trackEquipmentTypeName).firstOrNull; + + double? _parseOrNull(String? source) { + return source != null ? double.parse(source) : null; + } + + @override + bool validate() { + return validateHasAttribute('name') && validateHasAttribute('startEndQualifier') && super.validate(); + } +} 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 aa1b3f8d..305f7af1 100644 --- a/das_client/lib/sfera/src/model/network_specific_parameter.dart +++ b/das_client/lib/sfera/src/model/network_specific_parameter.dart @@ -14,3 +14,9 @@ class NetworkSpecificParameter extends SferaXmlElement { return validateHasAttribute('name') && validateHasAttribute('value') && super.validate(); } } + +// extensions + +extension NetworkSpecificParametersExtension on Iterable { + NetworkSpecificParameter? withName(String name) => where((it) => it.name == name).firstOrNull; +} 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 9b75892e..c959cf1a 100644 --- a/das_client/lib/sfera/src/model/network_specific_point.dart +++ b/das_client/lib/sfera/src/model/network_specific_point.dart @@ -14,4 +14,4 @@ class NetworkSpecificPoint extends SpGenericPoint { bool validate() { return validateHasChildOfType() && super.validate(); } -} +} \ No newline at end of file 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 c34ca5dd..67851e12 100644 --- a/das_client/lib/sfera/src/model/segment_profile_list.dart +++ b/das_client/lib/sfera/src/model/segment_profile_list.dart @@ -1,5 +1,7 @@ +import 'package:das_client/sfera/src/model/enums/temporary_constraint_type.dart'; 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'; class SegmentProfileList extends SferaXmlElement { @@ -17,6 +19,10 @@ class SegmentProfileList extends SferaXmlElement { Iterable get timingPointsContraints => children.whereType(); + Iterable get asrTemporaryConstrains => children + .whereType() + .where((it) => it.temporaryConstraintType == TemporaryConstraintType.asr); + @override bool validate() { return validateHasAttribute('SP_ID') && diff --git a/das_client/lib/sfera/src/model/sfera_xml_element.dart b/das_client/lib/sfera/src/model/sfera_xml_element.dart index a6c794c3..ec591684 100644 --- a/das_client/lib/sfera/src/model/sfera_xml_element.dart +++ b/das_client/lib/sfera/src/model/sfera_xml_element.dart @@ -42,6 +42,21 @@ class SferaXmlElement { return true; } + bool validateHasAttributeInt(String attribute) { + if (!attributes.containsKey(attribute)) { + Fimber.w('Validation failed for $type because attribute=$attribute is missing'); + return false; + } + + if (int.tryParse(attributes[attribute]!) == null) { + Fimber.w( + 'Validation failed for $type because attribute=$attribute with value=${attributes[attribute]} could not be parsed to int'); + return false; + } + + return true; + } + bool validateHasChild(String type) { if (childrenWithType(type).isEmpty) { Fimber.w('Validation failed for ${this.type} because it has no child of type $type'); diff --git a/das_client/lib/sfera/src/model/signal.dart b/das_client/lib/sfera/src/model/signal.dart index ef335c70..fe8440b0 100644 --- a/das_client/lib/sfera/src/model/signal.dart +++ b/das_client/lib/sfera/src/model/signal.dart @@ -1,5 +1,7 @@ import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; +import 'package:das_client/sfera/src/model/signal_function.dart'; import 'package:das_client/sfera/src/model/signal_id.dart'; +import 'package:das_client/sfera/src/model/signal_physical_characteristics.dart'; class Signal extends SferaXmlElement { static const String elementType = 'Signal'; @@ -8,6 +10,10 @@ class Signal extends SferaXmlElement { SignalId get id => children.whereType().first; + SignalPhysicalCharacteristics? get physicalCharacteristics => children.whereType().firstOrNull; + + Iterable get functions => children.whereType(); + @override bool validate() { return validateHasChildOfType() && super.validate(); diff --git a/das_client/lib/sfera/src/model/signal_function.dart b/das_client/lib/sfera/src/model/signal_function.dart new file mode 100644 index 00000000..26fa378c --- /dev/null +++ b/das_client/lib/sfera/src/model/signal_function.dart @@ -0,0 +1,7 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class SignalFunction extends SferaXmlElement { + static const String elementType = 'SignalFunction'; + + SignalFunction({super.type = elementType, super.attributes, super.children, super.value}); +} diff --git a/das_client/lib/sfera/src/model/signal_id.dart b/das_client/lib/sfera/src/model/signal_id.dart index d0279d34..6ba8e497 100644 --- a/das_client/lib/sfera/src/model/signal_id.dart +++ b/das_client/lib/sfera/src/model/signal_id.dart @@ -7,10 +7,10 @@ class SignalId extends SferaXmlElement { String get physicalId => attributes['signal_ID_Physical']!; - String get location => attributes['location']!; + double get location => double.parse(attributes['location']!); @override bool validate() { - return validateHasAttribute('signal_ID_Physical') && validateHasAttribute('location') && super.validate(); + return validateHasAttribute('signal_ID_Physical') && validateHasAttributeDouble('location') && super.validate(); } } diff --git a/das_client/lib/sfera/src/model/signal_physical_characteristics.dart b/das_client/lib/sfera/src/model/signal_physical_characteristics.dart new file mode 100644 index 00000000..b26d6463 --- /dev/null +++ b/das_client/lib/sfera/src/model/signal_physical_characteristics.dart @@ -0,0 +1,14 @@ +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class SignalPhysicalCharacteristics extends SferaXmlElement { + static const String elementType = 'SignalPhysicalCharacteristics'; + + SignalPhysicalCharacteristics({super.type = elementType, super.attributes, super.children, super.value}); + + String get visualIdentifier => attributes['visualIdentifier']!; + + @override + bool validate() { + return validateHasAttribute('visualIdentifier') && super.validate(); + } +} diff --git a/das_client/lib/sfera/src/model/sp_areas.dart b/das_client/lib/sfera/src/model/sp_areas.dart index a3b1b89e..431a94d6 100644 --- a/das_client/lib/sfera/src/model/sp_areas.dart +++ b/das_client/lib/sfera/src/model/sp_areas.dart @@ -1,10 +1,15 @@ +import 'package:das_client/sfera/src/model/network_specific_area.dart'; import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; import 'package:das_client/sfera/src/model/taf_tap_location.dart'; class SpAreas extends SferaXmlElement { static const String elementType = 'SP_Areas'; + static const String _nonStandardTrackEquipmentName = 'nonStandardTrackEquipment'; SpAreas({super.type = elementType, super.attributes, super.children, super.value}); Iterable get tafTapLocations => children.whereType(); + + Iterable get nonStandardTrackEquipments => + children.whereType().where((it) => it.name == _nonStandardTrackEquipmentName); } diff --git a/das_client/lib/sfera/src/model/sp_points.dart b/das_client/lib/sfera/src/model/sp_points.dart index fbe92ea2..e763ff77 100644 --- a/das_client/lib/sfera/src/model/sp_points.dart +++ b/das_client/lib/sfera/src/model/sp_points.dart @@ -7,6 +7,7 @@ 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}); @@ -14,6 +15,9 @@ 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 => diff --git a/das_client/lib/sfera/src/model/temporary_constraint_reason.dart b/das_client/lib/sfera/src/model/temporary_constraint_reason.dart new file mode 100644 index 00000000..dd28299f --- /dev/null +++ b/das_client/lib/sfera/src/model/temporary_constraint_reason.dart @@ -0,0 +1,7 @@ +import 'package:das_client/sfera/src/model/multilingual_text.dart'; + +class TemporaryConstraintReason extends MultilingualText { + static const String elementType = 'TemporaryConstraintReason'; + + TemporaryConstraintReason({super.type = elementType, super.attributes, super.children, super.value}); +} diff --git a/das_client/lib/sfera/src/model/temporary_constraints.dart b/das_client/lib/sfera/src/model/temporary_constraints.dart new file mode 100644 index 00000000..be2814fe --- /dev/null +++ b/das_client/lib/sfera/src/model/temporary_constraints.dart @@ -0,0 +1,36 @@ +import 'package:das_client/sfera/src/model/additional_speed_restriction.dart'; +import 'package:das_client/sfera/src/model/enums/start_end_qualifier.dart'; +import 'package:das_client/sfera/src/model/enums/temporary_constraint_type.dart'; +import 'package:das_client/sfera/src/model/enums/xml_enum.dart'; +import 'package:das_client/sfera/src/model/sfera_xml_element.dart'; + +class TemporaryConstraints extends SferaXmlElement { + static const String elementType = 'TemporaryConstraints'; + + TemporaryConstraints({super.type = elementType, super.attributes, super.children, super.value}); + + StartEndQualifier get startEndQualifier => + XmlEnum.valueOf(StartEndQualifier.values, attributes['startEndQualifier']!)!; + + double? get startLocation => + attributes['startLocation'] != null ? double.tryParse(attributes['startLocation']!) : null; + + double? get endLocation => attributes['endLocation'] != null ? double.tryParse(attributes['endLocation']!) : null; + + DateTime? get startTime => attributes['startTime'] != null ? DateTime.tryParse(attributes['startTime']!) : null; + + DateTime? get endTime => attributes['endTime'] != null ? DateTime.tryParse(attributes['endTime']!) : null; + + TemporaryConstraintType get temporaryConstraintType => + XmlEnum.valueOf(TemporaryConstraintType.values, attributes['temporaryConstraintType']!)!; + + AdditionalSpeedRestriction? get additionalSpeedRestriction => + children.whereType().firstOrNull; + + @override + bool validate() { + return validateHasAttribute('startEndQualifier') && + validateHasAttribute('temporaryConstraintType') && + super.validate(); + } +} diff --git a/das_client/lib/sfera/src/sfera_reply_parser.dart b/das_client/lib/sfera/src/sfera_reply_parser.dart index 4973d4c9..8fdbdb00 100644 --- a/das_client/lib/sfera/src/sfera_reply_parser.dart +++ b/das_client/lib/sfera/src/sfera_reply_parser.dart @@ -1,3 +1,4 @@ +import 'package:das_client/sfera/src/model/additional_speed_restriction.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'; @@ -11,6 +12,7 @@ import 'package:das_client/sfera/src/model/km_reference.dart'; import 'package:das_client/sfera/src/model/location_ident.dart'; import 'package:das_client/sfera/src/model/message_header.dart'; import 'package:das_client/sfera/src/model/multilingual_text.dart'; +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'; @@ -19,7 +21,9 @@ import 'package:das_client/sfera/src/model/segment_profile_list.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'; +import 'package:das_client/sfera/src/model/signal_function.dart'; import 'package:das_client/sfera/src/model/signal_id.dart'; +import 'package:das_client/sfera/src/model/signal_physical_characteristics.dart'; import 'package:das_client/sfera/src/model/sp_areas.dart'; import 'package:das_client/sfera/src/model/sp_characteristics.dart'; import 'package:das_client/sfera/src/model/sp_context_information.dart'; @@ -32,6 +36,8 @@ 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/temporary_constraint_reason.dart'; +import 'package:das_client/sfera/src/model/temporary_constraints.dart'; import 'package:das_client/sfera/src/model/timing_point.dart'; import 'package:das_client/sfera/src/model/timing_point_constraints.dart'; import 'package:das_client/sfera/src/model/timing_point_reference.dart'; @@ -109,6 +115,10 @@ class SferaReplyParser { return Signal(type: type, attributes: attributes, children: children, value: value); case SignalId.elementType: return SignalId(type: type, attributes: attributes, children: children, value: value); + case SignalPhysicalCharacteristics.elementType: + return SignalPhysicalCharacteristics(type: type, attributes: attributes, children: children, value: value); + case SignalFunction.elementType: + return SignalFunction(type: type, attributes: attributes, children: children, value: value); case VirtualBalise.elementType: return VirtualBalise(type: type, attributes: attributes, children: children, value: value); case VirtualBalisePosition.elementType: @@ -155,6 +165,14 @@ class SferaReplyParser { return SpCharacteristics(type: type, attributes: attributes, children: children, value: value); case NetworkSpecificPoint.elementType: return NetworkSpecificPoint(type: type, attributes: attributes, children: children, value: value); + case NetworkSpecificArea.elementType: + 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: + return TemporaryConstraints(type: type, attributes: attributes, children: children, value: value); + case TemporaryConstraintReason.elementType: + return TemporaryConstraintReason(type: type, attributes: attributes, children: children, value: value); default: return SferaXmlElement(type: type, attributes: attributes, children: children, value: value); } diff --git a/das_client/test/model/journey/signal_test.dart b/das_client/test/model/journey/signal_test.dart new file mode 100644 index 00000000..1674f1ec --- /dev/null +++ b/das_client/test/model/journey/signal_test.dart @@ -0,0 +1,24 @@ +import 'package:das_client/model/journey/signal.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Test string factory for signal functions', () { + // when + final randomType = SignalFunction.from('random-type'); + final block = SignalFunction.from('block'); + final entry = SignalFunction.from('entry'); + final exit = SignalFunction.from('exit'); + final intermediate = SignalFunction.from('intermediate'); + final laneChange = SignalFunction.from('laneChange'); + final protection = SignalFunction.from('protection'); + + // then + expect(randomType, SignalFunction.unknown); + expect(block, SignalFunction.block); + expect(entry, SignalFunction.entry); + expect(exit, SignalFunction.exit); + expect(intermediate, SignalFunction.intermediate); + expect(laneChange, SignalFunction.laneChange); + expect(protection, SignalFunction.protection); + }); +} diff --git a/das_client/test/model/journey/track_equipment_test.dart b/das_client/test/model/journey/track_equipment_test.dart new file mode 100644 index 00000000..799402ce --- /dev/null +++ b/das_client/test/model/journey/track_equipment_test.dart @@ -0,0 +1,108 @@ +import 'package:das_client/model/journey/track_equipment.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Test track equipment', () { + test('Test track equipment on location with start and end location', () { + // given + final trackEquipment = TrackEquipment( + type: TrackEquipmentType.etcsL2ExtSpeedReversingPossible, + startLocation: 100.0, + endLocation: 300.0, + appliesToWholeSp: false, + ); + + // when + final belowStart = trackEquipment.isOnLocation(50.0); + final onStart = trackEquipment.isOnLocation(100.0); + final between = trackEquipment.isOnLocation(200.0); + final onEnd = trackEquipment.isOnLocation(300.0); + final aboveEnd = trackEquipment.isOnLocation(400.0); + + // then + expect(belowStart, isFalse); + expect(onStart, isTrue); + expect(between, isTrue); + expect(onEnd, isTrue); + expect(aboveEnd, isFalse); + }); + + test('Test track equipment on location that applies whole segment', () async { + final trackEquipment = TrackEquipment( + type: TrackEquipmentType.etcsL2ExtSpeedReversingPossible, + appliesToWholeSp: true, + ); + + // this combination would normally not happen but should be tested. + final trackEquipmentWithStartEnd = TrackEquipment( + type: TrackEquipmentType.etcsL2ExtSpeedReversingPossible, + startLocation: 100.0, + endLocation: 300.0, + appliesToWholeSp: true, + ); + + // when + final onLocation1 = trackEquipment.isOnLocation(0); + final onLocation2 = trackEquipment.isOnLocation(9999); + final belowStart = trackEquipmentWithStartEnd.isOnLocation(50.0); + final onStart = trackEquipmentWithStartEnd.isOnLocation(100.0); + final between = trackEquipmentWithStartEnd.isOnLocation(200.0); + final onEnd = trackEquipmentWithStartEnd.isOnLocation(300.0); + final aboveEnd = trackEquipmentWithStartEnd.isOnLocation(400.0); + + // then + expect(onLocation1, isTrue); + expect(onLocation2, isTrue); + expect(belowStart, isTrue); + expect(onStart, isTrue); + expect(between, isTrue); + expect(onEnd, isTrue); + expect(aboveEnd, isTrue); + }); + + test('Test track equipment on location with only start or end location', () async { + final trackEquipmentWithStart = TrackEquipment( + type: TrackEquipmentType.etcsL2ExtSpeedReversingPossible, + startLocation: 300.0, + ); + + final trackEquipmentWithEnd = TrackEquipment( + type: TrackEquipmentType.etcsL2ExtSpeedReversingPossible, + endLocation: 400.0, + ); + + // when + final belowStart = trackEquipmentWithStart.isOnLocation(100); + final onStart = trackEquipmentWithStart.isOnLocation(300); + final aboveStart = trackEquipmentWithStart.isOnLocation(400); + final belowEnd = trackEquipmentWithEnd.isOnLocation(100); + final onEnd = trackEquipmentWithEnd.isOnLocation(400); + final aboveEnd = trackEquipmentWithEnd.isOnLocation(600); + + // then + expect(belowStart, isFalse); + expect(onStart, isTrue); + expect(aboveStart, isTrue); + expect(belowEnd, isTrue); + expect(onEnd, isTrue); + expect(aboveEnd, isFalse); + }); + }); + + + group('Test track equipment type', () { + test('Test string factory for track equipment type', () { + // when + final tracksWithSingleTrackEquipment = TrackEquipmentType.from('ETCS-L1LS-2TracksWithSingleTrackEquipment'); + final convSpeedReversingImpossible = TrackEquipmentType.from('ETCS-L2-convSpeedReversingImpossible'); + final extSpeedReversingPossible = TrackEquipmentType.from('ETCS-L2-extSpeedReversingPossible'); + final extSpeedReversingImpossible = TrackEquipmentType.from('ETCS-L2-extSpeedReversingImpossible'); + + // then + expect(tracksWithSingleTrackEquipment, TrackEquipmentType.etcsL1ls2TracksWithSingleTrackEquipment); + expect(convSpeedReversingImpossible, TrackEquipmentType.etcsL2ConvSpeedReversingImpossible); + expect(extSpeedReversingPossible, TrackEquipmentType.etcsL2ExtSpeedReversingPossible); + expect(extSpeedReversingImpossible, TrackEquipmentType.etcsL2ExtSpeedReversingImpossible); + }); + }); +} diff --git a/das_client/test/sfera/mapper/sfera_mapper_test.dart b/das_client/test/sfera/mapper/sfera_mapper_test.dart index 975936f5..b07aadd4 100644 --- a/das_client/test/sfera/mapper/sfera_mapper_test.dart +++ b/das_client/test/sfera/mapper/sfera_mapper_test.dart @@ -1,9 +1,13 @@ import 'dart:io'; +import 'package:das_client/model/journey/additional_speed_restriction_data.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'; 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/track_equipment.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/journey_profile.dart'; @@ -22,13 +26,13 @@ void main() { return files; } - Journey getJourney(String trainNumber, int spCount) { + Journey getJourney(String trainNumber, int spCount, {String? spTrainNumber}) { final journeyFile = File('test_resources/jp/SFERA_JP_$trainNumber.xml'); final journeyProfile = SferaReplyParser.parse(journeyFile.readAsStringSync()); expect(journeyProfile.validate(), true); final List segmentProfiles = []; - for (final File file in getFilesForSp('SFERA_SP_$trainNumber', spCount)) { + for (final File file in getFilesForSp('SFERA_SP_${spTrainNumber ?? trainNumber}', spCount)) { final segmentProfile = SferaReplyParser.parse(file.readAsStringSync()); expect(segmentProfile.validate(), true); segmentProfiles.add(segmentProfile); @@ -57,29 +61,199 @@ void main() { expect(servicePoints[4].name.de, 'Klammerbahnhof D1'); }); + test('Test journey data types correctly generated', () async { + final journey = getJourney('9999', 5); + + expect(journey.valid, true); + expect(journey.data, hasLength(17)); + + // segment 1 + expect(journey.data[0], TypeMatcher()); + expect(journey.data[1], TypeMatcher()); + expect(journey.data[2], TypeMatcher()); + expect(journey.data[3], TypeMatcher()); + expect(journey.data[4], TypeMatcher()); + // segment 2 + expect(journey.data[5], TypeMatcher()); + expect(journey.data[6], TypeMatcher()); + expect(journey.data[7], TypeMatcher()); + expect(journey.data[8], TypeMatcher()); + // segment 3 + expect(journey.data[9], TypeMatcher()); + expect(journey.data[10], TypeMatcher()); + expect(journey.data[11], TypeMatcher()); + // segment 4 + expect(journey.data[12], TypeMatcher()); + expect(journey.data[13], TypeMatcher()); + // segment 5 + expect(journey.data[14], TypeMatcher()); + expect(journey.data[15], TypeMatcher()); + expect(journey.data[16], TypeMatcher()); + }); + test('Test kilometre are parsed correctly', () async { final journey = getJourney('9999', 5); expect(journey.valid, true); - expect(journey.data, hasLength(5)); - expect(journey.data[0].kilometre[0], 0.5); - expect(journey.data[1].kilometre[0], 1.5); - expect(journey.data[2].kilometre[0], 2.4); - expect(journey.data[3].kilometre[0], 3.7); - expect(journey.data[3].kilometre[1], 0); - expect(journey.data[4].kilometre[0], 0.6); + expect(journey.data, hasLength(17)); + + // segment 1 + expect(journey.data[0].kilometre[0], 0.2); + expect(journey.data[1].kilometre[0], 0.5); + expect(journey.data[2].kilometre[0], 0.6); + expect(journey.data[3].kilometre[0], 0.7); + expect(journey.data[4].kilometre[0], 0.9); + // segment 2 + expect(journey.data[5].kilometre[0], 1.2); + expect(journey.data[6].kilometre[0], 1.5); + expect(journey.data[7].kilometre[0], 1.7); + expect(journey.data[8].kilometre[0], 1.8); + // segment 3 + expect(journey.data[9].kilometre[0], 2.1); + expect(journey.data[10].kilometre[0], 2.4); + expect(journey.data[11].kilometre[0], 2.6); + // segment 4 + expect(journey.data[12].kilometre[0], 3.7); + expect(journey.data[12].kilometre[1], 0); + expect(journey.data[13].kilometre[0], 0.2); + // segment 5 + expect(journey.data[14].kilometre[0], 0.4); + expect(journey.data[15].kilometre[0], 0.6); + expect(journey.data[16].kilometre[0], 0.8); }); test('Test order is generated correctly', () async { final journey = getJourney('9999', 5); expect(journey.valid, true); - expect(journey.data, hasLength(5)); - expect(journey.data[0].order, 000500); - expect(journey.data[1].order, 100500); - expect(journey.data[2].order, 200400); - expect(journey.data[3].order, 300700); - expect(journey.data[4].order, 400300); + expect(journey.data, hasLength(17)); + + // segment 1 + expect(journey.data[0].order, 000200); + expect(journey.data[1].order, 000500); + expect(journey.data[2].order, 000600); + expect(journey.data[3].order, 000700); + expect(journey.data[4].order, 000900); + // segment 2 + expect(journey.data[5].order, 100200); + expect(journey.data[6].order, 100500); + expect(journey.data[7].order, 100700); + expect(journey.data[8].order, 100800); + // segment 3 + expect(journey.data[9].order, 200100); + expect(journey.data[10].order, 200400); + expect(journey.data[11].order, 200600); + // segment 4 + expect(journey.data[12].order, 300700); + expect(journey.data[13].order, 300900); + // segment 5 + expect(journey.data[14].order, 400100); + expect(journey.data[15].order, 400300); + expect(journey.data[16].order, 400500); + }); + + test('Test track equipment is generated correctly', () async { + final journey = getJourney('9999', 5); + + expect(journey.valid, true); + expect(journey.data, hasLength(17)); + + // segment 1 + expect(journey.data[0].trackEquipment, isEmpty); + expect(journey.data[1].trackEquipment, isEmpty); + expect(journey.data[2].trackEquipment, isEmpty); + expect(journey.data[4].trackEquipment, isEmpty); + // segment 2 + expect(journey.data[5].trackEquipment, hasLength(1)); + expect(journey.data[5].trackEquipment[0].appliesToWholeSp, isTrue); + expect(journey.data[5].trackEquipment[0].type, TrackEquipmentType.etcsL1ls2TracksWithSingleTrackEquipment); + expect(journey.data[6].trackEquipment, hasLength(1)); + expect(journey.data[6].trackEquipment[0].type, TrackEquipmentType.etcsL1ls2TracksWithSingleTrackEquipment); + expect(journey.data[7].trackEquipment, hasLength(1)); + expect(journey.data[7].trackEquipment[0].type, TrackEquipmentType.etcsL1ls2TracksWithSingleTrackEquipment); + expect(journey.data[8].trackEquipment, hasLength(1)); + expect(journey.data[8].trackEquipment[0].type, TrackEquipmentType.etcsL1ls2TracksWithSingleTrackEquipment); + // segment 3 + expect(journey.data[9].trackEquipment, hasLength(1)); + expect(journey.data[9].trackEquipment[0].appliesToWholeSp, isFalse); + expect(journey.data[9].trackEquipment[0].startLocation, 100.0); + expect(journey.data[9].trackEquipment[0].endLocation, 400.0); + expect(journey.data[9].trackEquipment[0].type, TrackEquipmentType.etcsL2ConvSpeedReversingImpossible); + expect(journey.data[10].trackEquipment, hasLength(1)); + expect(journey.data[10].trackEquipment[0].type, TrackEquipmentType.etcsL2ConvSpeedReversingImpossible); + expect(journey.data[11].trackEquipment, hasLength(1)); + expect(journey.data[11].trackEquipment[0].type, TrackEquipmentType.etcsL2ExtSpeedReversingPossible); + expect(journey.data[11].trackEquipment[0].appliesToWholeSp, isFalse); + expect(journey.data[11].trackEquipment[0].startLocation, 500.0); + expect(journey.data[11].trackEquipment[0].endLocation, isNull); + // segment 4 + expect(journey.data[12].trackEquipment, hasLength(1)); + expect(journey.data[12].trackEquipment[0].type, TrackEquipmentType.etcsL2ExtSpeedReversingPossible); + expect(journey.data[12].trackEquipment[0].appliesToWholeSp, isFalse); + expect(journey.data[12].trackEquipment[0].startLocation, isNull); + expect(journey.data[12].trackEquipment[0].endLocation, 800.0); + expect(journey.data[13].trackEquipment, isEmpty); + // segment 5 + expect(journey.data[14].trackEquipment, isEmpty); + expect(journey.data[15].trackEquipment, isEmpty); + expect(journey.data[16].trackEquipment, hasLength(1)); + expect(journey.data[16].trackEquipment[0].startLocation, 400.0); + expect(journey.data[16].trackEquipment[0].endLocation, 800.0); + expect(journey.data[16].trackEquipment[0].type, TrackEquipmentType.etcsL2ExtSpeedReversingImpossible); + expect(journey.data[16].trackEquipment[0].appliesToWholeSp, isFalse); + }); + + test('Test signals are generated correctly', () async { + final journey = getJourney('9999', 5); + final signals = journey.data.where((it) => it.type == Datatype.signal).cast().toList(); + + expect(journey.valid, true); + expect(signals, hasLength(7)); + expect(signals[0].visualIdentifier, 'B1'); + expect(signals[0].functions, hasLength(1)); + expect(signals[0].functions[0], SignalFunction.block); + expect(signals[1].visualIdentifier, 'S1'); + expect(signals[1].functions, hasLength(1)); + expect(signals[1].functions[0], SignalFunction.laneChange); + expect(signals[2].visualIdentifier, 'E1'); + expect(signals[2].functions, hasLength(1)); + expect(signals[2].functions[0], SignalFunction.entry); + expect(signals[3].visualIdentifier, 'A1'); + expect(signals[3].functions, hasLength(1)); + expect(signals[3].functions[0], SignalFunction.exit); + expect(signals[4].visualIdentifier, 'AB1'); + expect(signals[4].functions, hasLength(1)); + expect(signals[4].functions[0], SignalFunction.intermediate); + expect(signals[5].visualIdentifier, 'D1'); + expect(signals[5].functions, hasLength(1)); + expect(signals[5].functions[0], SignalFunction.protection); + expect(signals[6].visualIdentifier, 'BAB1'); + expect(signals[6].functions, hasLength(2)); + expect(signals[6].functions[0], SignalFunction.block); + expect(signals[6].functions[1], SignalFunction.intermediate); + }); + + test('Test curvePoint are generated correctly', () async { + final journey = getJourney('9999', 5); + final curvePoints = journey.data.where((it) => it.type == Datatype.curvePoint).cast().toList(); + + expect(journey.valid, true); + expect(curvePoints, hasLength(5)); + expect(curvePoints[0].curvePointType, CurvePointType.begin); + expect(curvePoints[0].curveType, CurveType.curve); + expect(curvePoints[0].comment, 'Kurve 1'); + expect(curvePoints[1].curvePointType, CurvePointType.end); + expect(curvePoints[1].curveType, isNull); + expect(curvePoints[1].comment, isNull); + expect(curvePoints[2].curvePointType, CurvePointType.begin); + expect(curvePoints[2].curveType, CurveType.curve); + expect(curvePoints[2].comment, 'Kurve 1'); + expect(curvePoints[3].curvePointType, CurvePointType.begin); + expect(curvePoints[3].curveType, CurveType.stationExitCurve); + expect(curvePoints[3].comment, 'Kurve 2'); + expect(curvePoints[4].curvePointType, CurvePointType.begin); + expect(curvePoints[4].curveType, CurveType.curveAfterHalt); + expect(curvePoints[4].comment, 'Kurve 3'); }); test('Test stop on demand is parsed correctly', () async { @@ -155,4 +329,89 @@ void main() { expect(protectionSections[5].isLong, false); expect(protectionSections[5].isOptional, false); }); + + test('Test additional speed restriction is parsed correctly no items between', () async { + final journey = getJourney('513', 1); + final speedRestrictions = journey.data.where((it) => it.type == Datatype.additionalSpeedRestriction).cast().toList(); + + expect(journey.valid, true); + expect(speedRestrictions, hasLength(1)); + expect(speedRestrictions[0].restriction.kmFrom, 64.2); + expect(speedRestrictions[0].restriction.kmTo, 63.2); + expect(speedRestrictions[0].restriction.orderFrom, 700); + expect(speedRestrictions[0].restriction.orderTo, 800); + expect(speedRestrictions[0].restriction.speed, 60); + + expect(journey.metadata.additionalSpeedRestrictions, hasLength(1)); + expect(journey.metadata.additionalSpeedRestrictions[0].kmFrom, 64.2); + expect(journey.metadata.additionalSpeedRestrictions[0].kmTo, 63.2); + expect(journey.metadata.additionalSpeedRestrictions[0].orderFrom, 700); + expect(journey.metadata.additionalSpeedRestrictions[0].orderTo, 800); + expect(journey.metadata.additionalSpeedRestrictions[0].speed, 60); + }); + + test('Test additional speed restriction is parsed correctly over multiple segments', () async { + final journey = getJourney('500', 3); + final speedRestrictions = journey.data.where((it) => it.type == Datatype.additionalSpeedRestriction).cast().toList(); + + expect(journey.valid, true); + expect(speedRestrictions, hasLength(2)); + expect(speedRestrictions[0].restriction.kmFrom, 64.2); + expect(speedRestrictions[0].restriction.kmTo, 47.2); + expect(speedRestrictions[0].restriction.orderFrom, 700); + expect(speedRestrictions[0].restriction.orderTo, 206800); + expect(speedRestrictions[0].restriction.speed, 60); + expect(speedRestrictions[0].order, 700); + expect(speedRestrictions[1].restriction.kmFrom, 64.2); + expect(speedRestrictions[1].restriction.kmTo, 47.2); + expect(speedRestrictions[1].restriction.orderFrom, 700); + expect(speedRestrictions[1].restriction.orderTo, 206800); + expect(speedRestrictions[1].restriction.speed, 60); + expect(speedRestrictions[1].order, 206800); + + expect(journey.metadata.additionalSpeedRestrictions, hasLength(1)); + expect(journey.metadata.additionalSpeedRestrictions[0].kmFrom, 64.2); + expect(journey.metadata.additionalSpeedRestrictions[0].kmTo, 47.2); + expect(journey.metadata.additionalSpeedRestrictions[0].orderFrom, 700); + expect(journey.metadata.additionalSpeedRestrictions[0].orderTo, 206800); + expect(journey.metadata.additionalSpeedRestrictions[0].speed, 60); + }); + + test('Test additional speed restriction without a date', () async { + final journey = getJourney('513_asp_no_date', 1, spTrainNumber: '513'); + final speedRestrictions = journey.data.where((it) => it.type == Datatype.additionalSpeedRestriction).cast().toList(); + + expect(journey.valid, true); + expect(speedRestrictions, hasLength(1)); + expect(speedRestrictions[0].restriction.kmFrom, 64.2); + expect(speedRestrictions[0].restriction.kmTo, 63.2); + expect(speedRestrictions[0].restriction.orderFrom, 700); + expect(speedRestrictions[0].restriction.orderTo, 800); + expect(speedRestrictions[0].restriction.speed, 60); + + expect(journey.metadata.additionalSpeedRestrictions, hasLength(1)); + expect(journey.metadata.additionalSpeedRestrictions[0].kmFrom, 64.2); + expect(journey.metadata.additionalSpeedRestrictions[0].kmTo, 63.2); + expect(journey.metadata.additionalSpeedRestrictions[0].orderFrom, 700); + expect(journey.metadata.additionalSpeedRestrictions[0].orderTo, 800); + expect(journey.metadata.additionalSpeedRestrictions[0].speed, 60); + }); + + test('Test additional speed restriction with date in the past', () async { + final journey = getJourney('513_asp_date_before', 1, spTrainNumber: '513'); + final speedRestrictions = journey.data.where((it) => it.type == Datatype.additionalSpeedRestriction).cast().toList(); + + expect(journey.valid, true); + expect(speedRestrictions, hasLength(0)); + expect(journey.metadata.additionalSpeedRestrictions, hasLength(0)); + }); + + test('Test additional speed restriction with date in the future', () async { + final journey = getJourney('513_asp_date_after', 1, spTrainNumber: '513'); + final speedRestrictions = journey.data.where((it) => it.type == Datatype.additionalSpeedRestriction).cast().toList(); + + expect(journey.valid, true); + expect(speedRestrictions, hasLength(0)); + expect(journey.metadata.additionalSpeedRestrictions, hasLength(0)); + }); } diff --git a/das_client/test/sfera/model/sfera_document_test.dart b/das_client/test/sfera/model/sfera_document_test.dart index b36bf0a7..30c6272e 100644 --- a/das_client/test/sfera/model/sfera_document_test.dart +++ b/das_client/test/sfera/model/sfera_document_test.dart @@ -65,7 +65,7 @@ void main() { expect(spPoint.signals, hasLength(9)); expect(spPoint.signals.first.id.physicalId, '102346'); - expect(spPoint.signals.first.id.location, '843'); + expect(spPoint.signals.first.id.location, 843.0); expect(spPoint.balise, hasLength(3)); expect(spPoint.balise.first.location, '0'); diff --git a/das_client/test_resources/jp/SFERA_JP_500.xml b/das_client/test_resources/jp/SFERA_JP_500.xml new file mode 100644 index 00000000..6ec92ee5 --- /dev/null +++ b/das_client/test_resources/jp/SFERA_JP_500.xml @@ -0,0 +1,141 @@ + + + + + 1085 + 500 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0085 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0085 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/jp/SFERA_JP_513.xml b/das_client/test_resources/jp/SFERA_JP_513.xml index 0a39af76..124c7628 100644 --- a/das_client/test_resources/jp/SFERA_JP_513.xml +++ b/das_client/test_resources/jp/SFERA_JP_513.xml @@ -24,12 +24,12 @@ - + - + @@ -118,5 +118,9 @@ + + + diff --git a/das_client/test_resources/jp/SFERA_JP_513_asp_date_after.xml b/das_client/test_resources/jp/SFERA_JP_513_asp_date_after.xml new file mode 100644 index 00000000..e46b99f1 --- /dev/null +++ b/das_client/test_resources/jp/SFERA_JP_513_asp_date_after.xml @@ -0,0 +1,126 @@ + + + + + 1085 + 513 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/jp/SFERA_JP_513_asp_date_before.xml b/das_client/test_resources/jp/SFERA_JP_513_asp_date_before.xml new file mode 100644 index 00000000..226b0bcd --- /dev/null +++ b/das_client/test_resources/jp/SFERA_JP_513_asp_date_before.xml @@ -0,0 +1,126 @@ + + + + + 1085 + 513 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/jp/SFERA_JP_513_asp_no_date.xml b/das_client/test_resources/jp/SFERA_JP_513_asp_no_date.xml new file mode 100644 index 00000000..4872fa1d --- /dev/null +++ b/das_client/test_resources/jp/SFERA_JP_513_asp_no_date.xml @@ -0,0 +1,126 @@ + + + + + 1085 + 513 + 2022-01-04 + + + + + 0085 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/sp/SFERA_SP_500_1.xml b/das_client/test_resources/sp/SFERA_SP_500_1.xml new file mode 100644 index 00000000..646b2a1c --- /dev/null +++ b/das_client/test_resources/sp/SFERA_SP_500_1.xml @@ -0,0 +1,144 @@ + + + + 0085 + + + + + + CH + 3002 + + + + + + + CH + 3003 + + + + + + + CH + 3004 + + + + + + + CH + 3005 + + + + + + + CH + 3006 + + + + + + + + + + + + + CH + 3002 + + + + + + + CH + 3003 + + + + + + + CH + 3004 + + + + + + + CH + 3005 + + + + + + + CH + 3006 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/sp/SFERA_SP_500_2.xml b/das_client/test_resources/sp/SFERA_SP_500_2.xml new file mode 100644 index 00000000..7782acb0 --- /dev/null +++ b/das_client/test_resources/sp/SFERA_SP_500_2.xml @@ -0,0 +1,190 @@ + + + + 0085 + + + + + + CH + 3007 + + + + + + + CH + 3008 + + + + + + + CH + 3009 + + + + + + + CH + 3010 + + + + + + + CH + 3011 + + + + + + + CH + 3012 + + + + + + + CH + 3013 + + + + + + + + + + + + + + + + CH + 3007 + + + + + + + CH + 3008 + + + + + + + CH + 3009 + + + + + + + CH + 3010 + + + + + + + CH + 3011 + + + + + + + CH + 3012 + + + + + + + CH + 3013 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/sp/SFERA_SP_500_3.xml b/das_client/test_resources/sp/SFERA_SP_500_3.xml new file mode 100644 index 00000000..0fc094d3 --- /dev/null +++ b/das_client/test_resources/sp/SFERA_SP_500_3.xml @@ -0,0 +1,255 @@ + + + + 0085 + + + + + + CH + 3014 + + + + + + + CH + 3015 + + + + + + + CH + 3016 + + + + + + + CH + 3017 + + + + + + + CH + 3018 + + + + + + + CH + 3019 + + + + + + + CH + 3020 + + + + + + + CH + 3021 + + + + + + + CH + 3022 + + + + + + + + + + + + + + + + + + + + + CH + 3014 + + + + + + + CH + 3015 + + + + + + + CH + 3016 + + + + + + + CH + 3017 + + + + + + + CH + 3018 + + + + + + + CH + 3019 + + + + + + + CH + 3020 + + + + + + + CH + 3021 + + + + + + + CH + 3022 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/das_client/test_resources/sp/SFERA_SP_513_1.xml b/das_client/test_resources/sp/SFERA_SP_513_1.xml index ea635970..382f2319 100644 --- a/das_client/test_resources/sp/SFERA_SP_513_1.xml +++ b/das_client/test_resources/sp/SFERA_SP_513_1.xml @@ -415,6 +415,14 @@ + + + + + + + + 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 0266583b..bc50681b 100644 --- a/das_client/test_resources/sp/SFERA_SP_9999_3.xml +++ b/das_client/test_resources/sp/SFERA_SP_9999_3.xml @@ -42,7 +42,7 @@ - + diff --git a/das_client/test_resources/sp/SFERA_SP_9999_4.xml b/das_client/test_resources/sp/SFERA_SP_9999_4.xml index 7e08be86..1e127c46 100644 --- a/das_client/test_resources/sp/SFERA_SP_9999_4.xml +++ b/das_client/test_resources/sp/SFERA_SP_9999_4.xml @@ -34,7 +34,7 @@ - +