diff --git a/README.md b/README.md index d08af73a1..511f4c8a7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ These are the available plugins in this repository. | [notifications](./packages/notifications) | Track device notifications. | ✔️ | ❌ | [![pub package](https://img.shields.io/pub/v/notifications.svg)](https://pub.dartlang.org/packages/notifications) | | [movisens_flutter](./packages/movisens_flutter) | Movisens sensor communication. | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/movisens_flutter.svg)](https://pub.dartlang.org/packages/movisens_flutter) | | [esense_flutter](./packages/esense_flutter) | eSense ear sensor plugin. | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/esense_flutter.svg)](https://pub.dartlang.org/packages/esense_flutter) | -| [health](./packages/health) | Apple HealthKit and Google Fit interface plugin. | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/health.svg)](https://pub.dartlang.org/packages/health) | +| [health](https://github.com/carp-dk/carp-health-flutter) | Apple HealthKit and Google Fit interface plugin. | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/health.svg)](https://pub.dartlang.org/packages/health) | | [activity_recognition](./packages/activity_recognition_flutter) | Activity Recognition | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/activity_recognition_flutter.svg)](https://pub.dartlang.org/packages/activity_recognition_flutter) | | [audio_streamer](./packages/audio_streamer) | Stream audio as PCM from mic| ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/audio_streamer.svg)](https://pub.dartlang.org/packages/audio_streamer) | | [mobility_features](./packages/mobility_features) | Compute daily mobility features from location data | ✔️ | ✔️ | [![pub package](https://img.shields.io/pub/v/mobility_features.svg)](https://pub.dartlang.org/packages/mobility_features) | diff --git a/packages/mobility_features/CHANGELOG.md b/packages/mobility_features/CHANGELOG.md index aa09baeed..fa6fbe74e 100644 --- a/packages/mobility_features/CHANGELOG.md +++ b/packages/mobility_features/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.0 + +* upgrade of stats package + ## 6.0.0 * using carp_serialization for JSON serialization resulting in a new JSON schema for serialization diff --git a/packages/mobility_features/example/android/app/build.gradle b/packages/mobility_features/example/android/app/build.gradle index 982ab50c7..591a48de2 100644 --- a/packages/mobility_features/example/android/app/build.gradle +++ b/packages/mobility_features/example/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId = "com.example.example" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = 23 + minSdkVersion = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/packages/mobility_features/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/mobility_features/example/android/gradle/wrapper/gradle-wrapper.properties index cfe88f690..d6e3ac7b3 100644 --- a/packages/mobility_features/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/mobility_features/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +# distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/packages/mobility_features/example/android/settings.gradle b/packages/mobility_features/example/android/settings.gradle index 536165d35..a034ebcce 100644 --- a/packages/mobility_features/example/android/settings.gradle +++ b/packages/mobility_features/example/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.7.10" apply false + id "com.android.application" version '8.10.1' apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } include ":app" diff --git a/packages/mobility_features/example/lib/main.dart b/packages/mobility_features/example/lib/main.dart index f7b42181d..40a319238 100644 --- a/packages/mobility_features/example/lib/main.dart +++ b/packages/mobility_features/example/lib/main.dart @@ -1,4 +1,4 @@ -library mobility_app; +library; import 'dart:async'; import 'package:flutter/material.dart'; @@ -14,13 +14,10 @@ void main() => runApp(const MyApp()); Widget entry(String key, String value, Icon icon) { return Container( - padding: const EdgeInsets.all(2), - margin: const EdgeInsets.all(3), - child: ListTile( - leading: icon, - title: Text(key), - trailing: Text(value), - )); + padding: const EdgeInsets.all(2), + margin: const EdgeInsets.all(3), + child: ListTile(leading: icon, title: Text(key), trailing: Text(value)), + ); } String formatDate(DateTime date) => '${date.year}/${date.month}/${date.day}'; @@ -55,9 +52,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Mobility Features Example', - theme: ThemeData( - primarySwatch: Colors.blue, - ), + theme: ThemeData(primarySwatch: Colors.blue), home: const HomePage(title: 'Mobility Features Example'), ); } @@ -123,16 +118,19 @@ class HomePageState extends State { // map from [LocationDto] to [LocationSample] Stream locationSampleStream = locationStream.map( - (location) => LocationSample( - GeoLocation(location.latitude, location.longitude), - DateTime.now())); + (location) => LocationSample( + GeoLocation(location.latitude, location.longitude), + DateTime.now(), + ), + ); // provide the [MobilityFeatures] instance with the LocationSample stream await MobilityFeatures().startListening(locationSampleStream); // start listening to incoming MobilityContext objects - mobilitySubscription = - MobilityFeatures().contextStream.listen(onMobilityContext); + mobilitySubscription = MobilityFeatures().contextStream.listen( + onMobilityContext, + ); } Future isLocationAlwaysGranted() async { @@ -204,26 +202,33 @@ class HomePageState extends State { children: [ entry("Stops", "${_mobilityContext.stops?.length}", stopIcon), entry("Moves", "${_mobilityContext.moves?.length}", moveIcon), - entry("Significant Places", - "${_mobilityContext.numberOfSignificantPlaces}", placeIcon), entry( - "Home Stay", - _mobilityContext.homeStay == null || _mobilityContext.homeStay! < 0 - ? "?" - : "${(_mobilityContext.homeStay! * 100).toStringAsFixed(1)}%", - homeStayIcon), + "Significant Places", + "${_mobilityContext.numberOfSignificantPlaces}", + placeIcon, + ), + entry( + "Home Stay", + _mobilityContext.homeStay == null || _mobilityContext.homeStay! < 0 + ? "?" + : "${(_mobilityContext.homeStay! * 100).toStringAsFixed(1)}%", + homeStayIcon, + ), entry( - "Distance Traveled", - "${(_mobilityContext.distanceTraveled! / 1000).toStringAsFixed(2)} km", - distanceTraveledIcon), + "Distance Traveled", + "${(_mobilityContext.distanceTraveled! / 1000).toStringAsFixed(2)} km", + distanceTraveledIcon, + ), entry( - "Normalized Entropy", - "${_mobilityContext.normalizedEntropy?.toStringAsFixed(2)}", - entropyIcon), + "Normalized Entropy", + "${_mobilityContext.normalizedEntropy?.toStringAsFixed(2)}", + entropyIcon, + ), entry( - "Location Variance", - "${(111.133 * _mobilityContext.locationVariance!).toStringAsFixed(5)} km", - varianceIcon), + "Location Variance", + "${(111.133 * _mobilityContext.locationVariance!).toStringAsFixed(5)} km", + varianceIcon, + ), ], ); } @@ -231,28 +236,29 @@ class HomePageState extends State { List get contentNoFeatures { return [ Container( - margin: const EdgeInsets.all(25), - child: const Text( - 'Move around to start generating features', - style: TextStyle(fontSize: 20), - )) + margin: const EdgeInsets.all(25), + child: const Text( + 'Move around to start generating features', + style: TextStyle(fontSize: 20), + ), + ), ]; } List get contentFeaturesReady { return [ Container( - margin: const EdgeInsets.all(25), - child: Column(children: [ - const Text( - 'Statistics for today,', - style: TextStyle(fontSize: 20), - ), + margin: const EdgeInsets.all(25), + child: Column( + children: [ + const Text('Statistics for today,', style: TextStyle(fontSize: 20)), Text( formatDate(_mobilityContext.date!), style: const TextStyle(fontSize: 20, color: Colors.blue), ), - ])), + ], + ), + ), Expanded(child: featuresOverview), ]; } @@ -274,16 +280,16 @@ class HomePageState extends State { } Widget get navBar => BottomNavigationBar( - onTap: onTabTapped, - currentIndex: _currentIndex, - type: BottomNavigationBarType.fixed, - items: const [ - BottomNavigationBarItem(icon: featuresIcon, label: 'Features'), - BottomNavigationBarItem(icon: stopIcon, label: 'Stops'), - BottomNavigationBarItem(icon: placeIcon, label: 'Places'), - BottomNavigationBarItem(icon: moveIcon, label: 'Moves') - ], - ); + onTap: onTabTapped, + currentIndex: _currentIndex, + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem(icon: featuresIcon, label: 'Features'), + BottomNavigationBarItem(icon: stopIcon, label: 'Stops'), + BottomNavigationBarItem(icon: placeIcon, label: 'Places'), + BottomNavigationBarItem(icon: moveIcon, label: 'Moves'), + ], + ); @override Widget build(BuildContext context) { @@ -312,10 +318,7 @@ class HomePageState extends State { ]; return Scaffold( - appBar: AppBar( - backgroundColor: Colors.teal, - title: Text(widget.title), - ), + appBar: AppBar(backgroundColor: Colors.teal, title: Text(widget.title)), body: pages[_currentIndex], bottomNavigationBar: navBar, ); diff --git a/packages/mobility_features/example/pubspec.yaml b/packages/mobility_features/example/pubspec.yaml index d83ac94d1..a2dee72ec 100644 --- a/packages/mobility_features/example/pubspec.yaml +++ b/packages/mobility_features/example/pubspec.yaml @@ -3,15 +3,15 @@ description: An example app for the Mobility Features package. publish_to: "none" environment: - sdk: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + sdk: ">=3.8.0 <4.0.0" + flutter: ">=3.19.0" dependencies: flutter: sdk: flutter permission_handler: ^11.3.1 carp_background_location: ^4.0.0 - + mobility_features: path: ../ diff --git a/packages/mobility_features/lib/mobility_features.dart b/packages/mobility_features/lib/mobility_features.dart index 736b93f35..5436e80e2 100644 --- a/packages/mobility_features/lib/mobility_features.dart +++ b/packages/mobility_features/lib/mobility_features.dart @@ -1,4 +1,4 @@ -library mobility_features; +library; import 'dart:async'; import 'dart:math'; @@ -14,8 +14,274 @@ import 'package:carp_serializable/carp_serializable.dart'; part 'src/mobility_context.dart'; part 'src/domain.dart'; -part 'src/main.dart'; part 'src/util.dart'; part 'src/mobility_functions.dart'; part 'src/serializer.dart'; + part 'mobility_features.g.dart'; + +/// Main entry for configuring and listening for mobility features. +/// Used as a singleton `MobilityFeatures()`. +class MobilityFeatures { + static final MobilityFeatures _instance = MobilityFeatures._(); + + double _stopRadius = 5, _placeRadius = 50; + Duration _stopDuration = const Duration(seconds: 20); + + final _streamController = StreamController.broadcast(); + StreamSubscription? _subscription; + MobilitySerializer? _serializerSamples; + late MobilitySerializer _serializerStops; + late MobilitySerializer _serializerMoves; + List _stops = []; + List _moves = []; + List _places = []; + List _cluster = []; + final List _buffer = [], _samples = []; + int _saveEvery = 10; + bool debug = false; + + void _print(dynamic x) { + if (debug) { + print(x); + } + } + + // Private constructor + MobilityFeatures._() { + FromJsonFactory().registerAll([ + GeoLocation(0, 0), + LocationSample(GeoLocation(0, 0), DateTime.now()), + Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), + Place(0, []), + Move( + Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), + Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), + ), + ]); + } + + /// Singleton instance of MobilityFeatures. + factory MobilityFeatures() => _instance; + + /// A stream of generated mobility context objects. + Stream get contextStream => _streamController.stream; + + /// Start listening to the [stream] of [LocationSample] updates. + /// This will start calculating [MobilityContext] instances, which will be + /// delivered on the [contextStream] stream. + /// + /// Use [stopListening] to stop listening to the location stream and hence stop + /// generating mobility context objects. + Future startListening(Stream stream) async { + await _handleInit(); + + if (_subscription != null) { + await _subscription!.cancel(); + } + _subscription = stream.listen(_onData); + } + + Future _handleInit() async { + _serializerSamples = _serializerSamples = + MobilitySerializer(); + _serializerStops = MobilitySerializer(); + _serializerMoves = MobilitySerializer(); + + _stops = (await _serializerStops.load() as List); + _moves = (await _serializerMoves.load() as List); + _cluster = (await _serializerSamples!.load() as List); + _stops = uniqueElements(_stops) as List; + _moves = uniqueElements(_moves) as List; + + if (_cluster.isNotEmpty) { + _print('Loaded ${_cluster.length} location samples from disk'); + } + if (_stops.isNotEmpty) { + _print('Loaded ${_stops.length} stops from disk'); + } + if (_moves.isNotEmpty) { + _print('Loaded ${_moves.length} moves from disk'); + } + + if (_stops.isNotEmpty) { + /// Only keeps stops and moves from the last known date + DateTime date = _stops.last.dateTime.midnight; + _stops = _getElementsForDate(_stops, date) as List; + _moves = _getElementsForDate(_moves, date) as List; + _places = _findPlaces(_stops, placeRadius: _placeRadius); + + // Compute features + MobilityContext context = MobilityContext.fromMobility( + date, + _stops, + _places, + _moves, + ); + _streamController.add(context); + } + } + + /// Cancel the [StreamSubscription] and stop listening. + Future stopListening() async { + if (_subscription != null) { + await _subscription!.cancel(); + } + } + + void _adjustSaveRate() { + final now = DateTime.now(); + + // If night hours, increase saving rate + if (22 <= now.hour && 8 <= now.hour) { + _saveEvery = 1; + } + } + + /// Call-back method for handling incoming [LocationSample]s + void _onData(LocationSample sample) { + _samples.add(sample); + _adjustSaveRate(); + + // If previous samples exist, check if we should compute anything + if (_cluster.isNotEmpty) { + // If previous sample was on a different date, reset everything + if (_cluster.last.dateTime.midnight != sample.dateTime.midnight) { + _createStopAndResetCluster(); + _clearEverything(); + } + // If previous sample was today + else { + // Compute median location of the collected samples + GeoLocation centroid = _computeCentroid(_cluster); + + // If the new data point is far away from cluster, make stop + if (Distance.fromGeoSpatial(centroid, sample) > _stopRadius) { + _createStopAndResetCluster(); + } + } + } + _addToBuffer(sample); + _cluster.add(sample); + } + + void _clearEverything() { + _print('cleared'); + _serializerStops.clear(); + _serializerMoves.clear(); + _stops.clear(); + _moves.clear(); + _places.clear(); + _cluster.clear(); + _buffer.clear(); + } + + /// Save a sample to the buffer and store samples on disk if buffer overflows + void _addToBuffer(LocationSample sample) { + _buffer.add(sample); + if (_buffer.length >= _saveEvery) { + _serializerSamples!.append(_buffer); + _print('Stored buffer to disk'); + _buffer.clear(); + } + } + + /// Converts the cluster into a stop, i.e. closing the cluster + void _createStopAndResetCluster() { + Stop s = Stop.fromLocationSamples(_cluster); + + // If the stop is too short, it is discarded + // Otherwise compute a context and send it via the stream + if (s.duration > _stopDuration) { + _print('----> Stop found: $s'); + Stop? stopPrev = _stops.isNotEmpty ? _stops.last : null; + + _stops.add(s); + + // Find places + _places = _findPlaces(_stops); + + // Merge stops and recompute places + _stops = _mergeStops(_stops); + _places = _findPlaces(_stops); + + // Store to disk + _serializerStops.clear(); + _serializerStops.append(_stops); + + // Extract date + DateTime date = _cluster.last.dateTime.midnight; + + if (stopPrev != null) { + _moves = _findMoves(_stops, _samples); + _serializerMoves.clear(); + _serializerMoves.append(_moves); + } + + // Compute features + MobilityContext context = MobilityContext.fromMobility( + date, + _stops, + _places, + _moves, + ); + _streamController.add(context); + } + + // Reset samples etc + _cluster.clear(); + _serializerSamples!.clear(); + _buffer.clear(); + } + + /// Configure the stop-duration for the stop algorithm + set stopDuration(Duration value) { + _stopDuration = value; + } + + /// Configure the stop-radius for the stop algorithm + set stopRadius(double value) { + _stopRadius = value; + } + + /// Configure the stop-radius for the place algorithm + set placeRadius(double value) => _placeRadius = value; + + Future> + get _locationSampleSerializer async { + _serializerSamples ??= MobilitySerializer(); + return _serializerSamples!; + } + + Future saveSamples(List samples) async { + final serializer = await _locationSampleSerializer; + serializer.append(samples); + } + + Future> loadSamples() async { + final serializer = await _locationSampleSerializer; + return (await serializer.load() as List); + } + + static List _getElementsForDate( + List elements, + DateTime date, + ) { + return elements.where((e) => e.dateTime.midnight == date).toList(); + } + + static List uniqueElements(List elements) { + List seen = []; + + elements.sort((a, b) => a.dateTime.compareTo(b.dateTime)); + + return elements.where((e) { + int ms = e.dateTime.millisecondsSinceEpoch; + if (!seen.contains(ms)) { + seen.add(ms); + return true; + } + return false; + }).toList(); + } +} diff --git a/packages/mobility_features/lib/mobility_features.g.dart b/packages/mobility_features/lib/mobility_features.g.dart index 376646864..584a7bf88 100644 --- a/packages/mobility_features/lib/mobility_features.g.dart +++ b/packages/mobility_features/lib/mobility_features.g.dart @@ -11,59 +11,44 @@ MobilityContext _$MobilityContextFromJson(Map json) => ..timestamp = json['timestamp'] == null ? null : DateTime.parse(json['timestamp'] as String) - ..date = - json['date'] == null ? null : DateTime.parse(json['date'] as String) + ..date = json['date'] == null + ? null + : DateTime.parse(json['date'] as String) ..numberOfStops = (json['numberOfStops'] as num?)?.toInt() ..numberOfMoves = (json['numberOfMoves'] as num?)?.toInt() - ..numberOfSignificantPlaces = - (json['numberOfSignificantPlaces'] as num?)?.toInt() + ..numberOfSignificantPlaces = (json['numberOfSignificantPlaces'] as num?) + ?.toInt() ..locationVariance = (json['locationVariance'] as num?)?.toDouble() ..entropy = (json['entropy'] as num?)?.toDouble() ..normalizedEntropy = (json['normalizedEntropy'] as num?)?.toDouble() ..homeStay = (json['homeStay'] as num?)?.toDouble() ..distanceTraveled = (json['distanceTraveled'] as num?)?.toDouble(); -Map _$MobilityContextToJson(MobilityContext instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('timestamp', instance.timestamp?.toIso8601String()); - writeNotNull('date', instance.date?.toIso8601String()); - writeNotNull('numberOfStops', instance.numberOfStops); - writeNotNull('numberOfMoves', instance.numberOfMoves); - writeNotNull('numberOfSignificantPlaces', instance.numberOfSignificantPlaces); - writeNotNull('locationVariance', instance.locationVariance); - writeNotNull('entropy', instance.entropy); - writeNotNull('normalizedEntropy', instance.normalizedEntropy); - writeNotNull('homeStay', instance.homeStay); - writeNotNull('distanceTraveled', instance.distanceTraveled); - return val; -} +Map _$MobilityContextToJson(MobilityContext instance) => + { + 'timestamp': ?instance.timestamp?.toIso8601String(), + 'date': ?instance.date?.toIso8601String(), + 'numberOfStops': ?instance.numberOfStops, + 'numberOfMoves': ?instance.numberOfMoves, + 'numberOfSignificantPlaces': ?instance.numberOfSignificantPlaces, + 'locationVariance': ?instance.locationVariance, + 'entropy': ?instance.entropy, + 'normalizedEntropy': ?instance.normalizedEntropy, + 'homeStay': ?instance.homeStay, + 'distanceTraveled': ?instance.distanceTraveled, + }; GeoLocation _$GeoLocationFromJson(Map json) => GeoLocation( - (json['latitude'] as num).toDouble(), - (json['longitude'] as num).toDouble(), - )..$type = json['__type'] as String?; - -Map _$GeoLocationToJson(GeoLocation instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } + (json['latitude'] as num).toDouble(), + (json['longitude'] as num).toDouble(), +)..$type = json['__type'] as String?; - writeNotNull('__type', instance.$type); - val['latitude'] = instance.latitude; - val['longitude'] = instance.longitude; - return val; -} +Map _$GeoLocationToJson(GeoLocation instance) => + { + '__type': ?instance.$type, + 'latitude': instance.latitude, + 'longitude': instance.longitude, + }; LocationSample _$LocationSampleFromJson(Map json) => LocationSample( @@ -71,85 +56,50 @@ LocationSample _$LocationSampleFromJson(Map json) => DateTime.parse(json['dateTime'] as String), )..$type = json['__type'] as String?; -Map _$LocationSampleToJson(LocationSample instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['dateTime'] = instance.dateTime.toIso8601String(); - val['geoLocation'] = instance.geoLocation.toJson(); - return val; -} +Map _$LocationSampleToJson(LocationSample instance) => + { + '__type': ?instance.$type, + 'dateTime': instance.dateTime.toIso8601String(), + 'geoLocation': instance.geoLocation.toJson(), + }; Stop _$StopFromJson(Map json) => Stop( - GeoLocation.fromJson(json['geoLocation'] as Map), - DateTime.parse(json['arrival'] as String), - DateTime.parse(json['departure'] as String), - (json['placeId'] as num?)?.toInt() ?? -1, - )..$type = json['__type'] as String?; - -Map _$StopToJson(Stop instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['geoLocation'] = instance.geoLocation.toJson(); - val['placeId'] = instance.placeId; - val['arrival'] = instance.arrival.toIso8601String(); - val['departure'] = instance.departure.toIso8601String(); - return val; -} + GeoLocation.fromJson(json['geoLocation'] as Map), + DateTime.parse(json['arrival'] as String), + DateTime.parse(json['departure'] as String), + (json['placeId'] as num?)?.toInt() ?? -1, +)..$type = json['__type'] as String?; + +Map _$StopToJson(Stop instance) => { + '__type': ?instance.$type, + 'geoLocation': instance.geoLocation.toJson(), + 'placeId': instance.placeId, + 'arrival': instance.arrival.toIso8601String(), + 'departure': instance.departure.toIso8601String(), +}; Place _$PlaceFromJson(Map json) => Place( - (json['id'] as num).toInt(), - (json['stops'] as List) - .map((e) => Stop.fromJson(e as Map)) - .toList(), - )..$type = json['__type'] as String?; - -Map _$PlaceToJson(Place instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['id'] = instance.id; - val['stops'] = instance.stops.map((e) => e.toJson()).toList(); - return val; -} + (json['id'] as num).toInt(), + (json['stops'] as List) + .map((e) => Stop.fromJson(e as Map)) + .toList(), +)..$type = json['__type'] as String?; + +Map _$PlaceToJson(Place instance) => { + '__type': ?instance.$type, + 'id': instance.id, + 'stops': instance.stops.map((e) => e.toJson()).toList(), +}; Move _$MoveFromJson(Map json) => Move( - Stop.fromJson(json['stopFrom'] as Map), - Stop.fromJson(json['stopTo'] as Map), - (json['distance'] as num?)?.toDouble(), - )..$type = json['__type'] as String?; - -Map _$MoveToJson(Move instance) { - final val = {}; - - void writeNotNull(String key, dynamic value) { - if (value != null) { - val[key] = value; - } - } - - writeNotNull('__type', instance.$type); - val['stopFrom'] = instance.stopFrom.toJson(); - val['stopTo'] = instance.stopTo.toJson(); - writeNotNull('distance', instance.distance); - return val; -} + Stop.fromJson(json['stopFrom'] as Map), + Stop.fromJson(json['stopTo'] as Map), + (json['distance'] as num?)?.toDouble(), +)..$type = json['__type'] as String?; + +Map _$MoveToJson(Move instance) => { + '__type': ?instance.$type, + 'stopFrom': instance.stopFrom.toJson(), + 'stopTo': instance.stopTo.toJson(), + 'distance': ?instance.distance, +}; diff --git a/packages/mobility_features/lib/src/main.dart b/packages/mobility_features/lib/src/main.dart deleted file mode 100644 index 74f10f916..000000000 --- a/packages/mobility_features/lib/src/main.dart +++ /dev/null @@ -1,264 +0,0 @@ -part of '../mobility_features.dart'; - -/// Main entry for configuring and listening for mobility features. -/// Used as a singleton `MobilityFeatures()`. -class MobilityFeatures { - static final MobilityFeatures _instance = MobilityFeatures._(); - - double _stopRadius = 5, _placeRadius = 50; - Duration _stopDuration = const Duration(seconds: 20); - - final _streamController = StreamController.broadcast(); - StreamSubscription? _subscription; - MobilitySerializer? _serializerSamples; - late MobilitySerializer _serializerStops; - late MobilitySerializer _serializerMoves; - List _stops = []; - List _moves = []; - List _places = []; - List _cluster = []; - final List _buffer = [], _samples = []; - int _saveEvery = 10; - bool debug = false; - - void _print(dynamic x) { - if (debug) { - print(x); - } - } - - // Private constructor - MobilityFeatures._() { - FromJsonFactory().registerAll([ - GeoLocation(0, 0), - LocationSample(GeoLocation(0, 0), DateTime.now()), - Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), - Place(0, []), - Move(Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now()), - Stop(GeoLocation(0, 0), DateTime.now(), DateTime.now())) - ]); - } - - /// Singleton instance of MobilityFeatures. - factory MobilityFeatures() => _instance; - - /// A stream of generated mobility context objects. - Stream get contextStream => _streamController.stream; - - /// Start listening to the [stream] of [LocationSample] updates. - /// This will start calculating [MobilityContext] instances, which will be - /// delivered on the [contextStream] stream. - /// - /// Use [stopListening] to stop listening to the location stream and hence stop - /// generating mobility context objects. - Future startListening(Stream stream) async { - await _handleInit(); - - if (_subscription != null) { - await _subscription!.cancel(); - } - _subscription = stream.listen(_onData); - } - - Future _handleInit() async { - _serializerSamples = - _serializerSamples = MobilitySerializer(); - _serializerStops = MobilitySerializer(); - _serializerMoves = MobilitySerializer(); - - _stops = (await _serializerStops.load() as List); - _moves = (await _serializerMoves.load() as List); - _cluster = (await _serializerSamples!.load() as List); - _stops = uniqueElements(_stops) as List; - _moves = uniqueElements(_moves) as List; - - if (_cluster.isNotEmpty) { - _print('Loaded ${_cluster.length} location samples from disk'); - } - if (_stops.isNotEmpty) { - _print('Loaded ${_stops.length} stops from disk'); - } - if (_moves.isNotEmpty) { - _print('Loaded ${_moves.length} moves from disk'); - } - - if (_stops.isNotEmpty) { - /// Only keeps stops and moves from the last known date - DateTime date = _stops.last.dateTime.midnight; - _stops = _getElementsForDate(_stops, date) as List; - _moves = _getElementsForDate(_moves, date) as List; - _places = _findPlaces(_stops, placeRadius: _placeRadius); - - // Compute features - MobilityContext context = MobilityContext.fromMobility( - date, - _stops, - _places, - _moves, - ); - _streamController.add(context); - } - } - - /// Cancel the [StreamSubscription] and stop listening. - Future stopListening() async { - if (_subscription != null) { - await _subscription!.cancel(); - } - } - - void _adjustSaveRate() { - final now = DateTime.now(); - - // If night hours, increase saving rate - if (22 <= now.hour && 8 <= now.hour) { - _saveEvery = 1; - } - } - - /// Call-back method for handling incoming [LocationSample]s - void _onData(LocationSample sample) { - _samples.add(sample); - _adjustSaveRate(); - - // If previous samples exist, check if we should compute anything - if (_cluster.isNotEmpty) { - // If previous sample was on a different date, reset everything - if (_cluster.last.dateTime.midnight != sample.dateTime.midnight) { - _createStopAndResetCluster(); - _clearEverything(); - } - - // If previous sample was today - else { - // Compute median location of the collected samples - GeoLocation centroid = _computeCentroid(_cluster); - - // If the new data point is far away from cluster, make stop - if (Distance.fromGeoSpatial(centroid, sample) > _stopRadius) { - _createStopAndResetCluster(); - } - } - } - _addToBuffer(sample); - _cluster.add(sample); - } - - void _clearEverything() { - _print('cleared'); - _serializerStops.clear(); - _serializerMoves.clear(); - _stops.clear(); - _moves.clear(); - _places.clear(); - _cluster.clear(); - _buffer.clear(); - } - - /// Save a sample to the buffer and store samples on disk if buffer overflows - void _addToBuffer(LocationSample sample) { - _buffer.add(sample); - if (_buffer.length >= _saveEvery) { - _serializerSamples!.append(_buffer); - _print('Stored buffer to disk'); - _buffer.clear(); - } - } - - /// Converts the cluster into a stop, i.e. closing the cluster - void _createStopAndResetCluster() { - Stop s = Stop.fromLocationSamples(_cluster); - - // If the stop is too short, it is discarded - // Otherwise compute a context and send it via the stream - if (s.duration > _stopDuration) { - _print('----> Stop found: $s'); - Stop? stopPrev = _stops.isNotEmpty ? _stops.last : null; - - _stops.add(s); - - // Find places - _places = _findPlaces(_stops); - - // Merge stops and recompute places - _stops = _mergeStops(_stops); - _places = _findPlaces(_stops); - - // Store to disk - _serializerStops.clear(); - _serializerStops.append(_stops); - - // Extract date - DateTime date = _cluster.last.dateTime.midnight; - - if (stopPrev != null) { - _moves = _findMoves(_stops, _samples); - _serializerMoves.clear(); - _serializerMoves.append(_moves); - } - - // Compute features - MobilityContext context = MobilityContext.fromMobility( - date, - _stops, - _places, - _moves, - ); - _streamController.add(context); - } - - // Reset samples etc - _cluster.clear(); - _serializerSamples!.clear(); - _buffer.clear(); - } - - /// Configure the stop-duration for the stop algorithm - set stopDuration(Duration value) { - _stopDuration = value; - } - - /// Configure the stop-radius for the stop algorithm - set stopRadius(double value) { - _stopRadius = value; - } - - /// Configure the stop-radius for the place algorithm - set placeRadius(double value) => _placeRadius = value; - - Future> - get _locationSampleSerializer async { - _serializerSamples ??= MobilitySerializer(); - return _serializerSamples!; - } - - Future saveSamples(List samples) async { - final serializer = await _locationSampleSerializer; - serializer.append(samples); - } - - Future> loadSamples() async { - final serializer = await _locationSampleSerializer; - return (await serializer.load() as List); - } - - static List _getElementsForDate( - List elements, DateTime date) { - return elements.where((e) => e.dateTime.midnight == date).toList(); - } - - static List uniqueElements(List elements) { - List seen = []; - - elements.sort((a, b) => a.dateTime.compareTo(b.dateTime)); - - return elements.where((e) { - int ms = e.dateTime.millisecondsSinceEpoch; - if (!seen.contains(ms)) { - seen.add(ms); - return true; - } - return false; - }).toList(); - } -} diff --git a/packages/mobility_features/lib/src/mobility_context.dart b/packages/mobility_features/lib/src/mobility_context.dart index 9a8cd4af0..50fc229dc 100644 --- a/packages/mobility_features/lib/src/mobility_context.dart +++ b/packages/mobility_features/lib/src/mobility_context.dart @@ -120,11 +120,13 @@ class MobilityContext { if (_stops!.length < 2) return 0.0; double latStd = Stats.fromData(_stops!.map((s) => (s.geoLocation.latitude))) - .standardDeviation as double; + .sampleValues + .standardDeviation; double lonStd = Stats.fromData(_stops!.map((s) => (s.geoLocation.longitude))) - .standardDeviation as double; + .sampleValues + .standardDeviation; return log(latStd * latStd + lonStd * lonStd + 1); } diff --git a/packages/mobility_features/lib/src/mobility_functions.dart b/packages/mobility_features/lib/src/mobility_functions.dart index ba6980f5e..3e0761904 100644 --- a/packages/mobility_features/lib/src/mobility_functions.dart +++ b/packages/mobility_features/lib/src/mobility_functions.dart @@ -60,11 +60,9 @@ List _findMoves(List stops, List samples) { GeoLocation _computeCentroid(List data) { double lat = - Stats.fromData(data.map((d) => (d.geoLocation.latitude)).toList()).median - as double; + Stats.fromData(data.map((d) => (d.geoLocation.latitude)).toList()).mean; double lon = - Stats.fromData(data.map((d) => (d.geoLocation.longitude)).toList()).median - as double; + Stats.fromData(data.map((d) => (d.geoLocation.longitude)).toList()).mean; return GeoLocation(lat, lon); } diff --git a/packages/mobility_features/pubspec.yaml b/packages/mobility_features/pubspec.yaml index 0d254b9d4..b182df8aa 100644 --- a/packages/mobility_features/pubspec.yaml +++ b/packages/mobility_features/pubspec.yaml @@ -1,17 +1,17 @@ name: mobility_features description: Calculation of real-time mobility features like places, stops, and home stay -version: 6.0.0 +version: 6.1.0 homepage: https://github.com/cph-cachet/flutter-plugins/ environment: - sdk: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + sdk: ">=3.8.0 <4.0.0" + flutter: ">=3.19.0" dependencies: flutter: sdk: flutter simple_cluster: ^0.3.0 - stats: ^2.0.0 + stats: ^3.0.0 path_provider: ^2.0.2 json_annotation: ^4.8.0 carp_serializable: ^2.0.0 diff --git a/packages/mobility_features/test/testdata/moves.json b/packages/mobility_features/test/testdata/moves.json index 679fd2845..7874de231 100644 --- a/packages/mobility_features/test/testdata/moves.json +++ b/packages/mobility_features/test/testdata/moves.json @@ -1,25 +1,25 @@ -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171386612651546,"longitude":11.563414930789028},"placeId":0,"arrival":"2020-02-13T00:00:34.570","departure":"2020-02-13T06:57:51.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15180786902203,"longitude":11.570675076139496},"placeId":1,"arrival":"2020-02-13T07:10:08.000","departure":"2020-02-13T07:11:26.000"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15180786902203,"longitude":11.570675076139496},"placeId":1,"arrival":"2020-02-13T07:10:08.000","departure":"2020-02-13T07:11:26.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1415814591214,"longitude":11.568183708822179},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1415814591214,"longitude":11.568183708822179},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1504296953681,"longitude":11.566743217383225},"placeId":3,"arrival":"2020-02-13T08:42:57.999","departure":"2020-02-13T08:43:18.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1504296953681,"longitude":11.566743217383225},"placeId":3,"arrival":"2020-02-13T08:42:57.999","departure":"2020-02-13T08:43:18.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787021104484,"longitude":11.565147249278777},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787021104484,"longitude":11.565147249278777},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17140848487236,"longitude":11.563477734901618},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17140848487236,"longitude":11.563477734901618},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166493289434975,"longitude":11.590099831774715},"placeId":5,"arrival":"2020-02-13T09:19:24.999","departure":"2020-02-13T09:19:46.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166493289434975,"longitude":11.590099831774715},"placeId":5,"arrival":"2020-02-13T09:19:24.999","departure":"2020-02-13T09:19:46.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21135811752502,"longitude":11.61641499268229},"placeId":6,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21135811752502,"longitude":11.61641499268229},"placeId":6,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26505202402048,"longitude":11.651737746005226},"placeId":7,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26505202402048,"longitude":11.651737746005226},"placeId":7,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26254938066689,"longitude":11.668640250356418},"placeId":8,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26254938066689,"longitude":11.668640250356418},"placeId":8,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257763760641,"longitude":11.666777112529939},"placeId":9,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257763760641,"longitude":11.666777112529939},"placeId":9,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262125438156204,"longitude":11.668677326491046},"placeId":8,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262125438156204,"longitude":11.668677326491046},"placeId":8,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26277057065003,"longitude":11.666868257639507},"placeId":9,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26277057065003,"longitude":11.666868257639507},"placeId":9,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.268050906046824,"longitude":11.671886509382954},"placeId":10,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.268050906046824,"longitude":11.671886509382954},"placeId":10,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26233590463306,"longitude":11.668574732199161},"placeId":8,"arrival":"2020-02-13T11:58:15.000","departure":"2020-02-13T12:27:20.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26233590463306,"longitude":11.668574732199161},"placeId":8,"arrival":"2020-02-13T11:58:15.000","departure":"2020-02-13T12:27:20.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286749915574,"longitude":11.666522133371},"placeId":9,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286749915574,"longitude":11.666522133371},"placeId":9,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262504482026245,"longitude":11.668389434158957},"placeId":8,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262504482026245,"longitude":11.668389434158957},"placeId":8,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286828553107,"longitude":11.666566430605975},"placeId":9,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286828553107,"longitude":11.666566430605975},"placeId":9,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258553968282,"longitude":11.667927814298238},"placeId":8,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:23:34.446"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258553968282,"longitude":11.667927814298238},"placeId":8,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:23:34.446"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262844228101784,"longitude":11.666359408625585},"placeId":9,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262844228101784,"longitude":11.666359408625585},"placeId":9,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262580403192686,"longitude":11.6678265961852},"placeId":8,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262580403192686,"longitude":11.6678265961852},"placeId":8,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211442262912854,"longitude":11.616452106516075},"placeId":6,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211442262912854,"longitude":11.616452106516075},"placeId":6,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19160641714296,"longitude":11.614063258104917},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19160641714296,"longitude":11.614063258104917},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663433495479,"longitude":11.590159019361},"placeId":5,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663433495479,"longitude":11.590159019361},"placeId":5,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1668372949315,"longitude":11.5782162847473},"placeId":12,"arrival":"2020-02-13T16:40:23.999","departure":"2020-02-13T16:45:02.999"},"distance":0.0} -{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1668372949315,"longitude":11.5782162847473},"placeId":12,"arrival":"2020-02-13T16:40:23.999","departure":"2020-02-13T16:45:02.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1713134906651,"longitude":11.56320208489327},"placeId":0,"arrival":"2020-02-13T16:53:41.143","departure":"2020-02-13T23:32:21.553"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17138744464749,"longitude":11.563412884140496},"placeId":0,"arrival":"2020-02-13T00:00:34.570","departure":"2020-02-13T06:57:51.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15181037807669,"longitude":11.570665956912427},"placeId":1,"arrival":"2020-02-13T07:10:07.000","departure":"2020-02-13T07:11:27.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15181037807669,"longitude":11.570665956912427},"placeId":1,"arrival":"2020-02-13T07:10:07.000","departure":"2020-02-13T07:11:27.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.141560602245654,"longitude":11.568206983433306},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.141560602245654,"longitude":11.568206983433306},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15043099091092,"longitude":11.566737811505702},"placeId":3,"arrival":"2020-02-13T08:42:52.999","departure":"2020-02-13T08:43:23.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15043099091092,"longitude":11.566737811505702},"placeId":3,"arrival":"2020-02-13T08:42:52.999","departure":"2020-02-13T08:43:23.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787067304352,"longitude":11.56514414507998},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787067304352,"longitude":11.56514414507998},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171374868469464,"longitude":11.563396114560735},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171374868469464,"longitude":11.563396114560735},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211354793058845,"longitude":11.616413261060726},"placeId":5,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211354793058845,"longitude":11.616413261060726},"placeId":5,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.265051592464715,"longitude":11.651737185485212},"placeId":6,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.265051592464715,"longitude":11.651737185485212},"placeId":6,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262552820635186,"longitude":11.668651519931618},"placeId":7,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262552820635186,"longitude":11.668651519931618},"placeId":7,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257699488373,"longitude":11.666780059134014},"placeId":8,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257699488373,"longitude":11.666780059134014},"placeId":8,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26212858625004,"longitude":11.668672476321127},"placeId":7,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26212858625004,"longitude":11.668672476321127},"placeId":7,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26276916615197,"longitude":11.666872862688992},"placeId":8,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26276916615197,"longitude":11.666872862688992},"placeId":8,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26804986561004,"longitude":11.671887275463597},"placeId":9,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26804986561004,"longitude":11.671887275463597},"placeId":9,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262335672093656,"longitude":11.66855787848682},"placeId":7,"arrival":"2020-02-13T11:59:05.166","departure":"2020-02-13T12:27:20.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262335672093656,"longitude":11.66855787848682},"placeId":7,"arrival":"2020-02-13T11:59:05.166","departure":"2020-02-13T12:27:20.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286647018148,"longitude":11.66651767067395},"placeId":8,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"},"distance":2.600249512823055} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286647018148,"longitude":11.66651767067395},"placeId":8,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262502084683064,"longitude":11.668390881113732},"placeId":7,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262502084683064,"longitude":11.668390881113732},"placeId":7,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286781941039,"longitude":11.66656400037947},"placeId":8,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286781941039,"longitude":11.66656400037947},"placeId":8,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258624239594,"longitude":11.668309904195645},"placeId":7,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:29:48.525"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258624239594,"longitude":11.668309904195645},"placeId":7,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:29:48.525"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26284310271396,"longitude":11.666360433227268},"placeId":8,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26284310271396,"longitude":11.666360433227268},"placeId":8,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257952568721,"longitude":11.667818913680104},"placeId":7,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257952568721,"longitude":11.667818913680104},"placeId":7,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21143746763828,"longitude":11.616452772006506},"placeId":5,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21143746763828,"longitude":11.616452772006506},"placeId":5,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.20355488183124,"longitude":11.61315763712757},"placeId":10,"arrival":"2020-02-13T16:26:52.000","departure":"2020-02-13T16:27:16.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.20355488183124,"longitude":11.61315763712757},"placeId":10,"arrival":"2020-02-13T16:26:52.000","departure":"2020-02-13T16:27:16.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19159348416877,"longitude":11.614055736293171},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19159348416877,"longitude":11.614055736293171},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663795121832,"longitude":11.590155787062058},"placeId":12,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663795121832,"longitude":11.590155787062058},"placeId":12,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166841315577926,"longitude":11.578216992019733},"placeId":13,"arrival":"2020-02-13T16:40:32.999","departure":"2020-02-13T16:45:04.999"},"distance":0.0} +{"__type":"Move","stopFrom":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166841315577926,"longitude":11.578216992019733},"placeId":13,"arrival":"2020-02-13T16:40:32.999","departure":"2020-02-13T16:45:04.999"},"stopTo":{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171312978565346,"longitude":11.563198771039355},"placeId":0,"arrival":"2020-02-13T16:52:06.999","departure":"2020-02-13T23:32:21.553"},"distance":0.0} diff --git a/packages/mobility_features/test/testdata/stops.json b/packages/mobility_features/test/testdata/stops.json index 8a59ba3ac..75a27da92 100644 --- a/packages/mobility_features/test/testdata/stops.json +++ b/packages/mobility_features/test/testdata/stops.json @@ -1,26 +1,26 @@ -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171386612651546,"longitude":11.563414930789028},"placeId":0,"arrival":"2020-02-13T00:00:34.570","departure":"2020-02-13T06:57:51.000"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15180786902203,"longitude":11.570675076139496},"placeId":1,"arrival":"2020-02-13T07:10:08.000","departure":"2020-02-13T07:11:26.000"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1415814591214,"longitude":11.568183708822179},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1504296953681,"longitude":11.566743217383225},"placeId":3,"arrival":"2020-02-13T08:42:57.999","departure":"2020-02-13T08:43:18.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787021104484,"longitude":11.565147249278777},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17140848487236,"longitude":11.563477734901618},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166493289434975,"longitude":11.590099831774715},"placeId":5,"arrival":"2020-02-13T09:19:24.999","departure":"2020-02-13T09:19:46.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21135811752502,"longitude":11.61641499268229},"placeId":6,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26505202402048,"longitude":11.651737746005226},"placeId":7,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26254938066689,"longitude":11.668640250356418},"placeId":8,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257763760641,"longitude":11.666777112529939},"placeId":9,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262125438156204,"longitude":11.668677326491046},"placeId":8,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26277057065003,"longitude":11.666868257639507},"placeId":9,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.268050906046824,"longitude":11.671886509382954},"placeId":10,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26233590463306,"longitude":11.668574732199161},"placeId":8,"arrival":"2020-02-13T11:58:15.000","departure":"2020-02-13T12:27:20.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286749915574,"longitude":11.666522133371},"placeId":9,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262504482026245,"longitude":11.668389434158957},"placeId":8,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286828553107,"longitude":11.666566430605975},"placeId":9,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258553968282,"longitude":11.667927814298238},"placeId":8,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:23:34.446"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262844228101784,"longitude":11.666359408625585},"placeId":9,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262580403192686,"longitude":11.6678265961852},"placeId":8,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211442262912854,"longitude":11.616452106516075},"placeId":6,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19160641714296,"longitude":11.614063258104917},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663433495479,"longitude":11.590159019361},"placeId":5,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1668372949315,"longitude":11.5782162847473},"placeId":12,"arrival":"2020-02-13T16:40:23.999","departure":"2020-02-13T16:45:02.999"} -{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.1713134906651,"longitude":11.56320208489327},"placeId":0,"arrival":"2020-02-13T16:53:41.143","departure":"2020-02-13T23:32:21.553"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.17138744464749,"longitude":11.563412884140496},"placeId":0,"arrival":"2020-02-13T00:00:34.570","departure":"2020-02-13T06:57:51.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15181037807669,"longitude":11.570665956912427},"placeId":1,"arrival":"2020-02-13T07:10:07.000","departure":"2020-02-13T07:11:27.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.141560602245654,"longitude":11.568206983433306},"placeId":2,"arrival":"2020-02-13T07:21:27.999","departure":"2020-02-13T08:31:04.202"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.15043099091092,"longitude":11.566737811505702},"placeId":3,"arrival":"2020-02-13T08:42:52.999","departure":"2020-02-13T08:43:23.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16787067304352,"longitude":11.56514414507998},"placeId":4,"arrival":"2020-02-13T08:51:35.999","departure":"2020-02-13T08:52:36.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171374868469464,"longitude":11.563396114560735},"placeId":0,"arrival":"2020-02-13T08:54:36.999","departure":"2020-02-13T09:11:32.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.211354793058845,"longitude":11.616413261060726},"placeId":5,"arrival":"2020-02-13T09:32:00.999","departure":"2020-02-13T09:32:28.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.265051592464715,"longitude":11.651737185485212},"placeId":6,"arrival":"2020-02-13T09:38:34.155","departure":"2020-02-13T09:39:25.198"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262552820635186,"longitude":11.668651519931618},"placeId":7,"arrival":"2020-02-13T09:44:41.000","departure":"2020-02-13T09:45:08.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257699488373,"longitude":11.666780059134014},"placeId":8,"arrival":"2020-02-13T09:46:54.000","departure":"2020-02-13T09:51:43.404"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26212858625004,"longitude":11.668672476321127},"placeId":7,"arrival":"2020-02-13T10:10:49.309","departure":"2020-02-13T10:12:39.252"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26276916615197,"longitude":11.666872862688992},"placeId":8,"arrival":"2020-02-13T10:14:22.142","departure":"2020-02-13T10:56:24.367"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26804986561004,"longitude":11.671887275463597},"placeId":9,"arrival":"2020-02-13T11:16:25.438","departure":"2020-02-13T11:45:52.024"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262335672093656,"longitude":11.66855787848682},"placeId":7,"arrival":"2020-02-13T11:59:05.166","departure":"2020-02-13T12:27:20.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286647018148,"longitude":11.66651767067395},"placeId":8,"arrival":"2020-02-13T12:30:02.134","departure":"2020-02-13T13:16:37.316"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.262502084683064,"longitude":11.668390881113732},"placeId":7,"arrival":"2020-02-13T13:19:52.804","departure":"2020-02-13T13:20:48.469"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26286781941039,"longitude":11.66656400037947},"placeId":8,"arrival":"2020-02-13T13:24:22.684","departure":"2020-02-13T14:10:26.719"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26258624239594,"longitude":11.668309904195645},"placeId":7,"arrival":"2020-02-13T14:17:09.447","departure":"2020-02-13T14:29:48.525"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26284310271396,"longitude":11.666360433227268},"placeId":8,"arrival":"2020-02-13T14:34:00.883","departure":"2020-02-13T16:04:55.366"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.26257952568721,"longitude":11.667818913680104},"placeId":7,"arrival":"2020-02-13T16:08:22.755","departure":"2020-02-13T16:13:23.823"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.21143746763828,"longitude":11.616452772006506},"placeId":5,"arrival":"2020-02-13T16:24:40.000","departure":"2020-02-13T16:25:04.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.20355488183124,"longitude":11.61315763712757},"placeId":10,"arrival":"2020-02-13T16:26:52.000","departure":"2020-02-13T16:27:16.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.19159348416877,"longitude":11.614055736293171},"placeId":11,"arrival":"2020-02-13T16:28:42.000","departure":"2020-02-13T16:29:15.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.16663795121832,"longitude":11.590155787062058},"placeId":12,"arrival":"2020-02-13T16:36:16.000","departure":"2020-02-13T16:36:42.000"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.166841315577926,"longitude":11.578216992019733},"placeId":13,"arrival":"2020-02-13T16:40:32.999","departure":"2020-02-13T16:45:04.999"} +{"__type":"Stop","geoLocation":{"__type":"GeoLocation","latitude":48.171312978565346,"longitude":11.563198771039355},"placeId":0,"arrival":"2020-02-13T16:52:06.999","departure":"2020-02-13T23:32:21.553"} diff --git a/packages/pedometer/lib/pedometer.dart b/packages/pedometer/lib/pedometer.dart index d23494e86..81fdbd51b 100644 --- a/packages/pedometer/lib/pedometer.dart +++ b/packages/pedometer/lib/pedometer.dart @@ -24,6 +24,11 @@ class Pedometer { return stream; } + /// Returns every time a step is detected. + /// This works only on Android. On IOS is will only return if the [PedestrianStatus] changes. + static Stream get stepStream => + _stepDetectionChannel.receiveBroadcastStream(); + /// Transformed stream for the Android platform static Stream _androidStream( Stream stream) {