implemented RMV commute integration in the timetable, added Nominatim geocoding for home station lookup, created CommuteCubit for daily trip management with TTL caching, and introduced specialized timetable tiles, detail sheets, and settings for transit connections and walking buffers

This commit is contained in:
2026-05-20 22:50:57 +02:00
parent 067012cc84
commit 46d6b3410e
20 changed files with 1513 additions and 20 deletions
+16
View File
@@ -0,0 +1,16 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'nominatim_result.freezed.dart';
part 'nominatim_result.g.dart';
@freezed
abstract class NominatimResult with _$NominatimResult {
const factory NominatimResult({
required String displayName,
required double lat,
required double lon,
}) = _NominatimResult;
factory NominatimResult.fromJson(Map<String, Object?> json) =>
_$NominatimResultFromJson(json);
}
@@ -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 'nominatim_result.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$NominatimResult {
String get displayName; double get lat; double get lon;
/// Create a copy of NominatimResult
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$NominatimResultCopyWith<NominatimResult> get copyWith => _$NominatimResultCopyWithImpl<NominatimResult>(this as NominatimResult, _$identity);
/// Serializes this NominatimResult to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is NominatimResult&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,displayName,lat,lon);
@override
String toString() {
return 'NominatimResult(displayName: $displayName, lat: $lat, lon: $lon)';
}
}
/// @nodoc
abstract mixin class $NominatimResultCopyWith<$Res> {
factory $NominatimResultCopyWith(NominatimResult value, $Res Function(NominatimResult) _then) = _$NominatimResultCopyWithImpl;
@useResult
$Res call({
String displayName, double lat, double lon
});
}
/// @nodoc
class _$NominatimResultCopyWithImpl<$Res>
implements $NominatimResultCopyWith<$Res> {
_$NominatimResultCopyWithImpl(this._self, this._then);
final NominatimResult _self;
final $Res Function(NominatimResult) _then;
/// Create a copy of NominatimResult
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? displayName = null,Object? lat = null,Object? lon = null,}) {
return _then(_self.copyWith(
displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
as String,lat: null == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable
as double,lon: null == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// Adds pattern-matching-related methods to [NominatimResult].
extension NominatimResultPatterns on NominatimResult {
/// 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 extends Object?>(TResult Function( _NominatimResult value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _NominatimResult() 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 extends Object?>(TResult Function( _NominatimResult value) $default,){
final _that = this;
switch (_that) {
case _NominatimResult():
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 extends Object?>(TResult? Function( _NominatimResult value)? $default,){
final _that = this;
switch (_that) {
case _NominatimResult() 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 extends Object?>(TResult Function( String displayName, double lat, double lon)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _NominatimResult() when $default != null:
return $default(_that.displayName,_that.lat,_that.lon);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 extends Object?>(TResult Function( String displayName, double lat, double lon) $default,) {final _that = this;
switch (_that) {
case _NominatimResult():
return $default(_that.displayName,_that.lat,_that.lon);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 extends Object?>(TResult? Function( String displayName, double lat, double lon)? $default,) {final _that = this;
switch (_that) {
case _NominatimResult() when $default != null:
return $default(_that.displayName,_that.lat,_that.lon);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _NominatimResult implements NominatimResult {
const _NominatimResult({required this.displayName, required this.lat, required this.lon});
factory _NominatimResult.fromJson(Map<String, dynamic> json) => _$NominatimResultFromJson(json);
@override final String displayName;
@override final double lat;
@override final double lon;
/// Create a copy of NominatimResult
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$NominatimResultCopyWith<_NominatimResult> get copyWith => __$NominatimResultCopyWithImpl<_NominatimResult>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$NominatimResultToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NominatimResult&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.lat, lat) || other.lat == lat)&&(identical(other.lon, lon) || other.lon == lon));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,displayName,lat,lon);
@override
String toString() {
return 'NominatimResult(displayName: $displayName, lat: $lat, lon: $lon)';
}
}
/// @nodoc
abstract mixin class _$NominatimResultCopyWith<$Res> implements $NominatimResultCopyWith<$Res> {
factory _$NominatimResultCopyWith(_NominatimResult value, $Res Function(_NominatimResult) _then) = __$NominatimResultCopyWithImpl;
@override @useResult
$Res call({
String displayName, double lat, double lon
});
}
/// @nodoc
class __$NominatimResultCopyWithImpl<$Res>
implements _$NominatimResultCopyWith<$Res> {
__$NominatimResultCopyWithImpl(this._self, this._then);
final _NominatimResult _self;
final $Res Function(_NominatimResult) _then;
/// Create a copy of NominatimResult
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? displayName = null,Object? lat = null,Object? lon = null,}) {
return _then(_NominatimResult(
displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
as String,lat: null == lat ? _self.lat : lat // ignore: cast_nullable_to_non_nullable
as double,lon: null == lon ? _self.lon : lon // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
// dart format on
+21
View File
@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'nominatim_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_NominatimResult _$NominatimResultFromJson(Map<String, dynamic> json) =>
_NominatimResult(
displayName: json['displayName'] as String,
lat: (json['lat'] as num).toDouble(),
lon: (json['lon'] as num).toDouble(),
);
Map<String, dynamic> _$NominatimResultToJson(_NominatimResult instance) =>
<String, dynamic>{
'displayName': instance.displayName,
'lat': instance.lat,
'lon': instance.lon,
};
+72
View File
@@ -0,0 +1,72 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../errors/network_exception.dart';
import '../errors/parse_exception.dart';
import '../errors/server_exception.dart';
import 'nominatim_result.dart';
/// Tiny wrapper around the public Nominatim geocoder. Only used in the
/// commute-settings flow to look up a home address; not called from any
/// hot path. The User-Agent header is **required** by the Nominatim usage
/// policy — without it the service throttles/blocks the client.
class NominatimSearch {
static const _userAgent = 'MarianumMobile/1.0 (contact@elias-mueller.com)';
static final Uri _base = Uri.parse('https://nominatim.openstreetmap.org/search');
/// Returns up to [limit] geocoded matches for the user-typed [query].
Future<List<NominatimResult>> run(String query, {int limit = 5}) async {
final uri = _base.replace(
queryParameters: {
'q': query,
'format': 'json',
'limit': limit.toString(),
'addressdetails': '0',
'accept-language': 'de',
},
);
final http.Response response;
try {
response = await http
.get(uri, headers: {'User-Agent': _userAgent, 'Accept': 'application/json'})
.timeout(const Duration(seconds: 15));
} on SocketException catch (e) {
throw NetworkException(technicalDetails: 'nominatim: ${e.message}');
} on TimeoutException catch (e) {
throw NetworkException.timeout(technicalDetails: 'nominatim: $e');
} on http.ClientException catch (e) {
throw NetworkException(technicalDetails: 'nominatim: ${e.message}');
}
if (response.statusCode > 299) {
throw ServerException(
statusCode: response.statusCode,
technicalDetails: 'nominatim HTTP ${response.statusCode}',
);
}
try {
final raw = jsonDecode(utf8.decode(response.bodyBytes)) as List;
return raw
.map((e) => _resultFromRaw(e as Map<String, dynamic>))
.toList(growable: false);
} catch (e) {
throw ParseException(technicalDetails: 'nominatim assemble: $e');
}
}
static NominatimResult _resultFromRaw(Map<String, dynamic> json) {
// Nominatim returns lat/lon as strings, not numbers. Normalise here.
final lat = double.parse(json['lat'].toString());
final lon = double.parse(json['lon'].toString());
return NominatimResult(
displayName: json['display_name'] as String? ?? '?',
lat: lat,
lon: lon,
);
}
}