From 067012cc845f54814eade0ee35183dd5c566c6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Wed, 20 May 2026 19:08:05 +0200 Subject: [PATCH] implemented RMV public transit module including trip search, station departures, and nearby stop lookup, added "Marianum Connect" API integration with bearer token authentication and auto-refresh logic, integrated geolocator for location-based station search, added persistent storage for favorite stations and recent trip queries, and implemented comprehensive UI for journey details, trip results, and disruption alerts --- android/app/src/main/AndroidManifest.xml | 6 +- .../main/res/xml/network_security_config.xml | 8 + ios/Runner/Info.plist | 15 + lib/api/connect/auth/login/login.dart | 24 + lib/api/connect/auth/login/login_request.dart | 18 + .../connect/auth/login/login_request.g.dart | 20 + .../connect/auth/login/login_response.dart | 65 + lib/api/connect/connect_api.dart | 179 + lib/api/connect/connect_auth_store.dart | 109 + lib/api/connect/connect_endpoint.dart | 12 + lib/api/connect/errors/connect_exception.dart | 25 + .../errors/rmv_rate_limited_exception.dart | 12 + .../errors/rmv_upstream_exception.dart | 28 + lib/api/connect/rmv/iso_duration.dart | 36 + .../connect/rmv/queries/_query_format.dart | 13 + lib/api/connect/rmv/queries/get_arrivals.dart | 32 + .../connect/rmv/queries/get_departures.dart | 32 + .../connect/rmv/queries/get_disruptions.dart | 20 + .../rmv/queries/get_journey_detail.dart | 23 + lib/api/connect/rmv/queries/more_trips.dart | 17 + lib/api/connect/rmv/queries/nearby_stops.dart | 31 + lib/api/connect/rmv/queries/search_stops.dart | 22 + lib/api/connect/rmv/queries/search_trips.dart | 31 + lib/api/connect/rmv/rmv_models.dart | 243 ++ lib/api/connect/rmv/rmv_models.freezed.dart | 3366 +++++++++++++++++ lib/api/connect/rmv/rmv_models.g.dart | 368 ++ lib/main.dart | 2 + lib/routing/app_routes.dart | 57 + lib/state/app/modules/app_modules.dart | 9 + lib/state/app/modules/rmv/bloc/rmv_bloc.dart | 29 + lib/state/app/modules/rmv/bloc/rmv_event.dart | 4 + lib/state/app/modules/rmv/bloc/rmv_state.dart | 15 + .../modules/rmv/bloc/rmv_state.freezed.dart | 283 ++ .../app/modules/rmv/bloc/rmv_state.g.dart | 19 + .../rmv/repository/rmv_repository.dart | 73 + lib/storage/modules_settings.g.dart | 1 + lib/storage/rmv_settings.dart | 41 + lib/storage/rmv_settings.g.dart | 49 + lib/storage/settings.dart | 3 + lib/storage/settings.g.dart | 4 + .../rmv/disruptions/disruptions_view.dart | 183 + lib/view/pages/rmv/favorites_controller.dart | 77 + .../rmv/journey/journey_detail_view.dart | 168 + lib/view/pages/rmv/rmv_view.dart | 83 + .../rmv/stations/nearby_stations_view.dart | 211 ++ .../rmv/stations/station_detail_view.dart | 197 + .../rmv/stations/station_overview_tab.dart | 148 + .../rmv/trip_search/trip_detail_view.dart | 107 + .../rmv/trip_search/trip_results_view.dart | 212 ++ .../rmv/trip_search/trip_search_tab.dart | 213 ++ .../rmv/widgets/departure_arrival_tile.dart | 119 + lib/view/pages/rmv/widgets/leg_tile.dart | 175 + lib/view/pages/rmv/widgets/product_chip.dart | 64 + lib/view/pages/rmv/widgets/realtime_time.dart | 82 + .../rmv/widgets/station_picker_sheet.dart | 229 ++ lib/view/pages/rmv/widgets/trip_tile.dart | 108 + lib/view/pages/rmv/widgets/when_picker.dart | 79 + .../pages/settings/data/default_settings.dart | 3 + pubspec.yaml | 1 + test/api/connect/iso_duration_test.dart | 57 + .../connect/rmv_upstream_exception_test.dart | 26 + 61 files changed, 7885 insertions(+), 1 deletion(-) create mode 100644 android/app/src/main/res/xml/network_security_config.xml create mode 100644 lib/api/connect/auth/login/login.dart create mode 100644 lib/api/connect/auth/login/login_request.dart create mode 100644 lib/api/connect/auth/login/login_request.g.dart create mode 100644 lib/api/connect/auth/login/login_response.dart create mode 100644 lib/api/connect/connect_api.dart create mode 100644 lib/api/connect/connect_auth_store.dart create mode 100644 lib/api/connect/connect_endpoint.dart create mode 100644 lib/api/connect/errors/connect_exception.dart create mode 100644 lib/api/connect/errors/rmv_rate_limited_exception.dart create mode 100644 lib/api/connect/errors/rmv_upstream_exception.dart create mode 100644 lib/api/connect/rmv/iso_duration.dart create mode 100644 lib/api/connect/rmv/queries/_query_format.dart create mode 100644 lib/api/connect/rmv/queries/get_arrivals.dart create mode 100644 lib/api/connect/rmv/queries/get_departures.dart create mode 100644 lib/api/connect/rmv/queries/get_disruptions.dart create mode 100644 lib/api/connect/rmv/queries/get_journey_detail.dart create mode 100644 lib/api/connect/rmv/queries/more_trips.dart create mode 100644 lib/api/connect/rmv/queries/nearby_stops.dart create mode 100644 lib/api/connect/rmv/queries/search_stops.dart create mode 100644 lib/api/connect/rmv/queries/search_trips.dart create mode 100644 lib/api/connect/rmv/rmv_models.dart create mode 100644 lib/api/connect/rmv/rmv_models.freezed.dart create mode 100644 lib/api/connect/rmv/rmv_models.g.dart create mode 100644 lib/state/app/modules/rmv/bloc/rmv_bloc.dart create mode 100644 lib/state/app/modules/rmv/bloc/rmv_event.dart create mode 100644 lib/state/app/modules/rmv/bloc/rmv_state.dart create mode 100644 lib/state/app/modules/rmv/bloc/rmv_state.freezed.dart create mode 100644 lib/state/app/modules/rmv/bloc/rmv_state.g.dart create mode 100644 lib/state/app/modules/rmv/repository/rmv_repository.dart create mode 100644 lib/storage/rmv_settings.dart create mode 100644 lib/storage/rmv_settings.g.dart create mode 100644 lib/view/pages/rmv/disruptions/disruptions_view.dart create mode 100644 lib/view/pages/rmv/favorites_controller.dart create mode 100644 lib/view/pages/rmv/journey/journey_detail_view.dart create mode 100644 lib/view/pages/rmv/rmv_view.dart create mode 100644 lib/view/pages/rmv/stations/nearby_stations_view.dart create mode 100644 lib/view/pages/rmv/stations/station_detail_view.dart create mode 100644 lib/view/pages/rmv/stations/station_overview_tab.dart create mode 100644 lib/view/pages/rmv/trip_search/trip_detail_view.dart create mode 100644 lib/view/pages/rmv/trip_search/trip_results_view.dart create mode 100644 lib/view/pages/rmv/trip_search/trip_search_tab.dart create mode 100644 lib/view/pages/rmv/widgets/departure_arrival_tile.dart create mode 100644 lib/view/pages/rmv/widgets/leg_tile.dart create mode 100644 lib/view/pages/rmv/widgets/product_chip.dart create mode 100644 lib/view/pages/rmv/widgets/realtime_time.dart create mode 100644 lib/view/pages/rmv/widgets/station_picker_sheet.dart create mode 100644 lib/view/pages/rmv/widgets/trip_tile.dart create mode 100644 lib/view/pages/rmv/widgets/when_picker.dart create mode 100644 test/api/connect/iso_duration_test.dart create mode 100644 test/api/connect/rmv_upstream_exception_test.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3bfcf55..73e3099 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,7 +4,8 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:fullBackupContent="@xml/backup_rules" - android:dataExtractionRules="@xml/data_extraction_rules"> + android:dataExtractionRules="@xml/data_extraction_rules" + android:networkSecurityConfig="@xml/network_security_config"> + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..27b7f84 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + + muelleel.ddns.net + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index f76a0d3..f0ef129 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -43,6 +43,21 @@ Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt. NSPhotoLibraryUsageDescription Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt. + NSLocationWhenInUseUsageDescription + Um Haltestellen in deiner Nähe im RMV-Fahrplan zu finden, wird dein aktueller Standort benötigt. + NSAppTransportSecurity + + NSExceptionDomains + + muelleel.ddns.net + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/lib/api/connect/auth/login/login.dart b/lib/api/connect/auth/login/login.dart new file mode 100644 index 0000000..87e65c6 --- /dev/null +++ b/lib/api/connect/auth/login/login.dart @@ -0,0 +1,24 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import 'login_request.dart'; +import 'login_response.dart'; + +class Login extends ConnectApi { + final LoginRequest payload; + + Login(this.payload) : super('auth/login'); + + @override + bool get requiresAuth => false; + + @override + ConnectHttpMethod get method => ConnectHttpMethod.post; + + @override + Object? get body => payload.toJson(); + + @override + LoginResponse assemble(String raw) => + LoginResponse.fromJson(jsonDecode(raw) as Map); +} diff --git a/lib/api/connect/auth/login/login_request.dart b/lib/api/connect/auth/login/login_request.dart new file mode 100644 index 0000000..e9d5eca --- /dev/null +++ b/lib/api/connect/auth/login/login_request.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login_request.g.dart'; + +@JsonSerializable() +class LoginRequest { + final String username; + final String password; + final String tokenName; + + LoginRequest({ + required this.username, + required this.password, + required this.tokenName, + }); + + Map toJson() => _$LoginRequestToJson(this); +} diff --git a/lib/api/connect/auth/login/login_request.g.dart b/lib/api/connect/auth/login/login_request.g.dart new file mode 100644 index 0000000..4d6c22b --- /dev/null +++ b/lib/api/connect/auth/login/login_request.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'login_request.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +LoginRequest _$LoginRequestFromJson(Map json) => LoginRequest( + username: json['username'] as String, + password: json['password'] as String, + tokenName: json['tokenName'] as String, +); + +Map _$LoginRequestToJson(LoginRequest instance) => + { + 'username': instance.username, + 'password': instance.password, + 'tokenName': instance.tokenName, + }; diff --git a/lib/api/connect/auth/login/login_response.dart b/lib/api/connect/auth/login/login_response.dart new file mode 100644 index 0000000..e114c15 --- /dev/null +++ b/lib/api/connect/auth/login/login_response.dart @@ -0,0 +1,65 @@ +/// Hand-rolled to be tolerant of the actual server payload: only [token] is +/// load-bearing. `expiresAt` may be `null` (server-issued tokens without an +/// explicit expiry); every other field shape is also tolerated so a stray +/// rename on the backend does not break login for everyone. +class LoginResponse { + final String token; + final String? tokenId; + + /// `null` when the backend did not provide an expiry. In that case the + /// token is treated as long-lived; callers should refresh on 401. + final DateTime? expiresAt; + final ConnectUserDto? user; + + LoginResponse({ + required this.token, + required this.tokenId, + required this.expiresAt, + required this.user, + }); + + factory LoginResponse.fromJson(Map json) { + final token = json['token']; + if (token is! String || token.isEmpty) { + throw const FormatException('login response missing "token" string'); + } + final expiresRaw = json['expiresAt']; + final expires = expiresRaw is String ? DateTime.tryParse(expiresRaw) : null; + final userJson = json['user']; + return LoginResponse( + token: token, + tokenId: json['tokenId']?.toString(), + expiresAt: expires, + user: userJson is Map + ? ConnectUserDto.fromJson(userJson) + : null, + ); + } +} + +class ConnectUserDto { + final String? id; + final String? username; + final String? firstName; + final String? lastName; + final String? userType; + final String? className; + + ConnectUserDto({ + this.id, + this.username, + this.firstName, + this.lastName, + this.userType, + this.className, + }); + + factory ConnectUserDto.fromJson(Map json) => ConnectUserDto( + id: json['id']?.toString(), + username: json['username']?.toString(), + firstName: json['firstName']?.toString(), + lastName: json['lastName']?.toString(), + userType: json['userType']?.toString(), + className: json['className']?.toString(), + ); +} diff --git a/lib/api/connect/connect_api.dart b/lib/api/connect/connect_api.dart new file mode 100644 index 0000000..1adb298 --- /dev/null +++ b/lib/api/connect/connect_api.dart @@ -0,0 +1,179 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:http/http.dart' as http; + +import '../api_request.dart'; +import '../errors/network_exception.dart'; +import '../errors/parse_exception.dart'; +import '../errors/server_exception.dart'; +import 'connect_auth_store.dart'; +import 'connect_endpoint.dart'; +import 'errors/connect_exception.dart'; +import 'errors/rmv_rate_limited_exception.dart'; +import 'errors/rmv_upstream_exception.dart'; + +enum ConnectHttpMethod { get, post } + +/// Mirrors the [MhslApi] pattern: each endpoint subclasses this, declares the +/// subpath/query/body, and implements [assemble]. Handles bearer-token +/// injection (via [ConnectAuthStore]), one transparent 401-retry after a +/// fresh login, and turns the structured `RmvController.wrap` error strings +/// into typed exceptions. +abstract class ConnectApi extends ApiRequest { + final String subpath; + + ConnectApi(this.subpath); + + /// Override to `false` for endpoints that must NOT receive a bearer token + /// (currently only login itself, to avoid an infinite refresh loop). + bool get requiresAuth => true; + + ConnectHttpMethod get method => ConnectHttpMethod.get; + + Map? get queryParameters => null; + + /// Returns the body to send for POST requests. Should be JSON-encodable. + Object? get body => null; + + T assemble(String raw); + + Future run() async { + final response = await _runOnce(forceTokenRefresh: false); + if (response.statusCode == 401 && requiresAuth) { + // Single transparent retry after a forced refresh, then bail. + await ConnectAuthStore.instance.invalidate(); + final retry = await _runOnce(forceTokenRefresh: true); + if (retry.statusCode == 401) { + throw ConnectException.authFailed( + technicalDetails: + 'connect $subpath HTTP 401 after token refresh: ${_safeBody(retry)}', + ); + } + return _handleResponse(retry); + } + return _handleResponse(response); + } + + Future _runOnce({required bool forceTokenRefresh}) async { + final uri = ConnectEndpoint.resolve(subpath).replace( + queryParameters: _normaliseQuery(queryParameters), + ); + + final headers = { + if (method == ConnectHttpMethod.post) 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + if (requiresAuth) { + final token = await ConnectAuthStore.instance.getToken( + forceRefresh: forceTokenRefresh, + ); + headers['Authorization'] = 'Bearer $token'; + } + + try { + switch (method) { + case ConnectHttpMethod.get: + return await http.get(uri, headers: headers); + case ConnectHttpMethod.post: + final payload = body; + return await http.post( + uri, + headers: headers, + body: payload == null ? null : jsonEncode(payload), + ); + } + } on SocketException catch (e) { + throw NetworkException( + technicalDetails: 'connect $subpath: ${e.message}', + ); + } on TimeoutException catch (e) { + throw NetworkException.timeout( + technicalDetails: 'connect $subpath: $e', + ); + } on http.ClientException catch (e) { + throw NetworkException( + technicalDetails: 'connect $subpath: ${e.message}', + ); + } on HandshakeException catch (e) { + throw NetworkException( + technicalDetails: 'connect $subpath TLS: ${e.message}', + ); + } + } + + T _handleResponse(http.Response response) { + final status = response.statusCode; + final bodyText = _safeBody(response); + + if (status == 503) { + final retryAfter = _parseRetryAfter(bodyText); + throw RmvRateLimitedException( + retryAfter: retryAfter, + technicalDetails: 'connect $subpath HTTP 503: $bodyText', + ); + } + if (status == 502) { + final code = _parseUpstreamErrorCode(bodyText); + throw RmvUpstreamException( + errorCode: code, + technicalDetails: 'connect $subpath HTTP 502: $bodyText', + ); + } + if (status > 299) { + throw ServerException( + statusCode: status, + technicalDetails: 'connect $subpath HTTP $status: $bodyText', + ); + } + + try { + return assemble(bodyText); + } catch (e, st) { + final preview = bodyText.length > 1024 + ? '${bodyText.substring(0, 1024)}…' + : bodyText; + log( + 'connect $subpath assemble failed: $e\nbody: $preview', + stackTrace: st, + ); + throw ParseException( + technicalDetails: 'connect $subpath assemble: $e', + ); + } + } + + String _safeBody(http.Response response) { + try { + return utf8.decode(response.bodyBytes); + } catch (_) { + return response.body; + } + } + + /// Body format from `RmvController.wrap`: `upstream_rate_limited|retryAfter=60`. + Duration _parseRetryAfter(String body) { + final match = RegExp(r'retryAfter=(\d+)').firstMatch(body); + final seconds = match == null ? 60 : int.tryParse(match.group(1)!) ?? 60; + return Duration(seconds: seconds); + } + + /// Body format: `upstream_error|H390` — the segment after the pipe is the + /// RMV/HaFAS error code. + String? _parseUpstreamErrorCode(String body) { + final idx = body.indexOf('|'); + if (idx < 0 || idx >= body.length - 1) return null; + return body.substring(idx + 1).trim(); + } + + Map? _normaliseQuery(Map? raw) { + if (raw == null) return null; + final cleaned = {}; + raw.forEach((key, value) { + if (value.isNotEmpty) cleaned[key] = value; + }); + return cleaned.isEmpty ? null : cleaned; + } +} diff --git a/lib/api/connect/connect_auth_store.dart b/lib/api/connect/connect_auth_store.dart new file mode 100644 index 0000000..9c45569 --- /dev/null +++ b/lib/api/connect/connect_auth_store.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +import '../../model/account_data.dart'; +import 'auth/login/login.dart'; +import 'auth/login/login_request.dart'; +import 'auth/login/login_response.dart'; +import 'errors/connect_exception.dart'; + +/// Holds the Bearer token issued by `POST /auth/login` so that subsequent +/// RMV calls can attach it without prompting the user. Uses the LDAP +/// credentials already kept in [AccountData], so this is transparent to the +/// user — no extra login UI. +class ConnectAuthStore { + static const _tokenKey = 'connect_bearer_token'; + static const _expiresAtKey = 'connect_token_expires_at'; + static const _tokenName = 'MarianumMobile App'; + static const _expiryGuard = Duration(minutes: 1); + + static final ConnectAuthStore _instance = ConnectAuthStore._(); + factory ConnectAuthStore() => _instance; + static ConnectAuthStore get instance => _instance; + ConnectAuthStore._(); + + final FlutterSecureStorage _storage = const FlutterSecureStorage(); + + String? _token; + DateTime? _expiresAt; + bool _hydrated = false; + Future? _inflightLogin; + + Future _hydrate() async { + if (_hydrated) return; + _token = await _storage.read(key: _tokenKey); + final rawExp = await _storage.read(key: _expiresAtKey); + _expiresAt = rawExp == null ? null : DateTime.tryParse(rawExp); + _hydrated = true; + } + + bool _isUsable() { + if (_token == null || _token!.isEmpty) return false; + final exp = _expiresAt; + if (exp == null) return true; + return DateTime.now().add(_expiryGuard).isBefore(exp); + } + + /// Returns a usable bearer token, logging in if necessary. Concurrent + /// callers share the same in-flight login future so a single 401 doesn't + /// trigger N parallel logins. + Future getToken({bool forceRefresh = false}) async { + await _hydrate(); + if (!forceRefresh && _isUsable()) return _token!; + return _inflightLogin ??= _login().whenComplete(() { + _inflightLogin = null; + }); + } + + Future _login() async { + if (!AccountData().isPopulated()) { + throw ConnectException.notAuthenticated(); + } + final username = AccountData().getUsername(); + final password = AccountData().getPassword(); + final LoginResponse response; + try { + response = await Login( + LoginRequest( + username: username, + password: password, + tokenName: _tokenName, + ), + ).run(); + } on ConnectException { + rethrow; + } catch (e, st) { + log('connect login threw: $e', stackTrace: st); + throw ConnectException( + userMessage: + 'Anmeldung am Connect-Server fehlgeschlagen. Bitte später erneut versuchen.', + technicalDetails: 'connect login failed: $e', + ); + } + _token = response.token; + _expiresAt = response.expiresAt; + await _storage.write(key: _tokenKey, value: response.token); + if (response.expiresAt != null) { + await _storage.write( + key: _expiresAtKey, + value: response.expiresAt!.toIso8601String(), + ); + } else { + await _storage.delete(key: _expiresAtKey); + } + return response.token; + } + + Future invalidate() async { + _token = null; + _expiresAt = null; + await _storage.delete(key: _tokenKey); + await _storage.delete(key: _expiresAtKey); + } + + /// Same as [invalidate] — separate method to make logout call-sites read + /// clearly. + Future clear() => invalidate(); +} diff --git a/lib/api/connect/connect_endpoint.dart b/lib/api/connect/connect_endpoint.dart new file mode 100644 index 0000000..5549d48 --- /dev/null +++ b/lib/api/connect/connect_endpoint.dart @@ -0,0 +1,12 @@ +/// Base URL for the MarianumConnect backend. Hardcoded against the test +/// instance for now; once the production URL is finalised this should move +/// into `EndpointData` alongside webuntis/nextcloud. +class ConnectEndpoint { + ConnectEndpoint._(); + + static const String _baseUrl = 'http://muelleel.ddns.net:8080'; + static const String _apiPrefix = '/api/mobile/v1'; + + static Uri resolve(String subpath) => + Uri.parse('$_baseUrl$_apiPrefix/${subpath.startsWith('/') ? subpath.substring(1) : subpath}'); +} diff --git a/lib/api/connect/errors/connect_exception.dart b/lib/api/connect/errors/connect_exception.dart new file mode 100644 index 0000000..64fed15 --- /dev/null +++ b/lib/api/connect/errors/connect_exception.dart @@ -0,0 +1,25 @@ +import '../../errors/app_exception.dart'; + +class ConnectException extends AppException { + const ConnectException({ + super.userMessage = + 'Verbindung zum Marianum-Connect-Server fehlgeschlagen.', + super.technicalDetails, + super.allowRetry, + }); + + factory ConnectException.authFailed({String? technicalDetails}) => + ConnectException( + userMessage: + 'Anmeldung am Connect-Server fehlgeschlagen. Bitte prüfe deine Anmeldedaten.', + technicalDetails: technicalDetails, + allowRetry: false, + ); + + factory ConnectException.notAuthenticated() => const ConnectException( + userMessage: + 'Für diese Funktion ist eine Anmeldung am Connect-Server nötig.', + technicalDetails: 'AccountData missing while trying to log in to connect', + allowRetry: false, + ); +} diff --git a/lib/api/connect/errors/rmv_rate_limited_exception.dart b/lib/api/connect/errors/rmv_rate_limited_exception.dart new file mode 100644 index 0000000..c9976a4 --- /dev/null +++ b/lib/api/connect/errors/rmv_rate_limited_exception.dart @@ -0,0 +1,12 @@ +import '../../errors/app_exception.dart'; + +class RmvRateLimitedException extends AppException { + final Duration retryAfter; + + RmvRateLimitedException({required this.retryAfter, super.technicalDetails}) + : super( + userMessage: + 'Die Fahrplanauskunft ist gerade überlastet. Bitte in ${retryAfter.inSeconds} Sekunden erneut versuchen.', + allowRetry: true, + ); +} diff --git a/lib/api/connect/errors/rmv_upstream_exception.dart b/lib/api/connect/errors/rmv_upstream_exception.dart new file mode 100644 index 0000000..843bf79 --- /dev/null +++ b/lib/api/connect/errors/rmv_upstream_exception.dart @@ -0,0 +1,28 @@ +import '../../errors/app_exception.dart'; + +class RmvUpstreamException extends AppException { + final String? errorCode; + + RmvUpstreamException({required this.errorCode, super.technicalDetails}) + : super(userMessage: _mapMessage(errorCode), allowRetry: true); + + static String _mapMessage(String? code) { + switch (code) { + case 'H390': + return 'Keine Verbindung gefunden.'; + case 'H891': + return 'Eine der angegebenen Stationen ist ungültig.'; + case 'H895': + return 'Start- und Zielhaltestelle sind identisch.'; + case 'H900': + case 'H892': + return 'Die Fahrplanauskunft ist gerade nicht verfügbar.'; + case 'H910': + return 'Die angegebene Zeit liegt außerhalb des Fahrplans.'; + case null: + return 'Die Fahrplanauskunft konnte keine Antwort liefern.'; + default: + return 'Die Fahrplanauskunft hat einen Fehler gemeldet ($code).'; + } + } +} diff --git a/lib/api/connect/rmv/iso_duration.dart b/lib/api/connect/rmv/iso_duration.dart new file mode 100644 index 0000000..244676e --- /dev/null +++ b/lib/api/connect/rmv/iso_duration.dart @@ -0,0 +1,36 @@ +/// ISO-8601 duration (`PT1H30M5S`) ↔ Dart `Duration`. Backend serialises +/// `java.time.Duration` in this format; Dart has no builtin parser. +class IsoDuration { + IsoDuration._(); + + static final RegExp _pattern = RegExp( + r'^P(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$', + ); + + static Duration? fromJson(String? iso) { + if (iso == null || iso.isEmpty) return null; + final match = _pattern.firstMatch(iso); + if (match == null) return null; + final hours = int.parse(match.group(1) ?? '0'); + final minutes = int.parse(match.group(2) ?? '0'); + final secondsRaw = match.group(3) ?? '0'; + final secondsValue = double.parse(secondsRaw); + return Duration( + hours: hours, + minutes: minutes, + milliseconds: (secondsValue * 1000).round(), + ); + } + + static String? toJson(Duration? d) { + if (d == null) return null; + final hours = d.inHours; + final minutes = d.inMinutes.remainder(60); + final seconds = d.inSeconds.remainder(60); + final buf = StringBuffer('PT'); + if (hours > 0) buf.write('${hours}H'); + if (minutes > 0) buf.write('${minutes}M'); + if (seconds > 0 || (hours == 0 && minutes == 0)) buf.write('${seconds}S'); + return buf.toString(); + } +} diff --git a/lib/api/connect/rmv/queries/_query_format.dart b/lib/api/connect/rmv/queries/_query_format.dart new file mode 100644 index 0000000..a2bdfc9 --- /dev/null +++ b/lib/api/connect/rmv/queries/_query_format.dart @@ -0,0 +1,13 @@ +/// Formats a [DateTime] as `2026-05-19T14:30:00` for Java's +/// `LocalDateTime` parser (no timezone, no millis). +String formatLocalDateTime(DateTime dt) { + String two(int v) => v.toString().padLeft(2, '0'); + return '${dt.year}-${two(dt.month)}-${two(dt.day)}T' + '${two(dt.hour)}:${two(dt.minute)}:${two(dt.second)}'; +} + +/// Formats a [DateTime] as `2026-05-19` for Java's `LocalDate` parser. +String formatLocalDate(DateTime dt) { + String two(int v) => v.toString().padLeft(2, '0'); + return '${dt.year}-${two(dt.month)}-${two(dt.day)}'; +} diff --git a/lib/api/connect/rmv/queries/get_arrivals.dart b/lib/api/connect/rmv/queries/get_arrivals.dart new file mode 100644 index 0000000..ad0cf7a --- /dev/null +++ b/lib/api/connect/rmv/queries/get_arrivals.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; +import '_query_format.dart'; + +class GetArrivals extends ConnectApi> { + final String stopId; + final DateTime? when; + final int durationMinutes; + final int maxJourneys; + + GetArrivals({ + required this.stopId, + this.when, + this.durationMinutes = 60, + this.maxJourneys = -1, + }) : super('rmv/arrivals'); + + @override + Map? get queryParameters => { + 'stopId': stopId, + if (when != null) 'when': formatLocalDateTime(when!), + 'duration': durationMinutes.toString(), + 'max': maxJourneys.toString(), + }; + + @override + List assemble(String raw) => (jsonDecode(raw) as List) + .map((e) => Arrival.fromJson(e as Map)) + .toList(growable: false); +} diff --git a/lib/api/connect/rmv/queries/get_departures.dart b/lib/api/connect/rmv/queries/get_departures.dart new file mode 100644 index 0000000..0c5feb0 --- /dev/null +++ b/lib/api/connect/rmv/queries/get_departures.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; +import '_query_format.dart'; + +class GetDepartures extends ConnectApi> { + final String stopId; + final DateTime? when; + final int durationMinutes; + final int maxJourneys; + + GetDepartures({ + required this.stopId, + this.when, + this.durationMinutes = 60, + this.maxJourneys = -1, + }) : super('rmv/departures'); + + @override + Map? get queryParameters => { + 'stopId': stopId, + if (when != null) 'when': formatLocalDateTime(when!), + 'duration': durationMinutes.toString(), + 'max': maxJourneys.toString(), + }; + + @override + List assemble(String raw) => (jsonDecode(raw) as List) + .map((e) => Departure.fromJson(e as Map)) + .toList(growable: false); +} diff --git a/lib/api/connect/rmv/queries/get_disruptions.dart b/lib/api/connect/rmv/queries/get_disruptions.dart new file mode 100644 index 0000000..470ddb8 --- /dev/null +++ b/lib/api/connect/rmv/queries/get_disruptions.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; +import '_query_format.dart'; + +class GetDisruptions extends ConnectApi> { + final DateTime? when; + + GetDisruptions({this.when}) : super('rmv/disruptions'); + + @override + Map? get queryParameters => + when == null ? null : {'when': formatLocalDateTime(when!)}; + + @override + List assemble(String raw) => (jsonDecode(raw) as List) + .map((e) => HimMessage.fromJson(e as Map)) + .toList(growable: false); +} diff --git a/lib/api/connect/rmv/queries/get_journey_detail.dart b/lib/api/connect/rmv/queries/get_journey_detail.dart new file mode 100644 index 0000000..9135c49 --- /dev/null +++ b/lib/api/connect/rmv/queries/get_journey_detail.dart @@ -0,0 +1,23 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; +import '_query_format.dart'; + +class GetJourneyDetail extends ConnectApi { + final String journeyRef; + final DateTime? date; + + GetJourneyDetail({required this.journeyRef, this.date}) + : super('rmv/journey'); + + @override + Map? get queryParameters => { + 'ref': journeyRef, + if (date != null) 'date': formatLocalDate(date!), + }; + + @override + JourneyDetail assemble(String raw) => + JourneyDetail.fromJson(jsonDecode(raw) as Map); +} diff --git a/lib/api/connect/rmv/queries/more_trips.dart b/lib/api/connect/rmv/queries/more_trips.dart new file mode 100644 index 0000000..ccbbab0 --- /dev/null +++ b/lib/api/connect/rmv/queries/more_trips.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; + +class MoreTrips extends ConnectApi { + final String ctx; + + MoreTrips({required this.ctx}) : super('rmv/trips/more'); + + @override + Map? get queryParameters => {'ctx': ctx}; + + @override + TripSearchResult assemble(String raw) => + TripSearchResult.fromJson(jsonDecode(raw) as Map); +} diff --git a/lib/api/connect/rmv/queries/nearby_stops.dart b/lib/api/connect/rmv/queries/nearby_stops.dart new file mode 100644 index 0000000..dc17694 --- /dev/null +++ b/lib/api/connect/rmv/queries/nearby_stops.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; + +class NearbyStops extends ConnectApi> { + final double lat; + final double lon; + final int radiusMeters; + final int max; + + NearbyStops({ + required this.lat, + required this.lon, + this.radiusMeters = 1000, + this.max = 20, + }) : super('rmv/stops/nearby'); + + @override + Map? get queryParameters => { + 'lat': lat.toString(), + 'lon': lon.toString(), + 'radius': radiusMeters.toString(), + 'max': max.toString(), + }; + + @override + List assemble(String raw) => (jsonDecode(raw) as List) + .map((e) => StopLocation.fromJson(e as Map)) + .toList(growable: false); +} diff --git a/lib/api/connect/rmv/queries/search_stops.dart b/lib/api/connect/rmv/queries/search_stops.dart new file mode 100644 index 0000000..2bfea26 --- /dev/null +++ b/lib/api/connect/rmv/queries/search_stops.dart @@ -0,0 +1,22 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; + +class SearchStops extends ConnectApi> { + final String query; + final int max; + + SearchStops({required this.query, this.max = 10}) : super('rmv/stops'); + + @override + Map? get queryParameters => { + 'q': query, + 'max': max.toString(), + }; + + @override + List assemble(String raw) => (jsonDecode(raw) as List) + .map((e) => StopLocation.fromJson(e as Map)) + .toList(growable: false); +} diff --git a/lib/api/connect/rmv/queries/search_trips.dart b/lib/api/connect/rmv/queries/search_trips.dart new file mode 100644 index 0000000..e7c618b --- /dev/null +++ b/lib/api/connect/rmv/queries/search_trips.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +import '../../connect_api.dart'; +import '../rmv_models.dart'; +import '_query_format.dart'; + +class SearchTrips extends ConnectApi { + final String fromStopId; + final String toStopId; + final DateTime? when; + final bool searchByArrival; + + SearchTrips({ + required this.fromStopId, + required this.toStopId, + this.when, + this.searchByArrival = false, + }) : super('rmv/trips'); + + @override + Map? get queryParameters => { + 'from': fromStopId, + 'to': toStopId, + if (when != null) 'when': formatLocalDateTime(when!), + 'searchByArrival': searchByArrival.toString(), + }; + + @override + TripSearchResult assemble(String raw) => + TripSearchResult.fromJson(jsonDecode(raw) as Map); +} diff --git a/lib/api/connect/rmv/rmv_models.dart b/lib/api/connect/rmv/rmv_models.dart new file mode 100644 index 0000000..1453085 --- /dev/null +++ b/lib/api/connect/rmv/rmv_models.dart @@ -0,0 +1,243 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'iso_duration.dart'; + +part 'rmv_models.freezed.dart'; +part 'rmv_models.g.dart'; + +@freezed +abstract class Product with _$Product { + const factory Product({ + String? name, + String? line, + String? displayNumber, + String? category, + String? categoryCode, + String? operator, + }) = _Product; + + factory Product.fromJson(Map json) => + _$ProductFromJson(json); +} + +@freezed +abstract class StopLocation with _$StopLocation { + const factory StopLocation({ + required String id, + String? extId, + required String name, + String? description, + double? lat, + double? lon, + int? products, + int? distanceMeters, + }) = _StopLocation; + + factory StopLocation.fromJson(Map json) => + _$StopLocationFromJson(json); +} + +@freezed +abstract class Departure with _$Departure { + const factory Departure({ + required String stopId, + String? stopExtId, + required String stopName, + required String name, + required String direction, + String? directionFlag, + required DateTime scheduledTime, + DateTime? realTime, + int? delayMinutes, + String? track, + String? realTrack, + @Default(false) bool cancelled, + @Default(true) bool reachable, + Product? product, + String? journeyRef, + }) = _Departure; + + factory Departure.fromJson(Map json) => + _$DepartureFromJson(json); +} + +@freezed +abstract class Arrival with _$Arrival { + const factory Arrival({ + required String stopId, + String? stopExtId, + required String stopName, + required String name, + required String origin, + required DateTime scheduledTime, + DateTime? realTime, + int? delayMinutes, + String? track, + String? realTrack, + @Default(false) bool cancelled, + Product? product, + String? journeyRef, + }) = _Arrival; + + factory Arrival.fromJson(Map json) => + _$ArrivalFromJson(json); +} + +@freezed +abstract class TripEndpoint with _$TripEndpoint { + const factory TripEndpoint({ + required String stopId, + String? stopExtId, + required String name, + double? lat, + double? lon, + required DateTime scheduledTime, + DateTime? realTime, + int? delayMinutes, + String? track, + String? realTrack, + String? type, + }) = _TripEndpoint; + + factory TripEndpoint.fromJson(Map json) => + _$TripEndpointFromJson(json); +} + +@freezed +abstract class JourneyStop with _$JourneyStop { + const factory JourneyStop({ + required String id, + String? extId, + required String name, + double? lat, + double? lon, + int? routeIdx, + DateTime? scheduledArrival, + DateTime? scheduledDeparture, + DateTime? realArrival, + DateTime? realDeparture, + String? arrTrack, + String? depTrack, + String? realArrTrack, + String? realDepTrack, + @Default(false) bool cancelled, + @Default(false) bool cancelledArrival, + @Default(false) bool cancelledDeparture, + }) = _JourneyStop; + + factory JourneyStop.fromJson(Map json) => + _$JourneyStopFromJson(json); +} + +enum LegType { + @JsonValue('JOURNEY') + journey, + @JsonValue('WALK') + walk, + @JsonValue('TRANSFER') + transfer, + @JsonValue('BIKE') + bike, + @JsonValue('CAR') + car, + @JsonValue('PARK_RIDE') + parkRide, + @JsonValue('TAXI') + taxi, + @JsonValue('CHECK_IN') + checkIn, + @JsonValue('CHECK_OUT') + checkOut, + @JsonValue('DUMMY') + dummy, + @JsonValue('UNKNOWN') + unknown, +} + +@freezed +abstract class Leg with _$Leg { + const factory Leg({ + required String id, + required int idx, + @Default(LegType.unknown) LegType type, + String? name, + String? category, + String? number, + String? direction, + required TripEndpoint origin, + required TripEndpoint destination, + @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) + Duration? duration, + @Default(false) bool cancelled, + @Default(false) bool partCancelled, + @Default(true) bool reachable, + Product? product, + String? journeyRef, + @Default([]) List stops, + }) = _Leg; + + factory Leg.fromJson(Map json) => _$LegFromJson(json); +} + +@freezed +abstract class Trip with _$Trip { + const factory Trip({ + String? tripId, + String? ctxRecon, + String? checksum, + @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) + Duration? duration, + @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) + Duration? realDuration, + int? transferCount, + @Default([]) List legs, + }) = _Trip; + + factory Trip.fromJson(Map json) => _$TripFromJson(json); +} + +@freezed +abstract class TripSearchResult with _$TripSearchResult { + const factory TripSearchResult({ + @Default([]) List trips, + String? scrollContextLater, + String? scrollContextEarlier, + }) = _TripSearchResult; + + factory TripSearchResult.fromJson(Map json) => + _$TripSearchResultFromJson(json); +} + +@freezed +abstract class JourneyDetail with _$JourneyDetail { + const factory JourneyDetail({ + String? journeyId, + Product? product, + String? direction, + @Default([]) List stops, + }) = _JourneyDetail; + + factory JourneyDetail.fromJson(Map json) => + _$JourneyDetailFromJson(json); +} + +@freezed +abstract class HimMessage with _$HimMessage { + const factory HimMessage({ + required String id, + String? externalId, + String? head, + String? lead, + String? text, + String? category, + String? company, + int? priority, + int? products, + DateTime? startValidity, + DateTime? endValidity, + DateTime? modified, + }) = _HimMessage; + + factory HimMessage.fromJson(Map json) => + _$HimMessageFromJson(json); +} diff --git a/lib/api/connect/rmv/rmv_models.freezed.dart b/lib/api/connect/rmv/rmv_models.freezed.dart new file mode 100644 index 0000000..f1e9cec --- /dev/null +++ b/lib/api/connect/rmv/rmv_models.freezed.dart @@ -0,0 +1,3366 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'rmv_models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$Product { + + String? get name; String? get line; String? get displayNumber; String? get category; String? get categoryCode; String? get operator; +/// Create a copy of Product +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ProductCopyWith get copyWith => _$ProductCopyWithImpl(this as Product, _$identity); + + /// Serializes this Product to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Product&&(identical(other.name, name) || other.name == name)&&(identical(other.line, line) || other.line == line)&&(identical(other.displayNumber, displayNumber) || other.displayNumber == displayNumber)&&(identical(other.category, category) || other.category == category)&&(identical(other.categoryCode, categoryCode) || other.categoryCode == categoryCode)&&(identical(other.operator, operator) || other.operator == operator)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,name,line,displayNumber,category,categoryCode,operator); + +@override +String toString() { + return 'Product(name: $name, line: $line, displayNumber: $displayNumber, category: $category, categoryCode: $categoryCode, operator: $operator)'; +} + + +} + +/// @nodoc +abstract mixin class $ProductCopyWith<$Res> { + factory $ProductCopyWith(Product value, $Res Function(Product) _then) = _$ProductCopyWithImpl; +@useResult +$Res call({ + String? name, String? line, String? displayNumber, String? category, String? categoryCode, String? operator +}); + + + + +} +/// @nodoc +class _$ProductCopyWithImpl<$Res> + implements $ProductCopyWith<$Res> { + _$ProductCopyWithImpl(this._self, this._then); + + final Product _self; + final $Res Function(Product) _then; + +/// Create a copy of Product +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? name = freezed,Object? line = freezed,Object? displayNumber = freezed,Object? category = freezed,Object? categoryCode = freezed,Object? operator = freezed,}) { + return _then(_self.copyWith( +name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,line: freezed == line ? _self.line : line // ignore: cast_nullable_to_non_nullable +as String?,displayNumber: freezed == displayNumber ? _self.displayNumber : displayNumber // ignore: cast_nullable_to_non_nullable +as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable +as String?,categoryCode: freezed == categoryCode ? _self.categoryCode : categoryCode // ignore: cast_nullable_to_non_nullable +as String?,operator: freezed == operator ? _self.operator : operator // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Product]. +extension ProductPatterns on Product { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Product value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Product() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Product value) $default,){ +final _that = this; +switch (_that) { +case _Product(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Product value)? $default,){ +final _that = this; +switch (_that) { +case _Product() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? name, String? line, String? displayNumber, String? category, String? categoryCode, String? operator)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Product() when $default != null: +return $default(_that.name,_that.line,_that.displayNumber,_that.category,_that.categoryCode,_that.operator);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? name, String? line, String? displayNumber, String? category, String? categoryCode, String? operator) $default,) {final _that = this; +switch (_that) { +case _Product(): +return $default(_that.name,_that.line,_that.displayNumber,_that.category,_that.categoryCode,_that.operator);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? name, String? line, String? displayNumber, String? category, String? categoryCode, String? operator)? $default,) {final _that = this; +switch (_that) { +case _Product() when $default != null: +return $default(_that.name,_that.line,_that.displayNumber,_that.category,_that.categoryCode,_that.operator);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Product implements Product { + const _Product({this.name, this.line, this.displayNumber, this.category, this.categoryCode, this.operator}); + factory _Product.fromJson(Map json) => _$ProductFromJson(json); + +@override final String? name; +@override final String? line; +@override final String? displayNumber; +@override final String? category; +@override final String? categoryCode; +@override final String? operator; + +/// Create a copy of Product +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ProductCopyWith<_Product> get copyWith => __$ProductCopyWithImpl<_Product>(this, _$identity); + +@override +Map toJson() { + return _$ProductToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Product&&(identical(other.name, name) || other.name == name)&&(identical(other.line, line) || other.line == line)&&(identical(other.displayNumber, displayNumber) || other.displayNumber == displayNumber)&&(identical(other.category, category) || other.category == category)&&(identical(other.categoryCode, categoryCode) || other.categoryCode == categoryCode)&&(identical(other.operator, operator) || other.operator == operator)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,name,line,displayNumber,category,categoryCode,operator); + +@override +String toString() { + return 'Product(name: $name, line: $line, displayNumber: $displayNumber, category: $category, categoryCode: $categoryCode, operator: $operator)'; +} + + +} + +/// @nodoc +abstract mixin class _$ProductCopyWith<$Res> implements $ProductCopyWith<$Res> { + factory _$ProductCopyWith(_Product value, $Res Function(_Product) _then) = __$ProductCopyWithImpl; +@override @useResult +$Res call({ + String? name, String? line, String? displayNumber, String? category, String? categoryCode, String? operator +}); + + + + +} +/// @nodoc +class __$ProductCopyWithImpl<$Res> + implements _$ProductCopyWith<$Res> { + __$ProductCopyWithImpl(this._self, this._then); + + final _Product _self; + final $Res Function(_Product) _then; + +/// Create a copy of Product +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? name = freezed,Object? line = freezed,Object? displayNumber = freezed,Object? category = freezed,Object? categoryCode = freezed,Object? operator = freezed,}) { + return _then(_Product( +name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,line: freezed == line ? _self.line : line // ignore: cast_nullable_to_non_nullable +as String?,displayNumber: freezed == displayNumber ? _self.displayNumber : displayNumber // ignore: cast_nullable_to_non_nullable +as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable +as String?,categoryCode: freezed == categoryCode ? _self.categoryCode : categoryCode // ignore: cast_nullable_to_non_nullable +as String?,operator: freezed == operator ? _self.operator : operator // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + + +/// @nodoc +mixin _$StopLocation { + + String get id; String? get extId; String get name; String? get description; double? get lat; double? get lon; int? get products; int? get distanceMeters; +/// Create a copy of StopLocation +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$StopLocationCopyWith get copyWith => _$StopLocationCopyWithImpl(this as StopLocation, _$identity); + + /// Serializes this StopLocation to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is StopLocation&&(identical(other.id, id) || other.id == id)&&(identical(other.extId, extId) || other.extId == extId)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon)&&(identical(other.products, products) || other.products == products)&&(identical(other.distanceMeters, distanceMeters) || other.distanceMeters == distanceMeters)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,extId,name,description,lat,lon,products,distanceMeters); + +@override +String toString() { + return 'StopLocation(id: $id, extId: $extId, name: $name, description: $description, lat: $lat, lon: $lon, products: $products, distanceMeters: $distanceMeters)'; +} + + +} + +/// @nodoc +abstract mixin class $StopLocationCopyWith<$Res> { + factory $StopLocationCopyWith(StopLocation value, $Res Function(StopLocation) _then) = _$StopLocationCopyWithImpl; +@useResult +$Res call({ + String id, String? extId, String name, String? description, double? lat, double? lon, int? products, int? distanceMeters +}); + + + + +} +/// @nodoc +class _$StopLocationCopyWithImpl<$Res> + implements $StopLocationCopyWith<$Res> { + _$StopLocationCopyWithImpl(this._self, this._then); + + final StopLocation _self; + final $Res Function(StopLocation) _then; + +/// Create a copy of StopLocation +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? extId = freezed,Object? name = null,Object? description = freezed,Object? lat = freezed,Object? lon = freezed,Object? products = freezed,Object? distanceMeters = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,extId: freezed == extId ? _self.extId : extId // ignore: cast_nullable_to_non_nullable +as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,lat: freezed == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable +as double?,lon: freezed == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable +as double?,products: freezed == products ? _self.products : products // ignore: cast_nullable_to_non_nullable +as int?,distanceMeters: freezed == distanceMeters ? _self.distanceMeters : distanceMeters // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [StopLocation]. +extension StopLocationPatterns on StopLocation { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _StopLocation value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _StopLocation() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _StopLocation value) $default,){ +final _that = this; +switch (_that) { +case _StopLocation(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _StopLocation value)? $default,){ +final _that = this; +switch (_that) { +case _StopLocation() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? extId, String name, String? description, double? lat, double? lon, int? products, int? distanceMeters)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _StopLocation() when $default != null: +return $default(_that.id,_that.extId,_that.name,_that.description,_that.lat,_that.lon,_that.products,_that.distanceMeters);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String? extId, String name, String? description, double? lat, double? lon, int? products, int? distanceMeters) $default,) {final _that = this; +switch (_that) { +case _StopLocation(): +return $default(_that.id,_that.extId,_that.name,_that.description,_that.lat,_that.lon,_that.products,_that.distanceMeters);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? extId, String name, String? description, double? lat, double? lon, int? products, int? distanceMeters)? $default,) {final _that = this; +switch (_that) { +case _StopLocation() when $default != null: +return $default(_that.id,_that.extId,_that.name,_that.description,_that.lat,_that.lon,_that.products,_that.distanceMeters);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _StopLocation implements StopLocation { + const _StopLocation({required this.id, this.extId, required this.name, this.description, this.lat, this.lon, this.products, this.distanceMeters}); + factory _StopLocation.fromJson(Map json) => _$StopLocationFromJson(json); + +@override final String id; +@override final String? extId; +@override final String name; +@override final String? description; +@override final double? lat; +@override final double? lon; +@override final int? products; +@override final int? distanceMeters; + +/// Create a copy of StopLocation +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$StopLocationCopyWith<_StopLocation> get copyWith => __$StopLocationCopyWithImpl<_StopLocation>(this, _$identity); + +@override +Map toJson() { + return _$StopLocationToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _StopLocation&&(identical(other.id, id) || other.id == id)&&(identical(other.extId, extId) || other.extId == extId)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon)&&(identical(other.products, products) || other.products == products)&&(identical(other.distanceMeters, distanceMeters) || other.distanceMeters == distanceMeters)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,extId,name,description,lat,lon,products,distanceMeters); + +@override +String toString() { + return 'StopLocation(id: $id, extId: $extId, name: $name, description: $description, lat: $lat, lon: $lon, products: $products, distanceMeters: $distanceMeters)'; +} + + +} + +/// @nodoc +abstract mixin class _$StopLocationCopyWith<$Res> implements $StopLocationCopyWith<$Res> { + factory _$StopLocationCopyWith(_StopLocation value, $Res Function(_StopLocation) _then) = __$StopLocationCopyWithImpl; +@override @useResult +$Res call({ + String id, String? extId, String name, String? description, double? lat, double? lon, int? products, int? distanceMeters +}); + + + + +} +/// @nodoc +class __$StopLocationCopyWithImpl<$Res> + implements _$StopLocationCopyWith<$Res> { + __$StopLocationCopyWithImpl(this._self, this._then); + + final _StopLocation _self; + final $Res Function(_StopLocation) _then; + +/// Create a copy of StopLocation +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? extId = freezed,Object? name = null,Object? description = freezed,Object? lat = freezed,Object? lon = freezed,Object? products = freezed,Object? distanceMeters = freezed,}) { + return _then(_StopLocation( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,extId: freezed == extId ? _self.extId : extId // ignore: cast_nullable_to_non_nullable +as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable +as String?,lat: freezed == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable +as double?,lon: freezed == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable +as double?,products: freezed == products ? _self.products : products // ignore: cast_nullable_to_non_nullable +as int?,distanceMeters: freezed == distanceMeters ? _self.distanceMeters : distanceMeters // ignore: cast_nullable_to_non_nullable +as int?, + )); +} + + +} + + +/// @nodoc +mixin _$Departure { + + String get stopId; String? get stopExtId; String get stopName; String get name; String get direction; String? get directionFlag; DateTime get scheduledTime; DateTime? get realTime; int? get delayMinutes; String? get track; String? get realTrack; bool get cancelled; bool get reachable; Product? get product; String? get journeyRef; +/// Create a copy of Departure +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DepartureCopyWith get copyWith => _$DepartureCopyWithImpl(this as Departure, _$identity); + + /// Serializes this Departure to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Departure&&(identical(other.stopId, stopId) || other.stopId == stopId)&&(identical(other.stopExtId, stopExtId) || other.stopExtId == stopExtId)&&(identical(other.stopName, stopName) || other.stopName == stopName)&&(identical(other.name, name) || other.name == name)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.directionFlag, directionFlag) || other.directionFlag == directionFlag)&&(identical(other.scheduledTime, scheduledTime) || other.scheduledTime == scheduledTime)&&(identical(other.realTime, realTime) || other.realTime == realTime)&&(identical(other.delayMinutes, delayMinutes) || other.delayMinutes == delayMinutes)&&(identical(other.track, track) || other.track == track)&&(identical(other.realTrack, realTrack) || other.realTrack == realTrack)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.reachable, reachable) || other.reachable == reachable)&&(identical(other.product, product) || other.product == product)&&(identical(other.journeyRef, journeyRef) || other.journeyRef == journeyRef)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,stopId,stopExtId,stopName,name,direction,directionFlag,scheduledTime,realTime,delayMinutes,track,realTrack,cancelled,reachable,product,journeyRef); + +@override +String toString() { + return 'Departure(stopId: $stopId, stopExtId: $stopExtId, stopName: $stopName, name: $name, direction: $direction, directionFlag: $directionFlag, scheduledTime: $scheduledTime, realTime: $realTime, delayMinutes: $delayMinutes, track: $track, realTrack: $realTrack, cancelled: $cancelled, reachable: $reachable, product: $product, journeyRef: $journeyRef)'; +} + + +} + +/// @nodoc +abstract mixin class $DepartureCopyWith<$Res> { + factory $DepartureCopyWith(Departure value, $Res Function(Departure) _then) = _$DepartureCopyWithImpl; +@useResult +$Res call({ + String stopId, String? stopExtId, String stopName, String name, String direction, String? directionFlag, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, bool reachable, Product? product, String? journeyRef +}); + + +$ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class _$DepartureCopyWithImpl<$Res> + implements $DepartureCopyWith<$Res> { + _$DepartureCopyWithImpl(this._self, this._then); + + final Departure _self; + final $Res Function(Departure) _then; + +/// Create a copy of Departure +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? stopId = null,Object? stopExtId = freezed,Object? stopName = null,Object? name = null,Object? direction = null,Object? directionFlag = freezed,Object? scheduledTime = null,Object? realTime = freezed,Object? delayMinutes = freezed,Object? track = freezed,Object? realTrack = freezed,Object? cancelled = null,Object? reachable = null,Object? product = freezed,Object? journeyRef = freezed,}) { + return _then(_self.copyWith( +stopId: null == stopId ? _self.stopId : stopId // ignore: cast_nullable_to_non_nullable +as String,stopExtId: freezed == stopExtId ? _self.stopExtId : stopExtId // ignore: cast_nullable_to_non_nullable +as String?,stopName: null == stopName ? _self.stopName : stopName // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String,directionFlag: freezed == directionFlag ? _self.directionFlag : directionFlag // ignore: cast_nullable_to_non_nullable +as String?,scheduledTime: null == scheduledTime ? _self.scheduledTime : scheduledTime // ignore: cast_nullable_to_non_nullable +as DateTime,realTime: freezed == realTime ? _self.realTime : realTime // ignore: cast_nullable_to_non_nullable +as DateTime?,delayMinutes: freezed == delayMinutes ? _self.delayMinutes : delayMinutes // ignore: cast_nullable_to_non_nullable +as int?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable +as String?,realTrack: freezed == realTrack ? _self.realTrack : realTrack // ignore: cast_nullable_to_non_nullable +as String?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,reachable: null == reachable ? _self.reachable : reachable // ignore: cast_nullable_to_non_nullable +as bool,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,journeyRef: freezed == journeyRef ? _self.journeyRef : journeyRef // ignore: cast_nullable_to_non_nullable +as String?, + )); +} +/// Create a copy of Departure +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [Departure]. +extension DeparturePatterns on Departure { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Departure value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Departure() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Departure value) $default,){ +final _that = this; +switch (_that) { +case _Departure(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Departure value)? $default,){ +final _that = this; +switch (_that) { +case _Departure() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String stopId, String? stopExtId, String stopName, String name, String direction, String? directionFlag, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, bool reachable, Product? product, String? journeyRef)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Departure() when $default != null: +return $default(_that.stopId,_that.stopExtId,_that.stopName,_that.name,_that.direction,_that.directionFlag,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.cancelled,_that.reachable,_that.product,_that.journeyRef);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String stopId, String? stopExtId, String stopName, String name, String direction, String? directionFlag, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, bool reachable, Product? product, String? journeyRef) $default,) {final _that = this; +switch (_that) { +case _Departure(): +return $default(_that.stopId,_that.stopExtId,_that.stopName,_that.name,_that.direction,_that.directionFlag,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.cancelled,_that.reachable,_that.product,_that.journeyRef);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String stopId, String? stopExtId, String stopName, String name, String direction, String? directionFlag, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, bool reachable, Product? product, String? journeyRef)? $default,) {final _that = this; +switch (_that) { +case _Departure() when $default != null: +return $default(_that.stopId,_that.stopExtId,_that.stopName,_that.name,_that.direction,_that.directionFlag,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.cancelled,_that.reachable,_that.product,_that.journeyRef);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Departure implements Departure { + const _Departure({required this.stopId, this.stopExtId, required this.stopName, required this.name, required this.direction, this.directionFlag, required this.scheduledTime, this.realTime, this.delayMinutes, this.track, this.realTrack, this.cancelled = false, this.reachable = true, this.product, this.journeyRef}); + factory _Departure.fromJson(Map json) => _$DepartureFromJson(json); + +@override final String stopId; +@override final String? stopExtId; +@override final String stopName; +@override final String name; +@override final String direction; +@override final String? directionFlag; +@override final DateTime scheduledTime; +@override final DateTime? realTime; +@override final int? delayMinutes; +@override final String? track; +@override final String? realTrack; +@override@JsonKey() final bool cancelled; +@override@JsonKey() final bool reachable; +@override final Product? product; +@override final String? journeyRef; + +/// Create a copy of Departure +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DepartureCopyWith<_Departure> get copyWith => __$DepartureCopyWithImpl<_Departure>(this, _$identity); + +@override +Map toJson() { + return _$DepartureToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Departure&&(identical(other.stopId, stopId) || other.stopId == stopId)&&(identical(other.stopExtId, stopExtId) || other.stopExtId == stopExtId)&&(identical(other.stopName, stopName) || other.stopName == stopName)&&(identical(other.name, name) || other.name == name)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.directionFlag, directionFlag) || other.directionFlag == directionFlag)&&(identical(other.scheduledTime, scheduledTime) || other.scheduledTime == scheduledTime)&&(identical(other.realTime, realTime) || other.realTime == realTime)&&(identical(other.delayMinutes, delayMinutes) || other.delayMinutes == delayMinutes)&&(identical(other.track, track) || other.track == track)&&(identical(other.realTrack, realTrack) || other.realTrack == realTrack)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.reachable, reachable) || other.reachable == reachable)&&(identical(other.product, product) || other.product == product)&&(identical(other.journeyRef, journeyRef) || other.journeyRef == journeyRef)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,stopId,stopExtId,stopName,name,direction,directionFlag,scheduledTime,realTime,delayMinutes,track,realTrack,cancelled,reachable,product,journeyRef); + +@override +String toString() { + return 'Departure(stopId: $stopId, stopExtId: $stopExtId, stopName: $stopName, name: $name, direction: $direction, directionFlag: $directionFlag, scheduledTime: $scheduledTime, realTime: $realTime, delayMinutes: $delayMinutes, track: $track, realTrack: $realTrack, cancelled: $cancelled, reachable: $reachable, product: $product, journeyRef: $journeyRef)'; +} + + +} + +/// @nodoc +abstract mixin class _$DepartureCopyWith<$Res> implements $DepartureCopyWith<$Res> { + factory _$DepartureCopyWith(_Departure value, $Res Function(_Departure) _then) = __$DepartureCopyWithImpl; +@override @useResult +$Res call({ + String stopId, String? stopExtId, String stopName, String name, String direction, String? directionFlag, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, bool reachable, Product? product, String? journeyRef +}); + + +@override $ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class __$DepartureCopyWithImpl<$Res> + implements _$DepartureCopyWith<$Res> { + __$DepartureCopyWithImpl(this._self, this._then); + + final _Departure _self; + final $Res Function(_Departure) _then; + +/// Create a copy of Departure +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? stopId = null,Object? stopExtId = freezed,Object? stopName = null,Object? name = null,Object? direction = null,Object? directionFlag = freezed,Object? scheduledTime = null,Object? realTime = freezed,Object? delayMinutes = freezed,Object? track = freezed,Object? realTrack = freezed,Object? cancelled = null,Object? reachable = null,Object? product = freezed,Object? journeyRef = freezed,}) { + return _then(_Departure( +stopId: null == stopId ? _self.stopId : stopId // ignore: cast_nullable_to_non_nullable +as String,stopExtId: freezed == stopExtId ? _self.stopExtId : stopExtId // ignore: cast_nullable_to_non_nullable +as String?,stopName: null == stopName ? _self.stopName : stopName // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String,directionFlag: freezed == directionFlag ? _self.directionFlag : directionFlag // ignore: cast_nullable_to_non_nullable +as String?,scheduledTime: null == scheduledTime ? _self.scheduledTime : scheduledTime // ignore: cast_nullable_to_non_nullable +as DateTime,realTime: freezed == realTime ? _self.realTime : realTime // ignore: cast_nullable_to_non_nullable +as DateTime?,delayMinutes: freezed == delayMinutes ? _self.delayMinutes : delayMinutes // ignore: cast_nullable_to_non_nullable +as int?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable +as String?,realTrack: freezed == realTrack ? _self.realTrack : realTrack // ignore: cast_nullable_to_non_nullable +as String?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,reachable: null == reachable ? _self.reachable : reachable // ignore: cast_nullable_to_non_nullable +as bool,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,journeyRef: freezed == journeyRef ? _self.journeyRef : journeyRef // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +/// Create a copy of Departure +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// @nodoc +mixin _$Arrival { + + String get stopId; String? get stopExtId; String get stopName; String get name; String get origin; DateTime get scheduledTime; DateTime? get realTime; int? get delayMinutes; String? get track; String? get realTrack; bool get cancelled; Product? get product; String? get journeyRef; +/// Create a copy of Arrival +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ArrivalCopyWith get copyWith => _$ArrivalCopyWithImpl(this as Arrival, _$identity); + + /// Serializes this Arrival to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Arrival&&(identical(other.stopId, stopId) || other.stopId == stopId)&&(identical(other.stopExtId, stopExtId) || other.stopExtId == stopExtId)&&(identical(other.stopName, stopName) || other.stopName == stopName)&&(identical(other.name, name) || other.name == name)&&(identical(other.origin, origin) || other.origin == origin)&&(identical(other.scheduledTime, scheduledTime) || other.scheduledTime == scheduledTime)&&(identical(other.realTime, realTime) || other.realTime == realTime)&&(identical(other.delayMinutes, delayMinutes) || other.delayMinutes == delayMinutes)&&(identical(other.track, track) || other.track == track)&&(identical(other.realTrack, realTrack) || other.realTrack == realTrack)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.product, product) || other.product == product)&&(identical(other.journeyRef, journeyRef) || other.journeyRef == journeyRef)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,stopId,stopExtId,stopName,name,origin,scheduledTime,realTime,delayMinutes,track,realTrack,cancelled,product,journeyRef); + +@override +String toString() { + return 'Arrival(stopId: $stopId, stopExtId: $stopExtId, stopName: $stopName, name: $name, origin: $origin, scheduledTime: $scheduledTime, realTime: $realTime, delayMinutes: $delayMinutes, track: $track, realTrack: $realTrack, cancelled: $cancelled, product: $product, journeyRef: $journeyRef)'; +} + + +} + +/// @nodoc +abstract mixin class $ArrivalCopyWith<$Res> { + factory $ArrivalCopyWith(Arrival value, $Res Function(Arrival) _then) = _$ArrivalCopyWithImpl; +@useResult +$Res call({ + String stopId, String? stopExtId, String stopName, String name, String origin, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, Product? product, String? journeyRef +}); + + +$ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class _$ArrivalCopyWithImpl<$Res> + implements $ArrivalCopyWith<$Res> { + _$ArrivalCopyWithImpl(this._self, this._then); + + final Arrival _self; + final $Res Function(Arrival) _then; + +/// Create a copy of Arrival +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? stopId = null,Object? stopExtId = freezed,Object? stopName = null,Object? name = null,Object? origin = null,Object? scheduledTime = null,Object? realTime = freezed,Object? delayMinutes = freezed,Object? track = freezed,Object? realTrack = freezed,Object? cancelled = null,Object? product = freezed,Object? journeyRef = freezed,}) { + return _then(_self.copyWith( +stopId: null == stopId ? _self.stopId : stopId // ignore: cast_nullable_to_non_nullable +as String,stopExtId: freezed == stopExtId ? _self.stopExtId : stopExtId // ignore: cast_nullable_to_non_nullable +as String?,stopName: null == stopName ? _self.stopName : stopName // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,origin: null == origin ? _self.origin : origin // ignore: cast_nullable_to_non_nullable +as String,scheduledTime: null == scheduledTime ? _self.scheduledTime : scheduledTime // ignore: cast_nullable_to_non_nullable +as DateTime,realTime: freezed == realTime ? _self.realTime : realTime // ignore: cast_nullable_to_non_nullable +as DateTime?,delayMinutes: freezed == delayMinutes ? _self.delayMinutes : delayMinutes // ignore: cast_nullable_to_non_nullable +as int?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable +as String?,realTrack: freezed == realTrack ? _self.realTrack : realTrack // ignore: cast_nullable_to_non_nullable +as String?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,journeyRef: freezed == journeyRef ? _self.journeyRef : journeyRef // ignore: cast_nullable_to_non_nullable +as String?, + )); +} +/// Create a copy of Arrival +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [Arrival]. +extension ArrivalPatterns on Arrival { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Arrival value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Arrival() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Arrival value) $default,){ +final _that = this; +switch (_that) { +case _Arrival(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Arrival value)? $default,){ +final _that = this; +switch (_that) { +case _Arrival() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String stopId, String? stopExtId, String stopName, String name, String origin, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, Product? product, String? journeyRef)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Arrival() when $default != null: +return $default(_that.stopId,_that.stopExtId,_that.stopName,_that.name,_that.origin,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.cancelled,_that.product,_that.journeyRef);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String stopId, String? stopExtId, String stopName, String name, String origin, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, Product? product, String? journeyRef) $default,) {final _that = this; +switch (_that) { +case _Arrival(): +return $default(_that.stopId,_that.stopExtId,_that.stopName,_that.name,_that.origin,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.cancelled,_that.product,_that.journeyRef);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String stopId, String? stopExtId, String stopName, String name, String origin, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, Product? product, String? journeyRef)? $default,) {final _that = this; +switch (_that) { +case _Arrival() when $default != null: +return $default(_that.stopId,_that.stopExtId,_that.stopName,_that.name,_that.origin,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.cancelled,_that.product,_that.journeyRef);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Arrival implements Arrival { + const _Arrival({required this.stopId, this.stopExtId, required this.stopName, required this.name, required this.origin, required this.scheduledTime, this.realTime, this.delayMinutes, this.track, this.realTrack, this.cancelled = false, this.product, this.journeyRef}); + factory _Arrival.fromJson(Map json) => _$ArrivalFromJson(json); + +@override final String stopId; +@override final String? stopExtId; +@override final String stopName; +@override final String name; +@override final String origin; +@override final DateTime scheduledTime; +@override final DateTime? realTime; +@override final int? delayMinutes; +@override final String? track; +@override final String? realTrack; +@override@JsonKey() final bool cancelled; +@override final Product? product; +@override final String? journeyRef; + +/// Create a copy of Arrival +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ArrivalCopyWith<_Arrival> get copyWith => __$ArrivalCopyWithImpl<_Arrival>(this, _$identity); + +@override +Map toJson() { + return _$ArrivalToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Arrival&&(identical(other.stopId, stopId) || other.stopId == stopId)&&(identical(other.stopExtId, stopExtId) || other.stopExtId == stopExtId)&&(identical(other.stopName, stopName) || other.stopName == stopName)&&(identical(other.name, name) || other.name == name)&&(identical(other.origin, origin) || other.origin == origin)&&(identical(other.scheduledTime, scheduledTime) || other.scheduledTime == scheduledTime)&&(identical(other.realTime, realTime) || other.realTime == realTime)&&(identical(other.delayMinutes, delayMinutes) || other.delayMinutes == delayMinutes)&&(identical(other.track, track) || other.track == track)&&(identical(other.realTrack, realTrack) || other.realTrack == realTrack)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.product, product) || other.product == product)&&(identical(other.journeyRef, journeyRef) || other.journeyRef == journeyRef)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,stopId,stopExtId,stopName,name,origin,scheduledTime,realTime,delayMinutes,track,realTrack,cancelled,product,journeyRef); + +@override +String toString() { + return 'Arrival(stopId: $stopId, stopExtId: $stopExtId, stopName: $stopName, name: $name, origin: $origin, scheduledTime: $scheduledTime, realTime: $realTime, delayMinutes: $delayMinutes, track: $track, realTrack: $realTrack, cancelled: $cancelled, product: $product, journeyRef: $journeyRef)'; +} + + +} + +/// @nodoc +abstract mixin class _$ArrivalCopyWith<$Res> implements $ArrivalCopyWith<$Res> { + factory _$ArrivalCopyWith(_Arrival value, $Res Function(_Arrival) _then) = __$ArrivalCopyWithImpl; +@override @useResult +$Res call({ + String stopId, String? stopExtId, String stopName, String name, String origin, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, bool cancelled, Product? product, String? journeyRef +}); + + +@override $ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class __$ArrivalCopyWithImpl<$Res> + implements _$ArrivalCopyWith<$Res> { + __$ArrivalCopyWithImpl(this._self, this._then); + + final _Arrival _self; + final $Res Function(_Arrival) _then; + +/// Create a copy of Arrival +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? stopId = null,Object? stopExtId = freezed,Object? stopName = null,Object? name = null,Object? origin = null,Object? scheduledTime = null,Object? realTime = freezed,Object? delayMinutes = freezed,Object? track = freezed,Object? realTrack = freezed,Object? cancelled = null,Object? product = freezed,Object? journeyRef = freezed,}) { + return _then(_Arrival( +stopId: null == stopId ? _self.stopId : stopId // ignore: cast_nullable_to_non_nullable +as String,stopExtId: freezed == stopExtId ? _self.stopExtId : stopExtId // ignore: cast_nullable_to_non_nullable +as String?,stopName: null == stopName ? _self.stopName : stopName // ignore: cast_nullable_to_non_nullable +as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,origin: null == origin ? _self.origin : origin // ignore: cast_nullable_to_non_nullable +as String,scheduledTime: null == scheduledTime ? _self.scheduledTime : scheduledTime // ignore: cast_nullable_to_non_nullable +as DateTime,realTime: freezed == realTime ? _self.realTime : realTime // ignore: cast_nullable_to_non_nullable +as DateTime?,delayMinutes: freezed == delayMinutes ? _self.delayMinutes : delayMinutes // ignore: cast_nullable_to_non_nullable +as int?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable +as String?,realTrack: freezed == realTrack ? _self.realTrack : realTrack // ignore: cast_nullable_to_non_nullable +as String?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,journeyRef: freezed == journeyRef ? _self.journeyRef : journeyRef // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +/// Create a copy of Arrival +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// @nodoc +mixin _$TripEndpoint { + + String get stopId; String? get stopExtId; String get name; double? get lat; double? get lon; DateTime get scheduledTime; DateTime? get realTime; int? get delayMinutes; String? get track; String? get realTrack; String? get type; +/// Create a copy of TripEndpoint +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TripEndpointCopyWith get copyWith => _$TripEndpointCopyWithImpl(this as TripEndpoint, _$identity); + + /// Serializes this TripEndpoint to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TripEndpoint&&(identical(other.stopId, stopId) || other.stopId == stopId)&&(identical(other.stopExtId, stopExtId) || other.stopExtId == stopExtId)&&(identical(other.name, name) || other.name == name)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon)&&(identical(other.scheduledTime, scheduledTime) || other.scheduledTime == scheduledTime)&&(identical(other.realTime, realTime) || other.realTime == realTime)&&(identical(other.delayMinutes, delayMinutes) || other.delayMinutes == delayMinutes)&&(identical(other.track, track) || other.track == track)&&(identical(other.realTrack, realTrack) || other.realTrack == realTrack)&&(identical(other.type, type) || other.type == type)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,stopId,stopExtId,name,lat,lon,scheduledTime,realTime,delayMinutes,track,realTrack,type); + +@override +String toString() { + return 'TripEndpoint(stopId: $stopId, stopExtId: $stopExtId, name: $name, lat: $lat, lon: $lon, scheduledTime: $scheduledTime, realTime: $realTime, delayMinutes: $delayMinutes, track: $track, realTrack: $realTrack, type: $type)'; +} + + +} + +/// @nodoc +abstract mixin class $TripEndpointCopyWith<$Res> { + factory $TripEndpointCopyWith(TripEndpoint value, $Res Function(TripEndpoint) _then) = _$TripEndpointCopyWithImpl; +@useResult +$Res call({ + String stopId, String? stopExtId, String name, double? lat, double? lon, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, String? type +}); + + + + +} +/// @nodoc +class _$TripEndpointCopyWithImpl<$Res> + implements $TripEndpointCopyWith<$Res> { + _$TripEndpointCopyWithImpl(this._self, this._then); + + final TripEndpoint _self; + final $Res Function(TripEndpoint) _then; + +/// Create a copy of TripEndpoint +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? stopId = null,Object? stopExtId = freezed,Object? name = null,Object? lat = freezed,Object? lon = freezed,Object? scheduledTime = null,Object? realTime = freezed,Object? delayMinutes = freezed,Object? track = freezed,Object? realTrack = freezed,Object? type = freezed,}) { + return _then(_self.copyWith( +stopId: null == stopId ? _self.stopId : stopId // ignore: cast_nullable_to_non_nullable +as String,stopExtId: freezed == stopExtId ? _self.stopExtId : stopExtId // ignore: cast_nullable_to_non_nullable +as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,lat: freezed == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable +as double?,lon: freezed == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable +as double?,scheduledTime: null == scheduledTime ? _self.scheduledTime : scheduledTime // ignore: cast_nullable_to_non_nullable +as DateTime,realTime: freezed == realTime ? _self.realTime : realTime // ignore: cast_nullable_to_non_nullable +as DateTime?,delayMinutes: freezed == delayMinutes ? _self.delayMinutes : delayMinutes // ignore: cast_nullable_to_non_nullable +as int?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable +as String?,realTrack: freezed == realTrack ? _self.realTrack : realTrack // ignore: cast_nullable_to_non_nullable +as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [TripEndpoint]. +extension TripEndpointPatterns on TripEndpoint { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _TripEndpoint value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _TripEndpoint() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _TripEndpoint value) $default,){ +final _that = this; +switch (_that) { +case _TripEndpoint(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _TripEndpoint value)? $default,){ +final _that = this; +switch (_that) { +case _TripEndpoint() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String stopId, String? stopExtId, String name, double? lat, double? lon, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, String? type)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _TripEndpoint() when $default != null: +return $default(_that.stopId,_that.stopExtId,_that.name,_that.lat,_that.lon,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.type);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String stopId, String? stopExtId, String name, double? lat, double? lon, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, String? type) $default,) {final _that = this; +switch (_that) { +case _TripEndpoint(): +return $default(_that.stopId,_that.stopExtId,_that.name,_that.lat,_that.lon,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.type);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String stopId, String? stopExtId, String name, double? lat, double? lon, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, String? type)? $default,) {final _that = this; +switch (_that) { +case _TripEndpoint() when $default != null: +return $default(_that.stopId,_that.stopExtId,_that.name,_that.lat,_that.lon,_that.scheduledTime,_that.realTime,_that.delayMinutes,_that.track,_that.realTrack,_that.type);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _TripEndpoint implements TripEndpoint { + const _TripEndpoint({required this.stopId, this.stopExtId, required this.name, this.lat, this.lon, required this.scheduledTime, this.realTime, this.delayMinutes, this.track, this.realTrack, this.type}); + factory _TripEndpoint.fromJson(Map json) => _$TripEndpointFromJson(json); + +@override final String stopId; +@override final String? stopExtId; +@override final String name; +@override final double? lat; +@override final double? lon; +@override final DateTime scheduledTime; +@override final DateTime? realTime; +@override final int? delayMinutes; +@override final String? track; +@override final String? realTrack; +@override final String? type; + +/// Create a copy of TripEndpoint +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TripEndpointCopyWith<_TripEndpoint> get copyWith => __$TripEndpointCopyWithImpl<_TripEndpoint>(this, _$identity); + +@override +Map toJson() { + return _$TripEndpointToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TripEndpoint&&(identical(other.stopId, stopId) || other.stopId == stopId)&&(identical(other.stopExtId, stopExtId) || other.stopExtId == stopExtId)&&(identical(other.name, name) || other.name == name)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon)&&(identical(other.scheduledTime, scheduledTime) || other.scheduledTime == scheduledTime)&&(identical(other.realTime, realTime) || other.realTime == realTime)&&(identical(other.delayMinutes, delayMinutes) || other.delayMinutes == delayMinutes)&&(identical(other.track, track) || other.track == track)&&(identical(other.realTrack, realTrack) || other.realTrack == realTrack)&&(identical(other.type, type) || other.type == type)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,stopId,stopExtId,name,lat,lon,scheduledTime,realTime,delayMinutes,track,realTrack,type); + +@override +String toString() { + return 'TripEndpoint(stopId: $stopId, stopExtId: $stopExtId, name: $name, lat: $lat, lon: $lon, scheduledTime: $scheduledTime, realTime: $realTime, delayMinutes: $delayMinutes, track: $track, realTrack: $realTrack, type: $type)'; +} + + +} + +/// @nodoc +abstract mixin class _$TripEndpointCopyWith<$Res> implements $TripEndpointCopyWith<$Res> { + factory _$TripEndpointCopyWith(_TripEndpoint value, $Res Function(_TripEndpoint) _then) = __$TripEndpointCopyWithImpl; +@override @useResult +$Res call({ + String stopId, String? stopExtId, String name, double? lat, double? lon, DateTime scheduledTime, DateTime? realTime, int? delayMinutes, String? track, String? realTrack, String? type +}); + + + + +} +/// @nodoc +class __$TripEndpointCopyWithImpl<$Res> + implements _$TripEndpointCopyWith<$Res> { + __$TripEndpointCopyWithImpl(this._self, this._then); + + final _TripEndpoint _self; + final $Res Function(_TripEndpoint) _then; + +/// Create a copy of TripEndpoint +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? stopId = null,Object? stopExtId = freezed,Object? name = null,Object? lat = freezed,Object? lon = freezed,Object? scheduledTime = null,Object? realTime = freezed,Object? delayMinutes = freezed,Object? track = freezed,Object? realTrack = freezed,Object? type = freezed,}) { + return _then(_TripEndpoint( +stopId: null == stopId ? _self.stopId : stopId // ignore: cast_nullable_to_non_nullable +as String,stopExtId: freezed == stopExtId ? _self.stopExtId : stopExtId // ignore: cast_nullable_to_non_nullable +as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,lat: freezed == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable +as double?,lon: freezed == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable +as double?,scheduledTime: null == scheduledTime ? _self.scheduledTime : scheduledTime // ignore: cast_nullable_to_non_nullable +as DateTime,realTime: freezed == realTime ? _self.realTime : realTime // ignore: cast_nullable_to_non_nullable +as DateTime?,delayMinutes: freezed == delayMinutes ? _self.delayMinutes : delayMinutes // ignore: cast_nullable_to_non_nullable +as int?,track: freezed == track ? _self.track : track // ignore: cast_nullable_to_non_nullable +as String?,realTrack: freezed == realTrack ? _self.realTrack : realTrack // ignore: cast_nullable_to_non_nullable +as String?,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + + +/// @nodoc +mixin _$JourneyStop { + + String get id; String? get extId; String get name; double? get lat; double? get lon; int? get routeIdx; DateTime? get scheduledArrival; DateTime? get scheduledDeparture; DateTime? get realArrival; DateTime? get realDeparture; String? get arrTrack; String? get depTrack; String? get realArrTrack; String? get realDepTrack; bool get cancelled; bool get cancelledArrival; bool get cancelledDeparture; +/// Create a copy of JourneyStop +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$JourneyStopCopyWith get copyWith => _$JourneyStopCopyWithImpl(this as JourneyStop, _$identity); + + /// Serializes this JourneyStop to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is JourneyStop&&(identical(other.id, id) || other.id == id)&&(identical(other.extId, extId) || other.extId == extId)&&(identical(other.name, name) || other.name == name)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon)&&(identical(other.routeIdx, routeIdx) || other.routeIdx == routeIdx)&&(identical(other.scheduledArrival, scheduledArrival) || other.scheduledArrival == scheduledArrival)&&(identical(other.scheduledDeparture, scheduledDeparture) || other.scheduledDeparture == scheduledDeparture)&&(identical(other.realArrival, realArrival) || other.realArrival == realArrival)&&(identical(other.realDeparture, realDeparture) || other.realDeparture == realDeparture)&&(identical(other.arrTrack, arrTrack) || other.arrTrack == arrTrack)&&(identical(other.depTrack, depTrack) || other.depTrack == depTrack)&&(identical(other.realArrTrack, realArrTrack) || other.realArrTrack == realArrTrack)&&(identical(other.realDepTrack, realDepTrack) || other.realDepTrack == realDepTrack)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.cancelledArrival, cancelledArrival) || other.cancelledArrival == cancelledArrival)&&(identical(other.cancelledDeparture, cancelledDeparture) || other.cancelledDeparture == cancelledDeparture)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,extId,name,lat,lon,routeIdx,scheduledArrival,scheduledDeparture,realArrival,realDeparture,arrTrack,depTrack,realArrTrack,realDepTrack,cancelled,cancelledArrival,cancelledDeparture); + +@override +String toString() { + return 'JourneyStop(id: $id, extId: $extId, name: $name, lat: $lat, lon: $lon, routeIdx: $routeIdx, scheduledArrival: $scheduledArrival, scheduledDeparture: $scheduledDeparture, realArrival: $realArrival, realDeparture: $realDeparture, arrTrack: $arrTrack, depTrack: $depTrack, realArrTrack: $realArrTrack, realDepTrack: $realDepTrack, cancelled: $cancelled, cancelledArrival: $cancelledArrival, cancelledDeparture: $cancelledDeparture)'; +} + + +} + +/// @nodoc +abstract mixin class $JourneyStopCopyWith<$Res> { + factory $JourneyStopCopyWith(JourneyStop value, $Res Function(JourneyStop) _then) = _$JourneyStopCopyWithImpl; +@useResult +$Res call({ + String id, String? extId, String name, double? lat, double? lon, int? routeIdx, DateTime? scheduledArrival, DateTime? scheduledDeparture, DateTime? realArrival, DateTime? realDeparture, String? arrTrack, String? depTrack, String? realArrTrack, String? realDepTrack, bool cancelled, bool cancelledArrival, bool cancelledDeparture +}); + + + + +} +/// @nodoc +class _$JourneyStopCopyWithImpl<$Res> + implements $JourneyStopCopyWith<$Res> { + _$JourneyStopCopyWithImpl(this._self, this._then); + + final JourneyStop _self; + final $Res Function(JourneyStop) _then; + +/// Create a copy of JourneyStop +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? extId = freezed,Object? name = null,Object? lat = freezed,Object? lon = freezed,Object? routeIdx = freezed,Object? scheduledArrival = freezed,Object? scheduledDeparture = freezed,Object? realArrival = freezed,Object? realDeparture = freezed,Object? arrTrack = freezed,Object? depTrack = freezed,Object? realArrTrack = freezed,Object? realDepTrack = freezed,Object? cancelled = null,Object? cancelledArrival = null,Object? cancelledDeparture = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,extId: freezed == extId ? _self.extId : extId // ignore: cast_nullable_to_non_nullable +as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,lat: freezed == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable +as double?,lon: freezed == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable +as double?,routeIdx: freezed == routeIdx ? _self.routeIdx : routeIdx // ignore: cast_nullable_to_non_nullable +as int?,scheduledArrival: freezed == scheduledArrival ? _self.scheduledArrival : scheduledArrival // ignore: cast_nullable_to_non_nullable +as DateTime?,scheduledDeparture: freezed == scheduledDeparture ? _self.scheduledDeparture : scheduledDeparture // ignore: cast_nullable_to_non_nullable +as DateTime?,realArrival: freezed == realArrival ? _self.realArrival : realArrival // ignore: cast_nullable_to_non_nullable +as DateTime?,realDeparture: freezed == realDeparture ? _self.realDeparture : realDeparture // ignore: cast_nullable_to_non_nullable +as DateTime?,arrTrack: freezed == arrTrack ? _self.arrTrack : arrTrack // ignore: cast_nullable_to_non_nullable +as String?,depTrack: freezed == depTrack ? _self.depTrack : depTrack // ignore: cast_nullable_to_non_nullable +as String?,realArrTrack: freezed == realArrTrack ? _self.realArrTrack : realArrTrack // ignore: cast_nullable_to_non_nullable +as String?,realDepTrack: freezed == realDepTrack ? _self.realDepTrack : realDepTrack // ignore: cast_nullable_to_non_nullable +as String?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,cancelledArrival: null == cancelledArrival ? _self.cancelledArrival : cancelledArrival // ignore: cast_nullable_to_non_nullable +as bool,cancelledDeparture: null == cancelledDeparture ? _self.cancelledDeparture : cancelledDeparture // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [JourneyStop]. +extension JourneyStopPatterns on JourneyStop { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _JourneyStop value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _JourneyStop() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _JourneyStop value) $default,){ +final _that = this; +switch (_that) { +case _JourneyStop(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _JourneyStop value)? $default,){ +final _that = this; +switch (_that) { +case _JourneyStop() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? extId, String name, double? lat, double? lon, int? routeIdx, DateTime? scheduledArrival, DateTime? scheduledDeparture, DateTime? realArrival, DateTime? realDeparture, String? arrTrack, String? depTrack, String? realArrTrack, String? realDepTrack, bool cancelled, bool cancelledArrival, bool cancelledDeparture)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _JourneyStop() when $default != null: +return $default(_that.id,_that.extId,_that.name,_that.lat,_that.lon,_that.routeIdx,_that.scheduledArrival,_that.scheduledDeparture,_that.realArrival,_that.realDeparture,_that.arrTrack,_that.depTrack,_that.realArrTrack,_that.realDepTrack,_that.cancelled,_that.cancelledArrival,_that.cancelledDeparture);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String? extId, String name, double? lat, double? lon, int? routeIdx, DateTime? scheduledArrival, DateTime? scheduledDeparture, DateTime? realArrival, DateTime? realDeparture, String? arrTrack, String? depTrack, String? realArrTrack, String? realDepTrack, bool cancelled, bool cancelledArrival, bool cancelledDeparture) $default,) {final _that = this; +switch (_that) { +case _JourneyStop(): +return $default(_that.id,_that.extId,_that.name,_that.lat,_that.lon,_that.routeIdx,_that.scheduledArrival,_that.scheduledDeparture,_that.realArrival,_that.realDeparture,_that.arrTrack,_that.depTrack,_that.realArrTrack,_that.realDepTrack,_that.cancelled,_that.cancelledArrival,_that.cancelledDeparture);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? extId, String name, double? lat, double? lon, int? routeIdx, DateTime? scheduledArrival, DateTime? scheduledDeparture, DateTime? realArrival, DateTime? realDeparture, String? arrTrack, String? depTrack, String? realArrTrack, String? realDepTrack, bool cancelled, bool cancelledArrival, bool cancelledDeparture)? $default,) {final _that = this; +switch (_that) { +case _JourneyStop() when $default != null: +return $default(_that.id,_that.extId,_that.name,_that.lat,_that.lon,_that.routeIdx,_that.scheduledArrival,_that.scheduledDeparture,_that.realArrival,_that.realDeparture,_that.arrTrack,_that.depTrack,_that.realArrTrack,_that.realDepTrack,_that.cancelled,_that.cancelledArrival,_that.cancelledDeparture);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _JourneyStop implements JourneyStop { + const _JourneyStop({required this.id, this.extId, required this.name, this.lat, this.lon, this.routeIdx, this.scheduledArrival, this.scheduledDeparture, this.realArrival, this.realDeparture, this.arrTrack, this.depTrack, this.realArrTrack, this.realDepTrack, this.cancelled = false, this.cancelledArrival = false, this.cancelledDeparture = false}); + factory _JourneyStop.fromJson(Map json) => _$JourneyStopFromJson(json); + +@override final String id; +@override final String? extId; +@override final String name; +@override final double? lat; +@override final double? lon; +@override final int? routeIdx; +@override final DateTime? scheduledArrival; +@override final DateTime? scheduledDeparture; +@override final DateTime? realArrival; +@override final DateTime? realDeparture; +@override final String? arrTrack; +@override final String? depTrack; +@override final String? realArrTrack; +@override final String? realDepTrack; +@override@JsonKey() final bool cancelled; +@override@JsonKey() final bool cancelledArrival; +@override@JsonKey() final bool cancelledDeparture; + +/// Create a copy of JourneyStop +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$JourneyStopCopyWith<_JourneyStop> get copyWith => __$JourneyStopCopyWithImpl<_JourneyStop>(this, _$identity); + +@override +Map toJson() { + return _$JourneyStopToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _JourneyStop&&(identical(other.id, id) || other.id == id)&&(identical(other.extId, extId) || other.extId == extId)&&(identical(other.name, name) || other.name == name)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon)&&(identical(other.routeIdx, routeIdx) || other.routeIdx == routeIdx)&&(identical(other.scheduledArrival, scheduledArrival) || other.scheduledArrival == scheduledArrival)&&(identical(other.scheduledDeparture, scheduledDeparture) || other.scheduledDeparture == scheduledDeparture)&&(identical(other.realArrival, realArrival) || other.realArrival == realArrival)&&(identical(other.realDeparture, realDeparture) || other.realDeparture == realDeparture)&&(identical(other.arrTrack, arrTrack) || other.arrTrack == arrTrack)&&(identical(other.depTrack, depTrack) || other.depTrack == depTrack)&&(identical(other.realArrTrack, realArrTrack) || other.realArrTrack == realArrTrack)&&(identical(other.realDepTrack, realDepTrack) || other.realDepTrack == realDepTrack)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.cancelledArrival, cancelledArrival) || other.cancelledArrival == cancelledArrival)&&(identical(other.cancelledDeparture, cancelledDeparture) || other.cancelledDeparture == cancelledDeparture)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,extId,name,lat,lon,routeIdx,scheduledArrival,scheduledDeparture,realArrival,realDeparture,arrTrack,depTrack,realArrTrack,realDepTrack,cancelled,cancelledArrival,cancelledDeparture); + +@override +String toString() { + return 'JourneyStop(id: $id, extId: $extId, name: $name, lat: $lat, lon: $lon, routeIdx: $routeIdx, scheduledArrival: $scheduledArrival, scheduledDeparture: $scheduledDeparture, realArrival: $realArrival, realDeparture: $realDeparture, arrTrack: $arrTrack, depTrack: $depTrack, realArrTrack: $realArrTrack, realDepTrack: $realDepTrack, cancelled: $cancelled, cancelledArrival: $cancelledArrival, cancelledDeparture: $cancelledDeparture)'; +} + + +} + +/// @nodoc +abstract mixin class _$JourneyStopCopyWith<$Res> implements $JourneyStopCopyWith<$Res> { + factory _$JourneyStopCopyWith(_JourneyStop value, $Res Function(_JourneyStop) _then) = __$JourneyStopCopyWithImpl; +@override @useResult +$Res call({ + String id, String? extId, String name, double? lat, double? lon, int? routeIdx, DateTime? scheduledArrival, DateTime? scheduledDeparture, DateTime? realArrival, DateTime? realDeparture, String? arrTrack, String? depTrack, String? realArrTrack, String? realDepTrack, bool cancelled, bool cancelledArrival, bool cancelledDeparture +}); + + + + +} +/// @nodoc +class __$JourneyStopCopyWithImpl<$Res> + implements _$JourneyStopCopyWith<$Res> { + __$JourneyStopCopyWithImpl(this._self, this._then); + + final _JourneyStop _self; + final $Res Function(_JourneyStop) _then; + +/// Create a copy of JourneyStop +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? extId = freezed,Object? name = null,Object? lat = freezed,Object? lon = freezed,Object? routeIdx = freezed,Object? scheduledArrival = freezed,Object? scheduledDeparture = freezed,Object? realArrival = freezed,Object? realDeparture = freezed,Object? arrTrack = freezed,Object? depTrack = freezed,Object? realArrTrack = freezed,Object? realDepTrack = freezed,Object? cancelled = null,Object? cancelledArrival = null,Object? cancelledDeparture = null,}) { + return _then(_JourneyStop( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,extId: freezed == extId ? _self.extId : extId // ignore: cast_nullable_to_non_nullable +as String?,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,lat: freezed == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable +as double?,lon: freezed == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable +as double?,routeIdx: freezed == routeIdx ? _self.routeIdx : routeIdx // ignore: cast_nullable_to_non_nullable +as int?,scheduledArrival: freezed == scheduledArrival ? _self.scheduledArrival : scheduledArrival // ignore: cast_nullable_to_non_nullable +as DateTime?,scheduledDeparture: freezed == scheduledDeparture ? _self.scheduledDeparture : scheduledDeparture // ignore: cast_nullable_to_non_nullable +as DateTime?,realArrival: freezed == realArrival ? _self.realArrival : realArrival // ignore: cast_nullable_to_non_nullable +as DateTime?,realDeparture: freezed == realDeparture ? _self.realDeparture : realDeparture // ignore: cast_nullable_to_non_nullable +as DateTime?,arrTrack: freezed == arrTrack ? _self.arrTrack : arrTrack // ignore: cast_nullable_to_non_nullable +as String?,depTrack: freezed == depTrack ? _self.depTrack : depTrack // ignore: cast_nullable_to_non_nullable +as String?,realArrTrack: freezed == realArrTrack ? _self.realArrTrack : realArrTrack // ignore: cast_nullable_to_non_nullable +as String?,realDepTrack: freezed == realDepTrack ? _self.realDepTrack : realDepTrack // ignore: cast_nullable_to_non_nullable +as String?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,cancelledArrival: null == cancelledArrival ? _self.cancelledArrival : cancelledArrival // ignore: cast_nullable_to_non_nullable +as bool,cancelledDeparture: null == cancelledDeparture ? _self.cancelledDeparture : cancelledDeparture // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + +/// @nodoc +mixin _$Leg { + + String get id; int get idx; LegType get type; String? get name; String? get category; String? get number; String? get direction; TripEndpoint get origin; TripEndpoint get destination;@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? get duration; bool get cancelled; bool get partCancelled; bool get reachable; Product? get product; String? get journeyRef; List get stops; +/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LegCopyWith get copyWith => _$LegCopyWithImpl(this as Leg, _$identity); + + /// Serializes this Leg to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Leg&&(identical(other.id, id) || other.id == id)&&(identical(other.idx, idx) || other.idx == idx)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.category, category) || other.category == category)&&(identical(other.number, number) || other.number == number)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.origin, origin) || other.origin == origin)&&(identical(other.destination, destination) || other.destination == destination)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.partCancelled, partCancelled) || other.partCancelled == partCancelled)&&(identical(other.reachable, reachable) || other.reachable == reachable)&&(identical(other.product, product) || other.product == product)&&(identical(other.journeyRef, journeyRef) || other.journeyRef == journeyRef)&&const DeepCollectionEquality().equals(other.stops, stops)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,idx,type,name,category,number,direction,origin,destination,duration,cancelled,partCancelled,reachable,product,journeyRef,const DeepCollectionEquality().hash(stops)); + +@override +String toString() { + return 'Leg(id: $id, idx: $idx, type: $type, name: $name, category: $category, number: $number, direction: $direction, origin: $origin, destination: $destination, duration: $duration, cancelled: $cancelled, partCancelled: $partCancelled, reachable: $reachable, product: $product, journeyRef: $journeyRef, stops: $stops)'; +} + + +} + +/// @nodoc +abstract mixin class $LegCopyWith<$Res> { + factory $LegCopyWith(Leg value, $Res Function(Leg) _then) = _$LegCopyWithImpl; +@useResult +$Res call({ + String id, int idx, LegType type, String? name, String? category, String? number, String? direction, TripEndpoint origin, TripEndpoint destination,@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, bool cancelled, bool partCancelled, bool reachable, Product? product, String? journeyRef, List stops +}); + + +$TripEndpointCopyWith<$Res> get origin;$TripEndpointCopyWith<$Res> get destination;$ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class _$LegCopyWithImpl<$Res> + implements $LegCopyWith<$Res> { + _$LegCopyWithImpl(this._self, this._then); + + final Leg _self; + final $Res Function(Leg) _then; + +/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? idx = null,Object? type = null,Object? name = freezed,Object? category = freezed,Object? number = freezed,Object? direction = freezed,Object? origin = null,Object? destination = null,Object? duration = freezed,Object? cancelled = null,Object? partCancelled = null,Object? reachable = null,Object? product = freezed,Object? journeyRef = freezed,Object? stops = null,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,idx: null == idx ? _self.idx : idx // ignore: cast_nullable_to_non_nullable +as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as LegType,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable +as String?,number: freezed == number ? _self.number : number // ignore: cast_nullable_to_non_nullable +as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String?,origin: null == origin ? _self.origin : origin // ignore: cast_nullable_to_non_nullable +as TripEndpoint,destination: null == destination ? _self.destination : destination // ignore: cast_nullable_to_non_nullable +as TripEndpoint,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable +as Duration?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,partCancelled: null == partCancelled ? _self.partCancelled : partCancelled // ignore: cast_nullable_to_non_nullable +as bool,reachable: null == reachable ? _self.reachable : reachable // ignore: cast_nullable_to_non_nullable +as bool,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,journeyRef: freezed == journeyRef ? _self.journeyRef : journeyRef // ignore: cast_nullable_to_non_nullable +as String?,stops: null == stops ? _self.stops : stops // ignore: cast_nullable_to_non_nullable +as List, + )); +} +/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$TripEndpointCopyWith<$Res> get origin { + + return $TripEndpointCopyWith<$Res>(_self.origin, (value) { + return _then(_self.copyWith(origin: value)); + }); +}/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$TripEndpointCopyWith<$Res> get destination { + + return $TripEndpointCopyWith<$Res>(_self.destination, (value) { + return _then(_self.copyWith(destination: value)); + }); +}/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [Leg]. +extension LegPatterns on Leg { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Leg value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Leg() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Leg value) $default,){ +final _that = this; +switch (_that) { +case _Leg(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Leg value)? $default,){ +final _that = this; +switch (_that) { +case _Leg() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, int idx, LegType type, String? name, String? category, String? number, String? direction, TripEndpoint origin, TripEndpoint destination, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, bool cancelled, bool partCancelled, bool reachable, Product? product, String? journeyRef, List stops)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Leg() when $default != null: +return $default(_that.id,_that.idx,_that.type,_that.name,_that.category,_that.number,_that.direction,_that.origin,_that.destination,_that.duration,_that.cancelled,_that.partCancelled,_that.reachable,_that.product,_that.journeyRef,_that.stops);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, int idx, LegType type, String? name, String? category, String? number, String? direction, TripEndpoint origin, TripEndpoint destination, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, bool cancelled, bool partCancelled, bool reachable, Product? product, String? journeyRef, List stops) $default,) {final _that = this; +switch (_that) { +case _Leg(): +return $default(_that.id,_that.idx,_that.type,_that.name,_that.category,_that.number,_that.direction,_that.origin,_that.destination,_that.duration,_that.cancelled,_that.partCancelled,_that.reachable,_that.product,_that.journeyRef,_that.stops);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, int idx, LegType type, String? name, String? category, String? number, String? direction, TripEndpoint origin, TripEndpoint destination, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, bool cancelled, bool partCancelled, bool reachable, Product? product, String? journeyRef, List stops)? $default,) {final _that = this; +switch (_that) { +case _Leg() when $default != null: +return $default(_that.id,_that.idx,_that.type,_that.name,_that.category,_that.number,_that.direction,_that.origin,_that.destination,_that.duration,_that.cancelled,_that.partCancelled,_that.reachable,_that.product,_that.journeyRef,_that.stops);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Leg implements Leg { + const _Leg({required this.id, required this.idx, this.type = LegType.unknown, this.name, this.category, this.number, this.direction, required this.origin, required this.destination, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) this.duration, this.cancelled = false, this.partCancelled = false, this.reachable = true, this.product, this.journeyRef, final List stops = const []}): _stops = stops; + factory _Leg.fromJson(Map json) => _$LegFromJson(json); + +@override final String id; +@override final int idx; +@override@JsonKey() final LegType type; +@override final String? name; +@override final String? category; +@override final String? number; +@override final String? direction; +@override final TripEndpoint origin; +@override final TripEndpoint destination; +@override@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) final Duration? duration; +@override@JsonKey() final bool cancelled; +@override@JsonKey() final bool partCancelled; +@override@JsonKey() final bool reachable; +@override final Product? product; +@override final String? journeyRef; + final List _stops; +@override@JsonKey() List get stops { + if (_stops is EqualUnmodifiableListView) return _stops; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_stops); +} + + +/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LegCopyWith<_Leg> get copyWith => __$LegCopyWithImpl<_Leg>(this, _$identity); + +@override +Map toJson() { + return _$LegToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Leg&&(identical(other.id, id) || other.id == id)&&(identical(other.idx, idx) || other.idx == idx)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.category, category) || other.category == category)&&(identical(other.number, number) || other.number == number)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.origin, origin) || other.origin == origin)&&(identical(other.destination, destination) || other.destination == destination)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.cancelled, cancelled) || other.cancelled == cancelled)&&(identical(other.partCancelled, partCancelled) || other.partCancelled == partCancelled)&&(identical(other.reachable, reachable) || other.reachable == reachable)&&(identical(other.product, product) || other.product == product)&&(identical(other.journeyRef, journeyRef) || other.journeyRef == journeyRef)&&const DeepCollectionEquality().equals(other._stops, _stops)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,idx,type,name,category,number,direction,origin,destination,duration,cancelled,partCancelled,reachable,product,journeyRef,const DeepCollectionEquality().hash(_stops)); + +@override +String toString() { + return 'Leg(id: $id, idx: $idx, type: $type, name: $name, category: $category, number: $number, direction: $direction, origin: $origin, destination: $destination, duration: $duration, cancelled: $cancelled, partCancelled: $partCancelled, reachable: $reachable, product: $product, journeyRef: $journeyRef, stops: $stops)'; +} + + +} + +/// @nodoc +abstract mixin class _$LegCopyWith<$Res> implements $LegCopyWith<$Res> { + factory _$LegCopyWith(_Leg value, $Res Function(_Leg) _then) = __$LegCopyWithImpl; +@override @useResult +$Res call({ + String id, int idx, LegType type, String? name, String? category, String? number, String? direction, TripEndpoint origin, TripEndpoint destination,@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, bool cancelled, bool partCancelled, bool reachable, Product? product, String? journeyRef, List stops +}); + + +@override $TripEndpointCopyWith<$Res> get origin;@override $TripEndpointCopyWith<$Res> get destination;@override $ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class __$LegCopyWithImpl<$Res> + implements _$LegCopyWith<$Res> { + __$LegCopyWithImpl(this._self, this._then); + + final _Leg _self; + final $Res Function(_Leg) _then; + +/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? idx = null,Object? type = null,Object? name = freezed,Object? category = freezed,Object? number = freezed,Object? direction = freezed,Object? origin = null,Object? destination = null,Object? duration = freezed,Object? cancelled = null,Object? partCancelled = null,Object? reachable = null,Object? product = freezed,Object? journeyRef = freezed,Object? stops = null,}) { + return _then(_Leg( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,idx: null == idx ? _self.idx : idx // ignore: cast_nullable_to_non_nullable +as int,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as LegType,name: freezed == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable +as String?,number: freezed == number ? _self.number : number // ignore: cast_nullable_to_non_nullable +as String?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String?,origin: null == origin ? _self.origin : origin // ignore: cast_nullable_to_non_nullable +as TripEndpoint,destination: null == destination ? _self.destination : destination // ignore: cast_nullable_to_non_nullable +as TripEndpoint,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable +as Duration?,cancelled: null == cancelled ? _self.cancelled : cancelled // ignore: cast_nullable_to_non_nullable +as bool,partCancelled: null == partCancelled ? _self.partCancelled : partCancelled // ignore: cast_nullable_to_non_nullable +as bool,reachable: null == reachable ? _self.reachable : reachable // ignore: cast_nullable_to_non_nullable +as bool,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,journeyRef: freezed == journeyRef ? _self.journeyRef : journeyRef // ignore: cast_nullable_to_non_nullable +as String?,stops: null == stops ? _self._stops : stops // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$TripEndpointCopyWith<$Res> get origin { + + return $TripEndpointCopyWith<$Res>(_self.origin, (value) { + return _then(_self.copyWith(origin: value)); + }); +}/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$TripEndpointCopyWith<$Res> get destination { + + return $TripEndpointCopyWith<$Res>(_self.destination, (value) { + return _then(_self.copyWith(destination: value)); + }); +}/// Create a copy of Leg +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// @nodoc +mixin _$Trip { + + String? get tripId; String? get ctxRecon; String? get checksum;@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? get duration;@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? get realDuration; int? get transferCount; List get legs; +/// Create a copy of Trip +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TripCopyWith get copyWith => _$TripCopyWithImpl(this as Trip, _$identity); + + /// Serializes this Trip to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Trip&&(identical(other.tripId, tripId) || other.tripId == tripId)&&(identical(other.ctxRecon, ctxRecon) || other.ctxRecon == ctxRecon)&&(identical(other.checksum, checksum) || other.checksum == checksum)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.realDuration, realDuration) || other.realDuration == realDuration)&&(identical(other.transferCount, transferCount) || other.transferCount == transferCount)&&const DeepCollectionEquality().equals(other.legs, legs)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,tripId,ctxRecon,checksum,duration,realDuration,transferCount,const DeepCollectionEquality().hash(legs)); + +@override +String toString() { + return 'Trip(tripId: $tripId, ctxRecon: $ctxRecon, checksum: $checksum, duration: $duration, realDuration: $realDuration, transferCount: $transferCount, legs: $legs)'; +} + + +} + +/// @nodoc +abstract mixin class $TripCopyWith<$Res> { + factory $TripCopyWith(Trip value, $Res Function(Trip) _then) = _$TripCopyWithImpl; +@useResult +$Res call({ + String? tripId, String? ctxRecon, String? checksum,@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration,@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? realDuration, int? transferCount, List legs +}); + + + + +} +/// @nodoc +class _$TripCopyWithImpl<$Res> + implements $TripCopyWith<$Res> { + _$TripCopyWithImpl(this._self, this._then); + + final Trip _self; + final $Res Function(Trip) _then; + +/// Create a copy of Trip +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? tripId = freezed,Object? ctxRecon = freezed,Object? checksum = freezed,Object? duration = freezed,Object? realDuration = freezed,Object? transferCount = freezed,Object? legs = null,}) { + return _then(_self.copyWith( +tripId: freezed == tripId ? _self.tripId : tripId // ignore: cast_nullable_to_non_nullable +as String?,ctxRecon: freezed == ctxRecon ? _self.ctxRecon : ctxRecon // ignore: cast_nullable_to_non_nullable +as String?,checksum: freezed == checksum ? _self.checksum : checksum // ignore: cast_nullable_to_non_nullable +as String?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable +as Duration?,realDuration: freezed == realDuration ? _self.realDuration : realDuration // ignore: cast_nullable_to_non_nullable +as Duration?,transferCount: freezed == transferCount ? _self.transferCount : transferCount // ignore: cast_nullable_to_non_nullable +as int?,legs: null == legs ? _self.legs : legs // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [Trip]. +extension TripPatterns on Trip { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Trip value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Trip() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Trip value) $default,){ +final _that = this; +switch (_that) { +case _Trip(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Trip value)? $default,){ +final _that = this; +switch (_that) { +case _Trip() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? tripId, String? ctxRecon, String? checksum, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? realDuration, int? transferCount, List legs)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Trip() when $default != null: +return $default(_that.tripId,_that.ctxRecon,_that.checksum,_that.duration,_that.realDuration,_that.transferCount,_that.legs);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? tripId, String? ctxRecon, String? checksum, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? realDuration, int? transferCount, List legs) $default,) {final _that = this; +switch (_that) { +case _Trip(): +return $default(_that.tripId,_that.ctxRecon,_that.checksum,_that.duration,_that.realDuration,_that.transferCount,_that.legs);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? tripId, String? ctxRecon, String? checksum, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? realDuration, int? transferCount, List legs)? $default,) {final _that = this; +switch (_that) { +case _Trip() when $default != null: +return $default(_that.tripId,_that.ctxRecon,_that.checksum,_that.duration,_that.realDuration,_that.transferCount,_that.legs);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _Trip implements Trip { + const _Trip({this.tripId, this.ctxRecon, this.checksum, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) this.duration, @JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) this.realDuration, this.transferCount, final List legs = const []}): _legs = legs; + factory _Trip.fromJson(Map json) => _$TripFromJson(json); + +@override final String? tripId; +@override final String? ctxRecon; +@override final String? checksum; +@override@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) final Duration? duration; +@override@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) final Duration? realDuration; +@override final int? transferCount; + final List _legs; +@override@JsonKey() List get legs { + if (_legs is EqualUnmodifiableListView) return _legs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_legs); +} + + +/// Create a copy of Trip +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TripCopyWith<_Trip> get copyWith => __$TripCopyWithImpl<_Trip>(this, _$identity); + +@override +Map toJson() { + return _$TripToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Trip&&(identical(other.tripId, tripId) || other.tripId == tripId)&&(identical(other.ctxRecon, ctxRecon) || other.ctxRecon == ctxRecon)&&(identical(other.checksum, checksum) || other.checksum == checksum)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.realDuration, realDuration) || other.realDuration == realDuration)&&(identical(other.transferCount, transferCount) || other.transferCount == transferCount)&&const DeepCollectionEquality().equals(other._legs, _legs)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,tripId,ctxRecon,checksum,duration,realDuration,transferCount,const DeepCollectionEquality().hash(_legs)); + +@override +String toString() { + return 'Trip(tripId: $tripId, ctxRecon: $ctxRecon, checksum: $checksum, duration: $duration, realDuration: $realDuration, transferCount: $transferCount, legs: $legs)'; +} + + +} + +/// @nodoc +abstract mixin class _$TripCopyWith<$Res> implements $TripCopyWith<$Res> { + factory _$TripCopyWith(_Trip value, $Res Function(_Trip) _then) = __$TripCopyWithImpl; +@override @useResult +$Res call({ + String? tripId, String? ctxRecon, String? checksum,@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? duration,@JsonKey(fromJson: IsoDuration.fromJson, toJson: IsoDuration.toJson) Duration? realDuration, int? transferCount, List legs +}); + + + + +} +/// @nodoc +class __$TripCopyWithImpl<$Res> + implements _$TripCopyWith<$Res> { + __$TripCopyWithImpl(this._self, this._then); + + final _Trip _self; + final $Res Function(_Trip) _then; + +/// Create a copy of Trip +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? tripId = freezed,Object? ctxRecon = freezed,Object? checksum = freezed,Object? duration = freezed,Object? realDuration = freezed,Object? transferCount = freezed,Object? legs = null,}) { + return _then(_Trip( +tripId: freezed == tripId ? _self.tripId : tripId // ignore: cast_nullable_to_non_nullable +as String?,ctxRecon: freezed == ctxRecon ? _self.ctxRecon : ctxRecon // ignore: cast_nullable_to_non_nullable +as String?,checksum: freezed == checksum ? _self.checksum : checksum // ignore: cast_nullable_to_non_nullable +as String?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable +as Duration?,realDuration: freezed == realDuration ? _self.realDuration : realDuration // ignore: cast_nullable_to_non_nullable +as Duration?,transferCount: freezed == transferCount ? _self.transferCount : transferCount // ignore: cast_nullable_to_non_nullable +as int?,legs: null == legs ? _self._legs : legs // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$TripSearchResult { + + List get trips; String? get scrollContextLater; String? get scrollContextEarlier; +/// Create a copy of TripSearchResult +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TripSearchResultCopyWith get copyWith => _$TripSearchResultCopyWithImpl(this as TripSearchResult, _$identity); + + /// Serializes this TripSearchResult to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TripSearchResult&&const DeepCollectionEquality().equals(other.trips, trips)&&(identical(other.scrollContextLater, scrollContextLater) || other.scrollContextLater == scrollContextLater)&&(identical(other.scrollContextEarlier, scrollContextEarlier) || other.scrollContextEarlier == scrollContextEarlier)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(trips),scrollContextLater,scrollContextEarlier); + +@override +String toString() { + return 'TripSearchResult(trips: $trips, scrollContextLater: $scrollContextLater, scrollContextEarlier: $scrollContextEarlier)'; +} + + +} + +/// @nodoc +abstract mixin class $TripSearchResultCopyWith<$Res> { + factory $TripSearchResultCopyWith(TripSearchResult value, $Res Function(TripSearchResult) _then) = _$TripSearchResultCopyWithImpl; +@useResult +$Res call({ + List trips, String? scrollContextLater, String? scrollContextEarlier +}); + + + + +} +/// @nodoc +class _$TripSearchResultCopyWithImpl<$Res> + implements $TripSearchResultCopyWith<$Res> { + _$TripSearchResultCopyWithImpl(this._self, this._then); + + final TripSearchResult _self; + final $Res Function(TripSearchResult) _then; + +/// Create a copy of TripSearchResult +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? trips = null,Object? scrollContextLater = freezed,Object? scrollContextEarlier = freezed,}) { + return _then(_self.copyWith( +trips: null == trips ? _self.trips : trips // ignore: cast_nullable_to_non_nullable +as List,scrollContextLater: freezed == scrollContextLater ? _self.scrollContextLater : scrollContextLater // ignore: cast_nullable_to_non_nullable +as String?,scrollContextEarlier: freezed == scrollContextEarlier ? _self.scrollContextEarlier : scrollContextEarlier // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [TripSearchResult]. +extension TripSearchResultPatterns on TripSearchResult { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _TripSearchResult value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _TripSearchResult() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _TripSearchResult value) $default,){ +final _that = this; +switch (_that) { +case _TripSearchResult(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _TripSearchResult value)? $default,){ +final _that = this; +switch (_that) { +case _TripSearchResult() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List trips, String? scrollContextLater, String? scrollContextEarlier)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _TripSearchResult() when $default != null: +return $default(_that.trips,_that.scrollContextLater,_that.scrollContextEarlier);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List trips, String? scrollContextLater, String? scrollContextEarlier) $default,) {final _that = this; +switch (_that) { +case _TripSearchResult(): +return $default(_that.trips,_that.scrollContextLater,_that.scrollContextEarlier);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List trips, String? scrollContextLater, String? scrollContextEarlier)? $default,) {final _that = this; +switch (_that) { +case _TripSearchResult() when $default != null: +return $default(_that.trips,_that.scrollContextLater,_that.scrollContextEarlier);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _TripSearchResult implements TripSearchResult { + const _TripSearchResult({final List trips = const [], this.scrollContextLater, this.scrollContextEarlier}): _trips = trips; + factory _TripSearchResult.fromJson(Map json) => _$TripSearchResultFromJson(json); + + final List _trips; +@override@JsonKey() List get trips { + if (_trips is EqualUnmodifiableListView) return _trips; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_trips); +} + +@override final String? scrollContextLater; +@override final String? scrollContextEarlier; + +/// Create a copy of TripSearchResult +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TripSearchResultCopyWith<_TripSearchResult> get copyWith => __$TripSearchResultCopyWithImpl<_TripSearchResult>(this, _$identity); + +@override +Map toJson() { + return _$TripSearchResultToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TripSearchResult&&const DeepCollectionEquality().equals(other._trips, _trips)&&(identical(other.scrollContextLater, scrollContextLater) || other.scrollContextLater == scrollContextLater)&&(identical(other.scrollContextEarlier, scrollContextEarlier) || other.scrollContextEarlier == scrollContextEarlier)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_trips),scrollContextLater,scrollContextEarlier); + +@override +String toString() { + return 'TripSearchResult(trips: $trips, scrollContextLater: $scrollContextLater, scrollContextEarlier: $scrollContextEarlier)'; +} + + +} + +/// @nodoc +abstract mixin class _$TripSearchResultCopyWith<$Res> implements $TripSearchResultCopyWith<$Res> { + factory _$TripSearchResultCopyWith(_TripSearchResult value, $Res Function(_TripSearchResult) _then) = __$TripSearchResultCopyWithImpl; +@override @useResult +$Res call({ + List trips, String? scrollContextLater, String? scrollContextEarlier +}); + + + + +} +/// @nodoc +class __$TripSearchResultCopyWithImpl<$Res> + implements _$TripSearchResultCopyWith<$Res> { + __$TripSearchResultCopyWithImpl(this._self, this._then); + + final _TripSearchResult _self; + final $Res Function(_TripSearchResult) _then; + +/// Create a copy of TripSearchResult +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? trips = null,Object? scrollContextLater = freezed,Object? scrollContextEarlier = freezed,}) { + return _then(_TripSearchResult( +trips: null == trips ? _self._trips : trips // ignore: cast_nullable_to_non_nullable +as List,scrollContextLater: freezed == scrollContextLater ? _self.scrollContextLater : scrollContextLater // ignore: cast_nullable_to_non_nullable +as String?,scrollContextEarlier: freezed == scrollContextEarlier ? _self.scrollContextEarlier : scrollContextEarlier // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + + +/// @nodoc +mixin _$JourneyDetail { + + String? get journeyId; Product? get product; String? get direction; List get stops; +/// Create a copy of JourneyDetail +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$JourneyDetailCopyWith get copyWith => _$JourneyDetailCopyWithImpl(this as JourneyDetail, _$identity); + + /// Serializes this JourneyDetail to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is JourneyDetail&&(identical(other.journeyId, journeyId) || other.journeyId == journeyId)&&(identical(other.product, product) || other.product == product)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other.stops, stops)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,journeyId,product,direction,const DeepCollectionEquality().hash(stops)); + +@override +String toString() { + return 'JourneyDetail(journeyId: $journeyId, product: $product, direction: $direction, stops: $stops)'; +} + + +} + +/// @nodoc +abstract mixin class $JourneyDetailCopyWith<$Res> { + factory $JourneyDetailCopyWith(JourneyDetail value, $Res Function(JourneyDetail) _then) = _$JourneyDetailCopyWithImpl; +@useResult +$Res call({ + String? journeyId, Product? product, String? direction, List stops +}); + + +$ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class _$JourneyDetailCopyWithImpl<$Res> + implements $JourneyDetailCopyWith<$Res> { + _$JourneyDetailCopyWithImpl(this._self, this._then); + + final JourneyDetail _self; + final $Res Function(JourneyDetail) _then; + +/// Create a copy of JourneyDetail +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? journeyId = freezed,Object? product = freezed,Object? direction = freezed,Object? stops = null,}) { + return _then(_self.copyWith( +journeyId: freezed == journeyId ? _self.journeyId : journeyId // ignore: cast_nullable_to_non_nullable +as String?,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String?,stops: null == stops ? _self.stops : stops // ignore: cast_nullable_to_non_nullable +as List, + )); +} +/// Create a copy of JourneyDetail +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// Adds pattern-matching-related methods to [JourneyDetail]. +extension JourneyDetailPatterns on JourneyDetail { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _JourneyDetail value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _JourneyDetail() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _JourneyDetail value) $default,){ +final _that = this; +switch (_that) { +case _JourneyDetail(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _JourneyDetail value)? $default,){ +final _that = this; +switch (_that) { +case _JourneyDetail() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String? journeyId, Product? product, String? direction, List stops)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _JourneyDetail() when $default != null: +return $default(_that.journeyId,_that.product,_that.direction,_that.stops);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String? journeyId, Product? product, String? direction, List stops) $default,) {final _that = this; +switch (_that) { +case _JourneyDetail(): +return $default(_that.journeyId,_that.product,_that.direction,_that.stops);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String? journeyId, Product? product, String? direction, List stops)? $default,) {final _that = this; +switch (_that) { +case _JourneyDetail() when $default != null: +return $default(_that.journeyId,_that.product,_that.direction,_that.stops);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _JourneyDetail implements JourneyDetail { + const _JourneyDetail({this.journeyId, this.product, this.direction, final List stops = const []}): _stops = stops; + factory _JourneyDetail.fromJson(Map json) => _$JourneyDetailFromJson(json); + +@override final String? journeyId; +@override final Product? product; +@override final String? direction; + final List _stops; +@override@JsonKey() List get stops { + if (_stops is EqualUnmodifiableListView) return _stops; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_stops); +} + + +/// Create a copy of JourneyDetail +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$JourneyDetailCopyWith<_JourneyDetail> get copyWith => __$JourneyDetailCopyWithImpl<_JourneyDetail>(this, _$identity); + +@override +Map toJson() { + return _$JourneyDetailToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _JourneyDetail&&(identical(other.journeyId, journeyId) || other.journeyId == journeyId)&&(identical(other.product, product) || other.product == product)&&(identical(other.direction, direction) || other.direction == direction)&&const DeepCollectionEquality().equals(other._stops, _stops)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,journeyId,product,direction,const DeepCollectionEquality().hash(_stops)); + +@override +String toString() { + return 'JourneyDetail(journeyId: $journeyId, product: $product, direction: $direction, stops: $stops)'; +} + + +} + +/// @nodoc +abstract mixin class _$JourneyDetailCopyWith<$Res> implements $JourneyDetailCopyWith<$Res> { + factory _$JourneyDetailCopyWith(_JourneyDetail value, $Res Function(_JourneyDetail) _then) = __$JourneyDetailCopyWithImpl; +@override @useResult +$Res call({ + String? journeyId, Product? product, String? direction, List stops +}); + + +@override $ProductCopyWith<$Res>? get product; + +} +/// @nodoc +class __$JourneyDetailCopyWithImpl<$Res> + implements _$JourneyDetailCopyWith<$Res> { + __$JourneyDetailCopyWithImpl(this._self, this._then); + + final _JourneyDetail _self; + final $Res Function(_JourneyDetail) _then; + +/// Create a copy of JourneyDetail +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? journeyId = freezed,Object? product = freezed,Object? direction = freezed,Object? stops = null,}) { + return _then(_JourneyDetail( +journeyId: freezed == journeyId ? _self.journeyId : journeyId // ignore: cast_nullable_to_non_nullable +as String?,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable +as Product?,direction: freezed == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as String?,stops: null == stops ? _self._stops : stops // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +/// Create a copy of JourneyDetail +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ProductCopyWith<$Res>? get product { + if (_self.product == null) { + return null; + } + + return $ProductCopyWith<$Res>(_self.product!, (value) { + return _then(_self.copyWith(product: value)); + }); +} +} + + +/// @nodoc +mixin _$HimMessage { + + String get id; String? get externalId; String? get head; String? get lead; String? get text; String? get category; String? get company; int? get priority; int? get products; DateTime? get startValidity; DateTime? get endValidity; DateTime? get modified; +/// Create a copy of HimMessage +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$HimMessageCopyWith get copyWith => _$HimMessageCopyWithImpl(this as HimMessage, _$identity); + + /// Serializes this HimMessage to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is HimMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.externalId, externalId) || other.externalId == externalId)&&(identical(other.head, head) || other.head == head)&&(identical(other.lead, lead) || other.lead == lead)&&(identical(other.text, text) || other.text == text)&&(identical(other.category, category) || other.category == category)&&(identical(other.company, company) || other.company == company)&&(identical(other.priority, priority) || other.priority == priority)&&(identical(other.products, products) || other.products == products)&&(identical(other.startValidity, startValidity) || other.startValidity == startValidity)&&(identical(other.endValidity, endValidity) || other.endValidity == endValidity)&&(identical(other.modified, modified) || other.modified == modified)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,externalId,head,lead,text,category,company,priority,products,startValidity,endValidity,modified); + +@override +String toString() { + return 'HimMessage(id: $id, externalId: $externalId, head: $head, lead: $lead, text: $text, category: $category, company: $company, priority: $priority, products: $products, startValidity: $startValidity, endValidity: $endValidity, modified: $modified)'; +} + + +} + +/// @nodoc +abstract mixin class $HimMessageCopyWith<$Res> { + factory $HimMessageCopyWith(HimMessage value, $Res Function(HimMessage) _then) = _$HimMessageCopyWithImpl; +@useResult +$Res call({ + String id, String? externalId, String? head, String? lead, String? text, String? category, String? company, int? priority, int? products, DateTime? startValidity, DateTime? endValidity, DateTime? modified +}); + + + + +} +/// @nodoc +class _$HimMessageCopyWithImpl<$Res> + implements $HimMessageCopyWith<$Res> { + _$HimMessageCopyWithImpl(this._self, this._then); + + final HimMessage _self; + final $Res Function(HimMessage) _then; + +/// Create a copy of HimMessage +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? externalId = freezed,Object? head = freezed,Object? lead = freezed,Object? text = freezed,Object? category = freezed,Object? company = freezed,Object? priority = freezed,Object? products = freezed,Object? startValidity = freezed,Object? endValidity = freezed,Object? modified = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,externalId: freezed == externalId ? _self.externalId : externalId // ignore: cast_nullable_to_non_nullable +as String?,head: freezed == head ? _self.head : head // ignore: cast_nullable_to_non_nullable +as String?,lead: freezed == lead ? _self.lead : lead // ignore: cast_nullable_to_non_nullable +as String?,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable +as String?,company: freezed == company ? _self.company : company // ignore: cast_nullable_to_non_nullable +as String?,priority: freezed == priority ? _self.priority : priority // ignore: cast_nullable_to_non_nullable +as int?,products: freezed == products ? _self.products : products // ignore: cast_nullable_to_non_nullable +as int?,startValidity: freezed == startValidity ? _self.startValidity : startValidity // ignore: cast_nullable_to_non_nullable +as DateTime?,endValidity: freezed == endValidity ? _self.endValidity : endValidity // ignore: cast_nullable_to_non_nullable +as DateTime?,modified: freezed == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [HimMessage]. +extension HimMessagePatterns on HimMessage { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _HimMessage value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _HimMessage() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _HimMessage value) $default,){ +final _that = this; +switch (_that) { +case _HimMessage(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _HimMessage value)? $default,){ +final _that = this; +switch (_that) { +case _HimMessage() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String id, String? externalId, String? head, String? lead, String? text, String? category, String? company, int? priority, int? products, DateTime? startValidity, DateTime? endValidity, DateTime? modified)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _HimMessage() when $default != null: +return $default(_that.id,_that.externalId,_that.head,_that.lead,_that.text,_that.category,_that.company,_that.priority,_that.products,_that.startValidity,_that.endValidity,_that.modified);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String id, String? externalId, String? head, String? lead, String? text, String? category, String? company, int? priority, int? products, DateTime? startValidity, DateTime? endValidity, DateTime? modified) $default,) {final _that = this; +switch (_that) { +case _HimMessage(): +return $default(_that.id,_that.externalId,_that.head,_that.lead,_that.text,_that.category,_that.company,_that.priority,_that.products,_that.startValidity,_that.endValidity,_that.modified);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? externalId, String? head, String? lead, String? text, String? category, String? company, int? priority, int? products, DateTime? startValidity, DateTime? endValidity, DateTime? modified)? $default,) {final _that = this; +switch (_that) { +case _HimMessage() when $default != null: +return $default(_that.id,_that.externalId,_that.head,_that.lead,_that.text,_that.category,_that.company,_that.priority,_that.products,_that.startValidity,_that.endValidity,_that.modified);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _HimMessage implements HimMessage { + const _HimMessage({required this.id, this.externalId, this.head, this.lead, this.text, this.category, this.company, this.priority, this.products, this.startValidity, this.endValidity, this.modified}); + factory _HimMessage.fromJson(Map json) => _$HimMessageFromJson(json); + +@override final String id; +@override final String? externalId; +@override final String? head; +@override final String? lead; +@override final String? text; +@override final String? category; +@override final String? company; +@override final int? priority; +@override final int? products; +@override final DateTime? startValidity; +@override final DateTime? endValidity; +@override final DateTime? modified; + +/// Create a copy of HimMessage +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$HimMessageCopyWith<_HimMessage> get copyWith => __$HimMessageCopyWithImpl<_HimMessage>(this, _$identity); + +@override +Map toJson() { + return _$HimMessageToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _HimMessage&&(identical(other.id, id) || other.id == id)&&(identical(other.externalId, externalId) || other.externalId == externalId)&&(identical(other.head, head) || other.head == head)&&(identical(other.lead, lead) || other.lead == lead)&&(identical(other.text, text) || other.text == text)&&(identical(other.category, category) || other.category == category)&&(identical(other.company, company) || other.company == company)&&(identical(other.priority, priority) || other.priority == priority)&&(identical(other.products, products) || other.products == products)&&(identical(other.startValidity, startValidity) || other.startValidity == startValidity)&&(identical(other.endValidity, endValidity) || other.endValidity == endValidity)&&(identical(other.modified, modified) || other.modified == modified)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,externalId,head,lead,text,category,company,priority,products,startValidity,endValidity,modified); + +@override +String toString() { + return 'HimMessage(id: $id, externalId: $externalId, head: $head, lead: $lead, text: $text, category: $category, company: $company, priority: $priority, products: $products, startValidity: $startValidity, endValidity: $endValidity, modified: $modified)'; +} + + +} + +/// @nodoc +abstract mixin class _$HimMessageCopyWith<$Res> implements $HimMessageCopyWith<$Res> { + factory _$HimMessageCopyWith(_HimMessage value, $Res Function(_HimMessage) _then) = __$HimMessageCopyWithImpl; +@override @useResult +$Res call({ + String id, String? externalId, String? head, String? lead, String? text, String? category, String? company, int? priority, int? products, DateTime? startValidity, DateTime? endValidity, DateTime? modified +}); + + + + +} +/// @nodoc +class __$HimMessageCopyWithImpl<$Res> + implements _$HimMessageCopyWith<$Res> { + __$HimMessageCopyWithImpl(this._self, this._then); + + final _HimMessage _self; + final $Res Function(_HimMessage) _then; + +/// Create a copy of HimMessage +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? externalId = freezed,Object? head = freezed,Object? lead = freezed,Object? text = freezed,Object? category = freezed,Object? company = freezed,Object? priority = freezed,Object? products = freezed,Object? startValidity = freezed,Object? endValidity = freezed,Object? modified = freezed,}) { + return _then(_HimMessage( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,externalId: freezed == externalId ? _self.externalId : externalId // ignore: cast_nullable_to_non_nullable +as String?,head: freezed == head ? _self.head : head // ignore: cast_nullable_to_non_nullable +as String?,lead: freezed == lead ? _self.lead : lead // ignore: cast_nullable_to_non_nullable +as String?,text: freezed == text ? _self.text : text // ignore: cast_nullable_to_non_nullable +as String?,category: freezed == category ? _self.category : category // ignore: cast_nullable_to_non_nullable +as String?,company: freezed == company ? _self.company : company // ignore: cast_nullable_to_non_nullable +as String?,priority: freezed == priority ? _self.priority : priority // ignore: cast_nullable_to_non_nullable +as int?,products: freezed == products ? _self.products : products // ignore: cast_nullable_to_non_nullable +as int?,startValidity: freezed == startValidity ? _self.startValidity : startValidity // ignore: cast_nullable_to_non_nullable +as DateTime?,endValidity: freezed == endValidity ? _self.endValidity : endValidity // ignore: cast_nullable_to_non_nullable +as DateTime?,modified: freezed == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable +as DateTime?, + )); +} + + +} + +// dart format on diff --git a/lib/api/connect/rmv/rmv_models.g.dart b/lib/api/connect/rmv/rmv_models.g.dart new file mode 100644 index 0000000..fad031c --- /dev/null +++ b/lib/api/connect/rmv/rmv_models.g.dart @@ -0,0 +1,368 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rmv_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_Product _$ProductFromJson(Map json) => _Product( + name: json['name'] as String?, + line: json['line'] as String?, + displayNumber: json['displayNumber'] as String?, + category: json['category'] as String?, + categoryCode: json['categoryCode'] as String?, + operator: json['operator'] as String?, +); + +Map _$ProductToJson(_Product instance) => { + 'name': instance.name, + 'line': instance.line, + 'displayNumber': instance.displayNumber, + 'category': instance.category, + 'categoryCode': instance.categoryCode, + 'operator': instance.operator, +}; + +_StopLocation _$StopLocationFromJson(Map json) => + _StopLocation( + id: json['id'] as String, + extId: json['extId'] as String?, + name: json['name'] as String, + description: json['description'] as String?, + lat: (json['lat'] as num?)?.toDouble(), + lon: (json['lon'] as num?)?.toDouble(), + products: (json['products'] as num?)?.toInt(), + distanceMeters: (json['distanceMeters'] as num?)?.toInt(), + ); + +Map _$StopLocationToJson(_StopLocation instance) => + { + 'id': instance.id, + 'extId': instance.extId, + 'name': instance.name, + 'description': instance.description, + 'lat': instance.lat, + 'lon': instance.lon, + 'products': instance.products, + 'distanceMeters': instance.distanceMeters, + }; + +_Departure _$DepartureFromJson(Map json) => _Departure( + stopId: json['stopId'] as String, + stopExtId: json['stopExtId'] as String?, + stopName: json['stopName'] as String, + name: json['name'] as String, + direction: json['direction'] as String, + directionFlag: json['directionFlag'] as String?, + scheduledTime: DateTime.parse(json['scheduledTime'] as String), + realTime: json['realTime'] == null + ? null + : DateTime.parse(json['realTime'] as String), + delayMinutes: (json['delayMinutes'] as num?)?.toInt(), + track: json['track'] as String?, + realTrack: json['realTrack'] as String?, + cancelled: json['cancelled'] as bool? ?? false, + reachable: json['reachable'] as bool? ?? true, + product: json['product'] == null + ? null + : Product.fromJson(json['product'] as Map), + journeyRef: json['journeyRef'] as String?, +); + +Map _$DepartureToJson(_Departure instance) => + { + 'stopId': instance.stopId, + 'stopExtId': instance.stopExtId, + 'stopName': instance.stopName, + 'name': instance.name, + 'direction': instance.direction, + 'directionFlag': instance.directionFlag, + 'scheduledTime': instance.scheduledTime.toIso8601String(), + 'realTime': instance.realTime?.toIso8601String(), + 'delayMinutes': instance.delayMinutes, + 'track': instance.track, + 'realTrack': instance.realTrack, + 'cancelled': instance.cancelled, + 'reachable': instance.reachable, + 'product': instance.product, + 'journeyRef': instance.journeyRef, + }; + +_Arrival _$ArrivalFromJson(Map json) => _Arrival( + stopId: json['stopId'] as String, + stopExtId: json['stopExtId'] as String?, + stopName: json['stopName'] as String, + name: json['name'] as String, + origin: json['origin'] as String, + scheduledTime: DateTime.parse(json['scheduledTime'] as String), + realTime: json['realTime'] == null + ? null + : DateTime.parse(json['realTime'] as String), + delayMinutes: (json['delayMinutes'] as num?)?.toInt(), + track: json['track'] as String?, + realTrack: json['realTrack'] as String?, + cancelled: json['cancelled'] as bool? ?? false, + product: json['product'] == null + ? null + : Product.fromJson(json['product'] as Map), + journeyRef: json['journeyRef'] as String?, +); + +Map _$ArrivalToJson(_Arrival instance) => { + 'stopId': instance.stopId, + 'stopExtId': instance.stopExtId, + 'stopName': instance.stopName, + 'name': instance.name, + 'origin': instance.origin, + 'scheduledTime': instance.scheduledTime.toIso8601String(), + 'realTime': instance.realTime?.toIso8601String(), + 'delayMinutes': instance.delayMinutes, + 'track': instance.track, + 'realTrack': instance.realTrack, + 'cancelled': instance.cancelled, + 'product': instance.product, + 'journeyRef': instance.journeyRef, +}; + +_TripEndpoint _$TripEndpointFromJson(Map json) => + _TripEndpoint( + stopId: json['stopId'] as String, + stopExtId: json['stopExtId'] as String?, + name: json['name'] as String, + lat: (json['lat'] as num?)?.toDouble(), + lon: (json['lon'] as num?)?.toDouble(), + scheduledTime: DateTime.parse(json['scheduledTime'] as String), + realTime: json['realTime'] == null + ? null + : DateTime.parse(json['realTime'] as String), + delayMinutes: (json['delayMinutes'] as num?)?.toInt(), + track: json['track'] as String?, + realTrack: json['realTrack'] as String?, + type: json['type'] as String?, + ); + +Map _$TripEndpointToJson(_TripEndpoint instance) => + { + 'stopId': instance.stopId, + 'stopExtId': instance.stopExtId, + 'name': instance.name, + 'lat': instance.lat, + 'lon': instance.lon, + 'scheduledTime': instance.scheduledTime.toIso8601String(), + 'realTime': instance.realTime?.toIso8601String(), + 'delayMinutes': instance.delayMinutes, + 'track': instance.track, + 'realTrack': instance.realTrack, + 'type': instance.type, + }; + +_JourneyStop _$JourneyStopFromJson(Map json) => _JourneyStop( + id: json['id'] as String, + extId: json['extId'] as String?, + name: json['name'] as String, + lat: (json['lat'] as num?)?.toDouble(), + lon: (json['lon'] as num?)?.toDouble(), + routeIdx: (json['routeIdx'] as num?)?.toInt(), + scheduledArrival: json['scheduledArrival'] == null + ? null + : DateTime.parse(json['scheduledArrival'] as String), + scheduledDeparture: json['scheduledDeparture'] == null + ? null + : DateTime.parse(json['scheduledDeparture'] as String), + realArrival: json['realArrival'] == null + ? null + : DateTime.parse(json['realArrival'] as String), + realDeparture: json['realDeparture'] == null + ? null + : DateTime.parse(json['realDeparture'] as String), + arrTrack: json['arrTrack'] as String?, + depTrack: json['depTrack'] as String?, + realArrTrack: json['realArrTrack'] as String?, + realDepTrack: json['realDepTrack'] as String?, + cancelled: json['cancelled'] as bool? ?? false, + cancelledArrival: json['cancelledArrival'] as bool? ?? false, + cancelledDeparture: json['cancelledDeparture'] as bool? ?? false, +); + +Map _$JourneyStopToJson(_JourneyStop instance) => + { + 'id': instance.id, + 'extId': instance.extId, + 'name': instance.name, + 'lat': instance.lat, + 'lon': instance.lon, + 'routeIdx': instance.routeIdx, + 'scheduledArrival': instance.scheduledArrival?.toIso8601String(), + 'scheduledDeparture': instance.scheduledDeparture?.toIso8601String(), + 'realArrival': instance.realArrival?.toIso8601String(), + 'realDeparture': instance.realDeparture?.toIso8601String(), + 'arrTrack': instance.arrTrack, + 'depTrack': instance.depTrack, + 'realArrTrack': instance.realArrTrack, + 'realDepTrack': instance.realDepTrack, + 'cancelled': instance.cancelled, + 'cancelledArrival': instance.cancelledArrival, + 'cancelledDeparture': instance.cancelledDeparture, + }; + +_Leg _$LegFromJson(Map json) => _Leg( + id: json['id'] as String, + idx: (json['idx'] as num).toInt(), + type: $enumDecodeNullable(_$LegTypeEnumMap, json['type']) ?? LegType.unknown, + name: json['name'] as String?, + category: json['category'] as String?, + number: json['number'] as String?, + direction: json['direction'] as String?, + origin: TripEndpoint.fromJson(json['origin'] as Map), + destination: TripEndpoint.fromJson( + json['destination'] as Map, + ), + duration: IsoDuration.fromJson(json['duration'] as String?), + cancelled: json['cancelled'] as bool? ?? false, + partCancelled: json['partCancelled'] as bool? ?? false, + reachable: json['reachable'] as bool? ?? true, + product: json['product'] == null + ? null + : Product.fromJson(json['product'] as Map), + journeyRef: json['journeyRef'] as String?, + stops: + (json['stops'] as List?) + ?.map((e) => JourneyStop.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$LegToJson(_Leg instance) => { + 'id': instance.id, + 'idx': instance.idx, + 'type': _$LegTypeEnumMap[instance.type]!, + 'name': instance.name, + 'category': instance.category, + 'number': instance.number, + 'direction': instance.direction, + 'origin': instance.origin, + 'destination': instance.destination, + 'duration': IsoDuration.toJson(instance.duration), + 'cancelled': instance.cancelled, + 'partCancelled': instance.partCancelled, + 'reachable': instance.reachable, + 'product': instance.product, + 'journeyRef': instance.journeyRef, + 'stops': instance.stops, +}; + +const _$LegTypeEnumMap = { + LegType.journey: 'JOURNEY', + LegType.walk: 'WALK', + LegType.transfer: 'TRANSFER', + LegType.bike: 'BIKE', + LegType.car: 'CAR', + LegType.parkRide: 'PARK_RIDE', + LegType.taxi: 'TAXI', + LegType.checkIn: 'CHECK_IN', + LegType.checkOut: 'CHECK_OUT', + LegType.dummy: 'DUMMY', + LegType.unknown: 'UNKNOWN', +}; + +_Trip _$TripFromJson(Map json) => _Trip( + tripId: json['tripId'] as String?, + ctxRecon: json['ctxRecon'] as String?, + checksum: json['checksum'] as String?, + duration: IsoDuration.fromJson(json['duration'] as String?), + realDuration: IsoDuration.fromJson(json['realDuration'] as String?), + transferCount: (json['transferCount'] as num?)?.toInt(), + legs: + (json['legs'] as List?) + ?.map((e) => Leg.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$TripToJson(_Trip instance) => { + 'tripId': instance.tripId, + 'ctxRecon': instance.ctxRecon, + 'checksum': instance.checksum, + 'duration': IsoDuration.toJson(instance.duration), + 'realDuration': IsoDuration.toJson(instance.realDuration), + 'transferCount': instance.transferCount, + 'legs': instance.legs, +}; + +_TripSearchResult _$TripSearchResultFromJson(Map json) => + _TripSearchResult( + trips: + (json['trips'] as List?) + ?.map((e) => Trip.fromJson(e as Map)) + .toList() ?? + const [], + scrollContextLater: json['scrollContextLater'] as String?, + scrollContextEarlier: json['scrollContextEarlier'] as String?, + ); + +Map _$TripSearchResultToJson(_TripSearchResult instance) => + { + 'trips': instance.trips, + 'scrollContextLater': instance.scrollContextLater, + 'scrollContextEarlier': instance.scrollContextEarlier, + }; + +_JourneyDetail _$JourneyDetailFromJson(Map json) => + _JourneyDetail( + journeyId: json['journeyId'] as String?, + product: json['product'] == null + ? null + : Product.fromJson(json['product'] as Map), + direction: json['direction'] as String?, + stops: + (json['stops'] as List?) + ?.map((e) => JourneyStop.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$JourneyDetailToJson(_JourneyDetail instance) => + { + 'journeyId': instance.journeyId, + 'product': instance.product, + 'direction': instance.direction, + 'stops': instance.stops, + }; + +_HimMessage _$HimMessageFromJson(Map json) => _HimMessage( + id: json['id'] as String, + externalId: json['externalId'] as String?, + head: json['head'] as String?, + lead: json['lead'] as String?, + text: json['text'] as String?, + category: json['category'] as String?, + company: json['company'] as String?, + priority: (json['priority'] as num?)?.toInt(), + products: (json['products'] as num?)?.toInt(), + startValidity: json['startValidity'] == null + ? null + : DateTime.parse(json['startValidity'] as String), + endValidity: json['endValidity'] == null + ? null + : DateTime.parse(json['endValidity'] as String), + modified: json['modified'] == null + ? null + : DateTime.parse(json['modified'] as String), +); + +Map _$HimMessageToJson(_HimMessage instance) => + { + 'id': instance.id, + 'externalId': instance.externalId, + 'head': instance.head, + 'lead': instance.lead, + 'text': instance.text, + 'category': instance.category, + 'company': instance.company, + 'priority': instance.priority, + 'products': instance.products, + 'startValidity': instance.startValidity?.toIso8601String(), + 'endValidity': instance.endValidity?.toIso8601String(), + 'modified': instance.modified?.toIso8601String(), + }; diff --git a/lib/main.dart b/lib/main.dart index 8ea5ab0..cc7d2f8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'api/connect/connect_auth_store.dart'; import 'api/marianumcloud/webdav/queries/list_files/list_files_cache.dart'; import 'api/mhsl/breaker/get_breakers/get_breakers_response.dart'; import 'app.dart'; @@ -319,6 +320,7 @@ Future _wipeUserState({ chatListBloc.reset(), chatBloc.reset(), breakerBloc.reset(), + ConnectAuthStore.instance.clear(), ]); final prefs = await SharedPreferences.getInstance(); await prefs.clear(); diff --git a/lib/routing/app_routes.dart b/lib/routing/app_routes.dart index 69803d6..0704942 100644 --- a/lib/routing/app_routes.dart +++ b/lib/routing/app_routes.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart'; +import '../api/connect/rmv/rmv_models.dart'; import '../api/marianumcloud/talk/room/get_room_response.dart'; import '../main.dart'; import '../model/account_data.dart'; @@ -18,6 +19,11 @@ import '../view/pages/marianum_message/marianum_message_view.dart'; import '../view/pages/more/feedback/feedback_dialog.dart'; import '../view/pages/more/roomplan/roomplan.dart'; import '../view/pages/more/share/qr_share_view.dart'; +import '../view/pages/rmv/disruptions/disruptions_view.dart'; +import '../view/pages/rmv/journey/journey_detail_view.dart'; +import '../view/pages/rmv/stations/station_detail_view.dart'; +import '../view/pages/rmv/trip_search/trip_detail_view.dart'; +import '../view/pages/rmv/trip_search/trip_results_view.dart'; import '../view/pages/settings/modules_settings_page.dart'; import '../view/pages/settings/settings.dart'; import '../view/pages/share_intent/share_chat_picker.dart'; @@ -106,6 +112,57 @@ class AppRoutes { pushScreen(context, withNavBar: false, screen: const Roomplan()); } + static void openRmvDisruptions(BuildContext context) { + pushScreen(context, withNavBar: false, screen: const DisruptionsView()); + } + + static void openRmvStationDetail(BuildContext context, StopLocation station) { + pushScreen( + context, + withNavBar: false, + screen: StationDetailView(station: station), + ); + } + + static void openRmvJourneyDetail( + BuildContext context, + String journeyRef, { + DateTime? date, + }) { + pushScreen( + context, + withNavBar: false, + screen: JourneyDetailView(journeyRef: journeyRef, date: date), + ); + } + + static void openRmvTripResults( + BuildContext context, { + required StopLocation from, + required StopLocation to, + DateTime? when, + bool byArrival = false, + }) { + pushScreen( + context, + withNavBar: false, + screen: TripResultsView( + from: from, + to: to, + when: when, + byArrival: byArrival, + ), + ); + } + + static void openRmvTripDetail(BuildContext context, Trip trip) { + pushScreen( + context, + withNavBar: false, + screen: TripDetailView(trip: trip), + ); + } + static void openShareTarget(BuildContext context, PendingShare share) { pushScreen( context, diff --git a/lib/state/app/modules/app_modules.dart b/lib/state/app/modules/app_modules.dart index e59ad63..347c320 100644 --- a/lib/state/app/modules/app_modules.dart +++ b/lib/state/app/modules/app_modules.dart @@ -11,6 +11,7 @@ import '../../../view/pages/holidays/holidays_view.dart'; import '../../../view/pages/marianum_dates/marianum_dates_view.dart'; import '../../../view/pages/marianum_message/marianum_message_list_view.dart'; import '../../../view/pages/more/roomplan/roomplan.dart'; +import '../../../view/pages/rmv/rmv_view.dart'; import '../../../view/pages/talk/chat_list.dart'; import '../../../view/pages/timetable/timetable.dart'; import '../../../widget/breaker/breaker.dart'; @@ -126,6 +127,13 @@ class AppModule { breakerArea: BreakerArea.more, create: MarianumDatesView.new, ), + Modules.rmv: AppModule( + Modules.rmv, + name: 'RMV-Fahrplan', + icon: () => Icon(Icons.directions_bus), + breakerArea: BreakerArea.more, + create: RmvView.new, + ), }; if (!showFiltered) { @@ -232,4 +240,5 @@ enum Modules { gradeAveragesCalculator, holidays, marianumDates, + rmv, } diff --git a/lib/state/app/modules/rmv/bloc/rmv_bloc.dart b/lib/state/app/modules/rmv/bloc/rmv_bloc.dart new file mode 100644 index 0000000..9002ce5 --- /dev/null +++ b/lib/state/app/modules/rmv/bloc/rmv_bloc.dart @@ -0,0 +1,29 @@ +import '../../../../../api/connect/rmv/rmv_models.dart'; +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart'; +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; +import '../repository/rmv_repository.dart'; +import 'rmv_event.dart'; +import 'rmv_state.dart'; + +class RmvBloc + extends LoadableHydratedBloc { + List getDisruptions() => innerState?.disruptions ?? const []; + + @override + RmvState fromNothing() => const RmvState(); + + @override + RmvState fromStorage(Map json) => RmvState.fromJson(json); + + @override + Map? toStorage(RmvState state) => state.toJson(); + + @override + Future gatherData() async { + final disruptions = await repo.disruptions(); + add(DataGathered((state) => state.copyWith(disruptions: disruptions))); + } + + @override + RmvRepository repository() => RmvRepository(); +} diff --git a/lib/state/app/modules/rmv/bloc/rmv_event.dart b/lib/state/app/modules/rmv/bloc/rmv_event.dart new file mode 100644 index 0000000..5cbd0c0 --- /dev/null +++ b/lib/state/app/modules/rmv/bloc/rmv_event.dart @@ -0,0 +1,4 @@ +import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart'; +import 'rmv_state.dart'; + +abstract class RmvEvent extends LoadableHydratedBlocEvent {} diff --git a/lib/state/app/modules/rmv/bloc/rmv_state.dart b/lib/state/app/modules/rmv/bloc/rmv_state.dart new file mode 100644 index 0000000..c20d570 --- /dev/null +++ b/lib/state/app/modules/rmv/bloc/rmv_state.dart @@ -0,0 +1,15 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../../../../api/connect/rmv/rmv_models.dart'; + +part 'rmv_state.freezed.dart'; +part 'rmv_state.g.dart'; + +@freezed +abstract class RmvState with _$RmvState { + const factory RmvState({@Default([]) List disruptions}) = + _RmvState; + + factory RmvState.fromJson(Map json) => + _$RmvStateFromJson(json); +} diff --git a/lib/state/app/modules/rmv/bloc/rmv_state.freezed.dart b/lib/state/app/modules/rmv/bloc/rmv_state.freezed.dart new file mode 100644 index 0000000..b699efb --- /dev/null +++ b/lib/state/app/modules/rmv/bloc/rmv_state.freezed.dart @@ -0,0 +1,283 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'rmv_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$RmvState { + + List get disruptions; +/// Create a copy of RmvState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$RmvStateCopyWith get copyWith => _$RmvStateCopyWithImpl(this as RmvState, _$identity); + + /// Serializes this RmvState to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is RmvState&&const DeepCollectionEquality().equals(other.disruptions, disruptions)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(disruptions)); + +@override +String toString() { + return 'RmvState(disruptions: $disruptions)'; +} + + +} + +/// @nodoc +abstract mixin class $RmvStateCopyWith<$Res> { + factory $RmvStateCopyWith(RmvState value, $Res Function(RmvState) _then) = _$RmvStateCopyWithImpl; +@useResult +$Res call({ + List disruptions +}); + + + + +} +/// @nodoc +class _$RmvStateCopyWithImpl<$Res> + implements $RmvStateCopyWith<$Res> { + _$RmvStateCopyWithImpl(this._self, this._then); + + final RmvState _self; + final $Res Function(RmvState) _then; + +/// Create a copy of RmvState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? disruptions = null,}) { + return _then(_self.copyWith( +disruptions: null == disruptions ? _self.disruptions : disruptions // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [RmvState]. +extension RmvStatePatterns on RmvState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _RmvState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _RmvState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _RmvState value) $default,){ +final _that = this; +switch (_that) { +case _RmvState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _RmvState value)? $default,){ +final _that = this; +switch (_that) { +case _RmvState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( List disruptions)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _RmvState() when $default != null: +return $default(_that.disruptions);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( List disruptions) $default,) {final _that = this; +switch (_that) { +case _RmvState(): +return $default(_that.disruptions);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List disruptions)? $default,) {final _that = this; +switch (_that) { +case _RmvState() when $default != null: +return $default(_that.disruptions);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _RmvState implements RmvState { + const _RmvState({final List disruptions = const []}): _disruptions = disruptions; + factory _RmvState.fromJson(Map json) => _$RmvStateFromJson(json); + + final List _disruptions; +@override@JsonKey() List get disruptions { + if (_disruptions is EqualUnmodifiableListView) return _disruptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_disruptions); +} + + +/// Create a copy of RmvState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$RmvStateCopyWith<_RmvState> get copyWith => __$RmvStateCopyWithImpl<_RmvState>(this, _$identity); + +@override +Map toJson() { + return _$RmvStateToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _RmvState&&const DeepCollectionEquality().equals(other._disruptions, _disruptions)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_disruptions)); + +@override +String toString() { + return 'RmvState(disruptions: $disruptions)'; +} + + +} + +/// @nodoc +abstract mixin class _$RmvStateCopyWith<$Res> implements $RmvStateCopyWith<$Res> { + factory _$RmvStateCopyWith(_RmvState value, $Res Function(_RmvState) _then) = __$RmvStateCopyWithImpl; +@override @useResult +$Res call({ + List disruptions +}); + + + + +} +/// @nodoc +class __$RmvStateCopyWithImpl<$Res> + implements _$RmvStateCopyWith<$Res> { + __$RmvStateCopyWithImpl(this._self, this._then); + + final _RmvState _self; + final $Res Function(_RmvState) _then; + +/// Create a copy of RmvState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? disruptions = null,}) { + return _then(_RmvState( +disruptions: null == disruptions ? _self._disruptions : disruptions // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +// dart format on diff --git a/lib/state/app/modules/rmv/bloc/rmv_state.g.dart b/lib/state/app/modules/rmv/bloc/rmv_state.g.dart new file mode 100644 index 0000000..ef0f235 --- /dev/null +++ b/lib/state/app/modules/rmv/bloc/rmv_state.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rmv_state.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_RmvState _$RmvStateFromJson(Map json) => _RmvState( + disruptions: + (json['disruptions'] as List?) + ?.map((e) => HimMessage.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$RmvStateToJson(_RmvState instance) => { + 'disruptions': instance.disruptions, +}; diff --git a/lib/state/app/modules/rmv/repository/rmv_repository.dart b/lib/state/app/modules/rmv/repository/rmv_repository.dart new file mode 100644 index 0000000..baa0bc0 --- /dev/null +++ b/lib/state/app/modules/rmv/repository/rmv_repository.dart @@ -0,0 +1,73 @@ +import '../../../../../api/connect/rmv/queries/get_arrivals.dart'; +import '../../../../../api/connect/rmv/queries/get_departures.dart'; +import '../../../../../api/connect/rmv/queries/get_disruptions.dart'; +import '../../../../../api/connect/rmv/queries/get_journey_detail.dart'; +import '../../../../../api/connect/rmv/queries/more_trips.dart'; +import '../../../../../api/connect/rmv/queries/nearby_stops.dart'; +import '../../../../../api/connect/rmv/queries/search_stops.dart'; +import '../../../../../api/connect/rmv/queries/search_trips.dart'; +import '../../../../../api/connect/rmv/rmv_models.dart'; +import '../../../infrastructure/repository/repository.dart'; +import '../bloc/rmv_state.dart'; + +class RmvRepository extends Repository { + Future> searchStops(String query, {int max = 10}) => + SearchStops(query: query, max: max).run(); + + Future> nearbyStops({ + required double lat, + required double lon, + int radiusMeters = 1000, + int max = 20, + }) => NearbyStops( + lat: lat, + lon: lon, + radiusMeters: radiusMeters, + max: max, + ).run(); + + Future> departures( + String stopId, { + DateTime? when, + int durationMinutes = 60, + int maxJourneys = -1, + }) => GetDepartures( + stopId: stopId, + when: when, + durationMinutes: durationMinutes, + maxJourneys: maxJourneys, + ).run(); + + Future> arrivals( + String stopId, { + DateTime? when, + int durationMinutes = 60, + int maxJourneys = -1, + }) => GetArrivals( + stopId: stopId, + when: when, + durationMinutes: durationMinutes, + maxJourneys: maxJourneys, + ).run(); + + Future searchTrips({ + required String fromStopId, + required String toStopId, + DateTime? when, + bool searchByArrival = false, + }) => SearchTrips( + fromStopId: fromStopId, + toStopId: toStopId, + when: when, + searchByArrival: searchByArrival, + ).run(); + + Future moreTrips(String ctx) => + MoreTrips(ctx: ctx).run(); + + Future journeyDetail(String ref, {DateTime? date}) => + GetJourneyDetail(journeyRef: ref, date: date).run(); + + Future> disruptions({DateTime? when}) => + GetDisruptions(when: when).run(); +} diff --git a/lib/storage/modules_settings.g.dart b/lib/storage/modules_settings.g.dart index e97774b..7799b4b 100644 --- a/lib/storage/modules_settings.g.dart +++ b/lib/storage/modules_settings.g.dart @@ -38,4 +38,5 @@ const _$ModulesEnumMap = { Modules.gradeAveragesCalculator: 'gradeAveragesCalculator', Modules.holidays: 'holidays', Modules.marianumDates: 'marianumDates', + Modules.rmv: 'rmv', }; diff --git a/lib/storage/rmv_settings.dart b/lib/storage/rmv_settings.dart new file mode 100644 index 0000000..abdd360 --- /dev/null +++ b/lib/storage/rmv_settings.dart @@ -0,0 +1,41 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../api/connect/rmv/rmv_models.dart'; + +part 'rmv_settings.g.dart'; + +@JsonSerializable(explicitToJson: true) +class RmvSettings { + List favoriteStations; + List recentStations; + List recentTripQueries; + + static const int maxRecents = 10; + + RmvSettings({ + this.favoriteStations = const [], + this.recentStations = const [], + this.recentTripQueries = const [], + }); + + factory RmvSettings.fromJson(Map json) => + _$RmvSettingsFromJson(json); + Map toJson() => _$RmvSettingsToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class RecentTripQuery { + final StopLocation from; + final StopLocation to; + final int timestampMs; + + RecentTripQuery({ + required this.from, + required this.to, + required this.timestampMs, + }); + + factory RecentTripQuery.fromJson(Map json) => + _$RecentTripQueryFromJson(json); + Map toJson() => _$RecentTripQueryToJson(this); +} diff --git a/lib/storage/rmv_settings.g.dart b/lib/storage/rmv_settings.g.dart new file mode 100644 index 0000000..8325a28 --- /dev/null +++ b/lib/storage/rmv_settings.g.dart @@ -0,0 +1,49 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rmv_settings.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +RmvSettings _$RmvSettingsFromJson(Map json) => RmvSettings( + favoriteStations: + (json['favoriteStations'] as List?) + ?.map((e) => StopLocation.fromJson(e as Map)) + .toList() ?? + const [], + recentStations: + (json['recentStations'] as List?) + ?.map((e) => StopLocation.fromJson(e as Map)) + .toList() ?? + const [], + recentTripQueries: + (json['recentTripQueries'] as List?) + ?.map((e) => RecentTripQuery.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$RmvSettingsToJson( + RmvSettings instance, +) => { + 'favoriteStations': instance.favoriteStations.map((e) => e.toJson()).toList(), + 'recentStations': instance.recentStations.map((e) => e.toJson()).toList(), + 'recentTripQueries': instance.recentTripQueries + .map((e) => e.toJson()) + .toList(), +}; + +RecentTripQuery _$RecentTripQueryFromJson(Map json) => + RecentTripQuery( + from: StopLocation.fromJson(json['from'] as Map), + to: StopLocation.fromJson(json['to'] as Map), + timestampMs: (json['timestampMs'] as num).toInt(), + ); + +Map _$RecentTripQueryToJson(RecentTripQuery instance) => + { + 'from': instance.from.toJson(), + 'to': instance.to.toJson(), + 'timestampMs': instance.timestampMs, + }; diff --git a/lib/storage/settings.dart b/lib/storage/settings.dart index e7fc579..5f0724b 100644 --- a/lib/storage/settings.dart +++ b/lib/storage/settings.dart @@ -7,6 +7,7 @@ import 'file_view_settings.dart'; import 'holidays_settings.dart'; import 'modules_settings.dart'; import 'notification_settings.dart'; +import 'rmv_settings.dart'; import 'talk_settings.dart'; import 'timetable_settings.dart'; @@ -23,6 +24,7 @@ class Settings { TalkSettings talkSettings; FileSettings fileSettings; HolidaysSettings holidaysSettings; + RmvSettings rmvSettings; FileViewSettings fileViewSettings; NotificationSettings notificationSettings; DevToolsSettings devToolsSettings; @@ -35,6 +37,7 @@ class Settings { required this.talkSettings, required this.fileSettings, required this.holidaysSettings, + required this.rmvSettings, required this.fileViewSettings, required this.notificationSettings, required this.devToolsSettings, diff --git a/lib/storage/settings.g.dart b/lib/storage/settings.g.dart index a3402be..9ffffe7 100644 --- a/lib/storage/settings.g.dart +++ b/lib/storage/settings.g.dart @@ -24,6 +24,9 @@ Settings _$SettingsFromJson(Map json) => Settings( holidaysSettings: HolidaysSettings.fromJson( json['holidaysSettings'] as Map, ), + rmvSettings: RmvSettings.fromJson( + json['rmvSettings'] as Map, + ), fileViewSettings: FileViewSettings.fromJson( json['fileViewSettings'] as Map, ), @@ -43,6 +46,7 @@ Map _$SettingsToJson(Settings instance) => { 'talkSettings': instance.talkSettings.toJson(), 'fileSettings': instance.fileSettings.toJson(), 'holidaysSettings': instance.holidaysSettings.toJson(), + 'rmvSettings': instance.rmvSettings.toJson(), 'fileViewSettings': instance.fileViewSettings.toJson(), 'notificationSettings': instance.notificationSettings.toJson(), 'devToolsSettings': instance.devToolsSettings.toJson(), diff --git a/lib/view/pages/rmv/disruptions/disruptions_view.dart b/lib/view/pages/rmv/disruptions/disruptions_view.dart new file mode 100644 index 0000000..1a3eb21 --- /dev/null +++ b/lib/view/pages/rmv/disruptions/disruptions_view.dart @@ -0,0 +1,183 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../api/errors/error_mapper.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; +import '../../../../widget/app_progress_indicator.dart'; +import '../../../../widget/details_bottom_sheet.dart'; + +class DisruptionsView extends StatefulWidget { + const DisruptionsView({super.key}); + + @override + State createState() => _DisruptionsViewState(); +} + +class _DisruptionsViewState extends State { + final RmvRepository _repo = RmvRepository(); + List? _items; + bool _loading = true; + Object? _error; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() { + _loading = true; + _error = null; + }); + try { + final r = await _repo.disruptions(); + if (!mounted) return; + r.sort((a, b) => (b.priority ?? 0).compareTo(a.priority ?? 0)); + setState(() { + _items = r; + _loading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e; + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar(title: const Text('Störungsmeldungen')), + body: _body(), + ); + + Widget _body() { + if (_loading) return const Center(child: AppProgressIndicator.large()); + final err = _error; + if (err != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + errorToUserMessage(err), + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 12), + FilledButton.icon( + icon: const Icon(Icons.refresh), + onPressed: _load, + label: const Text('Erneut versuchen'), + ), + ], + ), + ), + ); + } + final list = _items ?? const []; + if (list.isEmpty) { + return RefreshIndicator( + onRefresh: _load, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [ + Padding( + padding: EdgeInsets.symmetric(vertical: 80, horizontal: 24), + child: Center(child: Text('Keine aktiven Meldungen.')), + ), + ], + ), + ); + } + return RefreshIndicator( + onRefresh: _load, + child: ListView.separated( + itemCount: list.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) => _tile(list[i]), + ), + ); + } + + Widget _tile(HimMessage msg) => ListTile( + leading: Icon( + Icons.warning_amber_outlined, + color: _priorityColor(msg.priority), + ), + title: Text( + msg.head ?? msg.lead ?? msg.text ?? 'Meldung', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + subtitle: msg.lead == null + ? null + : Text(msg.lead!, maxLines: 2, overflow: TextOverflow.ellipsis), + trailing: const Icon(Icons.chevron_right), + onTap: () => _showDetails(msg), + ); + + Color _priorityColor(int? priority) { + if (priority == null) return Colors.orange; + if (priority >= 100) return Colors.red; + if (priority >= 50) return Colors.orange; + return Colors.amber; + } + + void _showDetails(HimMessage msg) { + showDetailsBottomSheet( + context, + header: Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Text( + msg.head ?? 'Störungsmeldung', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + children: (ctx) => [ + if (msg.lead != null) + ListTile( + leading: const Icon(Icons.short_text), + title: Text(msg.lead!), + ), + if (msg.text != null) + ListTile( + leading: const Icon(Icons.notes), + title: Text(msg.text!), + ), + if (msg.category != null) + ListTile( + leading: const Icon(Icons.category_outlined), + title: Text(msg.category!), + ), + if (msg.company != null) + ListTile( + leading: const Icon(Icons.business_outlined), + title: Text(msg.company!), + ), + if (msg.startValidity != null || msg.endValidity != null) + ListTile( + leading: const Icon(Icons.event_outlined), + title: Text(_validityRange(msg)), + ), + if (msg.modified != null) + ListTile( + leading: const Icon(Icons.update_outlined), + title: Text('Aktualisiert: ${msg.modified!.formatDateTime()}'), + ), + ], + ); + } + + String _validityRange(HimMessage msg) { + final start = msg.startValidity?.formatDateTime(); + final end = msg.endValidity?.formatDateTime(); + if (start != null && end != null) return '$start – $end'; + return start ?? end ?? ''; + } +} diff --git a/lib/view/pages/rmv/favorites_controller.dart b/lib/view/pages/rmv/favorites_controller.dart new file mode 100644 index 0000000..31f5b1a --- /dev/null +++ b/lib/view/pages/rmv/favorites_controller.dart @@ -0,0 +1,77 @@ +import '../../../api/connect/rmv/rmv_models.dart'; +import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../storage/rmv_settings.dart'; + +/// Thin wrapper around the global [SettingsCubit] that keeps the RMV +/// favorites/recents bookkeeping out of every view. All mutations go through +/// here so that the cubit's write/emit cycle and the recent-list trimming +/// stay consistent. +class RmvFavoritesController { + final SettingsCubit _settings; + + RmvFavoritesController(this._settings); + + RmvSettings get _rmv => _settings.val().rmvSettings; + + bool isFavorite(StopLocation stop) => + _rmv.favoriteStations.any((s) => s.id == stop.id); + + void toggleFavorite(StopLocation stop) { + if (isFavorite(stop)) { + removeFavorite(stop); + } else { + addFavorite(stop); + } + } + + void addFavorite(StopLocation stop) { + final mutable = _settings.val(write: true).rmvSettings; + if (mutable.favoriteStations.any((s) => s.id == stop.id)) return; + mutable.favoriteStations = [...mutable.favoriteStations, stop]; + } + + void removeFavorite(StopLocation stop) { + final mutable = _settings.val(write: true).rmvSettings; + mutable.favoriteStations = mutable.favoriteStations + .where((s) => s.id != stop.id) + .toList(); + } + + void addRecent(StopLocation stop) { + final mutable = _settings.val(write: true).rmvSettings; + final filtered = + mutable.recentStations.where((s) => s.id != stop.id).toList(); + filtered.insert(0, stop); + if (filtered.length > RmvSettings.maxRecents) { + filtered.removeRange(RmvSettings.maxRecents, filtered.length); + } + mutable.recentStations = filtered; + } + + void clearRecents() { + _settings.val(write: true).rmvSettings.recentStations = const []; + } + + void addRecentTrip(StopLocation from, StopLocation to) { + final mutable = _settings.val(write: true).rmvSettings; + final filtered = mutable.recentTripQueries + .where((q) => q.from.id != from.id || q.to.id != to.id) + .toList(); + filtered.insert( + 0, + RecentTripQuery( + from: from, + to: to, + timestampMs: DateTime.now().millisecondsSinceEpoch, + ), + ); + if (filtered.length > RmvSettings.maxRecents) { + filtered.removeRange(RmvSettings.maxRecents, filtered.length); + } + mutable.recentTripQueries = filtered; + } + + void clearRecentTrips() { + _settings.val(write: true).rmvSettings.recentTripQueries = const []; + } +} diff --git a/lib/view/pages/rmv/journey/journey_detail_view.dart b/lib/view/pages/rmv/journey/journey_detail_view.dart new file mode 100644 index 0000000..fe55379 --- /dev/null +++ b/lib/view/pages/rmv/journey/journey_detail_view.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../api/errors/error_mapper.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; +import '../../../../widget/app_progress_indicator.dart'; +import '../widgets/product_chip.dart'; +import '../widgets/realtime_time.dart'; + +class JourneyDetailView extends StatefulWidget { + final String journeyRef; + final DateTime? date; + + const JourneyDetailView({super.key, required this.journeyRef, this.date}); + + @override + State createState() => _JourneyDetailViewState(); +} + +class _JourneyDetailViewState extends State { + final RmvRepository _repo = RmvRepository(); + JourneyDetail? _detail; + bool _loading = true; + Object? _error; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() { + _loading = true; + _error = null; + }); + try { + final detail = await _repo.journeyDetail( + widget.journeyRef, + date: widget.date, + ); + if (!mounted) return; + setState(() { + _detail = detail; + _loading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e; + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final detail = _detail; + return Scaffold( + appBar: AppBar( + title: Row( + children: [ + ProductChip(product: detail?.product), + const SizedBox(width: 8), + Expanded( + child: Text( + detail?.direction ?? 'Fahrt', + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + body: _body(), + ); + } + + Widget _body() { + if (_loading) { + return const Center(child: AppProgressIndicator.large()); + } + final err = _error; + if (err != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + errorToUserMessage(err), + textAlign: TextAlign.center, + ), + ), + ); + } + final stops = _detail?.stops ?? const []; + if (stops.isEmpty) { + return const Center(child: Text('Keine Halte verfügbar.')); + } + return RefreshIndicator( + onRefresh: _load, + child: ListView.separated( + itemCount: stops.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) => _stopTile(stops[i], i, stops.length), + ), + ); + } + + Widget _stopTile(JourneyStop stop, int idx, int total) { + final isFirst = idx == 0; + final isLast = idx == total - 1; + final arrival = stop.realArrival ?? stop.scheduledArrival; + final departure = stop.realDeparture ?? stop.scheduledDeparture; + final track = (stop.realDepTrack?.isNotEmpty ?? false) + ? stop.realDepTrack + : stop.depTrack; + return ListTile( + leading: SizedBox( + width: 24, + child: Icon( + isFirst + ? Icons.trip_origin + : (isLast ? Icons.place : Icons.fiber_manual_record), + size: isFirst || isLast ? 20 : 10, + color: stop.cancelled + ? Colors.red + : Theme.of(context).colorScheme.primary, + ), + ), + title: Text( + stop.name, + style: TextStyle( + decoration: stop.cancelled ? TextDecoration.lineThrough : null, + fontWeight: (isFirst || isLast) ? FontWeight.bold : null, + ), + ), + subtitle: (track == null || track.isEmpty) + ? null + : Text('Gleis $track'), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (stop.scheduledArrival != null) + RealtimeTime( + scheduled: stop.scheduledArrival!, + realtime: stop.realArrival, + cancelled: stop.cancelledArrival, + style: Theme.of(context).textTheme.bodyMedium, + ), + if (stop.scheduledDeparture != null && + stop.scheduledDeparture != stop.scheduledArrival) + RealtimeTime( + scheduled: stop.scheduledDeparture!, + realtime: stop.realDeparture, + cancelled: stop.cancelledDeparture, + style: Theme.of(context).textTheme.bodyMedium, + ), + if (arrival == null && departure == null) const Text('-'), + ], + ), + isThreeLine: arrival != null && departure != null && arrival != departure, + ); + } +} + +/// Allows showing "departure 14:35" as a tooltip in the journey timeline. +String formatStopMoment(DateTime t) => t.formatHm(); diff --git a/lib/view/pages/rmv/rmv_view.dart b/lib/view/pages/rmv/rmv_view.dart new file mode 100644 index 0000000..1eea095 --- /dev/null +++ b/lib/view/pages/rmv/rmv_view.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; + +import '../../../routing/app_routes.dart'; +import '../../../state/app/infrastructure/loadable_state/loadable_state.dart'; +import '../../../state/app/infrastructure/utility_widgets/bloc_module.dart'; +import '../../../state/app/modules/rmv/bloc/rmv_bloc.dart'; +import '../../../state/app/modules/rmv/bloc/rmv_state.dart'; +import 'stations/station_overview_tab.dart'; +import 'trip_search/trip_search_tab.dart'; + +class RmvView extends StatelessWidget { + const RmvView({super.key}); + + @override + Widget build(BuildContext context) => + BlocModule>( + create: (context) => RmvBloc(), + autoRebuild: true, + child: (context, bloc, state) { + final disruptions = bloc.getDisruptions(); + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('RMV-Fahrplan'), + bottom: const TabBar( + tabs: [ + Tab(icon: Icon(Icons.alt_route), text: 'Verbindung'), + Tab(icon: Icon(Icons.directions_bus), text: 'Stationen'), + ], + ), + actions: [ + Builder( + builder: (ctx) => IconButton( + icon: _disruptionsIcon(disruptions.length), + tooltip: 'Störungsmeldungen', + onPressed: () => AppRoutes.openRmvDisruptions(ctx), + ), + ), + ], + ), + body: const TabBarView( + children: [ + TripSearchTab(), + StationOverviewTab(), + ], + ), + ), + ); + }, + ); + + Widget _disruptionsIcon(int count) { + if (count <= 0) return const Icon(Icons.warning_amber_outlined); + return Stack( + clipBehavior: Clip.none, + children: [ + const Icon(Icons.warning_amber_outlined), + Positioned( + right: -6, + top: -6, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(10), + ), + constraints: const BoxConstraints(minWidth: 16, minHeight: 16), + child: Text( + count > 99 ? '99+' : '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ); + } +} diff --git a/lib/view/pages/rmv/stations/nearby_stations_view.dart b/lib/view/pages/rmv/stations/nearby_stations_view.dart new file mode 100644 index 0000000..ad213d7 --- /dev/null +++ b/lib/view/pages/rmv/stations/nearby_stations_view.dart @@ -0,0 +1,211 @@ +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../api/errors/error_mapper.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; +import '../../../../widget/app_progress_indicator.dart'; +import '../../../../widget/centered_leading.dart'; + +class NearbyStationsView extends StatefulWidget { + const NearbyStationsView({super.key}); + + @override + State createState() => _NearbyStationsViewState(); +} + +class _NearbyStationsViewState extends State { + final RmvRepository _repo = RmvRepository(); + List? _stops; + bool _loading = true; + String? _userError; + Object? _apiError; + int _radiusMeters = 1000; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() { + _loading = true; + _userError = null; + _apiError = null; + }); + final position = await _resolvePosition(); + if (!mounted) return; + if (position == null) { + // _userError is set by _resolvePosition + setState(() => _loading = false); + return; + } + try { + final stops = await _repo.nearbyStops( + lat: position.latitude, + lon: position.longitude, + radiusMeters: _radiusMeters, + max: 30, + ); + if (!mounted) return; + stops.sort((a, b) => + (a.distanceMeters ?? 0).compareTo(b.distanceMeters ?? 0)); + setState(() { + _stops = stops; + _loading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _apiError = e; + _loading = false; + }); + } + } + + Future _resolvePosition() async { + if (!await Geolocator.isLocationServiceEnabled()) { + _userError = + 'Bitte aktiviere die Standortdienste in den System-Einstellungen.'; + return null; + } + var permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + if (permission == LocationPermission.deniedForever) { + _userError = + 'Standortzugriff dauerhaft verweigert. Bitte in den App-Einstellungen aktivieren.'; + return null; + } + if (permission == LocationPermission.denied) { + _userError = 'Ohne Standortzugriff können keine Stationen in der Nähe gefunden werden.'; + return null; + } + try { + return await Geolocator.getCurrentPosition( + locationSettings: const LocationSettings( + accuracy: LocationAccuracy.medium, + timeLimit: Duration(seconds: 10), + ), + ); + } catch (e) { + _userError = 'Standort konnte nicht ermittelt werden: $e'; + return null; + } + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + title: const Text('In meiner Nähe'), + actions: [ + PopupMenuButton( + tooltip: 'Suchradius', + icon: const Icon(Icons.tune), + onSelected: (r) { + setState(() => _radiusMeters = r); + _load(); + }, + itemBuilder: (_) => [500, 1000, 2000, 5000] + .map( + (r) => CheckedPopupMenuItem( + value: r, + checked: r == _radiusMeters, + child: Text(r >= 1000 ? '${r ~/ 1000} km' : '$r m'), + ), + ) + .toList(), + ), + ], + ), + body: _body(), + ); + + Widget _body() { + if (_loading) return const Center(child: AppProgressIndicator.large()); + if (_userError != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_userError!, textAlign: TextAlign.center), + const SizedBox(height: 12), + Wrap( + spacing: 8, + children: [ + OutlinedButton.icon( + icon: const Icon(Icons.refresh), + onPressed: _load, + label: const Text('Erneut versuchen'), + ), + OutlinedButton.icon( + icon: const Icon(Icons.settings), + onPressed: Geolocator.openAppSettings, + label: const Text('Einstellungen'), + ), + ], + ), + ], + ), + ), + ); + } + if (_apiError != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + errorToUserMessage(_apiError), + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 12), + FilledButton.icon( + icon: const Icon(Icons.refresh), + onPressed: _load, + label: const Text('Erneut versuchen'), + ), + ], + ), + ), + ); + } + final list = _stops ?? const []; + if (list.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(24), + child: Text( + 'Keine Stationen im gewählten Umkreis gefunden.', + textAlign: TextAlign.center, + ), + ), + ); + } + return RefreshIndicator( + onRefresh: _load, + child: ListView.separated( + itemCount: list.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) => _tile(list[i]), + ), + ); + } + + Widget _tile(StopLocation stop) => ListTile( + leading: const CenteredLeading(Icon(Icons.directions_transit)), + title: Text(stop.name), + subtitle: stop.distanceMeters == null + ? null + : Text('${stop.distanceMeters} m entfernt'), + onTap: () => AppRoutes.openRmvStationDetail(context, stop), + ); +} diff --git a/lib/view/pages/rmv/stations/station_detail_view.dart b/lib/view/pages/rmv/stations/station_detail_view.dart new file mode 100644 index 0000000..c52647d --- /dev/null +++ b/lib/view/pages/rmv/stations/station_detail_view.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../api/errors/error_mapper.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../widget/app_progress_indicator.dart'; +import '../favorites_controller.dart'; +import '../widgets/departure_arrival_tile.dart'; + +enum _Direction { departures, arrivals } + +class StationDetailView extends StatefulWidget { + final StopLocation station; + const StationDetailView({super.key, required this.station}); + + @override + State createState() => _StationDetailViewState(); +} + +class _StationDetailViewState extends State { + final RmvRepository _repo = RmvRepository(); + _Direction _direction = _Direction.departures; + List? _departures; + List? _arrivals; + bool _loading = false; + Object? _error; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + setState(() { + _loading = true; + _error = null; + }); + try { + if (_direction == _Direction.departures) { + final result = await _repo.departures(widget.station.id); + if (!mounted) return; + setState(() { + _departures = result; + _loading = false; + }); + } else { + final result = await _repo.arrivals(widget.station.id); + if (!mounted) return; + setState(() { + _arrivals = result; + _loading = false; + }); + } + } catch (e) { + if (!mounted) return; + setState(() { + _error = e; + _loading = false; + }); + } + } + + void _switch(_Direction d) { + if (d == _direction) return; + setState(() { + _direction = d; + _departures = null; + _arrivals = null; + }); + _load(); + } + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final favCtrl = RmvFavoritesController(settings); + final isFav = favCtrl.isFavorite(widget.station); + return Scaffold( + appBar: AppBar( + title: Text(widget.station.name), + actions: [ + IconButton( + tooltip: isFav ? 'Favorit entfernen' : 'Als Favorit speichern', + icon: Icon(isFav ? Icons.star : Icons.star_border), + onPressed: () => favCtrl.toggleFavorite(widget.station), + ), + ], + ), + body: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: SegmentedButton<_Direction>( + segments: const [ + ButtonSegment( + value: _Direction.departures, + icon: Icon(Icons.north_east), + label: Text('Abfahrten'), + ), + ButtonSegment( + value: _Direction.arrivals, + icon: Icon(Icons.south_west), + label: Text('Ankünfte'), + ), + ], + selected: {_direction}, + onSelectionChanged: (s) => _switch(s.first), + ), + ), + Expanded(child: _body()), + ], + ), + ); + } + + Widget _body() { + if (_loading) { + return const Center(child: AppProgressIndicator.large()); + } + final err = _error; + if (err != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + errorToUserMessage(err), + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 12), + FilledButton.icon( + icon: const Icon(Icons.refresh), + onPressed: _load, + label: const Text('Erneut versuchen'), + ), + ], + ), + ), + ); + } + if (_direction == _Direction.departures) { + final list = _departures ?? const []; + if (list.isEmpty) return _emptyState('Keine Abfahrten gefunden.'); + return RefreshIndicator( + onRefresh: _load, + child: ListView.separated( + itemCount: list.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) => DepartureArrivalTile.fromDeparture( + list[i], + onTap: () => _openJourney(list[i].journeyRef), + ), + ), + ); + } + final list = _arrivals ?? const []; + if (list.isEmpty) return _emptyState('Keine Ankünfte gefunden.'); + return RefreshIndicator( + onRefresh: _load, + child: ListView.separated( + itemCount: list.length, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) => DepartureArrivalTile.fromArrival( + list[i], + onTap: () => _openJourney(list[i].journeyRef), + ), + ), + ); + } + + Widget _emptyState(String text) => RefreshIndicator( + onRefresh: _load, + child: ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 80, horizontal: 24), + child: Center( + child: Text(text, style: Theme.of(context).textTheme.bodyLarge), + ), + ), + ], + ), + ); + + void _openJourney(String? ref) { + if (ref == null) return; + AppRoutes.openRmvJourneyDetail(context, ref); + } +} diff --git a/lib/view/pages/rmv/stations/station_overview_tab.dart b/lib/view/pages/rmv/stations/station_overview_tab.dart new file mode 100644 index 0000000..2d5386e --- /dev/null +++ b/lib/view/pages/rmv/stations/station_overview_tab.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../storage/settings.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../favorites_controller.dart'; +import '../widgets/station_picker_sheet.dart'; +import 'nearby_stations_view.dart'; + +class StationOverviewTab extends StatelessWidget { + const StationOverviewTab({super.key}); + + @override + Widget build(BuildContext context) => + BlocBuilder(builder: _buildBody); + + Widget _buildBody(BuildContext context, Settings settings) { + final rmv = settings.rmvSettings; + final favorites = rmv.favoriteStations; + final recents = rmv.recentStations; + final favCtrl = RmvFavoritesController(context.read()); + + final children = [ + _searchBar(context), + _nearbyButton(context), + if (favorites.isEmpty && recents.isEmpty) _emptyState(context), + if (favorites.isNotEmpty) ...[ + _sectionHeader(context, 'Favoriten', null), + ...favorites.map((s) => _stationTile(context, s, favCtrl, isFavorite: true)), + ], + if (recents.isNotEmpty) ...[ + _sectionHeader( + context, + 'Zuletzt verwendet', + IconButton( + icon: const Icon(Icons.delete_sweep_outlined), + tooltip: 'Alle löschen', + onPressed: () => _confirmClearRecents(context, favCtrl), + ), + ), + ...recents.map((s) => _stationTile(context, s, favCtrl, isFavorite: false)), + ], + ]; + + return ListView(children: children); + } + + Widget _searchBar(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: FilledButton.tonalIcon( + icon: const Icon(Icons.search), + label: const Text('Station suchen…'), + onPressed: () async { + final picked = await showStationPickerSheet(context); + if (picked != null && context.mounted) { + AppRoutes.openRmvStationDetail(context, picked); + } + }, + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(48), + alignment: Alignment.centerLeft, + ), + ), + ); + + Widget _nearbyButton(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: OutlinedButton.icon( + icon: const Icon(Icons.my_location), + label: const Text('In meiner Nähe'), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const NearbyStationsView()), + ), + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(40), + ), + ), + ); + + Widget _sectionHeader(BuildContext context, String title, Widget? trailing) => + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 8, 4), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + ?trailing, + ], + ), + ); + + Widget _emptyState(BuildContext context) => Padding( + padding: const EdgeInsets.fromLTRB(24, 40, 24, 16), + child: Center( + child: Text( + 'Noch keine Stationen gespeichert. Suche eine Station, um sie zu öffnen oder als Favorit zu markieren.', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ); + + Widget _stationTile( + BuildContext context, + StopLocation station, + RmvFavoritesController favCtrl, { + required bool isFavorite, + }) => ListTile( + leading: CenteredLeading( + Icon(isFavorite ? Icons.star : Icons.directions_transit), + ), + title: Text(station.name), + subtitle: station.description == null ? null : Text(station.description!), + trailing: IconButton( + icon: Icon( + favCtrl.isFavorite(station) ? Icons.star : Icons.star_border, + ), + tooltip: favCtrl.isFavorite(station) + ? 'Favorit entfernen' + : 'Als Favorit speichern', + onPressed: () => favCtrl.toggleFavorite(station), + ), + onTap: () => AppRoutes.openRmvStationDetail(context, station), + ); + + Future _confirmClearRecents( + BuildContext context, + RmvFavoritesController favCtrl, + ) async { + ConfirmDialog( + title: 'Verlauf leeren?', + content: + 'Die zuletzt verwendeten Stationen werden aus der Übersicht entfernt. Favoriten bleiben bestehen.', + confirmButton: 'Leeren', + onConfirm: () => favCtrl.clearRecents(), + ).asDialog(context); + } +} diff --git a/lib/view/pages/rmv/trip_search/trip_detail_view.dart b/lib/view/pages/rmv/trip_search/trip_detail_view.dart new file mode 100644 index 0000000..b3649a2 --- /dev/null +++ b/lib/view/pages/rmv/trip_search/trip_detail_view.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../routing/app_routes.dart'; +import '../widgets/leg_tile.dart'; +import '../widgets/trip_tile.dart'; + +class TripDetailView extends StatelessWidget { + final Trip trip; + + const TripDetailView({super.key, required this.trip}); + + @override + Widget build(BuildContext context) { + final first = trip.legs.isEmpty ? null : trip.legs.first; + final last = trip.legs.isEmpty ? null : trip.legs.last; + final duration = trip.realDuration ?? trip.duration; + final transfers = + trip.transferCount ?? _journeyLegs(trip).length.clamp(1, 99) - 1; + return Scaffold( + appBar: AppBar( + title: first == null + ? const Text('Verbindung') + : Text('${first.origin.name} → ${last!.destination.name}'), + ), + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: _summary(context, duration, transfers, first, last), + ), + const Divider(height: 24), + ...trip.legs.map( + (l) => LegTile( + leg: l, + onShowJourneyDetail: l.journeyRef == null + ? null + : () => AppRoutes.openRmvJourneyDetail( + context, + l.journeyRef!, + date: l.origin.scheduledTime, + ), + ), + ), + ], + ), + ); + } + + Widget _summary( + BuildContext context, + Duration? duration, + int transfers, + Leg? first, + Leg? last, + ) { + if (first == null || last == null) return const SizedBox.shrink(); + return Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + first.origin.scheduledTime.formatDateRelativeShort(), + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 2), + Text( + '${first.origin.scheduledTime.formatHm()} – ${last.destination.scheduledTime.formatHm()}', + style: Theme.of(context).textTheme.titleLarge, + ), + ], + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (duration != null) + Row( + children: [ + const Icon(Icons.schedule, size: 14), + const SizedBox(width: 4), + Text(formatTripDuration(duration)), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + const Icon(Icons.swap_horiz, size: 14), + const SizedBox(width: 4), + Text( + transfers == 0 + ? 'Direkt' + : '$transfers Umstieg${transfers > 1 ? 'e' : ''}', + ), + ], + ), + ], + ), + ], + ); + } + + Iterable _journeyLegs(Trip t) => + t.legs.where((l) => l.type == LegType.journey); +} diff --git a/lib/view/pages/rmv/trip_search/trip_results_view.dart b/lib/view/pages/rmv/trip_search/trip_results_view.dart new file mode 100644 index 0000000..23dc356 --- /dev/null +++ b/lib/view/pages/rmv/trip_search/trip_results_view.dart @@ -0,0 +1,212 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../api/errors/error_mapper.dart'; +import '../../../../extensions/date_time.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; +import '../../../../widget/app_progress_indicator.dart'; +import '../widgets/trip_tile.dart'; + +class TripResultsView extends StatefulWidget { + final StopLocation from; + final StopLocation to; + final DateTime? when; + final bool byArrival; + + const TripResultsView({ + super.key, + required this.from, + required this.to, + this.when, + this.byArrival = false, + }); + + @override + State createState() => _TripResultsViewState(); +} + +class _TripResultsViewState extends State { + final RmvRepository _repo = RmvRepository(); + final List _trips = []; + String? _scrollLater; + String? _scrollEarlier; + bool _loading = true; + bool _loadingMore = false; + Object? _error; + + @override + void initState() { + super.initState(); + _initial(); + } + + Future _initial() async { + setState(() { + _loading = true; + _error = null; + _trips.clear(); + _scrollEarlier = null; + _scrollLater = null; + }); + try { + final r = await _repo.searchTrips( + fromStopId: widget.from.id, + toStopId: widget.to.id, + when: widget.when, + searchByArrival: widget.byArrival, + ); + if (!mounted) return; + setState(() { + _trips.addAll(r.trips); + _scrollEarlier = r.scrollContextEarlier; + _scrollLater = r.scrollContextLater; + _loading = false; + }); + } catch (e) { + if (!mounted) return; + setState(() { + _error = e; + _loading = false; + }); + } + } + + Future _loadMore({required bool later}) async { + final ctx = later ? _scrollLater : _scrollEarlier; + if (ctx == null || _loadingMore) return; + setState(() => _loadingMore = true); + try { + final r = await _repo.moreTrips(ctx); + if (!mounted) return; + setState(() { + if (later) { + _trips.addAll(r.trips); + } else { + _trips.insertAll(0, r.trips); + } + if (r.scrollContextEarlier != null) { + _scrollEarlier = r.scrollContextEarlier; + } + if (r.scrollContextLater != null) { + _scrollLater = r.scrollContextLater; + } + _loadingMore = false; + }); + } catch (e) { + if (!mounted) return; + setState(() => _loadingMore = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(errorToUserMessage(e))), + ); + } + } + + @override + Widget build(BuildContext context) { + final whenLabel = widget.when == null + ? 'jetzt' + : '${widget.byArrival ? 'an' : 'ab'} ${widget.when!.formatDateTime()}'; + return Scaffold( + appBar: AppBar( + title: Text('${widget.from.name} → ${widget.to.name}'), + bottom: PreferredSize( + preferredSize: const Size.fromHeight(24), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + whenLabel, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Colors.white70, + ), + ), + ), + ), + ), + ), + body: _body(), + ); + } + + Widget _body() { + if (_loading) return const Center(child: AppProgressIndicator.large()); + final err = _error; + if (err != null) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + errorToUserMessage(err), + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 12), + FilledButton.icon( + icon: const Icon(Icons.refresh), + onPressed: _initial, + label: const Text('Erneut versuchen'), + ), + ], + ), + ), + ); + } + if (_trips.isEmpty) { + return const Center(child: Text('Keine Verbindungen gefunden.')); + } + return RefreshIndicator( + onRefresh: _initial, + child: ListView.separated( + itemCount: _trips.length + 2, + separatorBuilder: (_, _) => const Divider(height: 1), + itemBuilder: (_, i) { + if (i == 0) { + return _scrollButton( + icon: Icons.arrow_upward, + label: 'Frühere Verbindungen', + enabled: _scrollEarlier != null, + onTap: () => _loadMore(later: false), + ); + } + if (i == _trips.length + 1) { + return _scrollButton( + icon: Icons.arrow_downward, + label: 'Spätere Verbindungen', + enabled: _scrollLater != null, + onTap: () => _loadMore(later: true), + ); + } + final trip = _trips[i - 1]; + return TripTile( + trip: trip, + onTap: () => AppRoutes.openRmvTripDetail(context, trip), + ); + }, + ), + ); + } + + Widget _scrollButton({ + required IconData icon, + required String label, + required bool enabled, + required VoidCallback onTap, + }) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: OutlinedButton.icon( + icon: _loadingMore + ? const AppProgressIndicator.small() + : Icon(icon), + label: Text(label), + onPressed: enabled && !_loadingMore ? onTap : null, + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(40), + ), + ), + ); +} diff --git a/lib/view/pages/rmv/trip_search/trip_search_tab.dart b/lib/view/pages/rmv/trip_search/trip_search_tab.dart new file mode 100644 index 0000000..c342531 --- /dev/null +++ b/lib/view/pages/rmv/trip_search/trip_search_tab.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../routing/app_routes.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../storage/rmv_settings.dart'; +import '../../../../storage/settings.dart'; +import '../../../../widget/centered_leading.dart'; +import '../../../../widget/confirm_dialog.dart'; +import '../favorites_controller.dart'; +import '../widgets/station_picker_sheet.dart'; +import '../widgets/when_picker.dart'; + +class TripSearchTab extends StatefulWidget { + const TripSearchTab({super.key}); + + @override + State createState() => _TripSearchTabState(); +} + +class _TripSearchTabState extends State { + StopLocation? _from; + StopLocation? _to; + DateTime? _when; + bool _byArrival = false; + + Future _pickFrom() async { + final s = await showStationPickerSheet( + context, + title: 'Von welcher Station?', + ); + if (s != null) setState(() => _from = s); + } + + Future _pickTo() async { + final s = await showStationPickerSheet( + context, + title: 'Wohin?', + ); + if (s != null) setState(() => _to = s); + } + + void _swap() { + setState(() { + final tmp = _from; + _from = _to; + _to = tmp; + }); + } + + void _search(BuildContext context) { + final from = _from; + final to = _to; + if (from == null || to == null) return; + RmvFavoritesController(context.read()) + .addRecentTrip(from, to); + AppRoutes.openRmvTripResults( + context, + from: from, + to: to, + when: _when, + byArrival: _byArrival, + ); + } + + @override + Widget build(BuildContext context) => + BlocBuilder(builder: _buildContent); + + Widget _buildContent(BuildContext context, Settings settings) { + final canSearch = _from != null && _to != null; + final recents = settings.rmvSettings.recentTripQueries; + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 8), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _stationField( + label: 'Von', + icon: Icons.trip_origin, + value: _from, + onTap: _pickFrom, + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: _stationField( + label: 'Nach', + icon: Icons.place, + value: _to, + onTap: _pickTo, + ), + ), + IconButton( + icon: const Icon(Icons.swap_vert), + tooltip: 'Start und Ziel tauschen', + onPressed: _from == null && _to == null ? null : _swap, + ), + ], + ), + const SizedBox(height: 12), + WhenPicker( + value: _when, + byArrival: _byArrival, + onValueChanged: (v) => setState(() => _when = v), + onByArrivalChanged: (v) => setState(() => _byArrival = v), + ), + const SizedBox(height: 12), + FilledButton.icon( + icon: const Icon(Icons.search), + label: const Text('Verbindungen suchen'), + onPressed: canSearch ? () => _search(context) : null, + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + ), + ], + ), + ), + const Divider(height: 32), + if (recents.isEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), + child: Center( + child: Text( + 'Noch keine Suchen. Wähle Start und Ziel, um die erste Verbindung zu suchen.', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + ) + else + _recentsSection(context, recents), + ], + ); + } + + Widget _stationField({ + required String label, + required IconData icon, + required StopLocation? value, + required VoidCallback onTap, + }) => InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: InputDecorator( + decoration: InputDecoration( + labelText: label, + prefixIcon: Icon(icon), + border: const OutlineInputBorder(), + suffixIcon: const Icon(Icons.arrow_drop_down), + ), + child: Text( + value?.name ?? 'Station wählen', + style: value == null + ? TextStyle(color: Theme.of(context).hintColor) + : null, + ), + ), + ); + + Widget _recentsSection( + BuildContext context, + List recents, + ) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 8, 4), + child: Row( + children: [ + Expanded( + child: Text( + 'Letzte Suchen', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + IconButton( + icon: const Icon(Icons.delete_sweep_outlined), + tooltip: 'Alle löschen', + onPressed: () => ConfirmDialog( + title: 'Suchverlauf leeren?', + content: + 'Die letzten Verbindungssuchen werden entfernt. Favoriten bleiben bestehen.', + confirmButton: 'Leeren', + onConfirm: () => RmvFavoritesController( + context.read(), + ).clearRecentTrips(), + ).asDialog(context), + ), + ], + ), + ), + ...recents.map( + (q) => ListTile( + leading: const CenteredLeading(Icon(Icons.history)), + title: Text('${q.from.name} → ${q.to.name}'), + onTap: () => setState(() { + _from = q.from; + _to = q.to; + }), + ), + ), + ], + ); +} diff --git a/lib/view/pages/rmv/widgets/departure_arrival_tile.dart b/lib/view/pages/rmv/widgets/departure_arrival_tile.dart new file mode 100644 index 0000000..288fdc6 --- /dev/null +++ b/lib/view/pages/rmv/widgets/departure_arrival_tile.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import 'product_chip.dart'; +import 'realtime_time.dart'; + +/// Renders a single departure or arrival row. Used in the station detail view. +class DepartureArrivalTile extends StatelessWidget { + final Product? product; + final String name; + + /// Direction (for departures) or origin (for arrivals). + final String towards; + final DateTime scheduled; + final DateTime? realtime; + final int? delayMinutes; + final String? track; + final String? realTrack; + final bool cancelled; + final VoidCallback? onTap; + + const DepartureArrivalTile({ + super.key, + required this.product, + required this.name, + required this.towards, + required this.scheduled, + this.realtime, + this.delayMinutes, + this.track, + this.realTrack, + this.cancelled = false, + this.onTap, + }); + + factory DepartureArrivalTile.fromDeparture( + Departure d, { + VoidCallback? onTap, + }) => DepartureArrivalTile( + product: d.product, + name: d.name, + towards: 'nach ${d.direction}', + scheduled: d.scheduledTime, + realtime: d.realTime, + delayMinutes: d.delayMinutes, + track: d.track, + realTrack: d.realTrack, + cancelled: d.cancelled, + onTap: onTap, + ); + + factory DepartureArrivalTile.fromArrival( + Arrival a, { + VoidCallback? onTap, + }) => DepartureArrivalTile( + product: a.product, + name: a.name, + towards: 'von ${a.origin}', + scheduled: a.scheduledTime, + realtime: a.realTime, + delayMinutes: a.delayMinutes, + track: a.track, + realTrack: a.realTrack, + cancelled: a.cancelled, + onTap: onTap, + ); + + @override + Widget build(BuildContext context) { + final effectiveTrack = (realTrack?.isNotEmpty ?? false) + ? realTrack! + : (track ?? ''); + final trackChanged = + realTrack != null && track != null && realTrack != track; + return ListTile( + onTap: onTap, + leading: SizedBox( + width: 72, + child: ProductChip(product: product, fallbackLabel: name), + ), + title: Text( + towards, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + decoration: cancelled ? TextDecoration.lineThrough : null, + ), + ), + subtitle: effectiveTrack.isEmpty + ? null + : Row( + children: [ + Icon( + Icons.directions_transit, + size: 14, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: 4), + Text( + 'Gleis $effectiveTrack', + style: TextStyle( + color: trackChanged + ? Colors.red + : Theme.of(context).colorScheme.secondary, + fontWeight: trackChanged ? FontWeight.bold : null, + ), + ), + ], + ), + trailing: RealtimeTime( + scheduled: scheduled, + realtime: realtime, + delayMinutes: delayMinutes, + cancelled: cancelled, + style: Theme.of(context).textTheme.titleMedium, + ), + ); + } +} diff --git a/lib/view/pages/rmv/widgets/leg_tile.dart b/lib/view/pages/rmv/widgets/leg_tile.dart new file mode 100644 index 0000000..1041d0c --- /dev/null +++ b/lib/view/pages/rmv/widgets/leg_tile.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../extensions/date_time.dart'; +import 'product_chip.dart'; +import 'realtime_time.dart'; + +/// Renders a single [Leg] of a trip with header (line/direction), origin and +/// destination times and (optionally) the list of intermediate stops. +class LegTile extends StatelessWidget { + final Leg leg; + final VoidCallback? onShowJourneyDetail; + + const LegTile({super.key, required this.leg, this.onShowJourneyDetail}); + + @override + Widget build(BuildContext context) { + final isWalk = + leg.type == LegType.walk || leg.type == LegType.transfer; + final cancelled = leg.cancelled || leg.partCancelled; + return Card( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _header(context, isWalk: isWalk, cancelled: cancelled), + _endpoint( + context, + label: leg.origin.name, + scheduled: leg.origin.scheduledTime, + realtime: leg.origin.realTime, + delayMinutes: leg.origin.delayMinutes?.toInt(), + track: (leg.origin.realTrack?.isNotEmpty ?? false) + ? leg.origin.realTrack + : leg.origin.track, + icon: Icons.trip_origin, + cancelled: cancelled, + ), + if (leg.stops.length > 2) + _stopsExpander(context, leg.stops), + _endpoint( + context, + label: leg.destination.name, + scheduled: leg.destination.scheduledTime, + realtime: leg.destination.realTime, + delayMinutes: leg.destination.delayMinutes?.toInt(), + track: (leg.destination.realTrack?.isNotEmpty ?? false) + ? leg.destination.realTrack + : leg.destination.track, + icon: Icons.place, + cancelled: cancelled, + ), + ], + ), + ); + } + + Widget _header( + BuildContext context, { + required bool isWalk, + required bool cancelled, + }) { + final headlineStyle = Theme.of(context).textTheme.titleSmall?.copyWith( + decoration: cancelled ? TextDecoration.lineThrough : null, + ); + final title = isWalk + ? 'Fußweg' + : (leg.direction != null + ? '${leg.name ?? ''} → ${leg.direction}' + : (leg.name ?? '—')); + final canOpenJourney = leg.journeyRef != null && !isWalk; + return Container( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + padding: const EdgeInsets.fromLTRB(12, 8, 8, 8), + child: Row( + children: [ + if (!isWalk) ProductChip(product: leg.product, fallbackLabel: leg.name), + if (!isWalk) const SizedBox(width: 8), + if (isWalk) + const Padding( + padding: EdgeInsets.only(right: 8), + child: Icon(Icons.directions_walk), + ), + Expanded( + child: Text( + title, + style: headlineStyle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + if (leg.duration != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Text( + '${leg.duration!.inMinutes} min', + style: Theme.of(context).textTheme.bodySmall, + ), + ), + if (canOpenJourney) + IconButton( + icon: const Icon(Icons.list_alt), + tooltip: 'Alle Halte', + onPressed: onShowJourneyDetail, + ), + ], + ), + ); + } + + Widget _endpoint( + BuildContext context, { + required String label, + required DateTime scheduled, + DateTime? realtime, + int? delayMinutes, + String? track, + required IconData icon, + required bool cancelled, + }) => ListTile( + leading: Icon(icon, size: 20), + title: Text( + label, + style: TextStyle( + decoration: cancelled ? TextDecoration.lineThrough : null, + ), + ), + subtitle: (track != null && track.isNotEmpty) + ? Text('Gleis $track') + : null, + trailing: RealtimeTime( + scheduled: scheduled, + realtime: realtime, + delayMinutes: delayMinutes, + cancelled: cancelled, + ), + dense: true, + ); + + Widget _stopsExpander(BuildContext context, List stops) { + final intermediate = stops.length > 2 + ? stops.sublist(1, stops.length - 1) + : const []; + if (intermediate.isEmpty) return const SizedBox.shrink(); + return ExpansionTile( + tilePadding: const EdgeInsets.symmetric(horizontal: 16), + childrenPadding: const EdgeInsets.symmetric(horizontal: 16), + title: Text( + '${intermediate.length} Zwischenhalt${intermediate.length > 1 ? 'e' : ''}', + style: Theme.of(context).textTheme.bodySmall, + ), + children: intermediate + .map( + (s) => ListTile( + dense: true, + visualDensity: VisualDensity.compact, + leading: const Icon(Icons.fiber_manual_record, size: 10), + title: Text(s.name), + trailing: Text(_stopTime(s)), + ), + ) + .toList(), + ); + } + + String _stopTime(JourneyStop s) { + final dep = s.realDeparture ?? s.scheduledDeparture; + final arr = s.realArrival ?? s.scheduledArrival; + if (arr != null && dep != null && arr != dep) { + return '${arr.formatHm()} / ${dep.formatHm()}'; + } + final t = dep ?? arr; + return t?.formatHm() ?? ''; + } +} diff --git a/lib/view/pages/rmv/widgets/product_chip.dart b/lib/view/pages/rmv/widgets/product_chip.dart new file mode 100644 index 0000000..8a84820 --- /dev/null +++ b/lib/view/pages/rmv/widgets/product_chip.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; + +/// Renders a transit line/product as a compact, colored chip +/// (e.g. `U7`, `S3`, `RB51`, `ICE`). Colour is derived from the category code +/// so the same line consistently has the same colour. +class ProductChip extends StatelessWidget { + final Product? product; + final String? fallbackLabel; + + const ProductChip({super.key, required this.product, this.fallbackLabel}); + + @override + Widget build(BuildContext context) { + final label = _label(); + if (label == null || label.isEmpty) return const SizedBox.shrink(); + final color = _colorFor(product?.category, product?.categoryCode); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(6), + ), + child: Text( + label, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ); + } + + String? _label() { + final p = product; + if (p == null) return fallbackLabel; + if (p.line != null && p.line!.isNotEmpty) return p.line; + if (p.displayNumber != null && p.displayNumber!.isNotEmpty) { + return '${p.category ?? ''}${p.displayNumber}'.trim(); + } + if (p.name != null && p.name!.isNotEmpty) return p.name; + return fallbackLabel; + } + + Color _colorFor(String? category, String? code) { + final key = (category ?? code ?? '').toLowerCase(); + if (key.startsWith('ice')) return const Color(0xFFD32F2F); + if (key.startsWith('ic') || key.startsWith('ec')) { + return const Color(0xFFE57373); + } + if (key.startsWith('s-bahn') || key == 's') return const Color(0xFF2E7D32); + if (key.startsWith('u-bahn') || key == 'u') return const Color(0xFF1565C0); + if (key.startsWith('tram') || key.startsWith('strab')) { + return const Color(0xFFEF6C00); + } + if (key.startsWith('bus')) return const Color(0xFF6A1B9A); + if (key.startsWith('rb') || key.startsWith('re')) { + return const Color(0xFF455A64); + } + return const Color(0xFF37474F); + } +} diff --git a/lib/view/pages/rmv/widgets/realtime_time.dart b/lib/view/pages/rmv/widgets/realtime_time.dart new file mode 100644 index 0000000..969dc81 --- /dev/null +++ b/lib/view/pages/rmv/widgets/realtime_time.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +import '../../../../extensions/date_time.dart'; + +/// Shows a scheduled time with optional realtime delay overlay. +/// +/// Examples: +/// - on-time: `14:35` +/// - 2 minutes late: `14:35` + green/red `+2'` chip +/// - cancelled: scheduled time struck through, red `Ausfall` chip +class RealtimeTime extends StatelessWidget { + final DateTime scheduled; + final DateTime? realtime; + final int? delayMinutes; + final bool cancelled; + final TextStyle? style; + + const RealtimeTime({ + super.key, + required this.scheduled, + this.realtime, + this.delayMinutes, + this.cancelled = false, + this.style, + }); + + @override + Widget build(BuildContext context) { + final base = style ?? Theme.of(context).textTheme.bodyMedium ?? const TextStyle(); + final scheduledText = Text( + scheduled.formatHm(), + style: base.copyWith( + decoration: cancelled ? TextDecoration.lineThrough : null, + ), + ); + if (cancelled) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + scheduledText, + const SizedBox(width: 6), + _badge(context, 'Ausfall', Colors.red), + ], + ); + } + final delay = delayMinutes; + if (delay != null && delay != 0) { + final positive = delay > 0; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + scheduledText, + const SizedBox(width: 4), + Text( + '${positive ? '+' : ''}$delay\'', + style: base.copyWith( + color: positive ? Colors.red : Colors.green, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + return scheduledText; + } + + Widget _badge(BuildContext context, String text, Color color) => Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + text, + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ); +} diff --git a/lib/view/pages/rmv/widgets/station_picker_sheet.dart b/lib/view/pages/rmv/widgets/station_picker_sheet.dart new file mode 100644 index 0000000..7b5b1fd --- /dev/null +++ b/lib/view/pages/rmv/widgets/station_picker_sheet.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../api/errors/error_mapper.dart'; +import '../../../../state/app/modules/rmv/repository/rmv_repository.dart'; +import '../../../../state/app/modules/settings/bloc/settings_cubit.dart'; +import '../../../../utils/debouncer.dart'; +import '../../../../widget/app_progress_indicator.dart'; +import '../../../../widget/centered_leading.dart'; +import '../favorites_controller.dart'; + +/// Modal search sheet for picking a [StopLocation]. Shows favorites + recents +/// when the search field is empty, switches to live search results as soon as +/// the user types. Returns the chosen stop via [Navigator.pop], or `null` if +/// the user dismisses the sheet. +Future showStationPickerSheet( + BuildContext context, { + String title = 'Station auswählen', +}) => showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + useSafeArea: true, + builder: (sheetCtx) => _StationPickerSheet(title: title), +); + +class _StationPickerSheet extends StatefulWidget { + final String title; + const _StationPickerSheet({required this.title}); + + @override + State<_StationPickerSheet> createState() => _StationPickerSheetState(); +} + +class _StationPickerSheetState extends State<_StationPickerSheet> { + static const _debounceTag = 'rmv_station_search'; + + final TextEditingController _controller = TextEditingController(); + final RmvRepository _repo = RmvRepository(); + List? _results; + bool _loading = false; + Object? _error; + String _query = ''; + + @override + void dispose() { + Debouncer.cancel(_debounceTag); + _controller.dispose(); + super.dispose(); + } + + void _onChanged(String value) { + final trimmed = value.trim(); + setState(() => _query = trimmed); + if (trimmed.length < 2) { + setState(() { + _results = null; + _error = null; + _loading = false; + }); + Debouncer.cancel(_debounceTag); + return; + } + Debouncer.debounce(_debounceTag, const Duration(milliseconds: 300), () { + _runSearch(trimmed); + }); + } + + Future _runSearch(String q) async { + setState(() { + _loading = true; + _error = null; + }); + try { + final results = await _repo.searchStops(q, max: 25); + if (!mounted || _query != q) return; + setState(() { + _results = results; + _loading = false; + }); + } catch (e) { + if (!mounted || _query != q) return; + setState(() { + _error = e; + _loading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + final settings = context.read(); + final favorites = + settings.val().rmvSettings.favoriteStations; + final recents = settings.val().rmvSettings.recentStations; + final viewInsets = MediaQuery.of(context).viewInsets; + + return Padding( + padding: EdgeInsets.only(bottom: viewInsets.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 8), + child: Text( + widget.title, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TextField( + controller: _controller, + autofocus: true, + onChanged: _onChanged, + decoration: InputDecoration( + hintText: 'Station suchen…', + prefixIcon: const Icon(Icons.search), + suffixIcon: _query.isEmpty + ? null + : IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _controller.clear(); + _onChanged(''); + }, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(height: 8), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.6, + minHeight: 200, + ), + child: _body(favorites, recents), + ), + ], + ), + ); + } + + Widget _body(List favorites, List recents) { + if (_loading) { + return const Center(child: AppProgressIndicator.medium()); + } + final err = _error; + if (err != null) { + return Padding( + padding: const EdgeInsets.all(16), + child: Text( + errorToUserMessage(err), + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ); + } + final results = _results; + if (results != null) { + if (results.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Keine Station für "$_query" gefunden.', + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ); + } + return ListView( + children: results + .map((s) => _tile(s, leadingIcon: Icons.directions_transit)) + .toList(), + ); + } + // Empty query → favorites + recents. + final widgets = []; + if (favorites.isNotEmpty) { + widgets.add(_sectionHeader('Favoriten')); + widgets.addAll( + favorites.map((s) => _tile(s, leadingIcon: Icons.star)), + ); + } + if (recents.isNotEmpty) { + widgets.add(_sectionHeader('Zuletzt verwendet')); + widgets.addAll( + recents.map((s) => _tile(s, leadingIcon: Icons.history)), + ); + } + if (widgets.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Tippe oben einen Stationsnamen ein, um die RMV-Datenbank zu durchsuchen.', + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + ); + } + return ListView(children: widgets); + } + + Widget _sectionHeader(String text) => Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + text, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ); + + Widget _tile(StopLocation stop, {required IconData leadingIcon}) => ListTile( + leading: CenteredLeading(Icon(leadingIcon)), + title: Text(stop.name), + subtitle: stop.description == null ? null : Text(stop.description!), + onTap: () { + RmvFavoritesController(context.read()).addRecent(stop); + Navigator.of(context).pop(stop); + }, + ); +} diff --git a/lib/view/pages/rmv/widgets/trip_tile.dart b/lib/view/pages/rmv/widgets/trip_tile.dart new file mode 100644 index 0000000..d007724 --- /dev/null +++ b/lib/view/pages/rmv/widgets/trip_tile.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; + +import '../../../../api/connect/rmv/rmv_models.dart'; +import '../../../../extensions/date_time.dart'; +import 'product_chip.dart'; +import 'realtime_time.dart'; + +/// Compact summary of a [Trip] used in the trip results list. +class TripTile extends StatelessWidget { + final Trip trip; + final VoidCallback? onTap; + + const TripTile({super.key, required this.trip, this.onTap}); + + @override + Widget build(BuildContext context) { + final firstLeg = trip.legs.isEmpty ? null : trip.legs.first; + final lastLeg = trip.legs.isEmpty ? null : trip.legs.last; + if (firstLeg == null || lastLeg == null) { + return const ListTile(title: Text('Verbindung ohne Halt')); + } + final scheduledStart = firstLeg.origin.scheduledTime; + final scheduledEnd = lastLeg.destination.scheduledTime; + final cancelled = + trip.legs.any((l) => l.cancelled || l.partCancelled); + final transfers = trip.transferCount ?? _countTransfers(trip); + final duration = trip.realDuration ?? trip.duration; + final productChips = trip.legs + .where((l) => l.type == LegType.journey && l.product != null) + .map((l) => Padding( + padding: const EdgeInsets.only(right: 4), + child: ProductChip(product: l.product, fallbackLabel: l.name), + )) + .toList(); + + return ListTile( + onTap: onTap, + isThreeLine: true, + title: Row( + children: [ + RealtimeTime( + scheduled: scheduledStart, + realtime: firstLeg.origin.realTime, + delayMinutes: firstLeg.origin.delayMinutes?.toInt(), + cancelled: cancelled, + style: Theme.of(context).textTheme.titleMedium, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 6), + child: Text('–'), + ), + RealtimeTime( + scheduled: scheduledEnd, + realtime: lastLeg.destination.realTime, + delayMinutes: lastLeg.destination.delayMinutes?.toInt(), + cancelled: cancelled, + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Wrap(runSpacing: 4, children: productChips), + const SizedBox(height: 4), + Row( + children: [ + if (duration != null) ...[ + const Icon(Icons.schedule, size: 14), + const SizedBox(width: 2), + Text(_formatDuration(duration)), + const SizedBox(width: 12), + ], + const Icon(Icons.swap_horiz, size: 14), + const SizedBox(width: 2), + Text( + transfers == 0 + ? 'Direkt' + : '$transfers Umstieg${transfers > 1 ? 'e' : ''}', + ), + ], + ), + ], + ), + trailing: const Icon(Icons.chevron_right), + ); + } + + int _countTransfers(Trip trip) { + final journeyLegs = + trip.legs.where((l) => l.type == LegType.journey).length; + return journeyLegs <= 1 ? 0 : journeyLegs - 1; + } +} + +String _formatDuration(Duration d) { + final hours = d.inHours; + final minutes = d.inMinutes.remainder(60); + if (hours == 0) return '$minutes min'; + return '$hours h ${minutes.toString().padLeft(2, '0')} min'; +} + +/// Re-export for trip detail screen. +String formatTripDuration(Duration d) => _formatDuration(d); + +/// Helper used in date headers on the trip results list. +String formatTripDateHeader(DateTime when) => when.formatDateRelativeShort(); diff --git a/lib/view/pages/rmv/widgets/when_picker.dart b/lib/view/pages/rmv/widgets/when_picker.dart new file mode 100644 index 0000000..8000113 --- /dev/null +++ b/lib/view/pages/rmv/widgets/when_picker.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import '../../../../extensions/date_time.dart'; + +/// Returns the "depart at / arrive by" time and the AB/AN-toggle. `null` for +/// [value] means "now" — the API treats an empty `when` parameter as the +/// current time. +class WhenPicker extends StatelessWidget { + final DateTime? value; + final bool byArrival; + final ValueChanged onValueChanged; + final ValueChanged onByArrivalChanged; + + const WhenPicker({ + super.key, + required this.value, + required this.byArrival, + required this.onValueChanged, + required this.onByArrivalChanged, + }); + + @override + Widget build(BuildContext context) { + final label = value == null + ? 'Jetzt' + : value!.formatDateRelativeShort() == 'Heute' + ? value!.formatHm() + : '${value!.formatDateRelativeShort()} ${value!.formatHm()}'; + return Row( + children: [ + Expanded( + child: OutlinedButton.icon( + icon: const Icon(Icons.schedule), + label: Text(label), + onPressed: () => _pick(context), + ), + ), + const SizedBox(width: 8), + SegmentedButton( + segments: const [ + ButtonSegment(value: false, label: Text('Ab')), + ButtonSegment(value: true, label: Text('An')), + ], + selected: {byArrival}, + onSelectionChanged: (s) => onByArrivalChanged(s.first), + ), + if (value != null) ...[ + const SizedBox(width: 4), + IconButton( + icon: const Icon(Icons.close), + tooltip: 'Zurück auf "Jetzt"', + onPressed: () => onValueChanged(null), + ), + ], + ], + ); + } + + Future _pick(BuildContext context) async { + final now = DateTime.now(); + final initial = value ?? now; + final date = await showDatePicker( + context: context, + initialDate: initial, + firstDate: now.subtract(const Duration(days: 7)), + lastDate: now.add(const Duration(days: 365)), + ); + if (date == null) return; + if (!context.mounted) return; + final time = await showTimePicker( + context: context, + initialTime: TimeOfDay(hour: initial.hour, minute: initial.minute), + ); + if (time == null) return; + onValueChanged( + DateTime(date.year, date.month, date.day, time.hour, time.minute), + ); + } +} diff --git a/lib/view/pages/settings/data/default_settings.dart b/lib/view/pages/settings/data/default_settings.dart index 84d888b..b7823a3 100644 --- a/lib/view/pages/settings/data/default_settings.dart +++ b/lib/view/pages/settings/data/default_settings.dart @@ -9,6 +9,7 @@ import '../../../../storage/file_view_settings.dart'; import '../../../../storage/holidays_settings.dart'; import '../../../../storage/modules_settings.dart'; import '../../../../storage/notification_settings.dart'; +import '../../../../storage/rmv_settings.dart'; import '../../../../storage/settings.dart'; import '../../../../storage/talk_settings.dart'; import '../../../../storage/timetable_settings.dart'; @@ -29,6 +30,7 @@ class DefaultSettings { Modules.gradeAveragesCalculator, Modules.holidays, Modules.marianumDates, + Modules.rmv, ], hiddenModules: [], autoFillBottomBar: true, @@ -53,6 +55,7 @@ class DefaultSettings { dismissedDisclaimer: false, showPastEvents: false, ), + rmvSettings: RmvSettings(), fileViewSettings: FileViewSettings(alwaysOpenExternally: Platform.isIOS), notificationSettings: NotificationSettings( askUsageDismissed: false, diff --git a/pubspec.yaml b/pubspec.yaml index 9f7ea00..fd10428 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,7 @@ dependencies: scrollable_positioned_list: ^0.3.8 flutter_split_view: ^0.1.2 flutter_svg: ^2.0.10 + geolocator: ^14.0.0 freezed_annotation: ^3.1.0 http: ^1.3.0 hydrated_bloc: ^11.0.0 diff --git a/test/api/connect/iso_duration_test.dart b/test/api/connect/iso_duration_test.dart new file mode 100644 index 0000000..ffa882d --- /dev/null +++ b/test/api/connect/iso_duration_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/api/connect/rmv/iso_duration.dart'; + +void main() { + group('IsoDuration.fromJson', () { + test('null and empty return null', () { + expect(IsoDuration.fromJson(null), isNull); + expect(IsoDuration.fromJson(''), isNull); + }); + + test('hours-minutes-seconds parse correctly', () { + expect( + IsoDuration.fromJson('PT1H30M15S'), + const Duration(hours: 1, minutes: 30, seconds: 15), + ); + }); + + test('hours only', () { + expect(IsoDuration.fromJson('PT2H'), const Duration(hours: 2)); + }); + + test('minutes only', () { + expect(IsoDuration.fromJson('PT45M'), const Duration(minutes: 45)); + }); + + test('seconds with fraction', () { + expect( + IsoDuration.fromJson('PT30.5S'), + const Duration(milliseconds: 30500), + ); + }); + + test('invalid string returns null', () { + expect(IsoDuration.fromJson('not a duration'), isNull); + }); + }); + + group('IsoDuration.toJson', () { + test('null returns null', () => expect(IsoDuration.toJson(null), isNull)); + + test('zero duration formats as PT0S', () { + expect(IsoDuration.toJson(Duration.zero), 'PT0S'); + }); + + test('hours and minutes only formats without seconds', () { + expect( + IsoDuration.toJson(const Duration(hours: 1, minutes: 30)), + 'PT1H30M', + ); + }); + + test('full duration roundtrips through parse', () { + const original = Duration(hours: 2, minutes: 15, seconds: 7); + expect(IsoDuration.fromJson(IsoDuration.toJson(original)), original); + }); + }); +} diff --git a/test/api/connect/rmv_upstream_exception_test.dart b/test/api/connect/rmv_upstream_exception_test.dart new file mode 100644 index 0000000..7c7e738 --- /dev/null +++ b/test/api/connect/rmv_upstream_exception_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:marianum_mobile/api/connect/errors/rmv_upstream_exception.dart'; + +void main() { + group('RmvUpstreamException', () { + test('H390 maps to no-connection message', () { + final e = RmvUpstreamException(errorCode: 'H390'); + expect(e.userMessage, contains('Keine Verbindung')); + }); + + test('H891 maps to invalid-station message', () { + final e = RmvUpstreamException(errorCode: 'H891'); + expect(e.userMessage, contains('ungültig')); + }); + + test('unknown code falls through to a generic but specific message', () { + final e = RmvUpstreamException(errorCode: 'HXYZ'); + expect(e.userMessage, contains('HXYZ')); + }); + + test('null code yields the generic upstream message', () { + final e = RmvUpstreamException(errorCode: null); + expect(e.userMessage, contains('keine Antwort')); + }); + }); +}